diff --git a/.gitignore b/.gitignore index e2a056b..e8ed600 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Developer hard-drive dump — not part of the release tree /sda4/ +# hard drive dump of a production system +/ALPHA_1/ + # Emulator working files (downloaded binaries, game image, captures) /emulator/dosbox-x/ /emulator/image/ diff --git a/emulator/.gitignore b/emulator/.gitignore index bc0527f..bbd95bf 100644 --- a/emulator/.gitignore +++ b/emulator/.gitignore @@ -4,3 +4,8 @@ image/ *.png dbx_out.txt dbx_err.txt +src/ +vpxlog.txt +sent_hex.txt +build.log +run_build.sh diff --git a/emulator/PHASE1-RESULTS.md b/emulator/PHASE1-RESULTS.md new file mode 100644 index 0000000..969d992 --- /dev/null +++ b/emulator/PHASE1-RESULTS.md @@ -0,0 +1,114 @@ +# Phase 1 — Interface Discovery: Results + +**Status: complete.** A custom DOSBox-X build with a VPX link-adapter logging +device captured the game's outbound boot conversation from the shipped binary. +The register map is confirmed and the first protocol stage is fully +characterized: the game resets the transputer and streams the entire +`VRENDMON.BTL` monitor over the link, byte for byte. + +## Instrument + +`emulator/src/src/hardware/vpxlog.cpp` — a DOSBox-X device (built into +`libhardware`, called from `VPXLOG_Init()` next to `GLIDE_Init()` in +`sdlmain.cpp`) that claims the C012 register range and logs every access. +It answers status reads so the game keeps transmitting: + +- `outputStatus` (0x153) → always ready (bit0 = 1) +- `inputStatus` (0x152) → no inbound data (bit0 = 0) +- `inputData` (0x150) → 0xFF (open-bus), logged if read + +Enabled only when the `VPXLOG` environment variable names a log path, so the +build behaves like stock DOSBox-X otherwise. Built from source with MSYS2 +mingw64 (`build-mingw-sdl2 --enable-debug=heavy`); the device compiles and +links cleanly (`VPXLOG_Init` present in the final `dosbox-x.exe`). + +Reproduce: + +``` +set VPXLOG=C:\VWE\TeslaRel410\emulator\vpxlog.txt +emulator\src\src\dosbox-x.exe -conf emulator\capture.conf +python emulator\analyze_capture.py emulator\vpxlog.txt emulator\image +``` + +## Confirmed register map (matches `sda4/DPL3/LINKIO.C`) + +Base `0x150` (from the shipped `DPLARG /device 0x150`): + +| Port | Register | Observed use | +|-------|--------------|--------------| +| 0x151 | outputData | **W** — every payload byte | +| 0x153 | outputStatus | **R** — polled (bit0) before every payload byte | +| 0x160 | resetRoot | **W** — reset pulse (see preamble) | +| 0x161 | analyseRoot | **W** — asserted low once at reset | +| 0x152 | inputStatus | **W** — one init write during reset | +| 0x150 | inputData | not yet exercised (monitor never booted to reply) | + +`inputData`/`inputStatus` reads will appear in Phase 2 once the emulated +monitor answers and the game starts reading responses. + +## Access summary (one capture, ~30 s, game killed while it waited for the monitor) + +``` +R outputStatus 85298 poll-before-send, one per payload byte +W outputData 85298 the monitor image +W resetRoot 3 reset pulse: 0, 1, 0 +W analyseRoot 1 0 +W outputStatus 1 0 (init) +W inputStatus 1 0 (init) +``` + +## Reset preamble (exact, from the capture) + +``` +seq 0 W analyseRoot 0x00 deassert analyse +seq 1 W resetRoot 0x00 reset low +seq 2 W resetRoot 0x01 assert reset +seq 3 W outputStatus 0x00 init +seq 4 W inputStatus 0x00 init +seq 5 W resetRoot 0x00 deassert reset -> transputer starts, listens on link +seq 6 R outputStatus 0x01 ready -> begin download +``` + +## The download — exact match + +The 85,298 bytes written to `outputData` are **byte-for-byte identical to +`VRENDMON.BTL`** (verified by `analyze_capture.py`). The first byte is `0xF0` +(240) — the transputer **boot-from-link primary-bootstrap length** — confirming +`VRENDMON.BTL` is a standard bootable transputer image (`.BTL` = bootable). +The game's own header (`sda4/BTLIVE/SETENV.BAT`) names it: +`DPLARG=/tranny~.\vrendmon.btl~...`. + +So protocol stage 1 is now precisely known: + +1. **Reset** the transputer with the preamble above. +2. **Stream** `VRENDMON.BTL` (the `/tranny` file) to `outputData`, polling + `outputStatus` bit0 before each byte. No handshake bytes are interleaved — + it is a straight boot-from-link download. + +After the last byte the game waits for the freshly-booted monitor to respond. +Our passive logger never answers, so the capture ends there. Driving the game +further (i860 code/data segment download via `/i860 vrnostex.mng`, then the +version handshake in `VR_COMMS.C`) is **Phase 2**, which requires the device to +actually behave as the transputer monitor rather than just log. + +## Production reference: ALPHA_1 + +A dump of a working production cockpit (**ALPHA_1**) was added at +`ALPHA_1/` (git-ignored). It contains the real pod boot chain +(`AUTOEXEC.BAT` → `PARAMETR.bat Rel410 BT POD SLOW SVGA` → Novell client + +`odipkt` packet driver + NetNub + `VGL_LABS\go.bat`) and **both** games under +`ALPHA_1/REL410/BT` and `.../RP`. Its BT resource is version **1.1.0.6** and its +`VRENDMON.BTL` is **byte-identical** to the BTRAVINE build used for this +capture — so this Phase 1 result is valid for the exact software that ran in +the cockpit. ALPHA_1 is the authoritative image for subsequent phases (it also +carries the RP side and the production launch/network configuration). + +## What Phase 1 retires from the risk list + +- **Register map / framing uncertainty** → resolved from the shipped binary, + not just the DPL3 sources. Confirmed base 0x150, C012 layout, poll-before-send. +- **"Protocol drift" between DPL3 sources and the linked LIBDPL** → the observed + behavior matches `LINKIO.C` exactly; stage 1 has no surprises. +- **Hidden bulk-transfer / interrupt path** → none seen; the download is plain + polled single-byte `outp` to 0x151. (`outsw`/`ok_to_fifo` were not used for + the monitor download.) diff --git a/emulator/analyze_capture.py b/emulator/analyze_capture.py new file mode 100644 index 0000000..b49b8c2 --- /dev/null +++ b/emulator/analyze_capture.py @@ -0,0 +1,67 @@ +"""Analyze a VPX link-adapter capture (emulator/vpxlog.txt) from the Phase 1 +logging device: summarize the access pattern, reconstruct the outbound byte +stream, and check it against the transputer monitor / i860 renderer files. + +Usage: + python analyze_capture.py [vpxlog.txt] [image_dir] +""" +import sys, os +from collections import OrderedDict + +log_path = sys.argv[1] if len(sys.argv) > 1 else "vpxlog.txt" +image_dir = sys.argv[2] if len(sys.argv) > 2 else "image" + +events = [] # (seq, dir, reg, value, run) +sent = bytearray() # bytes written to outputData, in order +for line in open(log_path): + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + seq, d, reg, val = parts[0], parts[1], parts[2], parts[3] + run = int(parts[4][1:]) if len(parts) > 4 and parts[4].startswith("x") else 1 + events.append((seq, d, reg, int(val, 16), run)) + if d == "W" and reg == "outputData": + sent.extend([int(val, 16)] * run) + +# access summary +counts = OrderedDict() +for _, d, reg, _, run in events: + counts[(d, reg)] = counts.get((d, reg), 0) + run +print("=== access summary (run-length expanded) ===") +for (d, reg), n in sorted(counts.items(), key=lambda x: -x[1]): + print(f" {d} {reg:<13} {n}") + +# reset preamble (writes before the first outputData byte) +print("\n=== reset preamble (up to first payload byte) ===") +for seq, d, reg, val, run in events: + if d == "W" and reg == "outputData": + break + print(f" {seq} {d} {reg:<13} 0x{val:02X}" + (f" x{run}" if run > 1 else "")) + +print(f"\n=== outbound stream: {len(sent)} bytes ===") + +def match(name): + p = os.path.join(image_dir, name) + if not os.path.exists(p): + return f" {name}: (not found)" + data = open(p, "rb").read() + if bytes(sent) == data: + return f" {name}: {len(data)} bytes EXACT MATCH (whole stream)" + if bytes(sent[:len(data)]) == data: + return f" {name}: {len(data)} bytes MATCHES stream prefix (stream continues)" + # first difference + for i, (a, b) in enumerate(zip(sent, data)): + if a != b: + return f" {name}: {len(data)} bytes differs at offset {i} (sent 0x{a:02X} vs 0x{b:02X})" + return f" {name}: {len(data)} bytes (stream is a prefix of file)" + +print("=== compare outbound stream to boot files ===") +for f in ("VRENDMON.BTL", "VRNOSTEX.MNG", "VREND.MNG"): + print(match(f)) + +if sent[:1]: + n = sent[0] + print(f"\n=== transputer boot-from-link ===") + print(f" first byte 0x{sent[0]:02X} ({sent[0]}) = primary-bootstrap length " + f"(a standard bootable .BTL transputer image)") diff --git a/emulator/capture.conf b/emulator/capture.conf new file mode 100644 index 0000000..dae9196 --- /dev/null +++ b/emulator/capture.conf @@ -0,0 +1,39 @@ +# DOSBox-X config for Phase 1: capture the game's VPX link conversation. +# Requires the custom build (emulator/src) with the VPXLOG device, and the +# VPXLOG environment variable set to a log path before launch. +# +# set VPXLOG=C:\VWE\TeslaRel410\emulator\vpxlog.txt (host env, before launch) +# dosbox-x.exe -conf capture.conf + +[sdl] +output=opengl + +[dosbox] +memsize=32 +machine=svga_s3 + +[cpu] +core=normal +cputype=pentium +cycles=20000 + +[serial] +# RIO/plasma still absent this phase; the game will report the RIO miss and +# proceed to the VPX link, which is what we are capturing. +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 CAPTURE-DONE +pause diff --git a/emulator/vpx-device/README.md b/emulator/vpx-device/README.md new file mode 100644 index 0000000..1d2e574 --- /dev/null +++ b/emulator/vpx-device/README.md @@ -0,0 +1,42 @@ +# VPX device — DOSBox-X integration + +Our original source for the emulated Division VPX link adapter. Kept here under +version control because the DOSBox-X source tree itself +(`emulator/src/`, ~490 MB) is git-ignored. + +- **`vpxlog.cpp`** — Phase 1 logging device. Impersonates the INMOS C012 link + adapter at I/O base `0x150`, answers status reads so the game keeps + transmitting, and logs every access to `$VPXLOG`. (Phase 2 will grow this + into a responding transputer-monitor + i860-loader + frame-stream renderer, + or fork into a separate `vpx.cpp`.) + +## Applying to a DOSBox-X source checkout + +Tested against DOSBox-X `v2026.06.02`, MSYS2 mingw64. + +1. Copy the device in: + ``` + cp emulator/vpx-device/vpxlog.cpp emulator/src/src/hardware/vpxlog.cpp + ``` +2. Add it to the hardware build — in `src/src/hardware/Makefile.am`, append + `vpxlog.cpp` to `libhardware_a_SOURCES` (we inserted it after `glide.cpp`). +3. Call the init — in `src/src/gui/sdlmain.cpp`, declare `void VPXLOG_Init();` + next to the other `*_Init()` prototypes and call `VPXLOG_Init();` right after + `GLIDE_Init();` in the machine bring-up sequence. +4. Build: + ``` + cd emulator/src + ./build-mingw-sdl2 --enable-debug=heavy + ``` + Output: `src/src/dosbox-x.exe`. + +## Running + +``` +set VPXLOG=C:\VWE\TeslaRel410\emulator\vpxlog.txt +emulator\src\src\dosbox-x.exe -conf emulator\capture.conf +python emulator\analyze_capture.py +``` + +With `VPXLOG` unset the device is inert and the build behaves like stock +DOSBox-X. diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp new file mode 100644 index 0000000..fb46c64 --- /dev/null +++ b/emulator/vpx-device/vpxlog.cpp @@ -0,0 +1,127 @@ +/* VPX link-adapter logging device (Tesla Rel 4.10 emulation, Phase 1) + * + * 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): + * + * 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+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 + * + * 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. + */ + +#include "dosbox.h" +#include "inout.h" +#include "logging.h" + +#include +#include +#include + +static const io_port_t VPX_BASE = 0x150; + +static FILE *vpx_fp = NULL; +static bool vpx_on = false; +static unsigned long vpx_seq = 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 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: return "?"; + } +} + +static void vpx_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 vpx_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(); + 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(); +} + +static Bitu vpx_read(Bitu port, Bitu /*iolen*/) { + io_port_t off = (io_port_t)port - VPX_BASE; + Bitu ret; + 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 */ + } + vpx_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); +} + +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: could not open log file '%s'", path); + return; + } + vpx_on = true; + + /* 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); +}