Files
TeslaRel410/emulator/vpx-device/vpxlog.cpp
T
CydandClaude Fable 5 939ea17d17 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>
2026-07-03 00:04:33 -05:00

382 lines
18 KiB
C++

/* VPX link-adapter device (Tesla Rel 4.10 emulation)
*
* Impersonates the INMOS C012 transputer link adapter the Division VPX board
* hung off the ISA bus (host source: sda4/DPL3/LINKIO.C, setLA):
*
* base+0 (0x150) inputData R byte board->host
* base+1 (0x151) outputData W byte host->board
* base+2 (0x152) inputStatus R bit0 = inbound byte available
* base+3 (0x153) outputStatus R bit0 = ready to accept outbound byte
* base+16 (0x160) resetRoot W board reset strobe
* base+17 (0x161) analyseRoot W board analyse strobe
*
* Two modes, selected by environment variables (device inert unless VPXLOG set):
*
* VPXLOG=<path> Phase 1: log every access to <path>.
* VPX_RESPOND=1 Phase 2: also answer as the transputer would, so the
* game gets past boot_xputer()'s startup_handshake().
* VPX_HANDSHAKES=N number of iserver transactions to feed (default 3;
* Phil's note in VR_COMMS.C: the transputer C runtime
* does 3 iserver transactions at startup).
*
* Protocol reference: sda4/DPL3/VR_COMMS.C.
* - receive_protocol(): host reads a 4-byte little-endian length/route word
* (bit31=iserver, bits16-23=sender, low16=payload length nb<=1040) then nb
* payload bytes. startup_handshake() does N such reads; each is answered by
* iserver_action() and only success is checked -- so feeding N well-formed
* iserver "version" requests (tag 42, no request-content dependency)
* satisfies the handshake.
* - The monitor (VRENDMON.BTL) and i860 (VRNOSTEX.MNG) segment downloads are
* pure host->board writes; the device just absorbs them.
*/
#include "dosbox.h"
#include "inout.h"
#include "logging.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const io_port_t VPX_BASE = 0x150;
static FILE *vpx_fp = NULL; /* log file, or NULL */
static bool vpx_respond = false;
static int vpx_max_handshakes = 3;
/* ---- board->host FIFO (what the emulated board sends the game) ---------- */
static unsigned char in_fifo[64];
static int in_len = 0, in_pos = 0;
/* ---- boot state machine ------------------------------------------------- */
enum Phase { P_INIT, P_HANDSHAKE, P_POSTBOOT };
static Phase phase = P_INIT;
static bool saw_write = false; /* game has written at least one byte */
static int handshakes_fed = 0;
/* ---- outbound frame parser (renderer messages, post-handshake) ---------- *
* Wire format (VR_COMMS.C velocirender_transmit, little-endian PC path):
* [length_word:4][action:4][data:(nb-4)] where nb = length_word & 0xffff.
* iserver responses keep the same nb = length_word & 0xffff, so byte-count
* alignment is preserved even when one slips through. We track the action of
* the last complete message so the reply can echo it (the renderer echoes the
* action it received: see velocirender_create/delete/sync in DPL_HOST.C). */
static void flush_run(void); /* fwd decl (logging, defined below) */
static bool parse_frames = false; /* true once handshake requests done */
static int wf_need_len = 4; /* bytes still needed for length_word */
static unsigned wf_lenbuf = 0, wf_lenshift = 0;
static int wf_payload_left = -1; /* -1 => currently reading length_word */
static int wf_action_pos = 0;
static unsigned wf_action = 0;
static int wf_last_nb = 0;
static unsigned last_action = 0;
static void parse_out_byte(unsigned char v) {
if (wf_payload_left < 0) { /* accumulating length_word */
wf_lenbuf |= ((unsigned)v) << wf_lenshift;
wf_lenshift += 8;
if (--wf_need_len == 0) {
int nb = (int)(wf_lenbuf & 0xffff);
wf_lenbuf = 0; wf_lenshift = 0; wf_need_len = 4;
wf_payload_left = nb > 0 ? nb : 0;
wf_last_nb = nb;
wf_action_pos = 0; wf_action = 0;
if (wf_payload_left == 0) wf_payload_left = -1; /* empty msg */
}
} else if (wf_payload_left > 0) { /* reading payload */
if (wf_action_pos < 4) {
wf_action |= ((unsigned)v) << (wf_action_pos * 8);
wf_action_pos++;
}
if (--wf_payload_left == 0) {
last_action = wf_action; /* message complete */
/* diagnostic: log small control frames (skip 512B i860 chunks) */
if (vpx_fp && wf_last_nb <= 64) {
flush_run();
fprintf(vpx_fp, "# FRAME action=%u nb=%d (slow)\n", wf_action, wf_last_nb);
fflush(vpx_fp);
}
wf_payload_left = -1;
}
}
}
/* ---- FIFO fast-path action extraction ---------------------------------- *
* After the i860 boots the host switches to the FIFO path (OUTSW.ASM): each
* message writes an outsw tag byte to outputData, then pumps the payload as
* 16-bit words to the FIFO data port (observed at 0x154/0x155, low/high). The
* payload of a control message begins with the 4-byte action. We arm on each
* outputData write and capture the next 4 FIFO bytes; if they form a valid
* action code (< 32, the vr_action enum range) we treat it as the action to
* echo. Data runs (e.g. text/coords) yield large values and are ignored. */
static bool fifo_arm = false;
static int fifo_cap_pos = 0;
static unsigned fifo_cap = 0;
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; }
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) {
unsigned w = fifo_cap;
fifo_cap = 0; fifo_cap_pos = 0;
if (vpx_fp) { flush_run();
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;
}
}
}
/* A well-formed iserver "version" request (VR_COMMS.C iserver_action case 42):
* length_word = nb | (sender<<16) | 0x80000000, little-endian; payload = tag.
* nb is padded even for iserver; use nb=2, payload {0x2a,0x00}. sender=0. */
static const unsigned char VERSION_REQUEST[6] = {
0x02, 0x00, 0x00, 0x80, /* length_word 0x80000002 LE */
0x2a, 0x00 /* tag 42 (version) + pad */
};
static void queue_version_request(void) {
memcpy(in_fifo, VERSION_REQUEST, sizeof VERSION_REQUEST);
in_len = (int)sizeof VERSION_REQUEST;
in_pos = 0;
}
/* A minimal non-iserver renderer reply for velocirender_receive() (VR_COMMS.C):
* top bit of length_word clear (non-iserver), nb>=8, first int32 of payload =
* 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 = 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) */
(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;
static io_port_t last_port = 0xFFFF; static unsigned last_val = ~0u;
static bool last_write = false; static unsigned long last_run = 0;
static char reg_name_buf[16];
static const char *reg_name(io_port_t port) {
switch (port - VPX_BASE) {
case 0: return "inputData";
case 1: return "outputData";
case 2: return "inputStatus";
case 3: return "outputStatus";
case 16: return "resetRoot";
case 17: return "analyseRoot";
default:
snprintf(reg_name_buf, sizeof reg_name_buf, "port_%03X", (unsigned)port);
return reg_name_buf;
}
}
static void flush_run(void) {
if (last_run == 0 || vpx_fp == NULL) return;
if (last_run == 1)
fprintf(vpx_fp, "%8lu %s %-13s 0x%02X\n", vpx_seq++,
last_write ? "W" : "R", reg_name(last_port), last_val & 0xFF);
else
fprintf(vpx_fp, "%8lu %s %-13s 0x%02X x%lu\n", vpx_seq++,
last_write ? "W" : "R", reg_name(last_port), last_val & 0xFF, last_run);
fflush(vpx_fp); last_run = 0;
}
static void record(io_port_t port, unsigned val, bool write) {
if (vpx_fp == NULL) return;
if (port == last_port && val == last_val && write == last_write) { last_run++; return; }
flush_run();
last_port = port; last_val = val; last_write = write; last_run = 1;
io_port_t off = port - VPX_BASE;
if (write && (off == 1 || off == 16 || off == 17)) flush_run();
}
static void note(const char *msg) {
if (vpx_fp) { flush_run(); fprintf(vpx_fp, "# %s\n", msg); fflush(vpx_fp); }
}
/* ---- I/O handlers ------------------------------------------------------- */
static Bitu vpx_read(Bitu port, Bitu /*iolen*/) {
io_port_t off = (io_port_t)port - VPX_BASE;
Bitu ret = 0xFF;
switch (off) {
case 3: /* outputStatus: always ready to accept a byte */
ret = 0x01;
break;
case 2: /* inputStatus: bit0 = inbound byte available */
ret = 0x00;
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;
if (phase == P_HANDSHAKE) {
if (handshakes_fed < vpx_max_handshakes) {
queue_version_request();
handshakes_fed++;
note("feeding iserver version request");
if (handshakes_fed >= vpx_max_handshakes)
parse_frames = true; /* i860 dl + init now framed */
ret = 0x01;
} else {
phase = P_POSTBOOT;
note("handshake complete; entering post-boot");
}
}
if (phase == P_POSTBOOT && in_pos >= in_len) {
/* 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 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++;
ret = 0x01;
}
}
}
}
break;
case 0: /* inputData: next byte from board->host FIFO */
if (vpx_respond && in_pos < in_len) ret = in_fifo[in_pos++];
else ret = 0xFF;
break;
default:
ret = 0xFF;
break;
}
record((io_port_t)port, (unsigned)ret, false);
return ret;
}
static bool warned_iolen = false;
static void vpx_write(Bitu port, Bitu val, Bitu iolen) {
io_port_t off = (io_port_t)port - VPX_BASE;
if (iolen != 1 && !warned_iolen && vpx_fp) {
warned_iolen = true; flush_run();
fprintf(vpx_fp, "# NOTE: non-byte write iolen=%u port=0x%03X val=0x%X\n",
(unsigned)iolen, (unsigned)port, (unsigned)val);
fflush(vpx_fp);
}
record((io_port_t)port, (unsigned)val, true);
if (off == 16 && (val & 1)) {
/* resetRoot asserted: board reset -> clear all state. */
phase = P_INIT; saw_write = false; handshakes_fed = 0;
postboot_acks = 0; in_len = in_pos = 0;
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. */
if (parse_frames) parse_fifo_byte((unsigned char)val);
}
}
void VPXLOG_Init(void) {
const char *env = getenv("VPXLOG");
if (env == NULL || env[0] == '\0') return;
const char *path = (strchr(env, '/') || strchr(env, '\\') ||
strchr(env, '.')) ? env : "vpxlog.txt";
vpx_fp = fopen(path, "w");
if (vpx_fp == NULL) { LOG_MSG("VPXLOG: cannot open '%s'", path); }
const char *r = getenv("VPX_RESPOND");
vpx_respond = (r && r[0] && r[0] != '0');
const char *h = getenv("VPX_HANDSHAKES");
if (h && atoi(h) > 0) vpx_max_handshakes = atoi(h);
IO_RegisterReadHandler(VPX_BASE, vpx_read, IO_MB, 18);
IO_RegisterWriteHandler(VPX_BASE, vpx_write, IO_MB, 18);
if (vpx_fp) {
fprintf(vpx_fp, "# VPX link adapter, base 0x%03X, respond=%d handshakes=%d\n",
VPX_BASE, vpx_respond ? 1 : 0, vpx_max_handshakes);
fprintf(vpx_fp, "# seq dir register value [run]\n");
fflush(vpx_fp);
}
LOG_MSG("VPXLOG: base 0x%03X respond=%d handshakes=%d log='%s'",
VPX_BASE, vpx_respond ? 1 : 0, vpx_max_handshakes, vpx_fp ? path : "(none)");
}