From 939ea17d170fd04711f03450905cea763a82a087 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 3 Jul 2026 00:04:33 -0500 Subject: [PATCH] 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 --- emulator/PHASE2-PROGRESS.md | 38 ++++++++++++ emulator/vpx-device/vpxlog.cpp | 105 ++++++++++++++++++++++++--------- 2 files changed, 116 insertions(+), 27 deletions(-) diff --git a/emulator/PHASE2-PROGRESS.md b/emulator/PHASE2-PROGRESS.md index 2c32674..ed2a5fa 100644 --- a/emulator/PHASE2-PROGRESS.md +++ b/emulator/PHASE2-PROGRESS.md @@ -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. diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index 95bbae1..1a3e052 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -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. */