RIO (Phase 5) validated with real hardware: with a physical RIO on COM1 and serial1=directserial realport:COM1, the game's 1996 RIO driver talks to the board — the 'RIO never came back' error is gone and expected activity was seen on the RIO board. VPX FIFO fast path decoded (OUTSW.ASM + capture): after i860 boot the render protocol pumps 16-bit words to a FIFO data port seen as 0x154/0x155 byte writes, payload starting with the 4-byte action. The device now extracts FIFO message actions (confirmed vr_init=0 with the args string, vr_create=1 with node type 0x2D) and echoes replies with handler-specific overrides from board source VR_REMOT.C (init/statistics reply action = 1). Blocked on one unknown: velocirender_sync checks the init reply against a constant that is neither 0 nor 1, so the Rel 4.10 board differs from the DPL3 dev source. Needs the Rel 4.10 LIBDPL source or disassembly of BTL4OPT.EXE at the format string (file offset 0x107772). Device has a VPX_INIT_REPLY experiment hook for candidate values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
331 lines
14 KiB
C++
331 lines
14 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; /* enum has 24 actions (0..23) */
|
|
|
|
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) {
|
|
fifo_arm = false;
|
|
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;
|
|
}
|
|
}
|
|
|
|
/* 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;
|
|
|
|
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 */
|
|
} 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) {
|
|
/* 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));
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
note("board reset");
|
|
} else if (off == 1) {
|
|
saw_write = true; /* outputData: a download/response byte */
|
|
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)");
|
|
}
|