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:
@@ -127,3 +127,41 @@ Console progression now: `BattleTech v4.10` → `BTL4Application` → RIO check
|
||||
(the FIFO-path render handshake, the next target). The log's `#` note lines
|
||||
trace the device's decisions (handshake requests fed, frames parsed, actions
|
||||
echoed, mystery ports resolved to 0x154/0x155).
|
||||
|
||||
## Update 3 (2026-07-03): full VPX protocol working — game reaches content load
|
||||
|
||||
Disassembling `BTL4OPT.EXE` (a PE-in-DOS32RTM binary; CODE @VA 0x401000,
|
||||
`velocirender_sync` @0x48D220) cracked the last protocol unknowns and the
|
||||
emulated board now carries the game through the **entire** boot + render
|
||||
protocol into its own game logic:
|
||||
|
||||
1. **velocirender_sync** (was the blocker): from the disassembly, it sends
|
||||
action **0x2D** (a Rel4.10 sync/ping outside the DPL3 enum, which is why my
|
||||
`<24` filter dropped it) with data `[token,0]`, then checks
|
||||
`cmp token, reply.node[0]` at 0x48D271 — the "unexpected action %d" message
|
||||
only *prints* the received action; the real test is the echoed **data
|
||||
token**. Device now replies to 0x2D with `node[0] = token`. **Sync passes.**
|
||||
|
||||
2. **Speculative-poll gating**: `velocirender_transmit` calls
|
||||
`handle_iserver_stuff()` which polls inputStatus once and proceeds if
|
||||
not-ready. Feeding a reply into those polls corrupted alignment. The device
|
||||
now only feeds a reply after several *consecutive* empty polls (a blocking
|
||||
`inRecord` spin), not single speculative checks.
|
||||
|
||||
3. **Frame-ack**: `velocirender_frameack` expects a message with action
|
||||
**9 (vr_draw_scene)**. The device tracks an outstanding `draw_scene` send and
|
||||
replies 9 to the frame-ack. **Frame-ack passes.**
|
||||
|
||||
With these, the game now: passes sync, loads the renderer config, builds the
|
||||
scene (create/flush/list_add/draw_scene), completes frame-ack, and enters its
|
||||
own content loader (`L4VIDEO.cpp`), where it tries to load BattleTech models
|
||||
(`buttee.bgf`, `mslr.bgf`, …). The remaining errors are **content packaging /
|
||||
paths** (the minimal `test.egg` lacks these objects) and an **empty renderer
|
||||
config** (`dpldflt.ini`) — *not* VPX protocol issues.
|
||||
|
||||
**The VPX board emulation is functionally complete for boot + scene setup +
|
||||
frame-ack.** Next: (a) run against a full mission/content set (a real `.egg`
|
||||
or the pod's content tree) so objects load; (b) provide a proper DPL config;
|
||||
(c) Phase 3 — decode the geometry/material/texture commands already flowing over
|
||||
the FIFO into the OpenGL backend (formats in `restoration/divformats.py`) to put
|
||||
actual pixels on screen.
|
||||
|
||||
@@ -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) {
|
||||
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 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. */
|
||||
* 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 /*init*/ ||
|
||||
last_action == 15 /*statistics*/) reply = 1;
|
||||
/* experiment hook: override init reply 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));
|
||||
postboot_acks++;
|
||||
if (vpx_fp) { flush_run();
|
||||
fprintf(vpx_fp, "# post-boot: reply action %u (sent %u)\n",
|
||||
reply, last_action); fflush(vpx_fp); }
|
||||
}
|
||||
postboot_acks++;
|
||||
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