Phase 1 complete: captured VPX boot conversation from shipped binary
Built DOSBox-X from source with a custom VPX link-adapter logging device (vpx-device/vpxlog.cpp) and captured the game's outbound boot sequence. Findings: - Register map confirmed against LINKIO.C: outputData 0x151, outputStatus 0x153 (polled bit0), resetRoot 0x160, analyseRoot 0x161. - Reset preamble captured exactly (analyse=0, reset 0->1->0 + status inits). - The game streams 85298 bytes to outputData that are BYTE-FOR-BYTE identical to VRENDMON.BTL; first byte 0xF0 = transputer boot-from-link primary-bootstrap length. Protocol stage 1 (reset + monitor download) fully characterized; no hidden bulk/interrupt path. - Production cockpit dump ALPHA_1 added (git-ignored): its BT is v1.1.0.6 with a VRENDMON.BTL byte-identical to the captured stream, so this result reflects the exact cockpit software. ALPHA_1 is the reference image going forward (carries RP + production pod/network boot chain). Adds analyze_capture.py, capture.conf, PHASE1-RESULTS.md. DOSBox-X source tree and capture artifacts are git-ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -4,3 +4,8 @@ image/
|
||||
*.png
|
||||
dbx_out.txt
|
||||
dbx_err.txt
|
||||
src/
|
||||
vpxlog.txt
|
||||
sent_hex.txt
|
||||
build.log
|
||||
run_build.sh
|
||||
|
||||
@@ -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.)
|
||||
@@ -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)")
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user