emulator: in-fork virtual RIO cockpit board (serial1=rio)

Emulate the cockpit RIO board natively inside the DOSBox-X fork, driven by
an SDL game controller + host keyboard -- no external vRIO process and no
named pipe. The game<->board round trip runs in-process at the emulated
UART's own cadence, so the serial round-trip dropouts (livelock, TXMAXIDLE,
rxburst, 15s retry) can't occur.

- serialrio.{cpp,h}: RIO 9600-8N1 device state machine + protocol codec +
  input mapping, transcribed from the vRIO app (VRioDevice + Protocol +
  InputRouter/BindingProfileFormat), minus transport/pacer/UI/locks.
  B0 = gamepad (5 axes + joystick column). B1 = keyboard field: MFD banks on
  the letter rows, F-keys = Secondary/Screen, numpad = flight controls,
  LShift/LCtrl throttle slew; PAUSE/ScrollLock toggle panel<->DOS; vRIO-
  grammar bindings file via bindings:/VWE_RIO_BINDINGS.
- vpx-device/README.md: device notes + apply steps (the DOSBox-X src tree is
  git-ignored, so this dir is the tracked source of truth; the three sdlmain
  keyboard-hook edits are documented there).
- net_{loop,rp}_rio.conf + deploy templates; render-bridge/
  gauge_arena_rio_sound.conf (standalone -egg trim); pod-launch --rio
  (mutually exclusive with --pipe).

