VPX_PF_WATCH (fork, cpu/paging.cpp): on a guest page fault at the watched linear address, dump guest registers, the last 64 serial RX deliveries with guest cs:eip at each, the code bytes at the faulting EIP and the stack top. serialnamedpipe's doReceive feeds the ring. Armed in podrun.sh and launch_pod.ps1. The first catch decoded the residual fault completely: CS:EIP 00FF:000066D4 in the DPMI host, EBX = F000CA60 -- an IVT entry read as a dword, segment F000 offset CA60, a BIOS default interrupt handler -- and the faulting access is [EBX+0x3004], whose 0x80000000 segment-base wrap gives exactly cr2 7000FA64. The host probes a word 0x3004 bytes past a real-mode vector value treated as a flat pointer: harmless for its own low-memory handlers, a fault for BIOS F000:xxxx defaults. The serial ring shows a steady 1-byte/1-3ms vRIO stream with nothing special at the fault -- the stream determines which vectors get walked, not the crash itself. The 0x3004 appears nowhere in DPMI32VM.OVL or 32RTM.EXE as an immediate, so the probe now also dumps code bytes at EIP; faulthunt.sh loops runs until the next catch. Shipped baseline streak: 4/4 clean -- consistent with exposure, not yet discriminating. The inside view now loads the COCKPIT skeleton: the fleet-wide X-variant naming convention (MAD->MAX etc., all 64 skeletons present) selects the same 25-joint chain with a single object -- max_cop.bgf, the MAX_COP canopy shell with the PUNCH-texel windows from the capture forensics. The donor names the same mechanism from the decomp side (inside = SkeletonType_A with '_cop' selection). Fallback to the body skeleton when no X file exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
268 lines
16 KiB
Markdown
268 lines
16 KiB
Markdown
# VPX + AWE32 devices — DOSBox-X integration
|
||
|
||
Our original source for the emulated pod hardware. Kept here under
|
||
version control because the DOSBox-X source tree itself
|
||
(`emulator/src/`, ~490 MB) is git-ignored.
|
||
|
||
- **`vpxlog.cpp`** — the Division VPX link adapter (INMOS C012 at I/O base
|
||
`0x150`), grown through Phase 3: iserver handshake responder, FIFO scene
|
||
decode, GL Division renderer (gallery shading model), VDB video-head
|
||
splitter windows. **RIO lamp/button bezel (B2, 2026-07-24, explode layout):**
|
||
`pal_draw` rings each mono-MFD head with its 8 RIO buttons (red, 4 top / 4
|
||
bottom, tucked under a 640x500 display so a 10px lip shows) and the radar
|
||
with the amber Secondary/Screen side columns + a centered bottom indicator
|
||
strip, lit from `serialrio`'s `RIO_GetPanelState` (off/dim/bright + white-hot
|
||
press; flashing lamps blink at the RioLampState rate). No labels -- the
|
||
display shows each button's function. **Click-to-press:** `rt_wndproc`
|
||
hit-tests a click against the bezel and presses that RIO address (thread-safe
|
||
via serialrio's queued `RIO_HostButton`); head windows are `WS_EX_NOACTIVATE`
|
||
so a click never pulls foreground off DOSBox (which would demote the emu
|
||
thread and stop the SDL pad poll). No-op when no `serial=rio` port is present.
|
||
- **`serialplasma.cpp` / `serialplasma.h`** — `serial<n>=plasma` backend
|
||
(2026-07-24): the pod's 128×32 dot-matrix plasma readout on COM2, emulated
|
||
in-fork like `serial=rio` (no vPLASMA process, no pipe). The game
|
||
(`L4PLASMA.CPP`) renders into its own 1bpp buffer and streams changed rows
|
||
with a single `ESC P` graphics command — receive-only, no replies — so the
|
||
device is just that parser filling a 128×32 framebuffer (transcribed from
|
||
vPLASMA `PlasmaProtocol` + `VPlasmaDevice`, graphics path; the factory
|
||
text-mode/fonts aren't used by the game and aren't rendered, but their escape
|
||
operands are still consumed so the parser can't desync). `vpxlog` draws it as
|
||
an amber-on-black window in the **explode** layout (seam `PLASMA_GetFrame`),
|
||
lazily created once the port exists, parked centered under the lower-left MFD
|
||
(`VPX_PLASMA="x,y,w,h"` overrides). Real pods keep `directserial
|
||
realport:COM2`; the vPLASMA app keeps `namedpipe pipe:vplasma`.
|
||
- **`vweawe.cpp`** — the dual-AWE32 sound device: two vendored EMU8000
|
||
cores at 0x620/0x640 (+0x400/+0x800 triplets), rear-card DSP/mixer stub
|
||
at 0x240, autonomous render thread with direct winmm output. Needs the
|
||
GM ROM (`emulator/roms/awe32.raw`) via `VWE_AWE_ROM` or SoundFont
|
||
uploads are refused (banks declare `irom=1MGM`). See
|
||
`emulator/SOUND-NOTES.md`.
|
||
- **`emu8k.cpp` / `emu8k.h` / `emu8k_shim.h`** — EMU8000 wavetable core
|
||
vendored from 86Box (GPL-2.0-or-later, same license as DOSBox-X), with
|
||
a minimal shim; local changes are listed in the emu8k.cpp header.
|
||
- **`serialnamedpipe.cpp` / `serialnamedpipe.h`** — `serial<n>=namedpipe
|
||
pipe:<name>` backend (2026-07-12): serial-over-named-pipe for vRIO/vPLASMA,
|
||
replacing com0com. DOSBox is the pipe CLIENT with 500ms background retry;
|
||
typed frames carry data (`0x00 len bytes`) and DTR/RTS line state (`0x01
|
||
bits`); the full wire contract is in the header comment (pinned with the
|
||
vRIO session). Supports the directserial `rxpollus`/`rxburst`/`rxdelay`
|
||
low-latency knobs. Smoke-tested end-to-end 2026-07-12.
|
||
- **`serialrio.cpp` / `serialrio.h`** — `serial<n>=rio` backend (2026-07-21,
|
||
keyboard field 2026-07-22): the cockpit RIO board emulated **in-fork**. It
|
||
speaks the device side of the RIO 9600-8N1 protocol straight to the game's
|
||
UART (no external process, no pipe) and sources pilot input from an **SDL
|
||
game controller + the host keyboard** inside the emulator. Because the
|
||
game↔board round trip is in-process at the emulated UART's own cadence —
|
||
like the real ISA UART on a pod — the serial round-trip dropouts (livelock,
|
||
`TXMAXIDLE`, `rxburst`, 15s retry) can't occur. The protocol state machine +
|
||
input mapping are transcribed from the validated vRIO app (`C:\VWE\vrio`
|
||
VRio.Core: `VRioDevice`, the `Protocol` codec, `Input/InputRouter` +
|
||
`BindingProfileFormat`); the transport, wire pacer, panel UI and thread
|
||
locks are dropped (single emulator thread). RX byte pacing reuses
|
||
directserial's state machine (same `rxpollus`/`rxburst` knobs), draining a
|
||
queue the board itself fills.
|
||
**Keyboard field (B1):** sdlmain offers every SDL key to `RIO_HostKeyEvent()`
|
||
first; while capture is ON (default) bound keys press RIO addresses or drive
|
||
axes and are swallowed; unbound keys and Ctrl/Alt/Gui chords pass through
|
||
(host hotkeys keep working; modifier keys BOUND as inputs are exempt from
|
||
the chord rule). Default field: number/QWERTY rows = upper MFD bank,
|
||
home/bottom rows = lower bank, F1–F12 = Secondary/Screen, arrows+Space =
|
||
hat+main, and keyboard FLIGHT controls (operator 2026-07-22): numpad
|
||
8/2/4/6 = stick, 7/9 = pedals, LShift/LCtrl = throttle slew ±0.7/s,
|
||
numpad-5 = throttle zero; the internal keypad is unbound (mission-review
|
||
plumbing only). **PAUSE or SCROLL LOCK toggles capture** panel↔DOS
|
||
(toggling off releases everything; needed at the netnub loop / DOS
|
||
prompts); `togglekey:<sdl-name>` adds a third toggle key for keyboards
|
||
lacking both. Focus loss releases held keys (`RIO_HostFocusLost` in
|
||
`GFX_LosingFocus`). A bindings file in vRIO's grammar REPLACES the default
|
||
profile (conf `bindings:<path>`, else env `VWE_RIO_BINDINGS`; key-axis
|
||
modes `deflect|slew|rate|set`, sign may be a separate token; vRIO's .NET
|
||
key names are aliased so a vRIO `bindings.txt` ports over).
|
||
Conf: `serial1=rio [pad:<n>] [inverty] [bindings:<path>]
|
||
[togglekey:<key>] [rxpollus:<us>] [rxburst:<n>]`. **Additive** — real pods keep `directserial realport:COM1`;
|
||
vRIO-over-pipe keeps `namedpipe pipe:vrio`. Confs: `net_loop_rio.conf` /
|
||
`net_rp_rio.conf`; `pod-launch --rio`. B0 (pad) LIVE-CONFIRMED 2026-07-22
|
||
from the repacked dist; keyboard-field live test pending.
|
||
- **`steamlink.cpp` / `steamlink.h`** — the Steam transport core (2026-07-24):
|
||
guest Ethernet **frames** over `ISteamNetworkingMessages`, so a private group
|
||
can fly missions over the internet. Shared VERBATIM by two consumers — the
|
||
fork's `backend=steam` and the host's standalone `vwe-steam-gw.exe`
|
||
steam↔pcap gateway — which is why it is a free-function namespace and not a
|
||
class. **L2, not L3, is the whole point:** the 1995 mesh dials peers by raw
|
||
IP on one flat subnet and ARPs for them, and NAT/hole-punching has nowhere to
|
||
put an ARP. Steamworks is bound at RUNTIME (`LoadLibrary`/`GetProcAddress` on
|
||
`steam_api64.dll`), exactly as the pcap backend binds `WPCAP.DLL`, so no SDK
|
||
is needed to build or link. Load-bearing: everything ships **Reliable +
|
||
NoNagle** (WATTCP's DOS-era retransmit timers are multi-second, so an outer
|
||
retransmit costs one RTT where an inner one costs seconds — the long
|
||
rationale is in the header, read it before "optimizing" to unreliable), and
|
||
the API is **not thread-safe by design** (one thread, pumped from the NE2000
|
||
tick). Wire format, session file and env handoff are frozen in
|
||
`emulator/steam/SESSION-CONTRACT.md`.
|
||
- **`ethernet_steam.cpp` / `ethernet_steam.h`** — the `[ne2000] backend=steam`
|
||
`EthernetConnection` that plugs `steamlink` into the emulated NIC. `Pump()`
|
||
runs at the top of `GetPackets()`, which `NE2000_Poller` calls off a
|
||
`TIMER_AddTickHandler` — i.e. once per emulated millisecond — so that tick
|
||
**is** the Steam callback pump and there is no new thread or timer anywhere.
|
||
RX policy deliberately mirrors the pcap backend's BPF (accept `dst==self` or
|
||
broadcast, drop other multicast) so `backend=steam` and `backend=pcap` are
|
||
indistinguishable from the guest's side, and `macaddr=` must be a stable
|
||
literal — never `random` — because the MAC is what peers route to. Same
|
||
power-of-2-throttled `STEAM TX/RX #n` log lines as pcap. Operator runbook:
|
||
`emulator/STEAM-NOTES.md`.
|
||
|
||
## Applying to a DOSBox-X source checkout
|
||
|
||
Tested against DOSBox-X `v2026.06.02`, MSYS2 mingw64.
|
||
|
||
1. Copy the devices in:
|
||
```
|
||
cp emulator/vpx-device/vpxlog.cpp emulator/src/src/hardware/
|
||
cp emulator/vpx-device/emu8k.cpp emulator/src/src/hardware/
|
||
cp emulator/vpx-device/emu8k.h emulator/src/src/hardware/
|
||
cp emulator/vpx-device/emu8k_shim.h emulator/src/src/hardware/
|
||
cp emulator/vpx-device/vweawe.cpp emulator/src/src/hardware/
|
||
```
|
||
2. Add them to the hardware build — in `src/src/hardware/Makefile.am`, append
|
||
`vpxlog.cpp emu8k.cpp vweawe.cpp` to `libhardware_a_SOURCES` (we inserted
|
||
them after `glide.cpp`; if you edit the generated `Makefile`/`Makefile.in`
|
||
by hand instead of re-running automake, also mirror the `$(OBJEXT)` list,
|
||
the `.Po` depfile list, and the depfile include markers — grep for how
|
||
`vpxlog` appears and copy the pattern).
|
||
3. Call the inits — in `src/src/gui/sdlmain.cpp`, declare `void VPXLOG_Init();`
|
||
and `void VWEAWE_Init();` next to the other `*_Init()` prototypes; call
|
||
`VPXLOG_Init();` right after `GLIDE_Init();` and `VWEAWE_Init();` right
|
||
after `SBLASTER_Init();` (it needs the mixer initialized).
|
||
3b. The namedpipe serial backend:
|
||
```
|
||
cp emulator/vpx-device/serialnamedpipe.cpp emulator/src/src/hardware/serialport/
|
||
cp emulator/vpx-device/serialnamedpipe.h emulator/src/src/hardware/serialport/
|
||
```
|
||
plus four small stock edits:
|
||
- `src/src/hardware/serialport/Makefile.am`: append
|
||
`serialnamedpipe.cpp serialnamedpipe.h` to `libserial_a_SOURCES`
|
||
(same generated-Makefile caveat as step 2).
|
||
- `src/include/serialport.h`: add `SERIAL_TYPE_NAMED_PIPE` to
|
||
`SerialTypesE` under `#if defined(WIN32)` (before
|
||
`SERIAL_TYPE_DIRECT_SERIAL`).
|
||
- `src/src/hardware/serialport/serialport.cpp`: `#include
|
||
"serialnamedpipe.h"`; add the `type=="namedpipe"` case to BOTH dispatch
|
||
switches (SERIALPORTS ctor + the SERIAL command) and `"namedpipe"` to
|
||
the `serialTypes[]` string table — grep for how `"file"` appears and
|
||
copy the pattern.
|
||
- `src/src/dosbox.cpp`: add `"namedpipe"` to the `serials[]` allowed-values
|
||
list (without this the config parser silently falls back to `dummy`).
|
||
3c. The in-fork RIO serial backend (needs SDL2 — the whole-program link already
|
||
pulls SDL2 in, and the vendored SDL2 include dir is on the global compile
|
||
flags, so no per-subdir cflag edits):
|
||
```
|
||
cp emulator/vpx-device/serialrio.cpp emulator/src/src/hardware/serialport/
|
||
cp emulator/vpx-device/serialrio.h emulator/src/src/hardware/serialport/
|
||
```
|
||
plus the same four stock edits as the namedpipe backend:
|
||
- `src/src/hardware/serialport/Makefile.am`: append
|
||
`serialrio.cpp serialrio.h` to `libserial_a_SOURCES` (same generated-
|
||
Makefile caveat as step 2).
|
||
- `src/include/serialport.h`: add `SERIAL_TYPE_RIO` to `SerialTypesE`
|
||
(UNguarded — SDL is cross-platform, unlike the WIN32-only namedpipe;
|
||
placed right after `SERIAL_TYPE_MOUSE`).
|
||
- `src/src/hardware/serialport/serialport.cpp`: `#include "serialrio.h"`;
|
||
add the `type=="rio"` case to BOTH dispatch switches and `"rio"` to the
|
||
`serialTypes[]` string table (copy the `namedpipe` pattern; keep it
|
||
unguarded).
|
||
- `src/src/dosbox.cpp`: add `"rio"` to the `serials[]` allowed-values list.
|
||
- `src/src/gui/sdlmain.cpp` (the B1 keyboard field, three small edits):
|
||
(a) declare the hooks above `GFX_LosingFocus`:
|
||
`bool RIO_HostKeyEvent(int sdl_scancode, bool pressed, bool repeat,
|
||
unsigned int sdl_mods); void RIO_HostFocusLost(void);`
|
||
(b) call `RIO_HostFocusLost();` inside `GFX_LosingFocus` right after
|
||
`MAPPER_LosingFocus();`
|
||
(c) in GFX_Events' SDL2 `case SDL_KEYDOWN: case SDL_KEYUP:` block, right
|
||
after the SCREEN_GAMELINK early-out, offer the event to
|
||
`RIO_HostKeyEvent((int)event.key.keysym.scancode, event.type ==
|
||
SDL_KEYDOWN, event.key.repeat != 0, event.key.keysym.mod)` and `break;`
|
||
when it returns true (consumed keys must never reach the mapper).
|
||
3d. The in-fork plasma serial backend (same seam pattern as RIO):
|
||
```
|
||
cp emulator/vpx-device/serialplasma.cpp emulator/src/src/hardware/serialport/
|
||
cp emulator/vpx-device/serialplasma.h emulator/src/src/hardware/serialport/
|
||
```
|
||
plus the four stock edits (copy the `rio` pattern exactly):
|
||
- `src/src/hardware/serialport/Makefile.am`: append
|
||
`serialplasma.cpp serialplasma.h` to `libserial_a_SOURCES`.
|
||
- `src/include/serialport.h`: add `SERIAL_TYPE_PLASMA` right after
|
||
`SERIAL_TYPE_RIO` (unguarded).
|
||
- `src/src/hardware/serialport/serialport.cpp`: `#include "serialplasma.h"`;
|
||
add the `type=="plasma"` case to BOTH dispatch switches and `"plasma"` to
|
||
`serialTypes[]`.
|
||
- `src/src/dosbox.cpp`: add `"plasma"` to the `serials[]` allowed-values list.
|
||
(The plasma WINDOW is drawn by `vpxlog.cpp` via the `PLASMA_GetFrame` extern
|
||
— already in this dir's `vpxlog.cpp`, so it lands with step 1.)
|
||
3e. The Steam ethernet backend (`[ne2000] backend=steam` — internet play; see
|
||
`../STEAM-NOTES.md` and `../steam/SESSION-CONTRACT.md`). These are `misc/`
|
||
files, not `hardware/`: they sit beside the pcap backend they are modelled
|
||
on.
|
||
```
|
||
cp emulator/vpx-device/steamlink.cpp emulator/src/src/misc/
|
||
cp emulator/vpx-device/steamlink.h emulator/src/src/misc/
|
||
cp emulator/vpx-device/ethernet_steam.cpp emulator/src/src/misc/
|
||
cp emulator/vpx-device/ethernet_steam.h emulator/src/src/misc/
|
||
```
|
||
plus three stock edits (grep for how `ethernet_pcap` appears and copy the
|
||
pattern):
|
||
- `src/src/misc/Makefile.am`: append `steamlink.cpp ethernet_steam.cpp` to
|
||
`libmisc_a_SOURCES` (we put them after `ethernet_nothing.cpp`). The
|
||
hand-mirrored generated-Makefile caveat from step 2 does **not** apply
|
||
here: `build-mingw-sdl2` re-runs `./autogen.sh` before configuring, so
|
||
`Makefile.in`/`Makefile` are regenerated from the `.am`.
|
||
- `src/src/misc/ethernet.cpp`: `#include "ethernet_steam.h"`; add the
|
||
`backend == "steam"` case to `OpenEthernetConnection()` next to the pcap
|
||
one — `new SteamEthernetConnection`, settings from
|
||
`control->GetSection("ethernet, steam")` — inside `#ifdef WIN32` (the
|
||
transport's DLL loader is Windows-only; keep the guard or a Linux build
|
||
breaks at link). Deliberately NOT in the `auto` ladder: steam is only ever
|
||
selected explicitly, since an internet session is never how a pod boots by
|
||
default. Add the `else if (backend == "steam")` arm to the failure
|
||
`LOG_MSG` chain too, otherwise a steam backend that fails to initialize
|
||
falls out as "Unknown ethernet backend".
|
||
- `src/src/dosbox.cpp`: add `"steam"` to the `backendopts[]` allowed-values
|
||
list (without this the config parser refuses the value and silently falls
|
||
back to `auto` — same trap as the `serials[]` list in 3b), and register
|
||
the settings section next to
|
||
`"ethernet, pcap"`:
|
||
`secprop = control->AddSection_prop("ethernet, steam", &Null_Init, true);`
|
||
with string props `peers`, `lobby`, `session` (all defaulting to `env`)
|
||
and int `channel` (0). Those names and defaults are frozen in
|
||
SESSION-CONTRACT.md §2 — the fork and the gateway both parse them.
|
||
No new `.conf.tmpl` is needed: the backend is selected per session with
|
||
`-set "ne2000 backend=steam"` on the DOSBox-X command line (`pod-launch
|
||
--steam` appends it), so the existing `net_loop_rio.conf` / `net_rp_rio.conf`
|
||
are reused as-is.
|
||
**The Steamworks SDK is OPTIONAL.** `steamlink.cpp` compiles by default
|
||
against hand-declared ABI structs and binds `steam_api64.dll` at runtime, so
|
||
there is nothing to install to build and nothing to link against. To
|
||
validate those structs against the real headers, drop the SDK at
|
||
`emulator/steamworks/` (git-ignored, same pattern as `emulator/src/`) and
|
||
build with `-DVWE_STEAM_USE_SDK_HEADERS` plus the SDK's `public/` dir on the
|
||
include path — worth doing once after an SDK bump, then go back to the plain
|
||
build. A build made without the SDK still **runs**: `backend=steam` just
|
||
fails Init with a readable message when `steam_api64.dll` is missing or
|
||
Steam isn't running, exactly the way a missing Npcap fails pcap.
|
||
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.
|