Phase 2 COMPLETE: full VPX protocol working; game reaches content loader
Disassembled BTL4OPT.EXE (PE-in-DOS32RTM) to crack the last unknowns: - velocirender_sync (@0x48D220): sends action 0x2D with a data token and checks cmp token, reply.node[0] (the 'unexpected action' text only prints the action; the real check is the echoed token). Device now echoes the token in node[0]. Sync passes. - Speculative-poll gating: only feed a reply after consecutive empty polls (a blocking inRecord spin), not single handle_iserver_stuff() checks. - Frame-ack: velocirender_frameack expects action 9 (vr_draw_scene); device tracks outstanding draw_scene and replies 9. Frame-ack passes. The game now passes sync, loads renderer config, builds the scene (create/flush/list_add/draw_scene), completes frame-ack, and enters its own content loader (L4VIDEO.cpp) loading BattleTech models. Remaining issues are content packaging/paths and an empty DPL config -- not VPX protocol. The board emulation is functionally complete for boot + scene setup + frame-ack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -113,7 +113,19 @@ static void parse_out_byte(unsigned char v) {
|
||||
static bool fifo_arm = false;
|
||||
static int fifo_cap_pos = 0;
|
||||
static unsigned fifo_cap = 0;
|
||||
static const unsigned VR_ACTION_MAX = 24; /* enum has 24 actions (0..23) */
|
||||
static const unsigned VR_ACTION_MAX = 24; /* vr_action enum has 24 (0..23) */
|
||||
static const unsigned VR_SYNC_ACTION = 0x2d; /* Rel4.10 sync/ping (from disasm) */
|
||||
|
||||
/* Sync protocol (velocirender_sync in BTL4OPT.EXE @0x48D220, disassembled):
|
||||
* sends action 0x2d with data [token, 0], receives, and checks that the
|
||||
* REPLY's node[0] == token. (The "unexpected action %d" message only prints
|
||||
* the received action; the real check at 0x48D271 is `cmp token, node[0]`.)
|
||||
* So the device must reply to a 0x2d message with node[0] = the sent token.
|
||||
* The token is the data word of the NEXT FIFO run after the 0x2d action. */
|
||||
static bool expect_sync_token = false;
|
||||
static bool sync_pending = false;
|
||||
static unsigned sync_token = 0;
|
||||
static bool frame_outstanding = false; /* draw_scene sent, frame-ack owed */
|
||||
|
||||
static void fifo_arm_action(void) { fifo_arm = true; fifo_cap_pos = 0; fifo_cap = 0; }
|
||||
|
||||
@@ -121,12 +133,22 @@ static void parse_fifo_byte(unsigned char v) {
|
||||
if (!fifo_arm) return;
|
||||
fifo_cap |= ((unsigned)v) << (fifo_cap_pos * 8);
|
||||
if (++fifo_cap_pos == 4) {
|
||||
fifo_arm = false;
|
||||
unsigned w = fifo_cap;
|
||||
fifo_cap = 0; fifo_cap_pos = 0;
|
||||
if (vpx_fp) { flush_run();
|
||||
fprintf(vpx_fp, "# FIFOCAP 0x%08X%s\n", fifo_cap,
|
||||
fifo_cap < VR_ACTION_MAX ? " (action)" : "");
|
||||
fflush(vpx_fp); }
|
||||
if (fifo_cap < VR_ACTION_MAX) last_action = fifo_cap;
|
||||
fprintf(vpx_fp, "# FIFOCAP 0x%08X\n", w); fflush(vpx_fp); }
|
||||
if (expect_sync_token) {
|
||||
sync_token = w; sync_pending = true; expect_sync_token = false;
|
||||
fifo_arm = false;
|
||||
} else if (w == VR_SYNC_ACTION) {
|
||||
expect_sync_token = true; /* token follows contiguously; stay armed */
|
||||
} else {
|
||||
/* don't clear sync_pending here: a pending sync token must survive
|
||||
* intervening sends until the sync receive consumes it. */
|
||||
if (w < VR_ACTION_MAX) last_action = w;
|
||||
if (w == 9 /*vr_draw_scene*/) frame_outstanding = true;
|
||||
fifo_arm = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,17 +171,21 @@ static void queue_version_request(void) {
|
||||
* action code. The post-boot vr_init reply is ignored by the caller
|
||||
* (DPL_HOST.C), so any action != vr_draw_scene_action satisfies it. */
|
||||
static int postboot_acks = 0;
|
||||
static int vpx_max_postboot_acks = 40; /* safety cap; tune via env */
|
||||
static void queue_render_ack(unsigned char action) {
|
||||
static int vpx_max_postboot_acks = 200; /* safety cap */
|
||||
static int empty_polls = 0;
|
||||
static const int POLL_THRESHOLD = 6; /* consecutive empty polls => blocking receive */
|
||||
static void queue_render_ack_node(unsigned char action, unsigned node) {
|
||||
unsigned char m[12] = {
|
||||
0x08, 0x00, 0x00, 0x00, /* length_word 0x00000008 (nb=8) */
|
||||
action, 0x00, 0x00, 0x00, /* payload[0..3] = action (LE) */
|
||||
0x00, 0x00, 0x00, 0x00 /* payload[4..7] = node (unused) */
|
||||
(unsigned char)(node), (unsigned char)(node >> 8),
|
||||
(unsigned char)(node >> 16), (unsigned char)(node >> 24) /* node[0] */
|
||||
};
|
||||
memcpy(in_fifo, m, sizeof m);
|
||||
in_len = (int)sizeof m;
|
||||
in_pos = 0;
|
||||
}
|
||||
static void queue_render_ack(unsigned char action) { queue_render_ack_node(action, 0); }
|
||||
|
||||
/* ---- logging (run-length coalesced) ------------------------------------- */
|
||||
static unsigned long vpx_seq = 0;
|
||||
@@ -217,6 +243,7 @@ static Bitu vpx_read(Bitu port, Bitu /*iolen*/) {
|
||||
if (vpx_respond) {
|
||||
if (in_pos < in_len) {
|
||||
ret = 0x01; /* still draining a queued message */
|
||||
empty_polls = 0;
|
||||
} else {
|
||||
/* FIFO empty: decide whether to start a new transaction. */
|
||||
if (phase == P_INIT && saw_write) phase = P_HANDSHAKE;
|
||||
@@ -234,26 +261,48 @@ static Bitu vpx_read(Bitu port, Bitu /*iolen*/) {
|
||||
}
|
||||
}
|
||||
if (phase == P_POSTBOOT && in_pos >= in_len) {
|
||||
/* game is reading -> expects a renderer reply
|
||||
* (first is the vr_init ack after i860 boot). */
|
||||
/* Distinguish a blocking receive (inRecord spins,
|
||||
* polling inputStatus many times) from a speculative
|
||||
* single poll (handle_iserver_stuff/altRecord polls
|
||||
* once and proceeds if not-ready). Only feed a reply
|
||||
* after several consecutive empty polls, so we don't
|
||||
* inject renderer replies into iserver-drain checks. */
|
||||
if (++empty_polls < POLL_THRESHOLD) { ret = 0x00; break; }
|
||||
empty_polls = 0;
|
||||
if (postboot_acks < vpx_max_postboot_acks) {
|
||||
/* Reply action is handler-specific (board side,
|
||||
* VR_REMOT.C): most handlers preserve data[0] (echo
|
||||
* the sent action), but a few overwrite it:
|
||||
* vr_init_action(0) -> *data = 1
|
||||
* vr_statistics_action(15) -> *data = 1
|
||||
* everything else echoes. */
|
||||
unsigned reply = last_action;
|
||||
if (last_action == 0 /*init*/ ||
|
||||
last_action == 15 /*statistics*/) reply = 1;
|
||||
/* experiment hook: override init reply action */
|
||||
{ const char *o = getenv("VPX_INIT_REPLY");
|
||||
if (o && last_action == 0) reply = (unsigned)atoi(o); }
|
||||
queue_render_ack((unsigned char)(reply & 0xff));
|
||||
if (sync_pending) {
|
||||
/* velocirender_sync: reply node[0] = token so
|
||||
* its `cmp token, node[0]` passes. Action field
|
||||
* is not checked (just printed on failure). */
|
||||
queue_render_ack_node((unsigned char)VR_SYNC_ACTION,
|
||||
sync_token);
|
||||
sync_pending = false;
|
||||
if (vpx_fp) { flush_run();
|
||||
fprintf(vpx_fp, "# post-boot: sync reply token=0x%X\n",
|
||||
sync_token); fflush(vpx_fp); }
|
||||
} else if (frame_outstanding) {
|
||||
/* velocirender_frameack expects a message with
|
||||
* action == vr_draw_scene_action (9). */
|
||||
frame_outstanding = false;
|
||||
queue_render_ack(9);
|
||||
if (vpx_fp) { flush_run();
|
||||
fprintf(vpx_fp, "# post-boot: frame ack (action 9)\n");
|
||||
fflush(vpx_fp); }
|
||||
} else {
|
||||
/* Reply action is handler-specific (board side,
|
||||
* VR_REMOT.C): most echo data[0] (the sent
|
||||
* action); a few overwrite it:
|
||||
* vr_init_action(0)/statistics(15) -> 1 */
|
||||
unsigned reply = last_action;
|
||||
if (last_action == 0 || last_action == 15) reply = 1;
|
||||
{ const char *o = getenv("VPX_INIT_REPLY");
|
||||
if (o && last_action == 0) reply = (unsigned)atoi(o); }
|
||||
queue_render_ack((unsigned char)(reply & 0xff));
|
||||
if (vpx_fp) { flush_run();
|
||||
fprintf(vpx_fp, "# post-boot: reply action %u (sent %u)\n",
|
||||
reply, last_action); fflush(vpx_fp); }
|
||||
}
|
||||
postboot_acks++;
|
||||
if (vpx_fp) { flush_run();
|
||||
fprintf(vpx_fp, "# post-boot: reply action %u (sent %u)\n",
|
||||
reply, last_action); fflush(vpx_fp); }
|
||||
ret = 0x01;
|
||||
}
|
||||
}
|
||||
@@ -292,9 +341,11 @@ static void vpx_write(Bitu port, Bitu val, Bitu iolen) {
|
||||
parse_frames = false; wf_need_len = 4; wf_lenbuf = 0; wf_lenshift = 0;
|
||||
wf_payload_left = -1; wf_action_pos = 0; wf_action = 0; last_action = 0;
|
||||
fifo_arm = false; fifo_cap_pos = 0; fifo_cap = 0;
|
||||
expect_sync_token = false; sync_pending = false; sync_token = 0; frame_outstanding = false;
|
||||
note("board reset");
|
||||
} else if (off == 1) {
|
||||
saw_write = true; /* outputData: a download/response byte */
|
||||
empty_polls = 0; /* a write means the game isn't blocking-reading */
|
||||
if (parse_frames) { parse_out_byte((unsigned char)val); fifo_arm_action(); }
|
||||
} else if (off == 4 || off == 5) {
|
||||
/* FIFO data port (link B): payload words, low/high bytes. */
|
||||
|
||||
Reference in New Issue
Block a user