Files
TeslaRel410/emulator/vpx-device
CydandClaude Fable 5 e69d761fb8 BT410 5.3.77: the overrun drain loop is not the trigger either
serialnamedpipe's P_RX_BLOCKED path drains its whole backlog when the guest
hasn't read in time -- directserial's `while (doReceive());`.  That looked
like the trigger, since a named pipe's backlog is unbounded where a real
port's is capped by the line rate.  Bounded it to one byte (real UART overrun
semantics) and re-ran the conf that had faulted 3/3: FAULT 283s, FAULT 226s,
FAULT 204s.  Killed.

The measurement that explains why it was never plausible: overruns during a
run are 1-3 per report period, because vRIO sends about a byte every 1-3ms --
the backlog is shallow and the loop had nothing to teleport.  The 494/499
counts all land after the game exits.  I read the code and inferred a burst
without measuring the queue depth it operates on.

Default restored to drain-all: it is the validated directserial behaviour the
RIO's rxpollus/rxburst tuning was calibrated against, and changing it on a
dead hypothesis would risk real-cockpit timing for nothing.
VPX_RX_OVERRUN_ONE=1 opts into the bounded form.

The roadmap now carries a fault ledger of every dead hypothesis so none get
re-run.  What survives: a deterministic DPMI-host path walking a
{next,handler} chain into a node whose pointer is an unhooked IVT value,
entered under RIO interrupt load.  Naming the owning routine needs a trace of
entries to host 0x66CF, clean vs faulting -- a DPMI32VM reversing
sub-project, now decoupled from the reconstruction (vRIO down = 100%
reliable, logged conf = ~70%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:33:05 -05:00
..

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.hserial<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.hserial<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.hserial<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, F1F12 = 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.