The RIO cockpit board now runs sustained sessions with button mashing; dropouts self-heal in ~1-3s. Documented in RIO-NOTES.md: - directserial RIO_TAP=<path> (host env): logs every TX/RX byte with host-us + emu-ms timestamps, plus config/RTS/DTR/break lines. This instrument found every root cause below. - Confs: rxburst:16 restored (no-burst reply pacing made the game ACK ~14ms late -> board dropped on the first long analog stream; the old 'rxburst corrupts boot' belief was the then-unpatched PCSPAK crash). priority=highest,highest (unfocused DOSBox was demoted and blew the ACK deadline). - BTL4OPT.EXE patch lineage (in ALPHA_1/, zip left pristine): v2 full DISABLE_AND_DIE NOPs (v1 left the IRQ/RTS-retract prologue live -> first protocol error deafened the driver), v3 TXMAXIDLE 4->32 (kills the button-press ACK-window livelock), v4 analog retry limit 15s->0.5s (dev value was 0.2s; recovery now near-instant). - Board firmware patch plan recorded for the EPROM-dump route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
256 lines
14 KiB
Markdown
256 lines
14 KiB
Markdown
# RIO cockpit controls — passthrough tuning (Phase 5+)
|
||
|
||
The RIO (Remote Input/Output) is the cockpit control board on **COM1** (the
|
||
plasma display is COM2, handled later). DOSBox-X talks to a real RIO through
|
||
`serial1=directserial realport:COM1` (`game_rio.conf`). On this host the RIO is
|
||
a **Prolific USB-to-Serial adapter enumerated as COM1**.
|
||
|
||
## The analog-poll latency problem (solved)
|
||
|
||
The initial RIO *check* request tolerates latency and passed easily. But the
|
||
runtime control loop ([L4CTRL.CPP:1145](../CODE/RP/MUNGA_L4/L4CTRL.CPP)) sends
|
||
an **analog request** every cycle and, if the reply doesn't return inside its
|
||
window, logs `LBE4ControlsManager::Execute, lost RIO analog request` and
|
||
re-requests. The board itself refuses/drops comms if the ACK is late by more
|
||
than a few milliseconds — a hard real-time deadline.
|
||
|
||
Empirical result (2026-07-03): a **slower** CPU made the RIO fail **sooner**.
|
||
So the dominant latency was how fast the game processes the RIO packet and
|
||
emits the ACK, not the serial wiring. The fix:
|
||
|
||
```
|
||
[cpu]
|
||
core=dynamic ; recompiler — many x faster than the 'normal' interpreter
|
||
cputype=pentium
|
||
cycles=max ; full host speed
|
||
```
|
||
|
||
With `core=dynamic + cycles=max` the RIO **stays in sync** (the user confirmed
|
||
"the rio behaved"). Notes:
|
||
- `cycles=fixed 20000` / `150000` and `core=normal` were all too slow — the RIO
|
||
dropped comms, faster at lower speeds.
|
||
- The DOSBox-X serial path is already low-latency on **transmit**
|
||
(`directserial.cpp` `transmitByte` calls `SERIAL_sendchar` immediately).
|
||
- Prolific PL2303 has no adjustable `LatencyTimer` registry value (unlike
|
||
FTDI); an FTDI adapter set to 1 ms latency would be the lowest-latency host
|
||
option if ever needed.
|
||
|
||
## Receive-latency fork options (2026-07-03)
|
||
|
||
Even with the fast CPU, intermittent timeouts remained and the game would
|
||
drop into its **15-second analog retry fallback** — the source shows
|
||
`limit = 15.0; // 0.2` in L4CTRL.CPP, so the shipped binary re-requests very
|
||
slowly once replies stop arriving ("the polling is really slow"). Two
|
||
emulator receive latencies were fixed with new `directserial` options:
|
||
|
||
- **`rxpollus:<us>`** (50–1000, stock 1000): host-port receive poll tick.
|
||
Stock DOSBox-X discovers inbound bytes on a 1 ms tick; 100 µs discovers
|
||
them ~10× sooner. First validated result: the sim advanced and the camera
|
||
moved in the render window.
|
||
- **`rxburst:<n>`** (1–64): stock DOSBox re-serializes each received byte at
|
||
emulated wire speed (~1 ms/byte at 9600) even though the bytes already
|
||
paid their wire time on the physical cable and sit in the host buffer — a
|
||
15-byte analog reply gained ~14 ms of artificial latency, single-handedly
|
||
blowing the RIO's few-ms ACK window. `rxburst:16` delivers buffered bytes
|
||
16× faster.
|
||
|
||
`game_rio.conf` uses `realport:COM1 rxpollus:100 rxburst:16`.
|
||
|
||
## The crash-to-desktop: PCSPAK's DISABLE_AND_DIE (patched)
|
||
|
||
The recurring hard fault (`Exception 0E`, write to `0xFFFFFFFF`-ish, e.g. at
|
||
`BTL4OPT CODE+0x7D1D1` with `EAX=3`) is **deliberate**: the RIO serial packet
|
||
driver (`CODE/RP/MUNGA_L4/PCSPAK.ASM`) was shipped built with
|
||
`DIE_ON_ERROR equ 1`, which compiles a `DISABLE_AND_DIE <code>` debugging
|
||
macro at 12 error sites — it retracts the UART IRQ, EOIs the PIC, and then
|
||
"crashes loudly" by writing the error code to address `0xFFFFFFFF`.
|
||
|
||
Our crash is error **3** (`PCSPAK.ASM:1630`): a byte ≥ 0x80 found in the TX
|
||
ring body (the protocol reserves high-bit bytes for commands). It gets hit
|
||
via the ACK/NAK-interrupt → restart path — exactly what physical RIO resets
|
||
and timeout storms exercise. On clean pod serial timing this never fired; on
|
||
a USB-serial rig with resets it does.
|
||
|
||
The source's release configuration (`DIE_ON_ERROR equ 0`) makes the macro
|
||
empty and the code **recovers** (the next instruction masks the byte with
|
||
`and al,7Fh` and continues). We reproduce that intended behavior by patching
|
||
all 12 die sequences (`50 52 BA FF FF FF FF B8 xx 00 00 00 89 02` → NOPs) in
|
||
the working image's `BTL4OPT.EXE`. Original preserved as `BTL4OPT.EXE.orig`.
|
||
Error-code map (from PCSPAK.ASM): 0/1/2 rx framing states, 3 tx body >0x7F,
|
||
4/5 tx state.
|
||
|
||
**Patch v2 (2026-07-03): the first patch was incomplete and wedged the
|
||
driver.** The macro's *prologue* also executes before the crash write: it
|
||
retracts UART `MCR` bits `IRQ+RTS` (`RETRACT_MCR`) and EOIs the master PIC —
|
||
23 bytes (`66 8B 15 <addr> 66 83 C2 04 EC 24 F5 EE 66 83 EA 04 B0 20 E6 20`)
|
||
that a release build would not compile at all. With only the crash write
|
||
NOPed, the first PCSPAK protocol error silently killed the UART IRQ and
|
||
dropped RTS: the game went deaf/mute while the RIO kept retransmitting into
|
||
the void (captured on the wire tap at t≈112s — board streaming
|
||
`89 00 09 FF FF FF FF FE` forever, game emitting one byte per 15s retry).
|
||
Fixed by NOPing the full 37-byte macro expansion at all 12 sites; the state
|
||
before this extension is preserved as `BTL4OPT.EXE.nop14`. After patch v2 the
|
||
driver recovers from every error, exactly like `DIE_ON_ERROR equ 0`.
|
||
|
||
## Serial wire tap (`RIO_TAP`, 2026-07-03)
|
||
|
||
The fork's `directserial.cpp` logs every serial byte when the host env
|
||
`RIO_TAP=<path>` is set: `<host-us relative> <emu-ms> T|R <hex>` plus `#`
|
||
lines for port config, RTS/DTR, and break changes. This is the instrument
|
||
that proved all of the above. Findings from tapped runs (9600 8N1):
|
||
|
||
- Analog request = `82 02 FF FF FF FF FE`; ACK = `FC`; packets are
|
||
`<cmd≥0x80> … FE`-framed. The shipped 15s retry shows up as exactly
|
||
15.02s between request storms.
|
||
- **Without `rxburst`**: reply bytes reach the game at ~1ms/byte (wire-speed
|
||
re-serialization), so the first long analog stream makes the game's ACK
|
||
~14ms late → board drops comms permanently (captured at t≈45.6s). With
|
||
`rxburst:16` the same phase streams for minutes. The earlier "rxburst
|
||
corrupts the boot handshake" belief was wrong — that corruption was the
|
||
unpatched DISABLE_AND_DIE error-3 crash. Both confs now use
|
||
`rxpollus:100 rxburst:16`.
|
||
- **Focus loss caused the self-recovering dropouts.** A 4-minute run while
|
||
the user multitasked showed ~25 self-recovering board-quiet windows
|
||
(1.5–15s); an identical hands-off run showed **zero** gaps after boot
|
||
(195s clean). DOSBox-X's default `[sdl] priority = higher,normal` demotes
|
||
the process to NORMAL class when unfocused. Both gauge confs now set
|
||
`priority=highest,highest` (HIGH_PRIORITY_CLASS; user saw one dropout at
|
||
`higher,higher`). (The ~71–86ms "turnaround tail" in the tap analysis was
|
||
benign — bursts that need no ACK.)
|
||
|
||
## Button-press livelock (2026-07-03, open)
|
||
|
||
At `highest,highest` the link ran clean for 120s, then the user pressed a
|
||
RIO button and the link died permanently (until board reset). Tap decode of
|
||
the packet protocol (PCSPAK.ASM equates: `FC`=ACK, `FD`=NAK, `FE`=RESTART,
|
||
`FF`=IDLE, cmd bytes 0x80-0xFB, packet = cmd + body + 7-bit checksum):
|
||
|
||
- Steady state = game `82 02` analog request → board `87 <12 analog> 07`
|
||
reply → game `FC` — at ~10Hz, clean for minutes.
|
||
- At t=123.65s the board truncated its in-flight analog reply exactly when
|
||
the button was pressed, then went ~3s silent, then began retransmitting
|
||
the button event `88 03 0B` every ~10ms forever. The game ACKs (`FC`)
|
||
every copy AND storms its own `82 02` retransmits (RESTART-paced:
|
||
packet + `FF`×4 idle + `FE` restart, back-to-back). Neither side ever
|
||
accepts: **livelock**.
|
||
- Mechanism (PCSPAK.ASM `txBodyState`/`txWaitState`, and the board firmware
|
||
is the same protocol): an ACK is honored only during the ~4ms
|
||
post-checksum idle window (`TXMAXIDLE=4` idle chars); an ACK arriving
|
||
during the peer's transmit phase = `TX_EARLY_ERR` → restart+retransmit.
|
||
With both sides' timing driven by each other's bytes, the phases lock and
|
||
the ACKs land in the wrong window forever. Retry budget `TXMAXRESET=3`/
|
||
`TXMAXERROR=3` per packet, but fresh packets keep the storm alive.
|
||
- Why the pod never saw this: ISA-UART ACK latency is sub-ms, so the ACK
|
||
always lands at the START of the 4ms window. Our Prolific USB-serial adds
|
||
1-10ms each way (invisible to the tap, which stamps host-side I/O), so a
|
||
collision can push the exchange into the locked phase.
|
||
|
||
Tried, in order:
|
||
- Prolific FIFO disabled in Device Manager: **no effect** — button mash
|
||
still livelocked the link at t≈110s.
|
||
- **TXMAXIDLE binary patch (v3, 2026-07-03): FIXED the livelock.** The two
|
||
`mov txIdleCount[esi],TXMAXIDLE` reloads (`C6 46 0B 04`, txIdleCount =
|
||
struct offset 0x0B) at exe offsets 0x7dff0/0x7e093 changed `04`→`20`
|
||
(4 → 32 idle chars ≈ 33ms). This widens the game's ACK-accept window 8x
|
||
and calms its retransmit storm so its own ACKs to the board transmit
|
||
promptly. Backup: `BTL4OPT.EXE.pre_idle`. Result of a 5-minute
|
||
button-mash run: three board-quiet gaps (1.9s/9.3s/14.6s), **all
|
||
self-recovered**; clean steady state otherwise. Previously one button
|
||
press = permanent livelock until manual board reset.
|
||
|
||
**Recovery-time patch (v4, 2026-07-04).** Collision recovery rode L4CTRL's
|
||
analog re-request fallback: `limit = 15.0; // 0.2` (L4CTRL.CPP:1132 — the
|
||
dev value was 0.2s; 15s looks like a forgotten debug slowdown). In the
|
||
binary this is two `mov dword [ebp-24h], 41700000h` (15.0f) immediates —
|
||
one per branch of the `RunningMission` if — at file offsets
|
||
0x763fa/0x76403, anchored by the `fld/fcomp` of delta_t and the
|
||
"lost RIO analog request" string push at 0x76441 directly after. Both
|
||
immediates changed to `3F000000h` (0.5f). Backup: `BTL4OPT.EXE.pre_limit`.
|
||
**Validated 2026-07-04**: 5-minute two-handed button-mash run — forced
|
||
dropouts now last 1.2s/2.9s (vs 9.3s/14.6s under the 15s limit), max
|
||
turnaround 199ms (vs 11.8s), and most collisions no longer register as
|
||
>1s gaps at all. User: "after one more button is pressed and the .5
|
||
seconds elapses it picks right back up."
|
||
|
||
BTL4OPT.EXE patch lineage: `.orig` (pristine) → `.nop14` (v1: crash writes
|
||
NOPed) → `.pre_idle` (v2: full 37-byte DISABLE_AND_DIE NOPs) →
|
||
`.pre_limit` (v3: TXMAXIDLE 4→32) → current (v4: retry limit 15s→0.5s).
|
||
|
||
## Board firmware patch plan (user wants to pursue)
|
||
|
||
Goal: fix the livelock's other half at the root — the board rejects ACKs
|
||
arriving outside its ~4ms post-checksum window (its firmware mirrors the
|
||
PCSPAK state machine, including the early-ACK=error behavior). The
|
||
game-side patches make failures self-healing; a board patch would make
|
||
collisions harmless entirely.
|
||
|
||
1. **Identify**: open the RIO board, photograph it, note the MCU part
|
||
number and the ROM chip. Era suggests an 8051-family MCU (80C31/32 with
|
||
external 27C256/27C512 EPROM) or 68HC11/Z80-class part. The observed
|
||
wire behavior (9600 8N1, ~10ms retransmit cadence = 3-byte packet +
|
||
~4 idle chars + restart) confirms a TXMAXIDLE≈4-equivalent constant in
|
||
firmware.
|
||
2. **Dump**: any TL866-class programmer reads the EPROM to a .bin. If the
|
||
code is in MCU-internal ROM instead, dumping gets harder (part-specific
|
||
tricks) — check the board first.
|
||
3. **Disassemble** (Claude's job): locate the protocol state machine by
|
||
searching for 0xFC/0xFD/0xFE/0xFF handling, the idle-counter reload
|
||
value 4, and checksum `AND 7Fh` operations — we know the protocol
|
||
byte-for-byte from the game side, so this is pattern matching.
|
||
4. **Patch** (preference order): (a) accept ACK in any TX state — delete
|
||
the early-ACK=error path; (b) widen the idle window 4→32 like the
|
||
game-side v3 patch; (c) raise the retry budget.
|
||
5. **Burn to a NEW EPROM**, socket it, label and store the original chip
|
||
untouched (preservation first).
|
||
6. **Validate** with the RIO_TAP button-mash protocol; compare dropout
|
||
counts against the 2026-07-03/04 baseline captures in the session
|
||
scratchpad (riotap_*.txt).
|
||
|
||
No firmware source or image exists in the archive (searched sda4 + CODE
|
||
for *.HEX/*.A51/*.S19/*.ROM and for the protocol constant names — only
|
||
PCSPAK.ASM, the game side, matches).
|
||
|
||
## Crash-on-advance fixed: arena terrain shadows
|
||
|
||
With the RIO in sync the sim advances and the game crashed dereferencing the
|
||
**mech terrain shadow** renderable: `*_TSHD.BGF` live only in
|
||
`VIDEO/GEO/ARENA/` + `…/POLAR/`, which the object search path misses, so
|
||
`thr_tshd.bgf` failed to load and left a null renderable that the moving sim
|
||
eventually touched. Fix: the 11 `*_TSHD.BGF` files are copied from `ARENA/`
|
||
into `VIDEO/GEO/` in the working image (reversible, one file each). After
|
||
the fix the game runs sustained (500+ frames, no crash).
|
||
|
||
## Next wall: mission content (a game-data issue, not protocol/RIO)
|
||
|
||
With the RIO feeding real input, the simulation advances — and then the game
|
||
faults:
|
||
|
||
```
|
||
32loader runtime error: Unhandled exception
|
||
Exception 0E at 00FF:0040223B
|
||
Module 'BTL4OPT.EXE' section 'CODE' offset 0000123B
|
||
[..] 8B 4D 0C (mov ecx,[ebp+0C]) 8B 11 (mov edx,[ecx]) ECX=2
|
||
The instruction referenced illegal address 00000002
|
||
```
|
||
|
||
A near-null pointer (`2`) dereference: a function got `2` where an object
|
||
pointer was expected. This is downstream of the failed content load logged
|
||
just before it:
|
||
|
||
```
|
||
Entity -1:63 class:42 couldn't figure out how to MakeEntityRenderables
|
||
L4VIDEO.cpp couldn't load object thr_tshd.bgf
|
||
```
|
||
|
||
`thr_tshd.bgf` exists in `VIDEO/GEO/ARENA/` and `VIDEO/GEO/POLAR/`, but not the
|
||
top `VIDEO/GEO/`. The game builds its object search path from **`objectpath`
|
||
entries in the mission notation file** ([L4VIDEO.CPP:1852](../CODE/RP/MUNGA_L4/L4VIDEO.CPP)),
|
||
i.e. from the `.egg`. The shipped `test.egg` is only mission parameters and
|
||
does not set up the arena object path, so arena-specific objects aren't found;
|
||
the entity gets a broken renderable and the sim eventually dereferences it.
|
||
|
||
**To progress past this needs a real mission/arena `.egg`** (or an object path
|
||
manually pointed at the selected arena's `VIDEO/GEO/<ARENA>` subdir). That is
|
||
content/mission configuration, separate from the VPX protocol, the renderer,
|
||
and the RIO — all of which now work.
|