Files
TeslaRel410/emulator/RIO-NOTES.md
T
CydandClaude Opus 4.8 2bb2ff7302 Weapons fire; hat-glance + MFD fixes; windowless bridge
vpx-device/vpxlog.cpp -- WEAPONS FIRE. The game's fire gate is
targetReticle.targetEntity (mech+0x388), filled every frame by a Division-board
reticle pick (dpl_RapidSectPixel) our HLE never answered, so every shot
misfired. The device now casts the camera centre ray against the live scene
(Moller-Trumbore) and returns the full hit -- instance + DCS + geogroup +
geometry -- piggybacked on the draw_scene reply; terrain is a valid target so
you can fire and miss. Sending geogroup=0 was hanging the game (GetAppSpecific
on a null); returning the real geogroup fixed it. Pick is default-on with a
single VPX_NO_PICK escape hatch. Also swaps the upper-left/right MFD explode
windows (screen location only -- decode untouched, real cause TBD on a pod).

dpl3-revive/patha/vrview.py -- HAT-GLANCE fix at the source. One cockpit DCS
(0xa2c) flushes a rank-2 rotation (shifted body -> wrong read window) that
collapsed the camera chain to rank 2 and smeared the head glance onto the wrong
axis (left/right hat read as pitch). chain_matrix(fix_degenerate) treats a
degenerate chain DCS as identity: preserves the look exactly and keeps stick-Y
torso pitch working (an earlier bridge-level yaw/pitch swap broke stick-Y and
was reverted). User-verified live: hat all 4 dirs + stick-Y both correct.

render-bridge/launch_pod.ps1 -- launch the bridge with pyw (windowless) so no
console window parks over the cockpit displays (Start-Process ignores
-WindowStyle once stdout/stderr are redirected).

render-bridge/live_bridge.py -- surface bridge render errors, flush the status
line; reverted glance-swap note.

Also vendored HUD (dpl2d) renderer work in vrboard/vrview_gl and RENDERER-COLLAB
/ RIO notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 09:53:14 -05:00