Additive: real pods keep directserial realport:COM1; vRIO-over-pipe keeps
namedpipe pipe:vrio. Validated live 2026-07-22 (pad + keyboard field +
console networking) on the dist install.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-24 10:43:41 -05:00
co-authored by Claude Opus 4.8
parent 76cfc64353
commit 73b597df35
10 changed files with 1638 additions and 1 deletions
+68
View File
@@ -0,0 +1,68 @@
# DEPLOY TEMPLATE -- rendered to net_loop_rio.conf by deploy/configure.ps1 at install.
# IN-FORK RIO TRIM: the RIO cockpit board is emulated NATIVELY inside DOSBox-X
# (serial1=rio) and driven by an SDL game controller -- no vRIO process, no
# pipe (launch: pod-launch <mode> --rio). Self-contained standalone/dev trim;
# arcade cockpits with the REAL RIO/plasma on COM ports use the non-suffixed
# confs, the vRIO-app dev rig uses the _pipe confs.
# Tokens (filled by postinstall): @@ROOT@@ = package dir (C:\games\<folder>),
# @@REALNIC@@ = this pod's NIC GUID fragment, @@MACADDR@@ = stable derived MAC.
# BattleTech, GO.BAT-style netnub loop (pod stays connected + picks up missions).
[sdl]
output=opengl
priority=highest,highest
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[ne2000]
ne2000=true
nicbase=340
# IRQ 10, NOT 3: COM2 (plasma) owns IRQ 3 in-game.
nicirq=10
macaddr=@@MACADDR@@
backend=pcap
[ethernet, pcap]
realnic=@@REALNIC@@
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
rate=44100
blocksize=1024
prebuffer=60
[joystick]
# the in-fork RIO owns the game controller (SDL); keep the DOS gameport +
# mapper auto-binding off so nothing else grabs the pad.
joysticktype=none
[serial]
# in-fork virtual RIO board on COM1 (pad:<n> and inverty are optional args)
serial1=rio
serial2=namedpipe pipe:vplasma
[autoexec]
mount c "@@ROOT@@\ALPHA_1"
mount d "@@ROOT@@\net-boot"
d:
echo === loading NE2000 packet-driver stack (pcap/bridge, port 340) ===
d:\lsl
d:\ne2000
d:\odipkt
c:
cd \rel410\bt
set VIDEOFORMAT=svga
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r f s p
echo === entering GO.BAT-style netnub loop (waits for console missions) ===
call d:\loop.bat
+69
View File
@@ -0,0 +1,69 @@
# DEPLOY TEMPLATE -- rendered to net_rp_rio.conf by deploy/configure.ps1 at install.
# IN-FORK RIO TRIM: the RIO cockpit board is emulated NATIVELY inside DOSBox-X
# (serial1=rio) and driven by an SDL game controller -- no vRIO process, no
# pipe (launch: pod-launch <mode> --rio). Self-contained standalone/dev trim;
# arcade cockpits with the REAL RIO/plasma on COM ports use the non-suffixed
# confs, the vRIO-app dev rig uses the _pipe confs.
# Tokens (filled by postinstall): @@ROOT@@ = package dir (C:\games\<folder>),
# @@REALNIC@@ = this pod's NIC GUID fragment, @@MACADDR@@ = stable derived MAC.
# Red Planet 4.10, networked pod boot (console pushes the mission egg over TCP).
# Loops netnub (loop_rp.bat) so the pod stays connected + picks up missions;
# runs until the supervisor (pod-launch) kills DOSBox. No single-shot in deploy.
[sdl]
output=opengl
priority=highest,highest
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[ne2000]
ne2000=true
nicbase=340
nicirq=10
macaddr=@@MACADDR@@
backend=pcap
[ethernet, pcap]
realnic=@@REALNIC@@
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
rate=44100
blocksize=1024
prebuffer=60
[joystick]
# the in-fork RIO owns the game controller (SDL); keep the DOS gameport +
# mapper auto-binding off so nothing else grabs the pad.
joysticktype=none
[serial]
# in-fork virtual RIO board on COM1 (pad:<n> and inverty are optional args)
serial1=rio
serial2=namedpipe pipe:vplasma
[autoexec]
mount c "@@ROOT@@\ALPHA_1"
mount d "@@ROOT@@\net-boot"
d:
echo === loading NE2000 packet-driver stack (pcap/bridge, port 340) ===
d:\lsl
d:\ne2000
d:\odipkt
c:
cd \rel410\rp
set VIDEOFORMAT=svga
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r f s p
echo === entering netnub loop (waits for console missions; supervisor kills to stop) ===
call d:\loop_rp.bat
+65
View File
@@ -0,0 +1,65 @@
# IN-FORK vRIO variant of net_loop_pipe.conf: the RIO cockpit board is emulated
# natively inside DOSBox-X (serial1=rio) and driven by an SDL game controller --
# no external vRIO process and no named pipe. Self-contained flyable cockpit.
# Everything else is identical to net_loop_pipe.conf (see it for the network /
# GO.BAT-loop rationale). The plasma readout stays on the vPLASMA pipe (optional;
# an unconnected pipe is just discarded). Real pods keep net_loop.conf
# (directserial realport:COM1); this file is dev / standalone only.
[sdl]
output=opengl
priority=highest,highest
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[ne2000]
ne2000=true
nicbase=340
# IRQ 10, NOT 3: COM2 (plasma) owns IRQ 3 in-game; see net_full.conf.
nicirq=10
backend=pcap
[ethernet, pcap]
realnic=F70BADD7A
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
rate=44100
blocksize=1024
prebuffer=60
[joystick]
# the in-fork RIO owns the game controller (SDL); keep the DOS gameport +
# mapper auto-binding off so nothing else grabs the pad.
joysticktype=none
[serial]
# in-fork virtual RIO board on COM1 (pad:<n> and inverty are optional args)
serial1=rio
serial2=namedpipe pipe:vplasma
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
mount d "C:\VWE\TeslaRel410\emulator\net-boot"
d:
echo === loading NE2000 packet-driver stack (pcap/bridge, port 340) ===
d:\lsl
d:\ne2000
d:\odipkt
c:
cd \rel410\bt
set VIDEOFORMAT=svga
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r f s p
echo === entering GO.BAT-style netnub loop (waits for console missions) ===
call d:\loop.bat
+68
View File
@@ -0,0 +1,68 @@
# IN-FORK vRIO variant of net_rp_pipe.conf: the RIO cockpit board is emulated
# natively inside DOSBox-X (serial1=rio) and driven by an SDL game controller --
# no external vRIO process and no named pipe. Self-contained flyable cockpit.
# Everything else is identical to net_rp_pipe.conf (see it for the RP boot
# rationale). Plasma stays on the vPLASMA pipe (optional). Real pods keep
# net_rp.conf (directserial realport:COM1); this file is dev / standalone only.
[sdl]
output=opengl
priority=highest,highest
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[ne2000]
ne2000=true
nicbase=340
nicirq=10
backend=pcap
[ethernet, pcap]
realnic=F70BADD7A
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
rate=44100
blocksize=1024
prebuffer=60
[joystick]
# the in-fork RIO owns the game controller (SDL); keep the DOS gameport +
# mapper auto-binding off so nothing else grabs the pad.
joysticktype=none
[serial]
# in-fork virtual RIO board on COM1 (pad:<n> and inverty are optional args)
serial1=rio
serial2=namedpipe pipe:vplasma
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
mount d "C:\VWE\TeslaRel410\emulator\net-boot"
d:
echo === loading NE2000 packet-driver stack (pcap/bridge, port 340) ===
d:\lsl
d:\ne2000
d:\odipkt
c:
cd \rel410\rp
set VIDEOFORMAT=svga
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r f s p
echo === launching Red Planet via NetNub (RIO + sound; waits for console) ===
32rtm.exe -x
netnub -p -f rpl4opt > nn.log
32rtm.exe -u
echo === RP-NET-RUN-DONE ===
pause
+12 -1
View File
@@ -27,6 +27,7 @@ namespace VwePod
public bool NoBridge;
public bool NoSound;
public bool Pipe;
public bool Rio;
public bool Mipmap;
public bool NoFocus;
public bool DryRun;
@@ -78,6 +79,7 @@ namespace VwePod
case "--no-bridge": o.NoBridge = true; break;
case "--no-sound": o.NoSound = true; break;
case "--pipe": o.Pipe = true; break;
case "--rio": o.Rio = true; break;
case "--mipmap": o.Mipmap = true; break;
case "--no-focus": o.NoFocus = true; break;
case "--dry-run": o.DryRun = true; break;
@@ -105,9 +107,14 @@ namespace VwePod
?? throw new ArgumentException("dosbox-x.exe not found under root; pass --dosbox");
o.DosBoxDir = Path.GetDirectoryName(o.DosBox);
if (o.Pipe && o.Rio)
throw new ArgumentException("--pipe and --rio are mutually exclusive (both select a COM1 backend)");
// --pipe: the <mode>_pipe conf variant (serials as named pipes to
// vRIO/vPLASMA -- the dev-rig trim; real cockpits keep COM ports).
string confBase = o.Mode.ConfBase + (o.Pipe ? "_pipe" : "");
// --rio: the <mode>_rio conf variant (RIO board emulated IN-FORK,
// driven by an SDL game controller -- no vRIO process, no pipe).
string confBase = o.Mode.ConfBase + (o.Rio ? "_rio" : o.Pipe ? "_pipe" : "");
o.Conf = confArg ?? FirstExisting(Path.Combine(o.Root, confBase + ".conf"));
if (o.Conf == null)
{
@@ -190,6 +197,10 @@ namespace VwePod
--pipe use the <mode>_pipe conf: serial1/2 as named pipes to
vRIO (pipe:vrio) + vPLASMA (pipe:vplasma). Dev-rig
default trim; arcade cockpits keep real COM ports
--rio use the <mode>_rio conf: the RIO cockpit board is
emulated IN-FORK (serial1=rio) and driven by an SDL
game controller -- no vRIO process, no pipe. Self-
contained; dev/standalone only. Excludes --pipe
--mipmap VRVIEW_MIPMAP=1 (RP floor shimmer fix; non-authentic)
--no-focus skip the window layout/focus pass
--dry-run resolve paths + print the launch plan, then exit");
+1
View File
@@ -71,6 +71,7 @@ see `../CAMERA-REVIEW-NOTES.md`. Default mode: `bt`.
| `--no-bridge` | pod only, no GL render window |
| `--no-focus` | skip the renderer-topmost / DOSBox-focus pass |
| `--pipe` | use the `<mode>_pipe` conf: serials as named pipes to vRIO (`pipe:vrio`) + vPLASMA (`pipe:vplasma`), no com0com. **Dev-rig policy (2026-07-17): always `--pipe` on the dev machine** — vRIO dist ≥ `20260716-68e3d1f` speaks the pipe; the plain confs (real COM ports) are for cockpits with the physical RIO/plasma |
| `--rio` | use the `<mode>_rio` conf: the RIO cockpit board is emulated **in-fork** (`serial1=rio`) and driven by an **SDL game controller + the host keyboard** inside DOSBox-X — no vRIO process, no pipe, no serial round-trip. Self-contained cockpit: pad = stick/throttle/pedals + trigger/hat/torso; keyboard = the 72-button MFD/Secondary/Screen field + internal keypad (vRIO's default layout; **PAUSE toggles** keyboard between panel and DOS; Ctrl/Alt chords always pass through; remap via a vRIO-grammar bindings file, conf `bindings:<path>` or env `VWE_RIO_BINDINGS`). See the fork's `serialrio.cpp`. Dev/standalone only; mutually exclusive with `--pipe`. Real pods still use the plain confs (physical RIO on COM1) |
### Window placement (per-rig; normally fixed, overridable per-launch)
@@ -0,0 +1,80 @@
# IN-FORK RIO variant of gauge_arena_pipe_sound.conf: bare -egg arena1 run
# (no netnub/console) with the RIO board emulated NATIVELY inside DOSBox-X
# (serial1=rio) -- SDL game controller + host keyboard field (PAUSE toggles
# panel/DOS). No vRIO process, no pipe. Everything else identical to the
# pipe trim (see its comments for the rationale of each section).
[sdl]
output=opengl
# higher,higher not highest: HIGH_PRIORITY_CLASS starved the host desktop;
# with the retry patches a rare dropout self-recovers (see gauge_rio.conf).
priority=higher,higher
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
# match the EMU8000s' native rate (no resample) and buffer ~60ms so brief
# emulation-thread stalls (RIO retry recovery) don't audibly chop
rate=44100
blocksize=1024
prebuffer=60
[ne2000]
# bare -egg run WITH the packet stack resident (as production AUTOEXEC did);
# slirp = user-mode NAT, no LAN peers -- fine for a solo run.
ne2000=true
nicbase=340
nicirq=3
backend=slirp
[joystick]
# the in-fork RIO owns the game controller (SDL); keep the DOS gameport +
# mapper auto-binding off so nothing else grabs the pad.
joysticktype=none
[serial]
# in-fork virtual RIO board on COM1 (pad:<n> / inverty / bindings:<path>
# optional); plasma display stays on the vPLASMA pipe (optional).
serial1=rio
serial2=namedpipe pipe:vplasma
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
mount d "C:\VWE\TeslaRel410\emulator\net-boot"
d:
echo === loading NE2000 packet-driver stack (as production AUTOEXEC did) ===
d:\lsl
d:\ne2000
d:\odipkt
c:
cd \REL410\BT
set VIDEOFORMAT=svga
rem production pod card init (PARAMETR.BAT:181-186): DIAGNOSE + AWEUTIL per
rem card -- AWEUTIL /S does the EMU8000 bring-up and DRAM detect the HMI SOS
rem driver relies on; skipping it left the cards uninitialized (silent).
rem aweutil /s SKIPPED for now: it verifies the AWE32 GM ROM, which the
rem emulated cards lack (hangs in a retry loop) -- restore once the ROM is
rem dumped from a real card. diagnose /s kept (passes, sets mixer config).
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
rem arena1 city mission (TESTARN.EGG: map=arena1, time=day), RIO attached,
rem bare -egg launch (no netnub) = the real-world test-egg setup.
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r f s p
32rtm.exe -x
rem stdout -> log (this optimized build still emits DEBUG_STREAM lines).
btl4opt.exe -egg testarn.egg > c:\weaponlog.txt
32rtm.exe -u
echo ALPHA1-RUN-DONE
pause
+66
View File
@@ -24,6 +24,42 @@ version control because the DOSBox-X source tree itself
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, 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.
## Applying to a DOSBox-X source checkout
@@ -66,6 +102,36 @@ Tested against DOSBox-X `v2026.06.02`, MSYS2 mingw64.
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).
4. Build:
```
cd emulator/src
File diff suppressed because it is too large Load Diff
+177
View File
@@ -0,0 +1,177 @@
/*
* VWE fork: in-fork virtual RIO cockpit board (serial1=rio).
*
* A native DOSBox-X serial backend that *is* the cockpit RIO board: it speaks
* the device side of the RIO 9600-8N1 protocol to the game's UART, and sources
* its pilot input from an SDL game controller + the host keyboard inside the
* emulator. There is no external process and no pipe, so the game<->board
* round trip happens entirely in-process at the emulated UART's own byte
* cadence -- the same timing a real ISA UART sees on a physical pod, which
* removes the whole class of serial / pipe round-trip-latency dropouts
* (livelock, TXMAXIDLE, rxburst, 15s retry).
*
* 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), minus its transport, wire pacer,
* panel UI and thread locks -- CSerial callbacks, the SDL reads and the host
* keyboard hook all run on the single emulator thread.
*
* Keyboard field (B1): sdlmain's SDL2 event loop offers every key to
* RIO_HostKeyEvent() first. While capture is ON (default; PAUSE or
* SCROLL LOCK toggles it -- plus an optional `togglekey:<name>`) a key bound
* in the profile presses its RIO address (button 0x00-0x47 / keypad
* 0x50-0x6F) or drives an axis, and is swallowed before the DOS keyboard
* sees it; unbound keys and Ctrl/Alt/Gui chords pass through (bound modifier
* keys are exempt from the chord rule), so DOSBox-X host hotkeys keep
* working. Toggling capture OFF releases everything held and gives the whole
* keyboard back to DOS (needed at the netnub loop / DOS prompts).
*
* Bindings: the built-in default profile = vRIO's pad + button field
* (number/QWERTY rows = upper MFD bank, home/bottom rows = lower bank,
* F1-F12 = Secondary/Screen columns, arrows+Space = hat+main) plus keyboard
* FLIGHT controls (operator 2026-07-22): numpad 8/2/4/6 = stick, 7/9 =
* pedals, LShift/LCtrl = throttle slew, numpad-5 = throttle zero; the
* internal keypad is unbound (mission-review-only plumbing). A bindings file
* in vRIO's grammar (`key <name> button <addr> [toggle]` / `key <name> axis
* <axis> deflect|slew|rate|set <n>` / `pad <button> button <addr> [toggle]`
* / `padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]`, '#'
* comments, sign may be a separate token) REPLACES the default: conf arg
* `bindings:<path>`, else env VWE_RIO_BINDINGS. vRIO's .NET key names (D1,
* NumPad0, OemMinus, ...) are aliased, so a vRIO bindings.txt ports over.
*
* This is an ADDITIVE dev / standalone backend. Real pods keep the physical
* board on `serial1=directserial realport:COM1`; vRIO-over-pipe keeps
* `serial1=namedpipe pipe:vrio`. Neither is touched by this file.
*
* Conf: serial1=rio [pad:<n>] [inverty] [bindings:<path>] [togglekey:<key>]
* [rxpollus:<us>] [rxburst:<n>]
* pad:<n> game-controller index to use (default: first present)
* inverty send the joystick Y in the physical direction (up = +)
* bindings:<p> bindings file (vRIO grammar; replaces the default)
* togglekey:<k> extra capture-toggle key (SDL name, e.g. End, F24)
* rx opts directserial semantics (board->game delivery pacing)
*/
#ifndef DOSBOX_SERIALRIO_H
#define DOSBOX_SERIALRIO_H
#include "dosbox.h"
#include "serialport.h"
#include <deque>
#include <map>
#include <set>
#include <string>
#include <vector>
// ---- host-side hooks (called from sdlmain's SDL2 event loop) ----------------
// Both are cheap no-ops when no serial<n>=rio port is installed.
// RIO_HostKeyEvent returns true when the key was consumed by the cockpit panel
// (bound + captured, or the PAUSE capture toggle) -- the caller must then skip
// its normal keyboard handling for that event.
bool RIO_HostKeyEvent(int sdl_scancode, bool pressed, bool repeat, unsigned int sdl_mods);
void RIO_HostFocusLost(void);
// ---- binding profile (transcribed from vRIO BindingProfile) -----------------
// key-axis modes: DEFLECT holds the axis at value while the key is down and
// springs back; RATE ("slew" in bindings files) walks the axis by value/second
// and the position sticks; SET snaps the axis position to value on key press.
enum { RIO_KAX_DEFLECT = 0, RIO_KAX_RATE = 1, RIO_KAX_SET = 2 };
struct RioKeyButtonBind { int scancode; int addr; bool toggle; };
struct RioKeyAxisBind { int scancode; int axis; int mode; float value; };
struct RioPadButtonBind { uint16_t bit; int addr; bool toggle; };
struct RioPadAxisBind { int src; int axis; bool invert; float dz; float rate; };
class CSerialRio : public CSerial {
public:
CSerialRio(Bitu id, CommandLine* cmd);
virtual ~CSerialRio();
void updatePortConfig(uint16_t divider, uint8_t lcr);
void updateMSR();
void transmitByte(uint8_t val, bool first);
void setBreak(bool value);
void setRTSDTR(bool rts, bool dtr);
void setRTS(bool val);
void setDTR(bool val);
void handleUpperEvent(uint16_t type);
// host keyboard (forwarded by the RIO_Host* free functions)
bool hostKeyEvent(int sdl_scancode, bool pressed, bool repeat, unsigned int sdl_mods);
void hostFocusLost();
private:
// ---- board -> game byte delivery (UART RX pacing; mirrors directserial) --
bool doReceive();
std::deque<uint8_t> rxq; // bytes the board wants to send to the game
float rx_poll_ms = 1.0f;
float rx_burst_div = 1.0f;
Bitu rx_retry = 0;
Bitu rx_retry_max = 0;
Bitu rx_state = 0;
// ---- game -> board protocol RX parser --------------------------------
void feedByte(uint8_t ch);
uint8_t parse_buf[16] = {};
int parse_remaining = 0; // bytes still expected (0 = not in a packet)
int parse_count = 0; // bytes stored for the current packet
// ---- device (board) state --------------------------------------------
void onPacket(const uint8_t* body, int bodyLen, bool checksumValid);
void onControl(uint8_t b);
void applyReset(uint8_t target);
void emit(const uint8_t* p, int n); // queue board->game bytes
void emitControl(uint8_t c);
void sendEvent(const uint8_t* p, int n); // remember (for NAK resend) + emit
void pressAddress(int addr);
void releaseAddress(int addr);
void setAxis(int axis, int value);
int16_t axes[5] = {};
uint8_t lamps[72] = {};
std::vector<uint8_t> lastEventPacket; // last button/key/test event
int resends = 0;
bool analogMuted = false; // v4.2 wedge bug -- default OFF
uint8_t verMajor = 4, verMinor = 2;
bool invertY = false;
long analogRequests = 0;
bool dtr_state = false;
// ---- input profile + routing state ------------------------------------
void loadDefaultProfile();
bool loadBindingsFile(const std::string& path);
void computeBoundModMask();
void openPad();
void pollInput();
void incHold(int addr);
void decHold(int addr);
void toggleAddress(int addr);
void releaseAllKeys();
std::vector<RioKeyButtonBind> keyButtons;
std::vector<RioKeyAxisBind> keyAxes;
std::vector<RioPadButtonBind> padButtons;
std::vector<RioPadAxisBind> padAxes;
void* pad = nullptr; // SDL_GameController* (opaque here)
int pad_index = -1; // requested index; -1 = first
double next_pad_open_ms = 0.0;
double last_tick_ms = 0.0;
float rate_i[5] = {}; // rate-integrator per axis
int last_sent[5] = {};
bool last_sent_valid[5] = {};
std::map<int,int> holdCounts; // held count per RIO address
std::set<int> heldKeys; // scancodes routed to the panel
std::set<int> toggled; // latched (toggle) addresses
uint16_t prevPadButtons = 0;
bool captureKeys = true; // toggle keys flip panel <-> DOS
int toggleKeys[3] = { 0, 0, 0 }; // scancodes; defaults Pause+ScrollLock,
// slot 2 = togglekey:<name> conf arg
unsigned int boundModMask = 0; // modifier bits bound as inputs
// (exempt from chord passthrough)
bool logv = false; // VWE_RIO_LOG stderr trace
void logf(const char* fmt, ...);
};
#endif // DOSBOX_SERIALRIO_H