diff --git a/emulator/PHASE2-PROGRESS.md b/emulator/PHASE2-PROGRESS.md new file mode 100644 index 0000000..c080ebd --- /dev/null +++ b/emulator/PHASE2-PROGRESS.md @@ -0,0 +1,88 @@ +# Phase 2 — Boot Protocol Emulation: Progress + +**Status: substantial progress; boot handshake + i860 download solved; render +transport (FIFO fast path) identified as the remaining sub-protocol.** + +The VPX device (`vpx-device/vpxlog.cpp`, env `VPX_RESPOND=1`) now drives the +production **BattleTech v4.10** (ALPHA_1 image) through the entire transputer/i860 +boot sequence — far past the Phase 1 wall — and reaches the live renderer +protocol. Each layer below was solved by reading the normative sources on the +drive (`sda4/DPL3/VR_COMMS.C`, `DPL_HOST.C`, `VRENDER/VR_REMOT.C`, `LINKIO.C`, +`OUTSW.ASM`) and iterating against the running binary. + +## Solved + +### 1. iserver boot handshake +After the monitor (`VRENDMON.BTL`) download, `boot_xputer()` calls +`startup_handshake()`, which does **3 iserver transactions** (Phil's comment in +`VR_COMMS.C`: "the C run-time boots up and does 3 iserver transactions"). Each +is a `receive_protocol()` read of a 4-byte little-endian length/route word +(bit31=iserver, low16=payload length) + payload, answered by `iserver_action()`; +only success is checked. The device feeds **3 well-formed iserver "version" +requests** (tag 42, no request-content dependency) and the handshake completes. + +### 2. i860 renderer download +`boot_860()` then streams the i860 image (`VRNOSTEX.MNG`) as framed renderer +messages on the slow byte path: `vr_860args` (21), `vr_860code` (18, ×118), +`vr_860data` (19, ×37), `vr_860bss` (20, ×9). The device parses the outbound +frame stream (`[length_word:4][action:4][data]`, `nb = length_word & 0xffff`) +and absorbs them, staying byte-aligned throughout. + +### 3. The echo-action reply model (validated against board source) +Renderer request/reply calls (`velocirender_create`/`delete`/`flush`/`sync` in +`DPL_HOST.C`) send an action and require the **same action echoed back**. The +board side confirms this exactly: `VRENDER/VR_REMOT.C remote_velocirender()` +replies with its `data` buffer whose first word is still the received action +(`reply()` → `dN_send`). The device tracks the last outbound action and echoes +it; this is the general model for every render call, not a one-off. + +## The remaining sub-protocol: the FIFO fast path + +After the i860 boots, the host stops using the slow byte path and switches to a +**FIFO fast path** for the render protocol. This is why the current build still +stops at `velocirender_sync` — the device only implements the slow path +(0x150–0x153). + +Discovered mechanics (from the capture + `OUTSW.ASM` + `LINKIO.C`): + +- The VPX board is **dual-register**: beyond the C012 slow link + (0x150 inputData / 0x151 outputData / 0x152 inputStatus / 0x153 outputStatus), + there is a **FIFO data port** reached at **0x154/0x155** (observed) and an + `ok_to_fifo` status at **0x160**. +- `send_FIFO_protocol()` / `velocirender_transmit()` (when `use_fifo`) call the + assembly `outsw()` (`OUTSW.ASM`): it writes a `0x40` "use fifo" tag byte, then + `add dx,3` and `REP OUTSW` — pumping the message as **16-bit words** into the + FIFO port. In the capture these land as alternating byte writes to 0x154/0x155 + and decode to the init/args string (e.g. `/device~0x150~/video~svga~…`). +- The host switches to `use_fifo` once the board asserts `ok_to_fifo` + (`fifo_ok_status()` reads 0x160 bit0); see the commented wait loop in + `boot_xputer()`. + +### Next step (concrete) +Extend the device to implement the FIFO transport: +1. Answer `ok_to_fifo` (0x160 bit0) at the right time so the host enables `use_fifo`. +2. Accept FIFO writes: the `0x40` tag, then word-stream data at the FIFO port + (0x154/0x155). Reassemble into `[length_word][action][data]` messages + (same framing as the slow path). +3. Serve FIFO reads for replies: `velocirender_receive()` in FIFO mode reads the + reply (length_word + `[action][node]`) from the FIFO/status ports. Echo the + sent action (model already validated), returning meaningful node handles for + `vr_create` so later `vr_delete`/geometry ops have valid remotes. +4. Then the render loop (`vr_draw_scene` + frame-ack, `velocirender_frameack()`) + — the Phase 3 boundary, where geometry/material/texture commands can be + decoded into the OpenGL backend (formats already implemented in + `restoration/divformats.py`). + +## Reproduce + +``` +set VPXLOG=C:\VWE\TeslaRel410\emulator\vpxresp.txt +set VPX_RESPOND=1 +emulator\src\src\dosbox-x.exe -conf emulator\respond.conf +``` + +Console progression now: `BattleTech v4.10` → `BTL4Application` → RIO check → +(handshake, i860 download succeed) → `unexpected action N in velocirender_sync` +(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). diff --git a/emulator/respond.conf b/emulator/respond.conf new file mode 100644 index 0000000..8ad8f90 --- /dev/null +++ b/emulator/respond.conf @@ -0,0 +1,26 @@ +[sdl] +output=opengl +[dosbox] +memsize=32 +machine=svga_s3 +[cpu] +core=normal +cputype=pentium +cycles=20000 +[serial] +serial1=disabled +serial2=disabled +[autoexec] +mount c "image" +c: +set L4CONTROLS=RIO,KEYBOARD +set L4TIMER= +set BLASTER=A220 I5 D1 H5 P330 T6 +set VIDEOFORMAT=svga +set TEMP=c:\ +set DPLARG=/tranny~.\vrendmon.btl~/i860~.\vrnostex.mng~/device~0x150~/video~svga~/pipes~1~/qual~0x14~/system_tex~0~ +32rtm.exe -x +btl4opt.exe -egg test.egg +32rtm.exe -u +echo RESPOND-DONE +pause diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index fb46c64..367a0a0 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -1,26 +1,33 @@ -/* VPX link-adapter logging device (Tesla Rel 4.10 emulation, Phase 1) +/* 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, at its documented register map (host source: - * sda4/DPL3/LINKIO.C, setLA): + * 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 / ok_to_fifo + * base+16 (0x160) resetRoot W board reset strobe * base+17 (0x161) analyseRoot W board analyse strobe * - * Phase 1 goal is DISCOVERY, not emulation: log every access so we can capture - * the game's outbound boot conversation (reset -> transputer monitor download - * -> i860 code/data segments) from the shipped binary. Status reads are - * answered so the game keeps writing: - * - outputStatus -> always ready (bit0=1) so OUTBYTE proceeds - * - inputStatus -> no data (bit0=0) so the game doesn't read phantom input - * - inputData -> 0xFF (logged) if the game reads anyway + * Two modes, selected by environment variables (device inert unless VPXLOG set): * - * Enabled only when the environment variable VPXLOG is set (harmless when off). - * Log file: $VPXLOG if it names a path, else "vpxlog.txt" in the CWD. + * VPXLOG= Phase 1: log every access to . + * 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" @@ -33,17 +40,105 @@ static const io_port_t VPX_BASE = 0x150; -static FILE *vpx_fp = NULL; -static bool vpx_on = false; +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\n", wf_action, wf_last_nb); + fflush(vpx_fp); + } + wf_payload_left = -1; + } + } +} + +/* 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 = 40; /* safety cap; tune via env */ +static void queue_render_ack(unsigned char action) { + 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) */ + }; + memcpy(in_fifo, m, sizeof m); + in_len = (int)sizeof m; + in_pos = 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; -/* Coalesce runs of identical status polls so the log shows the conversation, - * not millions of "read status = ready" lines. */ -static io_port_t last_port = 0xFFFF; -static unsigned last_val = 0xFFFFFFFF; -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"; @@ -52,55 +147,115 @@ static const char *reg_name(io_port_t port) { case 3: return "outputStatus"; case 16: return "resetRoot"; case 17: return "analyseRoot"; - default: return "?"; + default: + snprintf(reg_name_buf, sizeof reg_name_buf, "port_%03X", (unsigned)port); + return reg_name_buf; } } - -static void vpx_flush_run(void) { +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; + 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 vpx_record(io_port_t port, unsigned val, bool write) { +static void record(io_port_t port, unsigned val, bool write) { if (vpx_fp == NULL) return; - /* Collapse identical consecutive accesses (typically status polls). */ - if (port == last_port && val == last_val && write == last_write) { - last_run++; - return; - } - vpx_flush_run(); + 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; - /* Payload writes and reset/analyse strobes are the interesting events; - * emit them immediately (don't wait for a differing access to flush). */ io_port_t off = port - VPX_BASE; - if (write && (off == 1 || off == 16 || off == 17)) - vpx_flush_run(); + 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; + Bitu ret = 0xFF; + switch (off) { - case 2: ret = 0x00; break; /* inputStatus: no inbound data */ - case 3: ret = 0x01; break; /* outputStatus: always ready to send */ - default: ret = 0xFF; break; /* inputData / unknown: open-bus */ + 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 */ + } 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) { + /* game is reading -> expects a renderer reply + * (first is the vr_init ack after i860 boot). */ + if (postboot_acks < vpx_max_postboot_acks) { + queue_render_ack((unsigned char)(last_action & 0xff)); + postboot_acks++; + if (vpx_fp) { flush_run(); + fprintf(vpx_fp, "# post-boot: echo action %u\n", + last_action); fflush(vpx_fp); } + 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; } - vpx_record((io_port_t)port, (unsigned)ret, false); + record((io_port_t)port, (unsigned)ret, false); return ret; } -static void vpx_write(Bitu port, Bitu val, Bitu /*iolen*/) { - vpx_record((io_port_t)port, (unsigned)val, true); +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; + note("board reset"); + } else if (off == 1) { + saw_write = true; /* outputData: a download/response byte */ + if (parse_frames) parse_out_byte((unsigned char)val); + } } void VPXLOG_Init(void) { @@ -110,18 +265,22 @@ void VPXLOG_Init(void) { const char *path = (strchr(env, '/') || strchr(env, '\\') || strchr(env, '.')) ? env : "vpxlog.txt"; vpx_fp = fopen(path, "w"); - if (vpx_fp == NULL) { - LOG_MSG("VPXLOG: could not open log file '%s'", path); - return; - } - vpx_on = true; + 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); - /* Cover 0x150..0x153 and 0x160..0x161 with one 18-port range. */ IO_RegisterReadHandler(VPX_BASE, vpx_read, IO_MB, 18); IO_RegisterWriteHandler(VPX_BASE, vpx_write, IO_MB, 18); - fprintf(vpx_fp, "# VPX link-adapter access log, base 0x%03X\n", VPX_BASE); - fprintf(vpx_fp, "# seq dir register value [run-length]\n"); - fflush(vpx_fp); - LOG_MSG("VPXLOG: logging VPX link adapter at 0x%03X to '%s'", VPX_BASE, path); + 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)"); }