351 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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>`** (501000, 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>`** (164): 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.515s); 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 ~7186ms "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).
**UPDATE 2026-07-04: steps 1-3 DONE — ROOT CAUSE FOUND.** Full writeup:
`emulator/rio-firmware/RIOv4_2-ANALYSIS.md` (disassembly via
`disasm_6811.py` -> `RIOv4_2.disasm.asm`). The wedge = an orphaned
"reply-in-progress" latch `$2521`: it gates every analog request (`$D758
TST $2521; BNE drop`), is set when a reply is generated (`$D84C`), but the
retry-exhausted give-up path (`$D9DD JMP $DA2F`) tears down reply state
WITHOUT clearing it (and the success teardown `$DA00` clears it only
conditionally on `$2522`). Leaked -> all analog requests dropped -> mute;
RX/event path stays alive; only a game-start host reset command (`$C686`
clears `$2521`) revives it = the observed button-resync ritual. Proposed
fix (untested, no spare chip): clear `$2521` on every teardown (redirect
`$D9DD` to a free-space stub at `$DFF0` clearing `$2521/$2522`; make
`$DA00`'s clear unconditional). Byte patches + validation plan in the doc.
Original first-look (MCU=Toshiba TMP68HC11, EPROM=AM27C512, code
$C000-$FFFF, reset $C000, SCI vector $D630) in
`emulator/rio-firmware/README.md` -
reset vector $C000, **SCI serial interrupt vector → $D630** (the protocol
state machine's entry point for step 3's disassembly). Also new evidence
for the wedge shape: a button press revives a mute board (event path
alive, reply path dead), so step 4 should look for a reply-path-only dead
state reachable from the SCI handler.
## 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.
## Board init handshake — what a virtual RIO must implement (2026-07-06)
The user's **vRIO** (virtual RIO on COM1) passed analog polling but the game
still printed its init complaint. Root cause decoded from
[L4RIO.CPP:901-975](../CODE/RP/MUNGA_L4/L4RIO.CPP) +
[L4RIO.HPP:138-152](../CODE/RP/MUNGA_L4/L4RIO.HPP) and verified byte-for-byte
against a real-board tap (riotap_arena.txt, fw v4.2):
Command map (packet = cmd + body + 7-bit checksum `sum&0x7F`; FC=ACK FD=NAK
FE=RESTART FF=IDLE): game→board `80` CheckRequest, `81` VersionRequest,
`82` AnalogRequest, `83` ResetRequest, `84` LampRequest; board→game
`85` CheckReply(status,unit; status: 0=BoardOk 1=BoardMissing 2=BoardBad
3=LampBad 4/5/6=Restart/Abandon/FullBuffer counts), `86` VersionReply(maj,min),
`87` AnalogReply(10 data bytes), `88`/`89` Button press/release,
`8A`/`8B` Key press/release, `8C` TestModeChange(1=enter 0=exit).
Init sequence (game side bombs with "RIO never came back from check request /
test mode / version request!" if any step times out at 5s):
1. game pulses DTR (assert 0.1s, drop, wait 1s) = hardware board reset
2. game `80 00` → board `FC`
3. **board `8C 01 0D` (TestModeChange ENTER)** ← the packet vRIO was missing;
game waits ≤5s for it, sets TestModeActive
4. board self-test (~0.6s on the real board), streaming `85 <status> <unit>
<ck>` per subsystem (these are swallowed during test mode, not host events;
the bench board reports status=1 BoardMissing for units 08,10,11,18,19,1A,30)
5. **board `8C 00 0C` (TestModeChange EXIT)** — game waits ≤5s
6. game `81 01` → board `FC`, then **`86 04 02 0C` (VersionReply v4.2)**;
game loops until MajorRevision != 0xFF
7. normal ops: `82 02` analog polls / button+key events. NOTE: the game sends
requests ONLY when `operational && !TestModeActive` — a board that enters
test mode and never exits mutes the game permanently.
Every board packet is retransmitted until the game ACKs `FC` (within the
TXMAXIDLE window; ~33ms with the patched EXE). Byte pacing at the 9600-baud
wire rate (~1ms/byte) matters: vRIO blasting bytes back-to-back caused NAK/
restart churn during init until the user added pacing.
## RIO button unit map (L4CTRL.HPP enum; wire unit byte == buttonGroup index)
Press = `88 <unit> <ck>`, release = `89 <unit> <ck>` (ck = sum&0x7F). The game
uses the unit byte DIRECTLY as its button index (L4CTRL.CPP RIO::ButtonPressedEvent).
| unit | button |
|-----------|--------------------------------------------------------|
| 0x00-0x07 | AuxLowerRight 8..1 (panel) |
| 0x08-0x0F | AuxLowerLeft 8..1 (panel) |
| 0x10-0x15 | Secondary 1..6 |
| 0x18-0x1D | Secondary 7..12 |
| 0x20-0x27 | AuxUpperCenter 8..1 |
| 0x28-0x2F | AuxUpperLeft 8..1 |
| 0x30-0x37 | AuxUpperRight 8..1 |
| 0x39-0x3B | IcomHeadPluggedIn / IcomSensor / IcomMikePluggedIn |
| 0x3C | Door |
| 0x3D | Panic |
| 0x3F | Throttle1 |
| 0x40 | **JoystickTrigger** (= FIRE) |
| 0x41/0x42 | JoystickHatDown / HatUp |
| 0x43/0x44 | **JoystickHatRight / HatLeft** (= TORSO TWIST R/L) |
| 0x45 | JoystickPinky (= torso/look DOWN per TORSO.CTL) |
| 0x46 | JoystickThumbLow (= TORSO CENTER) |
| 0x47 | JoystickThumbHigh (= torso/look UP) |
CORRECTION (user, operated the real pods): in the PRODUCTION cockpit torso
twist is AXIS-driven; TORSO.CTL's button mappings (TorsoLeft=HatLeft etc.,
limits per TORSO.SUB: +-80deg horiz @20deg/s, +10/-30 vert @40deg/s) were the
DEV-rig fallback for setups without the full cockpit. On the real pod the HAT
gives momentary GLANCE views (left/right/rear). Keypad keys are a SEPARATE
event type (8A KeyPressed: unit,key). Verified live 2026-07-06: vRIO hat
glances DO work in-game (units 0x41-0x44 arriving), so the 88/89 button path
is validated end-to-end -- weapons-not-firing is NOT a unit-code problem;
suspect weapon-group arming/mission state (see MECHWEAP.CTL) instead.