Cockpit camera complete + namedpipe transport + 832x512 native res

Render bridge (live_bridge.py, vrview_gl.py):
- Hat glances render (left/right frame the canopy, hat-down = clean rear).
  Root bug was a stale _ckpt['fix'] key -> KeyError every glance frame ->
  render aborted (screen froze on hold, snapped back on release). The glance
  itself is the authentic eye-DCS action-0x1f reflush fp_cam already applies.
- Torso twist turret-true (root-axis yaw, zero parallax); lasers follow torso.
- Rear glance drops the canopy shell for a clean view (original-hardware
  behavior); mission-fade shroud 9fd hidden.
- Wireframe debug mode (VRVIEW_WIREFRAME / 'w' key), scene-pass scoped.
- Renderer output = 832x512, the dPL3 board's native framebuffer res.

DOSBox-X fork: namedpipe serial backend (serialnamedpipe.cpp/.h) for
vRIO/vPLASMA, replacing com0com; overlapped non-blocking I/O; typed frames
(0x00 data / 0x01 DTR+RTS). Tracked copies + apply steps in vpx-device.

Docs: COCKPIT-CAGE-NOTES (full glance/twist/rear forensics), XP-PORT-PLAN
(back-burnered), RIO-NOTES (namedpipe + keypad), pipe/egg conf variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-14 10:08:15 -05:00
co-authored by Claude Opus 4.8
parent 873882b4be
commit aadda7f9a5
16 changed files with 1801 additions and 16 deletions
+9
View File
@@ -218,6 +218,12 @@ void main() {
# VRVIEW_HUDSCALE once the right size is found vs period footage.
HUDSCALE = [float(os.environ.get('VRVIEW_HUDSCALE', '1.0'))]
# Scene-pass wireframe (debug): VRVIEW_WIREFRAME=1 or the bridge's 'w' key.
# Scoped to the 3D instance/particle passes only -- global GL wireframe would
# also line-ify the fullscreen present blit and blank the window. (The period
# equivalent, dpl wireframe / ALT-W, is dead code in shipped BTL4OPT.)
WIREFRAME = [os.environ.get('VRVIEW_WIREFRAME', '0') != '0']
HUD_FS = """
#version 330
uniform vec3 u_col;
@@ -499,6 +505,8 @@ class GLRenderer(vrview.Renderer):
FIX = np.eye(4)
FIX[:3, :3] = [[0, 0, -1], [0, 1, 0], [1, 0, 0]]
ctx.wireframe = bool(WIREFRAME[0]) # scene pass only (see WIREFRAME)
for inst in c.instances:
if not vrview.inst_visible(board.nodes, inst):
continue
@@ -536,6 +544,7 @@ class GLRenderer(vrview.Renderer):
self._draw_particles(board, V, vp, self.skip / 60.0)
self._draw_psfx(board, V)
ctx.wireframe = False # HUD + present stay solid
self._draw_hud2d_gl(board, vp)
# present: FBO -> window with the DAC gamma
+5 -3
View File
@@ -213,10 +213,12 @@ as StopMission on `:1501`, after which the supervisor exits with DOSBox.
## OPEN items
1. Renderer packaging -- freeze (PyInstaller, one exe) vs embeddable Python
(with David).
2. Per-rig display RECTS (already isolated in `pod_deploy.ps1`; deployment
1. Per-rig display RECTS (already isolated in `pod_deploy.ps1`; deployment
variant of the window layout).
(Renderer packaging was OPEN here; RESOLVED 2026-07-10 -- frozen renderer.exe,
see the archive section. An XP/Win11 fleet-floor port is planned but
BACK-BURNERED: `XP-PORT-PLAN.md`.)
Related: `NET-NOTES.md`, `LAUNCH.md`, `ENV-VARS.md`, memory
`pod-multiplayer-mesh`, `pod-deployment`.
+13 -1
View File
@@ -24,12 +24,24 @@ view, then parks DOSBox at 10,10 with focus (keeps the RIO fed).
```
Layout (edit the RECTS at the top of `pod_deploy.ps1` for this rig's outputs):
main/bridge `0,0` 800x600 · radar/win0 `800,0` · 3-color MFD/win4 `1440,0` ·
main/bridge `0,0` 832x512 · radar/win0 `800,0` · 3-color MFD/win4 `1440,0` ·
2-color MFD/win3 `2080,0` (heads 640x480). Re-assert DOSBox focus anytime:
`& .\emulator\render-bridge\focus_dosbox.ps1`. TODO: borderless-fullscreen main
(BRIDGE_BORDERLESS), portrait-radar rotation in cockpit mode if this pod's radar
CRT is sideways.
**Main render = 832x512 (2026-07-11), the dPL3 board's NATIVE framebuffer
resolution** — the original Division card rendered 832x512 and scanned it out
itself (NTSC/SVGA off the card). Three independent confirmations: the game's
view flush carries an 832x512 screen (`dpl3-revive/spec/VELOCIRENDER_PROTOCOL.md`),
BT's death-camera stream has floats [10..11] = 832x512
(`render-bridge/DEATH-SEQUENCE-NOTES.md`), and pick/fire events arrive in
832x512 board-pixel coordinates (`dpl3-revive/patha/vrboard.py`). We render 1:1
at that size (pod_deploy RECTS + pod-launch `--bridge-size` default) until
everything is wired and verified; presenting on an 800x600 output (letterbox /
scale — the old 800x600 window was a suspected source of rendering issues) is
an OPEN decision after that.
## Networked (console-driven) — the main ones
```powershell
+54
View File
@@ -5,6 +5,31 @@ 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**.
## Named-pipe transport for vRIO/vPLASMA (2026-07-12, replaces com0com)
The fork now has a `namedpipe` serial backend so the VIRTUAL peripherals need
no com0com pair (no kernel driver, no signing pain, sub-ms latency):
```
serial1=namedpipe pipe:vrio rxpollus:100 rxburst:16 # vRIO
serial2=namedpipe pipe:vplasma # plasma readout
```
DOSBox is the pipe **client** (500ms background retry; unconnected pipe =
unplugged cable: modem-in lines low, TX discarded). vRIO/vPLASMA are the
**servers** on `\\.\pipe\vrio` / `\\.\pipe\vplasma`. One duplex byte-mode
pipe, typed frames both directions: `0x00 <len:u8> <bytes>` = serial data,
`0x01 <lines:u8>` = sender's own DTR(bit0)/RTS(bit1), receiver applies the
null-modem cross (their DTR -> our DSR+CD, their RTS -> our CTS); each side
sends one `0x01` on connect (vRIO sends 0x03 = board present); unknown type =
drop the connection. Contract pinned with the vRIO session 2026-07-12; source
of truth = the `serialnamedpipe.h` header comment (tracked copy in
`emulator/vpx-device/`, apply steps in its README). Smoke-tested end-to-end
(connect, line handshake, DTR/RTS edges on DOS COM open, data both ways,
clean disconnect). Real pods keep `directserial realport:COM1` — the real
board is untouched by this. vRIO side pending: NamedPipeLink + framer (other
session builds it; DTR edge feeds the existing HostHandshake event).
## The analog-poll latency problem (solved)
The initial RIO *check* request tolerates latency and passed easily. But the
@@ -348,3 +373,32 @@ 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.
## RIO keypad usage in the games (2026-07-11)
Keypad = the separate `8A`/`8B` key press/release events, body = (unit, key):
unit selects the keyboard -- 0 = KeyboardPilot (cockpit internal keypad),
1 = KeyboardExternal (operator keypad), 2 = KeyboardPC -- and the engine
converts key to ASCII (0-9 -> '0'-'9', >=10 -> 'A'...) before feeding
`keyboardGroup[unit]` plus a typed-string matcher
([L4CTRL.CPP:2329](../CODE/RP/MUNGA_L4/L4CTRL.CPP)).
- **RP: the keypad is dead plumbing.** Nothing subscribes to KeyboardPilot or
KeyboardExternal; the only string registration in the shipped source --
`stringManager.Add("A90", KeyboardPilot, ...)`, type A-9-0 to begin
calibration -- is COMMENTED OUT ([L4CTRL.CPP:850](../CODE/RP/MUNGA_L4/L4CTRL.CPP)).
- **BT: pilot keypad works ONLY in mission review.** Ground truth from the
shipped BTL4OPT binary (C:\VWE\BT411 reconstruction, btl4mppr.cpp):
`MechRIOMapper` subscribes KeyboardPilot -> Mech `KeypressMessageID` under
mode mask `0x200000`, and that mode bit is toggled by the Mech's
mission-review flag -- so keypad presses reach the game only while
reviewing (review navigation; the Mech-side key handler is not yet
recovered, so per-key meanings are still open). In normal cockpit missions
BOTH games ignore the keypad entirely.
- **KeyboardExternal (operator keypad) has no subscriber in either game.**
- Related panel-button findings from the same dig: BT's eight AuxUpperRight
buttons (`0x30-0x37`) are the **"hotbox" pilot-target-select group**, each
with a linked lamp, and the Panic button's (`0x3D`) lamp doubles as the
review-mode/config lamp under `0x200000`.
- **vRIO implication:** keypad emulation is not needed for normal missions;
it only matters if/when mission-review pods are emulated.
+202
View File
@@ -0,0 +1,202 @@
# One archive, Windows XP -> Windows 11 (fleet-floor plan)
> **STATUS 2026-07-11: BACK-BURNERED (operator call).** This is the plan of
> record for when the port is picked up, not active work. Active effort stays
> on the modern archive's remaining details (DEPLOYMENT-PLAN OPEN items +
> the Phase 0 fleet answers below, which are cheap to gather meanwhile).
Fleet reality (operator, 2026-07-11): the surviving ~140 cockpits are
**overwhelmingly Windows XP**, a **few Windows 2000**, **4 original DOS PCs**
(Japan), and **Win10 migrations are only now starting**. The cockpit PCs are
**more modern and powerful than "runs XP" suggests** -- the OS floor is a
software constraint, not a hardware one. So the deployable archive must
install and run on **XP SP3 (32-bit) through Windows 11 (x64)** from the
**same zip** -- not an XP fork.
Current state (see `DEPLOYMENT-PLAN.md`): the packaged pod is INSTALL-VERIFIED
on modern Windows, but every shipped binary is x64 and the floor is effectively
Win10: pod-launch is .NET 8, renderer.exe is frozen Python 3.13 (moderngl,
GL 3.3), configure.ps1 needs the Win8+ NetAdapter cmdlets, and the capture
stack policy assumes Npcap (Win8.1+). None of it loads on XP.
## Scope decisions (settled unless marked OPEN)
- **Floor = XP SP3, 32-bit.** Every artifact *inside the zip* becomes 32-bit
x86, PE subsystem 5.1, XP-safe API surface. 32-bit runs unchanged on
Win10/11 under WoW64 -- that is the whole dual-target trick.
- **The 4 DOS cockpits are out of scope** -- they run BT/RP 4.10 natively on
the original hardware; the emulation archive is irrelevant there.
- **Windows 2000 is OUT OF SCOPE (operator call, 2026-07-11).** The few 2000
units either get lifted to XP SP3 or wait for their Win10 migration; the
archive never targets 2000.
- **Dev-side tooling stays modern.** `package.ps1`, `freeze.ps1`, the build
rigs -- none of it ships; only pod-side artifacts change.
- **The TeslaConsole/TeslaLauncher contract is unchanged** (zip + one folder +
`postinstall.bat` elevated; console registers/invokes/kills). XP has no UAC,
so "elevated" degrades gracefully to the admin account TeslaLauncher already
runs under on the fleet.
## The five blockers -> five workstreams
| # | Component | Today | XP->11 plan |
|---|---|---|---|
| A | dosbox-x.exe | x64 MinGW64 static (v2026.06.02 + our patches) | 32-bit XP-compatible build of the same patched tree |
| B | pod-launch | .NET 8 win-x64 | native Win32 C++ supervisor, 32-bit, /SUBSYSTEM 5.01 |
| C | renderer.exe | PyInstaller Py3.13 + moderngl (GL 3.3) | native port of Dave's vrview (fixed-function GL) -- the crux |
| D | configure.ps1 / postinstall | PowerShell 3+ cmdlets | fold into the supervisor exe (`--configure`); batch stays batch |
| E | capture stack | Npcap (manual, Win8.1+) | WinPcap 4.1.3 on XP / Npcap on Win10+, same wpcap.dll API |
### A. DOSBox-X: 32-bit XP-compatible build of our patched tree
Upstream DOSBox-X has continuously shipped "Windows XP compatible" 32-bit
MinGW builds; our tree is `emulator/src` = upstream **v2026.06.02** plus our
self-contained patches (VPX device, pcap RX filter, `serial3=file`, etc.).
1. Verify v2026.06.02 still has the XP build recipe (their MinGW "lowend"
configs / release channel). If upstream dropped it, find the last
XP-supporting tag and forward-port our patches (they are small and
peripheral -- device layer, pcap filter, serial -- not core-emulation
surgery).
2. Reproduce their XP toolchain (pinned 32-bit MinGW, msvcrt-linked -- NOT
today's MSYS2 MINGW64/UCRT) and build **static**, same as the current
packaging rule: imports = Windows system DLLs + delay-load `wpcap.dll` only.
3. SDL choice follows upstream's XP builds (their in-tree SDL1 is the
conservative default; SDL2-for-XP only if their recipe blesses it).
`output=surface`/`ddraw` on XP -- do not assume a GL-capable desktop.
4. Re-verify on XP the features the pod depends on: VPX TCP frame stream
(:8621), NE2000+pcap against WinPcap, AWE32/EMU8000 (bt/rp REQUIRE sound --
the FAST SOS clock dies without it), `serial3=file` logging, window title /
positioning hooks that `pod_deploy` relies on.
### B. Supervisor: native Win32 C++ (replaces .NET 8 pod-launch)
The design survives verbatim -- Job Object + `KILL_ON_JOB_CLOSE` exists since
Win2000, and `JobObject.cs`/`ChildProcess.cs`/`Focus.cs` are already thin
P/Invoke wrappers around the exact Win32 calls the C++ version will make
directly. Port, don't redesign: CreateJobObject/SetInformationJobObject/
AssignProcessToJobObject/CreateProcess suspended -> assign -> resume ->
WaitForSingleObject, plus the window-layout/focus pass.
- Build 32-bit, XP-targeting toolset (MSVC `v141_xp` or the same pinned MinGW
as A), statically linked CRT -- **zero runtime deps** (rejecting .NET
Framework 4.0: it would add an air-gapped framework install to every XP
cockpit for no benefit).
- **XP has no nested job objects** (one job per process before Win8). OPEN
(Phase 0 gate): does TeslaLauncher on the fleet already put children in a
job? If yes: launch the supervisor `CREATE_BREAKAWAY_FROM_JOB` (needs
launcher-side `JOB_OBJECT_LIMIT_BREAKAWAY_OK`) or fall back to a
watchdog/kill-tree mode. If no (likely -- native titles are plain children):
nothing changes.
- Drop `createdump.exe`-style diagnostics; a supervisor log file replaces it.
### C. Renderer: native vrview port -- the crux, and a decision with Dave
Python is a dead end on XP: CPython dropped XP at 3.5, Dave's GL backend needs
moderngl (GL 3.3 core -- no XP-era GPU/driver has it), and the numpy software
rasterizer (`vrview.Renderer`, today's fallback in `_backend.py`) is a
debugging reference, not something a P4 will push at mission rate.
**Plan of record: port vrview to C/C++ against fixed-function OpenGL
(1.1 floor, 1.4-1.5 typical on XP-era GeForce/Radeon).** The scene is
1995-vintage art (untextured/lightly-textured polys, flat/gouraud lighting,
the death-camera pass) -- it predates shaders; fixed-function multitexture
covers it. One binary then runs XP through Win11 (GL 1.x still works
everywhere), and the modern moderngl renderer becomes a dev-rig reference.
- The interface is already frozen and OS-agnostic: `renderer.exe tcp:8621
<live.fifodump>` -- a TCP client consuming the VPX frame stream. The port
swaps cleanly under pod-launch and package.ps1 with zero contract change.
- Spike first (with Dave): inventory what `vrview_gl` actually uses of GL 3.3
and map each feature to fixed-function or CPU-side transform. Frame
coalescing and SceneCache logic translate 1:1.
- Fallbacks, in order: (1) **two-artifact split** -- ship both renderers,
dispatcher picks by OS (modern rigs keep the verified moderngl exe; XP gets
the port) -- acceptable but two codebases; (2) software rasterizer in C
(SSE2) if the GPU census says the XP boxes have no usable GL driver at all;
(3) companion mini-PC driving the main view head over the existing TCP
bridge -- architecturally free, but it changes cockpit wiring, so it is a
per-site rescue, not the plan.
### D. Install tooling: one native exe configures, batch stays batch
`postinstall.bat` is already cmd-only and runs on anything NT -- keep it.
`configure.ps1` cannot run on XP (PowerShell 2.0 max, no NetAdapter module),
and VBScript is being removed from Win11, so scripting is the wrong home.
**Fold steps 2-4 into the supervisor: `pod-supervisor --configure -Root ...`.**
Same logic, Win32 APIs that exist on both ends: `GetAdaptersAddresses` (XP+)
for the physical-adapter/static-IPv4 scan (keep the fail-loud ambiguity rule
and `-BayIp`/`-ConsoleIp` overrides), the same letter-leading `realnic=` GUID
fragment, bayIP+100 derivation, `02:00:...` MAC, WATTCP.CFG stamping, and
`@@TOKEN@@` template rendering. postinstall calls the exe instead of
PowerShell. One compiled artifact then owns configure + launch + kill on every
OS, and the PowerShell dependency leaves the archive entirely.
Also: drop `vc_redist.x64.exe` handling (wrong arch for the fleet; statically
linked artifacts need no redist at all).
### E. Capture stack: WinPcap on XP, Npcap on Win10+, one dlopen surface
DOSBox-X talks to `wpcap.dll`, and WinPcap 4.1.3 and Npcap both provide it
(Npcap in WinPcap-compat mode). The manual-install-per-cockpit policy
(operator consensus 2026-07-10) already fits: it becomes **"install the
capture stack for your OS, once"** -- WinPcap 4.1.3 on XP, Npcap on Win10/11.
- WinPcap's BSD-style license permits redistribution, so the XP installer CAN
be bundled in `deploy\` and auto-installed by postinstall (unlike Npcap,
whose free license forbids bundling -- part of why the manual policy
exists). postinstall detection becomes: `sc query npcap || sc query npf`.
- The co-located checksum-offload killer is Npcap/host-capture specific;
real deployments (console on its own machine) are immune on any OS. If a
co-located XP rig is ever needed, offload disable lives in the NIC's
advanced properties instead of `Disable-NetAdapterChecksumOffload`.
## CPU headroom: verify, but likely a non-issue
BT/RP 4.10 targeted P90-PPro200; DOSBox-X's 32-bit dynamic core wants a
multi-GHz host to emulate that class of CPU *while also* running EMU8000
synthesis and feeding the VPX stream -- and the renderer competes for the same
cores. Per the operator (2026-07-11) the cockpit PCs are notably more powerful
than their XP installs suggest, so this is expected to pass; the Phase 1 bench
on a representative box stays as the cheap confirmation gate (Core2-class or
better = comfortable), not as a program risk.
## Phasing
- **Phase 0 -- census + contract checks (blocks everything).**
Per-cockpit survey template: CPU/RAM, GPU + driver GL caps, video head
count/wiring, NIC model, OS + SP, free disk. Fleet answers needed:
TeslaLauncher job-object usage (B), which console build the venues run and
where the `+100 DOSBox flag` lands (console workstream, unchanged from
DEPLOYMENT-PLAN), Win2000 unit count (scope), representative XP loaner box
for the bench.
- **Phase 1 -- DOSBox-X XP build + THE BENCH (biggest unknown first).**
Workstream A; exit gate = egg->mission at FAST clock with sound on
representative XP hardware, WinPcap mesh verified between two XP boxes.
- **Phase 2 -- supervisor + configure exe (B + D).** Exit gate = fresh zip
extract -> postinstall -> configure -> launch -> kill-cascade on XP SP3 VM
and Win11, same zip.
- **Phase 3 -- renderer port (C, with Dave).** Spike -> port -> live-verify
against the frozen VPX stream on both OS ends.
- **Phase 4 -- packaging + validation matrix.** package.ps1 swaps in the
32-bit artifact set (it already auto-bundles what's present). Matrix: XP SP3
VM (bridged pcap), Win11 x64, then a real XP cockpit soak; renderer decay /
death-camera / mission-review passes re-run per `CAMERA-REVIEW-NOTES.md`.
The current x64 package keeps shipping to Win10/11 rigs throughout; the XP
work replaces it only when the same zip passes the full matrix on both ends.
## OPEN items
1. Phase 0 census results -- GPU/driver GL caps decide the renderer fallback
(CPU expected fine per operator; confirm with the bench).
2. TeslaLauncher job-object behavior on the fleet (breakaway or not).
3. Fleet console build + where the per-title DOSBox `+100` flag is applied.
4. Upstream v2026.06.02 XP-build recipe intact? (else: last XP-supporting tag
+ patch forward-port).
5. Renderer port ownership: likely **in-house** (operator, 2026-07-11 --
"we may have to do the renderer work ourselves"); Dave's involvement
reduces to the vrview_gl feature inventory / consultation. The native
fixed-function port stays the plan of record either way -- vrview (the
software reference) plus the frozen VPX stream give us a full spec and a
pixel-comparison oracle without needing the original author.
+63
View File
@@ -0,0 +1,63 @@
# Red Planet 4.10 networked pod boot (production path, no -egg): console pushes
# the mission egg over TCP 1501; pod runs RPL4OPT under netnub. Same pod HW as
# BT: VPX board @0x150, RIO COM1, dual AWE32, plasma COM2. RP's setenv.bat
# auto-selects rpdpl.ini. NIC on IRQ 10 (COM2/plasma owns IRQ 3 -- see
# net_full.conf header). The dpl3-revive bridge renders RP content (same
# VelociRender wire). Modern TeslaConsole is RP-native, so this is its home game.
[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=DB5521D
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
rate=44100
blocksize=1024
prebuffer=60
[serial]
serial1=namedpipe pipe:vrio rxpollus:100 rxburst:16
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
+6 -3
View File
@@ -31,8 +31,11 @@ namespace VwePod
public bool DryRun;
public string BridgePos = "0,0"; // main out-the-window head
public int BridgeW = 800;
public int BridgeH = 600;
// 832x512 = the dPL3 board's native framebuffer res (the game's view
// flush / pick coords are 832x512). Render 1:1 until the 800x600
// presentation question is settled (see LAUNCH.md).
public int BridgeW = 832;
public int BridgeH = 512;
public int DosBoxX = 10;
public int DosBoxY = 10;
@@ -167,7 +170,7 @@ namespace VwePod
--bridge-script <py> live_bridge.py (dev)
--awe-rom <raw> AWE32 SoundFont ROM
--bridge-pos x,y main render window position (default 0,0)
--bridge-size w,h main render window size (default 800,600)
--bridge-size w,h main render window size (default 832,512 = dPL3-native)
--dosbox-xy x,y DOSBox window position + focus (default 10,10)
--no-bridge pod only, no GL render window
--no-sound skip the ~4-min SoundFont upload. WARNING: no AWE32 =
+1 -1
View File
@@ -75,7 +75,7 @@ see `../CAMERA-REVIEW-NOTES.md`. Default mode: `bt`.
| Arg | Effect |
|---|---|
| `--bridge-pos x,y` | main render window position (default `0,0`) |
| `--bridge-size w,h` | main render size (default `800,600`) |
| `--bridge-size w,h` | main render size (default `832,512` — the dPL3 board's native framebuffer res; see `../LAUNCH.md`) |
| `--dosbox-xy x,y` | DOSBox window position + focus (default `10,10`) |
### Path / asset overrides (dev / edge — `postinstall` already wires these)
@@ -0,0 +1,500 @@
# Cockpit cage, glances, and the "black rear view" (2026-07-11)
> **SUPERSEDED IN PART -- see "2026-07-12: the BT411 cross-check" at the
> bottom.** The geometry forensics below stand; the identities and the fix
> plan were revised after reading the BT411 port's reconstruction.
Operator report: hat-down (rear glance) = black display; sliding the seat cam
back showed "a large black cube-ish". Full forensics from a live-session
fifodump snapshot (offline renders + wire scans). Resolution shipped in
`live_bridge.py` (cage twist + unframed glances).
## What the geometry actually is
Two cockpit fixtures ride the player mech (own-chain, `gated`):
| inst | dcs | mount (wire) | shape |
|---|---|---|---|
| `9fd` **cockpit CAGE** | `9fe` | `dcs_link` DIRECTLY under the vehicle root (`7 9fc 9fe`) | 45 verts / 40 tris, 21x21x42, **all faces inward** -- open framed panes forward, SOLID textured wall aft; invisible from outside (backface-culled) |
| `a11` canopy trim | `a10` | linked into the head rig (`7 a12 a10`, `7 a10 a3e`) | 5 geoms, r=3.4, at head height; the near-cull (<10 units) hides it in first person |
Both DCS create-records carry **zone `9fb`** (type-2 node, created at mission
staging alongside the two boot zones). The three ER-laser beam instances are
own-chain + gated too (r=2000; the fixture filter excludes them by radius).
- **The black rear view = the cage's solid rear wall**, verified by rendering
the snapshot offline: forward through the panes is a perfect canopy view,
rear is wall-to-wall black. Seat-cam slid back = the same shell seen from
its edge (plus the canopy un-culling past 10 units).
- The mech has NO real model instance of itself in the scene -- from outside
(chase cam) the player mech IS a small black box. Other mechs are full
models; your own is cage + canopy + beams only.
## What the wire does NOT do
Scanned the whole session dump (~43k records):
- The game **never updates** the cage/canopy nodes after staging -- no
arm/disarm, no DCS re-pose, nothing per-glance.
- **No view switching** during glances: ONE type-3 view node (handle 3); its
36 flushes are projection/fog only (incl. the mission-start fog sweep and
the screen dims -- the game itself programs **832x512**, w12/w13).
- Zone masks (`ffffffff` -> `0fffffff`) are set once at boot/staging for all
zones uniformly -- not per-glance.
- Texture alpha-cutout is NOT the rear-wall mechanism: zero of the 15 texmap
nodes carry the alpha flag, and the wall texels (17,17,17 sum=51) wouldn't
pass the <=24 discard threshold anyway.
- Glances arrive as ordinary head-DCS chain motion (nothing else flushes).
## Ground truth (operator, ran the real pods)
Tank-turret model: **torso twist = turning the turret; the cockpit cage
tracks the twist.** The hat moves the PILOT'S HEAD on top of that (twist 90
right + hat right = looking back down your path). And **glance views are
UNFRAMED** -- the real pods showed a clean world view with no cockpit framing
on hat glances. (So hat-rear was never "through the cage's rear wall" -- the
real views simply didn't render the cockpit fixtures.)
## Fix shipped (live_bridge.py -- bridge policy, Dave's core untouched)
1. **Cage twist**: `hook_chain_matrix` wraps `Renderer.chain_matrix`; chains
rooted at the cage DCS get the camera's composed torso-twist yaw
pre-multiplied in model space (`v @ Y @ M`; cage chain is [cage_dcs, root]
with identity local, so Y lands exactly as a yaw about the mech axis).
`CAGE_TWIST_SIGN` flips (default follows the camera's twist).
2. **Unframed glances**: `fp_cam` now reports `.glance` = look direction
deviating > `GLANCE_DEG` (45) HORIZONTALLY from the twisted hull heading
(stick-Y pitch can't trip it; TORSO.SUB vertical limits are +10/-30).
While glancing, `render()` draws with the cockpit fixtures filtered out.
`GLANCE_HIDE=0` disables.
3. Fixture identification (`cockpit_refresh`, after every SceneCache
rebuild): own-chain + gated + not billboard + radius < 100; the cage =
the one hanging directly under the root (chain length 2, radius > 5).
Verified on the snapshot: picks exactly {9fd, a11}, cage dcs 9fe.
## Open / related
- **Laser aim vs torso** (operator): beams render/aim along the hull heading,
missiles track the torso. The beam chains ride the DCS rig, and the rig's
flushed values EXCLUDE joint angles (the torso joint arrives only as 0x1f
(sin,cos) entries) -- composing joint rotations into instance chains at the
right chain position is the proper fix (would move the beams' origin AND
direction with the twist). The pick ray (CAM backchannel) already follows
the twisted look -- likely why missiles track.
- Canopy trim (`a11`) twist: same joint-composition gap; currently masked by
the near-cull in first person.
- Whether the ORIGINAL board treated zone-9fb instances specially
(camera-space / per-view zone enables) is still undetermined -- our fix
implements the operator-specified behavior at the bridge level instead.
- The pod crashed at session end with a guest "Illegal Unhandled Interrupt 6"
storm (game-side; unrelated to the bridge -- device untouched today).
## 2026-07-12: the BT411 cross-check (C:\VWE\BT411, fresh pull) -- REVISED PICTURE
The concurrent BT411 port hit the SAME black-enclosure cockpit problem, and
their reconstruction settles the identities and the authentic mechanism:
1. **Instance `a11` = `MAX_COP.BGF`, the REAL cockpit shell** -- byte-proven:
272 verts / extent 2.5x2.3x3.3 / Y 0.4..2.7 matches a11's five geoms
exactly (every mech has one: `*_COP.BGF`, measured with
scratchpad cop_bounds.py). Per BT411 (open-questions.md): it is **the
TORSO segment's inside-skeleton mesh** -- so it rotates with the torso on
the real rig (operator's turret model confirmed) -- and **its window panes
are PUNCH texels**: "with punch live the black pane texels become holes
and the world shows through the frame". THE PUNCH CUTOUT is why real
glances read as (nearly) unframed world -- the shell is never hidden. The
operator's memory of seeing "the cage or the mech's arm" on left/right
glances fits: thin frame lines + arm through the punched side panes.
(BT411's interim policy is ours too: they hide `_cop` by name filter
until their punch/skeleton branch works, `BT_INSIDE_COCKPIT=1` reveals.)
2. **Instance `9fd` (the 21x21x42 inward box, root-linked, hidden-until-
armed) = the mission fade shroud** (`BTPOVStartEndRenderable`, armed for
MissionStartingState=3 / MissionEndingState=4 with dplMainZone+
dplDeathZone). Session 1's "black rear wall" happened because THAT
mission had ENDED (egg timer expired) and the shroud was armed; session 2
(fresh mission) never armed it -- which is why the twist hook "did
nothing": the visible frame was a11 all along.
3. **Glances = `EyepointRotation`** (mech attribute; mapper look states
LookLeft/Right/Behind/Down; LookBehind = yaw pi + pitch), composed into
the view every frame by DPLEyeRenderable. The look commit ALSO walks the
weapon roster setting the weapon+0x3e0 gate per look state -- **glancing
inhibits FIRE, it does NOT hide cockpit fixtures** (my "camera children"
read was wrong; those offsets are the weapon fields from the fire-gate
disasm). BT411 has the whole commit block deferred (offset conflicts) --
their port has no glances yet either.
4. **On our wire, glances/twist ride the 0x1f articulation batches only**
(cam-chain DCS bodies flush exactly once at creation -- verified across
the whole session dump). The "hats click but nothing renders" at session
end: the final 10% of the dump has ZERO 0x1f batches -- the game had
stopped articulating entirely (wedged/ended state, pre-crash), not a
bridge regression.
### GLANCES SOLVED + LIVE-CONFIRMED 2026-07-14 -- it was a KeyError crash
**Operator confirmed hat glances now render correctly.** The whole saga
resolved to a one-line bug of my own making, found via the operator's key
symptom: "when the hat is held the screen STOPS rendering and snaps to the
current view on release." That = render() was THROWING during glances.
Root cause: render()'s glance-hide guard referenced `_ckpt['fix']`, but I had
renamed that key to `'fixh'` days earlier (cockpit_refresh) and never updated
this line -> **KeyError EVERY frame where fp_cam.glance was true** (i.e. every
glance frame). The exception was swallowed by render()'s try/except, so NO
frame drew during a hold (screen froze), and on release fp_cam.glance went
false, the guard short-circuited before the bad key, and the draw resumed
("snaps to current"). fp_cam was computing the glance camera correctly the
WHOLE time (out_yaw provably swung with the hat) -- the crash was downstream
of it, in the draw dispatch.
Fix: REMOVED the glance-hide block entirely (always `r.draw(board)`). This
both kills the crash AND implements the already-decided descope (pilots
confirm the canopy stays visible during glances -- a11/MAX_COP has no rear
geometry so rear reads unframed naturally). The glance rides the camera via
fp_cam, which reads the eye-DCS 0x1f absolute pose -- exactly the authentic
game->board wire op the operator insisted it was.
**The operator was right at every turn:** it was a wire operation (action
0x1f eye-DCS reflush), NOT input/transport/binding; do NOT tee from vRIO. My
successive "game-side / fix_degenerate / not-emitted" theories were all
wrong; the persistence on "find where we drop it in the data path" was
correct -- we were crashing on it.
### REAR VIEW (hat-down) 2026-07-14 -- canopy drop, LIVE-CONFIRMED
Right after glances started rendering, the rear glance (hat-down / LookBehind)
came up BLACK, and sliding the seat back showed a black box. Chased it:
- NOT the 9fd shroud: in gameplay 9fd is inst_visible=0 (unarmed, not drawn);
hiding it was a no-op. The live probe's "own drawn" list showed the ONLY
own-mech instance drawn is a11 (the canopy, MAX_COP, r3).
- The canopy is a shell around the head: open front, dark solid BACK. Looking
forward = through the open front (world + strut framing); looking behind =
the canopy's dark interior back = black. GL render confirmed: rear WITH a11
= black, rear WITHOUT a11 = full arena (geometry is all there behind).
OPERATOR (ran the real pods): **hat-down on original hardware gave a CLEAR
rear view, NO canopy framing** (left/right glances DO keep the canopy). So the
authentic rule is direction-dependent. Fix: fp_cam computes glance_dev (look
deviation from twisted hull heading); render() drops the canopy fixtures when
glance_dev > REAR_DEG (110 deg default -- left/right ~50 deg keep it, rear
~180 deg drops it). Operator confirmed "acceptable behaviour."
COCKPIT CAMERA COMPLETE + ALL LIVE-CONFIRMED: twist (turret-true, zero
parallax) + lasers follow torso + hat glances left/right (canopy framed) +
hat-down clean rear view + the pipe transport (vRIO/vPLASMA). Remaining is
cosmetic (IR/thermal palette on action 0x1b, texture interpolation, per-mech
seat trim). Diagnostic scaffolding (glance_probe, fp_cam.out_yaw telemetry,
own-drawn list, BRIDGE_AUTOSAVE) left in, harmless; trim in a cleanup pass.
### GLANCE: THE forensics trail 2026-07-14 (era A/B -- superseded by the fix above)
**There is no glance regression -- the wire NEVER carried a hat glance, in
any era.** Prompted by the operator's challenge ("engine unchanged, only the
transport changed com0com->pipe -- so why did it break?"), diffed a 7/07
working-era capture vs 7/13:
- Cam-chain DCSs flush ONCE at construction in BOTH eras; no per-glance
reflush ever (era_diff.py).
- `fix_degenerate` provably changes the look yaw by ZERO (bisect_glance.py:
raw == fixd at every checkpoint) -- EXONERATED as the culprit.
- Decomposing the look into hull + twist + residual (find_glance.py): the
residual is FLAT across both 7/07 captures (only the +-180 chain-
construction artifact). No independent glance rotation in the wire.
**What actually happened (from our own memory + these forensics):** the game
never emitted a chain-independent glance through our HLE device. The single
time hat glances "worked" (2026-07-07) was a BRIDGE fp_cam hack (recover the
glance from the degenerate chain, swap yaw<->pitch) that the operator
confirmed "momentarily" -- and it was REVERTED the same day because it
corrupted stick-Y vertical aim into yaw. The "source fix" that replaced it
(`fix_degenerate`) is inert for glances (proven above). So there is NOTHING
to bisect: glances were a reverted experiment, not a lost feature; the
com0com->pipe transport change coincided with "stopped working" purely by
timing. This RETIRES the prior "game-side binding / keyboard-dead" and
"fix_degenerate ate it" theories -- both wrong.
**Path forward (operator principle 2026-07-14: DO NOT tee from vRIO -- the
glance is an authentic game->board WIRE op; find where we drop/misdecode
it).** This is CORRECT and cracked open the key protocol fact:
**RUNTIME DCS UPDATES TRANSMIT AS ACTION 0x1f, NOT 3.** BT411 FUN_0048ffa4
(flush dirty-DCS list) + FUN_0048e440 -> FUN_00492580(**0x1f**, ...): a
runtime DCS matrix update -- INCLUDING the eyepoint reflush -- packs per
DCS either 12 floats (type-0, full matrix) or 2 words (type 1-3) and sends
on **action 0x1f**, the same action as the articulation batch. So every
earlier "no reflush" scan (which looked at action 3) was blind to it. The
glance lives in the 0x1f stream.
**Where our render stands vs that:** the eye DCS = the node the game
list-adds to the view (6df in glance_snap); it IS a 12-float 0x1f node and
IS in our cam_chain -> chain_matrix already applies it, so a real glance
reflush WOULD render for free. BUT: our fp_cam only composes 0x1f *joints*
(2-float sin/cos) for cam_chain members (the twist sum); a glance carried
as a 2-word entry on a DCS OUTSIDE our heuristic chain would be parsed and
DISCARDED -- a concrete renderer gap if that's the form.
**What the captures show (glance_snap, plateau.py):** NO 0x1f handle shows a
sustained hold-then-return matching the 5s hat holds -- the eye DCS (6df)
moves only with hull/drive, walk joints just oscillate (runs 15-45), static
mounts sit at 180. So in OUR broken-setup capture the game isn't emitting a
glance reflush. But this capture is from the NON-working setup, so it can't
distinguish "renderer drops it" from "game doesn't emit it" -- need a
capture that contains a real glance.
**DECISIVE next step (probe rewritten -- glance_probe now watches the RIGHT
signal: eye/leaf 0x1f yaw + out-of-chain 0x1f movers, per 4s report):** a
NARRATED 5s hold next session settles it -- leaf yaw plateaus with the hold
= game emits, we must render it (bridge application bug, find it);
out-of-chain mover plateaus = we parse-and-drop it (apply that handle);
both flat = game not emitting (then it's upstream, the hat->look-state
binding). NO vRIO tee.
### GLANCE MECHANISM DECODED 2026-07-14 (disasm) + eyepoint pipeline (reference)
Chased the hat glance through the shipped BTL4OPT binary (BT411 decomp).
The camera path, end to end:
- The piloted mech builds as **insideEntity** (btl4vid.cpp:300-304) -> the
**eyepoint camera is a board DCS** created by **DPLEyeRenderable**
(ctor @004579a8): eye DCS handle at [this+0x10], and a pointer to the
mech's **EyepointRotation** attribute at [this+0x48].
- Per frame, **DPLEyeRenderable update @00457b48**: if EyepointRotation
changed (vs cached [this+0x4c]), recompose the eye DCS matrix from the
euler and mark it dirty (@0045fbf4 appends to the renderer's dirty-DCS
list @+0x2c8) -> the eye DCS gets **reflushed to the board**.
- EyepointRotation (mech+0x360) is written by the **mapper look-commit**
(mechmppr.cpp KeypressMessageHandler / InterpretControls, part_013.c:418):
when look-state (0x198, derived from lookLeft/Right/Behind/Down at
param+0x134..0x140) changes, it writes the euler (LookBehind = yaw pi =
0x40490fdb, pitch from torso+0x570) and re-slaves the camera children.
**So a glance = an eye-DCS reflush, and that DCS is IN our cam chain --
we'd render it for FREE if it happened.** It doesn't: the glance-probe
premise (validated on glance_snap: all 11 cam-DCS reflushes are at staging
22.6%, ZERO during the 80-98% holds) proves the game never moves the eye
during our glances.
**Root cause (narrowed):** EyepointRotation never changes => the mapper
look-commit never fires => the look-state fields never change. Those fields
are set by the ARROW keys via KeypressMessageHandler (keyboard is DEAD in
the pod, see above) -- and the HAT button reaches an audio-mapped action
(hence the glance SOUND) but, in this build/config, does not set the
look-state fields. Both glance routes are therefore blocked in the current
-egg trim; "worked on 7/07" was a different session config. Overturns two
earlier claims: NOT a view/zone re-parent (the only view list-ops are at
staging) and NOT a wire-parse blind spot (zero 0x1f parse failures).
**Decisive next step (telemetry SHIPPED, live_bridge glance_probe):** a
NARRATED hat hold ("holding left NOW") vs the `GLANCE-PROBE: cam DCS ..
REFLUSHED` line settles it: a reflush during a hold = camera IS moving
(bridge bug, unlikely per forensics); silence = game-side gate confirmed,
and the fix moves to the input binding (make the hat set the look-state
fields -- CTL binding / the same fields the dead arrow keys drive), OR a
bridge-synthetic glance (compose glance yaw in fp_cam like twist -- but the
bridge needs a hat-state feed it doesn't currently have; the game's
per-hat instance-visibility blip on inst-701-family + zone is the only
wire-observable candidate and needs the narrated capture to decode).
The in-game keyboard being dead is now ON the glance critical path -- fixing
it (or binding the hat to look) may be the whole fix.
### PUNCH-CUTOUT DESCOPED 2026-07-13 (pilot testimony)
Operators who flew the pods report **seeing canopy geometry when hat-glancing
left/right** -- the shell structure was VISIBLE in glance views. This matches
a11/MAX_COP's measured geometry exactly: all five geoms sit FORWARD of the
head (bbox z -3.4..-0.7, NO rear geometry), so rear glance = naturally
unframed world and side glances = sweeping past the struts. NO punch cutout
is needed for glance correctness; the 7/12 "black rear wall" was the 9fd
fade shroud (mission-end state), not the canopy. Consequences:
- The punch-cutout work item is DROPPED for the canopy (BT411's D3D port
still needs it for their blx_cop texture; and the punch texel semantics
may still matter for OTHER textures someday -- footnote only).
- **The GLANCE-HIDE policy must RETIRE once glances render**: authentic
glances show the canopy sweeping past, so hiding fixtures during glances
is anti-authentic. Keep GLANCE_HIDE only until the eyepoint pipeline
works, then default it OFF.
### Revised fix plan
- **The authentic fix for the shell = implement the PUNCH texel cutout** for
`_cop` textures in the renderer (their windows render as holes). Our wire
carries NO texmap alpha-flag words (all 15 = 0), and the shell's dark
texels (17,17,17, sum 51) exceed vrview_gl's alpha-cut threshold (<=24) --
so BT punch is a DIFFERENT flag/semantic than the FLYK-era cutout: find
where the punch attribute travels on the VPX wire (texture upload header?
b2z tag?) or key it on the `_cop` texture content. With punch live, the
glance-hide policy for a11 RETIRES (authentic = shell always on, windows
see through); keep hiding 9fd behavior as-is (it is a fade shroud -- its
blackness is the point).
- **Twist**: a11 is torso-mounted per BT411, and the operator watched it
stay chassis-locked -- so our chains genuinely lack the torso joint
rotation (confirmed joint-composition gap). Bridge yaw hook targets
fixture DCS a10; first live test DOUBLED (sign inverted in model space);
`CAGE_TWIST_SIGN=-1` is loaded but UNTESTED (sim was retired for the
night). The proper engine-level fix stays: compose the 0x1f joint
(sin,cos) rotations into instance chains at the joint DCS -- BT411's
gyro/eye-joint reconstruction (IntegrateEyeJoint @004b2ec0, currently a
NaN-stub for them) is the reference for WHERE the joints sit.
- **Lasers vs missiles**: BT411 confirms the crosshair = torso boresight
(stick twists the torso to aim; no free-aim on twist mechs). Their
open item (b): "on a TWIST-CAPABLE mech the crosshair should deflect with
the torso twist AND the torso should visibly lead the legs". Our beam
render + aim ride the joint-less chain = hull heading; the pick ray (CAM
backchannel) is twist-aware = missiles track. Same joint-composition fix
covers the beams (origin AND direction).
- 9fd arming = mission state; a mission-review/production run will show the
start blackout + end blackout arming on the wire (watch instance body
word4 flushes on 9fd).
## 2026-07-13: twist SOLVED (root-pivot + mirrored basis); glances = wire-silent
First full pipe-transport session (namedpipe vRIO/vPLASMA: controls + plasma
confirmed good). Offline forensics on the glance/twist capture
(scratchpad glance_snap.fifodump, torso held to +68deg at 92.2%):
**LIVE-CONFIRMED 2026-07-13 night (operator): the canopy tracks the torso
twist (turret-true), and the LASER BEAMS follow the torso too.** Final
recipe: root-pivot yaw conjugation (M inv(Mr) Y Mr) at CAGE_TWIST_SIGN=+1
applied to ALL own gated instances (canopy + shroud + the three gun-mount
beam instances -- twist_dcs; beams excluded from the glance-hide set), plus
the camera EYE swung along the same turret arc in fp_cam (operator: the
head is rigid with the torso -- zero shell/view parallax; the eye-position
arc is the authentic world-view motion). Closes both "cage locked to
chassis" and "lasers fire where the chassis faces".
ALSO FOUND 2026-07-13 night: **in-game PC keyboard input is DEAD in the
pod** ('l' searchlight dev key, '+'/'-' zoom, arrows: nothing; DOS-prompt
typing fine). Never historically validated -- may have been broken forever.
Suspects for a cold test: the shipped binary's L4CONTROLS parse
(setenv echoes RIO,KEYBOARD so policy is right), or the VPX device's extra
SDL head windows eating keystrokes. Consequence: the arrow-key glance
experiment is VOID -- keyboard nulls prove nothing about the look pipeline.
Glance regression suspect narrowed to vRIO hold semantics (old COM build
glanced fine yesterday; glances are the hat's only HOLD-driven function;
ask vRIO's log: 88 on deflect, 89 only on release?).
**Twist sign CORRECTED 2026-07-13 late (rendered A/B, ab2_*.png): default
CAGE_TWIST_SIGN=+1.** The -1 default came from a numeric calibration that
mixed yaw-sign conventions; the rendered A/B at the held +68deg twist shows
+1 brings the shell around WITH the twist (turret behavior; the small
parallax = head-off-axis, authentic) while -1 rotates it OUT of view (the
"canopy does not rotate" live report -- it was leaving the frustum).
Also learned: handle namespaces alternate between missions (9fc/a10 vs
69c/6b0 rigs) -- never hardcode; the fixture detection handles it.
One live confirm pending.
**BT411 new-pull digest (2026-07-13 evening, renderer-relevant):**
- FOG restored (task #63): authored per-map/time/WEATHER in BTDPL.INI
(weather = an EGG field: clear/fog/soup); the arcade fog =
dpl_fog_type_pixel_lin (per-pixel linear) -- Dave's GL shader is already
per-pixel linear, so our fog is authentic; useful for checking per-egg
fog VALUES.
- Gait (task #59): piloted mech plays INTERIOR 'i'-suffix clips -- NO hip
lean (level cockpit) + jointshakey cockpit rattle; exterior mechs lean
-8/-11 deg into walk/run. Our wire carries whatever the game plays =>
the bridge inherits level+rattle natively; other mechs SHOULD visibly
lean (check live).
- Torso twist live (task #57/58) w/ centered boresight crosshair --
reference for our laser-aim/joint-composition item.
- Missile splash/damage-economy fixes + DAFC muzzle flash (task #60-62)
and the i860 specialfx forensics (065c114) -- board-side effect engine
decode, groundwork for rendering detonations/impact FX in the bridge.
**Twist -- measured and closed.** Yaw ledger: the camera = hull yaw (chain
Z-row, contains NO twist) + JOINTS twist sum; the canopy is HULL-LOCKED
natively (its world yaw tracks the hull exactly through twists). The three
confusing live observations all came from ONE bug: the hook's model-space
pre-multiply yawed the canopy about its own local origin, swinging it
sideways in an arc. Fix: compose the yaw ABOUT THE VEHICLE-ROOT AXIS
(M' = M inv(Mr) Y Mr, Mr = root pose from anim_abs) -- distance-from-axis
preserved exactly (pivot_check.py). One subtlety: the rig's root basis is
MIRRORED (det -1, same family as FP_RIGHT_SIGN/fliplr), which inverts the
yaw sense under conjugation -- hence default CAGE_TWIST_SIGN=-1, calibrated
offline (canopy lands exactly on native+twist). AWAITING one live confirm.
**Glances -- the game never puts them on the wire (in the test egg).**
Around-the-clock 5s holds captured; exhaustive scans of the tail: NO pose
node rotates, NO joint pulses match holds (the 12-joint flurry = the legs
WALKING; the +-64deg sweeps = the twist test itself; the zero-translation
anim nodes = STATIC rear-facing mounts at yaw 180 always -- NOT eyepoints;
my 6b3 identification was wrong). Meanwhile the game DOES react internally
(range-bar jumps on hats). Best hypothesis, matching BT411: the glance
renders via the GYROSCOPE's eye-joint integrators (IntegrateEyeJoint
@004b2ec0 -- incomplete/NaN-stubbed in their port too), and that subsystem
never emits in our -egg trim -- OR earlier "working glances" (7/07) came
through a path that today's state doesn't drive. The 7/07 sessions DID
render glances, so the signal exists under some conditions -- find what
differs (egg? mission state? control mode BAS/MID/ADV? gyro power?).
## 2026-07-13: vision modes (IR/thermal) + the shipped keyboard map
Looked up the rumored CTRL-W / CTRL-T vision keys in BTL4OPT.EXE
(BT411 decomp + reconstruction). Verdict: **no such bindings exist in the
shipped binary.** Plain `w`/`t` are pilot-target select (slots 0/3). The
RP-era engine SOURCE had ALT-W = wireframe and ALT-V = "predator vision"
debug keys (L4APP.cpp), but in the shipped BT binary the wireframe toggle
(@0045fdd8, prints "wireframe ON/OFF") is DEAD CODE -- no caller, no
pointer-table reference -- and no app-level ALT-key switch exists at all.
The tip most likely drifted from ALT-W/ALT-V or a different build.
**The REAL IR/thermal mode -- and how to render it:**
- `ThermalSight` subsystem (BT411 thermalsight.hpp, classID 0x0BDE, a
PowerWatcher): driven by the cockpit IR BUTTON (`requestedOn`), gated by
generator power + heat + being the locally-viewed mech.
- Its sim calls the global pvision toggle @0045fe44 (prints "pvision
ON"/"pvision OFF") which issues **`dpl_Effect`(type 0, handle 0,
{c,c,c}, mode) with mode = -1 (ON) / -2 (OFF)** -- a special view-effect
= the Division board's thermal palette flip. THAT is the wire signature
our renderer must honor: watch for the effect/fog-mode carrying -1/-2 on
the view and swap the output palette (the "IR button = thermal render
mode, nothing to render it" note from 7/06 closes here).
- dpl_Effect type map (from callers): 0 = view color effect/fog-family
(color + mode), 1 = per-handle effect, 2 = clear/reset. The pvision call
is type 0 on handle 0 (the main view).
- **Wire encoding (host stub @0048e168): type-0 effects transmit ACTION
0x1b, payload = [mode:u32][node:u32][0x40-byte color struct]** -- fog uses
the same action with ordinary modes (0x1b already appears in our session
histograms); **pvision = mode 0xFFFFFFFF (ON) / 0xFFFFFFFE (OFF)**. Bridge
implementation = watch action 0x1b word0 for -1/-2, flip a thermal
palette in the present pass. (Type-1 effects = action 0x1d, 0x108 bytes;
type-2 = action 0x23, 8 bytes.)
**"Some mechs use IR, some use another vision mode" (operator) -- tracked
down 2026-07-13, game-side verdict: there is exactly ONE vision mode.**
Every mech roster in BTL4.RES carries the SAME ThermalSight subsystem
(BT411 weapsub audit, all chains), it has no per-mech model fields, the IR
button gate at record+0x25c is the electrical-short event flag (not a
capability switch), and the pvision toggle takes no parameters. Candidate
explanations for the memory, for the old pilots to arbitrate:
(a) the OTHER vision aid on every roster is the SEARCHLIGHT (night
illumination cone) -- "IR mech vs searchlight mech" habits; (b) ThermalSight
SHUTS OFF at heat alarm level >= 2 -- hot energy-boat mechs lose IR
constantly, reading as "this mech doesn't have IR"; (c) a different build
('96 BTLIVE vs 4.10) or the sensor-MFD display modes. Ask: WHICH mechs had
which mode, and what did it look like (white-hot vs green)?
Archaeology bonus: sda4/DPL3/VRENDER/VWE.DOC = Division musing on a
Pixel-Planes 5 / DEC Alpha board for VWE (the PXPL5SUP dir) -- pvision's
official name in the engine is "predator vision".
**Shipped keyboard map (PC keyboard, KeypressMessageHandler @004d1bf0)**,
useful for dev testing from the DOSBox window:
`+`/`-` target-range zoom; `1-5` + `a b c d f g s v x z` weapon/preset
modes; `w e r t y u i o` pilot-target select; `j`/`l` fake LRM10 /
Searchlight buttons; F1/F2 joystick align begin/end; 0x13d/0x13e cycle
control/display mode; **ARROW KEYS = the eyepoint look keys** (UP=forward,
LEFT/RIGHT=glance left/right, DOWN=look BEHIND, CTRL-UP=look down);
Ctrl-F1..F10 = fake subsystem buttons (GeneratorA-D, Condenser1, LRM15
fire/ammo/load, Avionics, Hud); `\` = mech message 0x19; `&` = stop
mission. NOTE for the glance hunt: the arrow keys drive the SAME
lookLeft/lookRight/lookBehind/lookDown fields as the hat -- a keyboard
glance test needs no vRIO and isolates the eyepoint->board question.
**Renderer backlog (operator, 2026-07-13): the Division card did TEXTURE
INTERPOLATION (bilinear filtering)** -- check vrview_gl's texture sampler
(likely NEAREST) and implement/verify linear filtering for authenticity.
**Bridge state:** the eyepoint camera compose is EXPLICIT OPT-IN only
(GLANCE_DCS=<hex>) -- auto-detection would have latched the static 180
mounts and flipped the camera permanently. New telemetry for the narrated
hunt: the 4s report prints `wire: NEW anim/joints=[...]` whenever handles
first appear in the 0x1f stream -- next session, have the pilot narrate
("holding hat-left NOW") and read the log; if nothing appears during holds,
compare against a 7/07-style run (gauge_arena_sound.conf, no net stack) to
isolate what enables the eyepoint emission.
@@ -0,0 +1,78 @@
[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]
# real pods loaded the ODI stack in AUTOEXEC before ANY game launch --
# weapons-fire test: bare -egg run WITH the packet stack resident (the game's
# WATTCP identity is 200.0.0.113 from REL410\BT\WATTCP.CFG). nicbase 340:
# 300 clashes with the VDB and blanks heads. backend=slirp: THIS build lacks
# pcap ("Backend not supported"); slirp = user-mode NAT, no LAN peers, but
# the packet driver functions and accepts sends -- enough for a solo fire
# test. For pod<->console runs use a pcap-enabled build (net_full.conf).
ne2000=true
nicbase=340
nicirq=3
backend=slirp
[serial]
# RIO on COM1 with the low-latency options (rxpollus/rxburst) so the board's
# few-ms ACK deadline is met; plasma display on COM2 (real pod has both).
serial1=namedpipe pipe:vrio rxpollus:100 rxburst:16
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 user's 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 (the
rem RIO retry spam proved it) -- capture the weapon fire-refusal reason.
rem DOS flushes in 4KB chunks; the tail lands on clean exit (mission timer).
btl4opt.exe -egg testarn.egg > c:\weaponlog.txt
32rtm.exe -u
echo ALPHA1-RUN-DONE
pause
+305 -6
View File
@@ -18,9 +18,10 @@ path = sys.argv[1]
catchup = sys.argv[2] if len(sys.argv) > 2 else None
board = VirtualBoard()
board.munga = False
# BRIDGE_W/BRIDGE_H size the main out-the-window render window (default the
# 832x512 debug size; pod deploy sets the main display's native res e.g.
# 800x600). BRIDGE_BORDERLESS=1 drops the title bar for a kiosk deploy.
# BRIDGE_W/BRIDGE_H size the main out-the-window render window. Default AND
# deploy standard = 832x512, the dPL3 board's native framebuffer res (see
# LAUNCH.md; 800x600-output presentation is an open decision).
# BRIDGE_BORDERLESS=1 drops the title bar for a kiosk deploy.
r = Renderer(w=int(os.environ.get('BRIDGE_W', '832')),
h=int(os.environ.get('BRIDGE_H', '512')),
title=f"dpl3-revive renderer (Dave) -- LIVE from our pod [{backend}]")
@@ -74,6 +75,182 @@ def track_joints(payload):
if nf in (2, 5):
JOINTS[h] = struct.unpack_from('<2f', p, o)
# --- cockpit fixtures (cage + canopy trim) ---------------------------------
# The game links the cockpit CAGE (an inward-facing shell around the mech,
# open panes forward / solid wall aft) directly under the VEHICLE ROOT and
# never touches it again -- but the pod behaves like a tank turret (operator,
# 2026-07-11): the cage must yaw WITH the torso twist, and glance views (hat:
# left/right/rear) are UNFRAMED -- real pods showed a clean world view, no
# cockpit framing (which is also why hat-rear looked "black": we were
# rendering the cage's solid rear wall the real views never showed).
# So per frame: (a) yaw the cage chain by the camera's torso-twist angle
# (CAGE_TWIST_SIGN flips, default follows FP_TWIST_SIGN); (b) when the look
# direction deviates > GLANCE_DEG (45) horizontally from the twisted hull
# heading, hide all cockpit fixtures (GLANCE_HIDE=0 disables).
# +1: calibrated by RENDERED A/B against the +68deg twist capture (ab2_*.png,
# 2026-07-13): +1 brings the shell around WITH the twist (turret behavior,
# small head-off-axis parallax is authentic); -1 rotates it out of view
# entirely ("canopy does not rotate" report). The earlier numeric-only
# calibration mixed yaw conventions -- trust the pictures.
CAGE_SIGN = float(os.environ.get('CAGE_TWIST_SIGN', '1'))
# REAR glance (hat-down / LookBehind) drops the canopy for a CLEAN rear view
# (operator 2026-07-14: original hardware gave a clear rear, NO canopy
# framing, on hat-down; left/right glances DO keep the canopy). Hide the
# fixtures when the look deviates more than this from the twisted hull
# heading. Left/right glances ~50 deg; rear ~180 deg -> 110 separates them.
REAR_DEG = float(os.environ.get('REAR_GLANCE_DEG', '110'))
GLANCE_HIDE = os.environ.get('GLANCE_HIDE', '1') != '0'
GLANCE_DEG = float(os.environ.get('GLANCE_DEG', '45'))
_ckpt = {'insts': None, 'fixh': frozenset(), 'cage': None,
'twist_dcs': frozenset(), 'shroud': None}
def eyepoint_refresh(cache):
"""EYEPOINT (hat-glance) camera compose -- EXPLICIT OPT-IN ONLY.
Wire forensics 2026-07-13: the game's glance (BT411 EyepointRotation /
gyro eye-joint chain) never reached the 0x1f stream in the test egg --
the zero-translation anim nodes are STATIC rear-facing mounts (yaw 180
always), so auto-detection would permanently flip the camera. Until a
live narrated capture identifies the real glance signal, the compose
activates only with GLANCE_DCS=<hex> naming the node explicitly."""
ov = os.environ.get('GLANCE_DCS')
_ckpt['eye'] = int(ov, 16) if ov else None
def glance_probe(cache):
"""THE decisive glance test (rev 2026-07-14). CORRECTION: runtime DCS
updates -- incl. the eyepoint reflush (BT411 FUN_0048e440 -> transmit
action 0x1f, NOT action 3) -- ride the 0x1f articulation stream. The
eye DCS = the cam-chain LEAF (the node the game list-adds to the view).
This logs the leaf's live yaw AND flags any large-swing 0x1f mover that
is OUTSIDE the cam chain (which we'd parse but never apply). A NARRATED
5s hat hold vs this line is the whole diagnosis: leaf yaw plateaus with
the hold = the game IS emitting the glance and we should render it (find
why we don't); leaf flat + an out-of-chain mover plateaus = we're
dropping it (apply that handle); both flat = the game isn't emitting
(upstream). Reports every ~4s alongside the frame line."""
chain = cache.cam_chain or []
if not chain:
return
leaf = chain[0]
f = board.anim_abs.get(leaf)
yaw = None
if f and len(f) >= 12:
yaw = math.degrees(math.atan2(f[6], -f[8]))
now = time.time()
if now - getattr(glance_probe, 'last', 0) > 4:
glance_probe.last = now
inchain = set(chain)
movers = []
base = getattr(glance_probe, 'base', {})
for h, sc in JOINTS.items():
if h in inchain:
continue
ang = math.degrees(math.atan2(sc[0], sc[1]))
b = base.setdefault(h, ang)
if abs(ang - b) > 30:
movers.append('%x=%+.0f' % (h, ang))
glance_probe.base = base
# which OWN-mech instances are actually being DRAWN (inst_visible)?
# the rear-black occluder is one of these -- identify it live.
import vrview as _vv
root = chain[-1]
drawn = []
for inst in cache.instances:
if root in inst['chain'] and _vv.inst_visible(board.nodes, inst):
drawn.append('%x(r%.0f)' % (inst['handle'],
inst.get('radius', 0)))
print(f"GLANCE-PROBE: eye(leaf {leaf:x}) yaw="
f"{('%+.1f' % yaw) if yaw is not None else 'n/a'}"
f" fp_cam.out_yaw={getattr(fp_cam, 'out_yaw', None)}"
f" own drawn: {drawn}"
f" movers: {movers[:4]}", flush=True)
def cockpit_refresh(cache):
"""(Re)identify cockpit fixtures after a SceneCache rebuild: gated
own-chain instances of small radius (beams are gated too but r=2000).
The cage = the shell hanging DIRECTLY under the root (chain length 2).
Keyed by HANDLE (instance dicts are recreated every rebuild) and
keep-last-good: a rebuild that transiently fails detection (e.g. empty
cam_chain mid-stream) must not wipe a previously found cage."""
if _ckpt['insts'] is cache.instances:
return
_ckpt['insts'] = cache.instances
root = cache.cam_chain[-1] if cache.cam_chain else None
fixh, cage, tdcs, shroud = set(), None, set(), None
if root is not None:
for inst in cache.instances:
ch = inst['chain']
if not (root in ch and inst.get('gated')
and not inst.get('billboard')):
continue
# ALL own gated instances twist with the turret (canopy, shroud,
# AND the laser beams -- gun mounts orbit the same root axis;
# fixes "lasers fire where the chassis faces", 2026-07-13)...
tdcs.add(ch[0])
if inst.get('radius', 0) >= 100:
continue # ...but beams never glance-HIDE
fixh.add(inst['handle'])
if len(ch) == 2 and inst.get('radius', 0) > 5:
cage = ch[0]
# ...and the big root-linked inward box = the mission-fade
# SHROUD (9fd/BTPOVStartEndRenderable). It surrounds the mech
# (open front, SOLID rear) so forward looks fine but a rear
# glance / seat-back sees its black wall. It is a start/end
# fade effect, NOT gameplay geometry -> hide it always
# (operator 2026-07-14: "large black cube behind the cage").
shroud = inst['handle']
if fixh:
changed = (fixh != _ckpt['fixh']) or (cage != _ckpt['cage'])
_ckpt['fixh'], _ckpt['cage'] = frozenset(fixh), cage
_ckpt['twist_dcs'] = frozenset(tdcs)
_ckpt['shroud'] = shroud
if changed:
print(f"cockpit fixtures: {['%x' % h for h in sorted(fixh)]} "
f"cage_dcs={'%x' % cage if cage else None} "
f"shroud={'%x' % shroud if shroud else None} "
f"twist_dcs={['%x' % d for d in sorted(tdcs)]} "
f"root={root:x} ninst={len(cache.instances)}", flush=True)
elif _ckpt['fixh']:
print(f"cockpit refresh found nothing (root="
f"{'%x' % root if root else None}, ninst="
f"{len(cache.instances)}) -- keeping previous", flush=True)
def hook_chain_matrix(r):
"""Wrap Renderer.chain_matrix: cockpit-fixture chains get the camera's
torso-twist yaw composed ABOUT THE VEHICLE-ROOT AXIS (M' = M inv(Mr) Y
Mr). Measured 2026-07-13: the canopy is hull-locked natively (its yaw
tracks the hull exactly through twists) while the camera = hull yaw +
JOINTS twist, so the fixture needs +twist -- but pivoted at the root:
the earlier model-space pre-multiply (Y @ M) yawed the canopy about its
OWN local origin, swinging it sideways in an arc ("moves further than
the view")."""
orig = r.chain_matrix
def cm(board_, chain_, **kw):
M = orig(board_, chain_, **kw)
if chain_ and chain_[0] in _ckpt['twist_dcs']:
a = CAGE_SIGN * getattr(fp_cam, 'twist_angle', 0.0)
now = time.time()
if now - cm.last > 3.0:
cm.last = now
print(f"fixture twist: dcs={chain_[0]:x} angle={a:+.2f}",
flush=True)
root = chain_[-1]
f = board_.anim_abs.get(root) if a else None
if a and f is not None and len(f) >= 12:
Mr = np.eye(4)
Mr[:3, :3] = np.array(f[:9], float).reshape(3, 3)
Mr[3, :3] = f[9:12]
cs, sn = math.cos(a), math.sin(a)
Y = np.eye(4)
Y[0, 0], Y[0, 2], Y[2, 0], Y[2, 2] = cs, -sn, sn, cs
try:
M = np.asarray(M, float) @ np.linalg.inv(Mr) @ Y @ Mr
except np.linalg.LinAlgError:
pass
return M
cm.last = 0.0
r.chain_matrix = cm
print(f"chain_matrix hook installed on {type(r).__name__}", flush=True)
def fp_cam(board, cache):
"""First-person cockpit camera, best source first:
1. HEAD-LOOK (default): the munga cam DCS chain's translation row = the
@@ -82,7 +259,10 @@ def fp_cam(board, cache):
rows collapse to +-Y), so only eye + Z row are trusted and the basis
is rebuilt y-up. FP_CAM=vehicle forces the fallback.
2. Fallback: the player vehicle's 0x1f root pose -- eye at hull + VEH_EYE,
forward = -Z row (FP_FWD_SIGN flips)."""
forward = -Z row (FP_FWD_SIGN flips).
Side products (function attrs): .root (vehicle root DCS), .twist_angle
(composed torso-twist yaw), .glance (look deviates > GLANCE_DEG from the
twisted hull heading -- drives the unframed-glance cockpit hide)."""
anim = board.anim_abs
chain = cache.cam_chain
h = None
@@ -103,6 +283,7 @@ def fp_cam(board, cache):
t = np.array(f[9:12])
worldup = np.array([0.0, 1.0, 0.0])
eye = fwd = None
fp_cam.path = 'vehicle' # telemetry: which camera source won
if os.environ.get('FP_CAM', 'chain') != 'vehicle':
try:
Mc = np.asarray(CHAIN_CAM(board), float)
@@ -112,6 +293,7 @@ def fp_cam(board, cache):
(t is None or np.linalg.norm(ec - t) < 100.0)):
eye = ec + worldup * UPOFF[0]
fwd = fc / n
fp_cam.path = 'chain'
except Exception:
pass
if eye is None and R is None:
@@ -135,15 +317,70 @@ def fp_cam(board, cache):
# +-Y) or in the vRIO input mapping, without touching the look-vector.
# torso twist: yaw the look direction by any chain-member joint angle
# (jointtorso lives IN the cam chain; the shadow joint does not)
twist_total = 0.0
if TWIST_ON:
for jh in (chain or ()):
sc = JOINTS.get(jh)
if sc is None:
continue
th = TWIST_SIGN * math.atan2(sc[0], sc[1])
twist_total += th
cs, sn = math.cos(th), math.sin(th)
fwd = np.array([cs * fwd[0] + sn * fwd[2], fwd[1],
-sn * fwd[0] + cs * fwd[2]])
# the EYE rides the torso too: swing it along the same turret arc about
# the vehicle-root axis (operator 2026-07-13: the pilot head and cockpit
# shell are rigid on the rotating torso -- ZERO shell/view parallax; the
# eye-position arc is the authentic residual motion of the world view)
if twist_total and t is not None:
er = eye - t
cs, sn = math.cos(twist_total), math.sin(twist_total)
eye = t + np.array([cs * er[0] + sn * er[2], er[1],
-sn * er[0] + cs * er[2]])
fp_cam.twist_angle = twist_total
# hat-glance: compose the EYEPOINT rotation (eyepoint_refresh finds the
# node; the game flushes it via 0x1f only while deflected, LookBehind =
# yaw pi) ON TOP of the twist -- turret model: the glance is the pilot's
# head relative to the twisted torso. The eyepoint hangs on a link
# branch OUTSIDE the cam chain, so the chain camera alone never sees it
# (wire-proven 2026-07-13). GLANCE_YAW_SIGN / GLANCE_PITCH_SIGN flip.
fp_cam.glance_yaw = 0.0
eh = _ckpt.get('eye')
ef = board.anim_abs.get(eh) if eh is not None else None
if ef is not None and len(ef) >= 12:
gy = math.atan2(ef[6], -ef[8]) * \
float(os.environ.get('GLANCE_YAW_SIGN', '1'))
gp = math.asin(max(-1.0, min(1.0, ef[7]))) * \
float(os.environ.get('GLANCE_PITCH_SIGN', '1'))
if abs(gy) > 0.005 or abs(gp) > 0.005:
fp_cam.glance_yaw = gy
cs, sn = math.cos(gy), math.sin(gy)
fwd = np.array([cs * fwd[0] + sn * fwd[2], fwd[1],
-sn * fwd[0] + cs * fwd[2]])
if abs(gp) > 0.005:
right = np.cross(worldup, fwd)
rn = np.linalg.norm(right)
if rn > 1e-6:
right /= rn
cp, sp = math.cos(gp), math.sin(gp)
fwd = fwd * cp + np.cross(right, fwd) * sp
# glance detection: horizontal angle between the look and the TWISTED
# hull heading; > GLANCE_DEG = a hat glance is held (drives the
# unframed-glance cockpit hide). Stick-Y pitch never trips this
# (horizontal-only; TORSO.SUB vertical limits are +10/-30 deg anyway).
fp_cam.glance = False
fp_cam.glance_dev = 0.0 # look deviation from twisted hull heading (deg)
if R is not None:
vf = float(os.environ.get('FP_FWD_SIGN', '-1')) * R[2]
cs, sn = math.cos(twist_total), math.sin(twist_total)
vf = np.array([cs * vf[0] + sn * vf[2], 0.0,
-sn * vf[0] + cs * vf[2]])
lf = np.array([fwd[0], 0.0, fwd[2]])
nv, nl = np.linalg.norm(vf), np.linalg.norm(lf)
if nv > 1e-6 and nl > 1e-6:
cosang = max(-1.0, min(1.0, float(np.dot(vf, lf) / (nv * nl))))
fp_cam.glance = cosang < math.cos(math.radians(GLANCE_DEG))
fp_cam.glance_dev = math.degrees(math.acos(cosang))
eye = eye + fwd * FWDOFF[0] # seat forward/back trim
back = -fwd
right = np.cross(worldup, back)
@@ -159,8 +396,11 @@ def fp_cam(board, cache):
right *= float(os.environ.get('FP_RIGHT_SIGN', '1'))
M = np.eye(4)
M[0, :3], M[1, :3], M[2, :3], M[3, :3] = right, up, back, eye
fp_cam.out_yaw = math.degrees(math.atan2(fwd[0], -fwd[2]))
return M
hook_chain_matrix(r) # cage twist rides every instance-chain evaluation
def send_cam(M):
# Backchannel on the fifosock: hand the device OUR camera (the
# user-validated cockpit view, twist + glance included) so its reticle
@@ -205,15 +445,60 @@ def render(board):
flush=True)
except Exception:
pass
if ev.key == pg.K_w: # scene wireframe (GL backend)
try:
import vrview_gl
vrview_gl.WIREFRAME[0] = not vrview_gl.WIREFRAME[0]
print(f"wireframe "
f"{'ON' if vrview_gl.WIREFRAME[0] else 'OFF'}",
flush=True)
except Exception:
pass
if hstep or fstep:
print(f"eye trim: height {UPOFF[0]:+.1f} "
f"forward {FWDOFF[0]:+.1f}", flush=True)
r.cache.maybe_rebuild(board)
cockpit_refresh(r.cache)
eyepoint_refresh(r.cache)
glance_probe(r.cache)
M = fp_cam(board, r.cache)
if M is not None:
r.cam_matrix = lambda _b, _M=M: _M
send_cam(M)
r.draw(board)
# Glance-hide RETIRED 2026-07-14: (1) it referenced the stale _ckpt
# key 'fix' (renamed to 'fixh'), throwing KeyError EVERY glance frame
# -> render aborted -> screen froze during a hold and snapped back on
# release (the "glances don't render" bug -- fp_cam was fine, the
# crash was here). (2) Pilots confirm the canopy IS visible when
# glancing (a11/MAX_COP has no rear geometry -> rear reads unframed
# naturally), so hiding fixtures was anti-authentic anyway.
# HIDE set: (a) the mission-fade SHROUD (9fd) always -- start/end fade
# effect, not gameplay geometry; (b) on a REAR glance, the CANOPY too
# (a11) -- hat-down gave a CLEAN rear view with NO framing on the
# original hardware (operator 2026-07-14). Left/right glances keep the
# canopy (dev ~50 < REAR_DEG); only the rear look (dev ~180) drops it.
hide = set()
if _ckpt.get('shroud') is not None:
hide.add(_ckpt['shroud'])
if getattr(fp_cam, 'glance_dev', 0.0) > REAR_DEG:
hide |= set(_ckpt.get('fixh', ())) # canopy + shroud
if hide:
saved = r.cache.instances
r.cache.instances = [i for i in saved if i['handle'] not in hide]
try:
r.draw(board)
finally:
r.cache.instances = saved
else:
r.draw(board)
# AUTOSAVE: dump the live-rendered frame every ~20 frames so a held
# glance can be grabbed and compared (diagnostic 2026-07-14).
if os.environ.get('BRIDGE_AUTOSAVE') and frames % 20 == 0:
try:
from _backend import save_frame
save_frame(r, os.environ['BRIDGE_AUTOSAVE'])
except Exception:
pass
except KeyboardInterrupt:
raise
except Exception as e:
@@ -349,7 +634,21 @@ while True:
time.sleep(0.02) # socket mode already waited in recv
if time.time() - last_report > 4:
last_report = time.time()
# narrated-test aid: announce articulation handles first seen since
# the last report (glance hunts: "holding hat-left NOW" vs this line)
wa = set(board.anim_abs) - globals().get('_seen_anim', set())
wj = set(JOINTS) - globals().get('_seen_joints', set())
globals()['_seen_anim'] = set(board.anim_abs)
globals()['_seen_joints'] = set(JOINTS)
if wa or wj:
print(f"wire: NEW anim={['%x' % h for h in sorted(wa)]} "
f"joints={['%x' % h for h in sorted(wj)]}", flush=True)
print(f"frames={frames} skipped={skipped} nodes={len(board.nodes)} "
f"uploads={len(board.uploads)} tex={len(board.tex)} "
f"munga={board.munga} anim_abs={len(board.anim_abs)} "
f"backlog={len(pending)}B", flush=True)
f"backlog={len(pending)}B "
f"twist={getattr(fp_cam, 'twist_angle', 0.0):+.2f} "
f"glance={int(getattr(fp_cam, 'glance', False))} "
f"gyaw={math.degrees(getattr(fp_cam, 'glance_yaw', 0.0)):+.0f} "
f"joints={len(JOINTS)} cam={getattr(fp_cam, 'path', '?')}",
flush=True)
+7 -2
View File
@@ -17,7 +17,12 @@ param(
$ErrorActionPreference = 'Stop'
# ---- POD DISPLAY LAYOUT (x,y[,w,h]); edit for this rig's outputs ----------
$MAIN = '0,0'; $MAIN_W = 800; $MAIN_H = 600 # GL bridge (out-the-window)
# Main render = 832x512, the dPL3 board's NATIVE framebuffer res (the game's
# view flush, pick coords, and death-cam stream are all 832x512; see
# dpl3-revive/spec/VELOCIRENDER_PROTOCOL.md). Rendering 1:1 removes the
# aspect/scale variable while wiring things up; how to present on an 800x600
# output (letterbox vs scale) is decided AFTER everything is verified at 1:1.
$MAIN = '0,0'; $MAIN_W = 832; $MAIN_H = 512 # GL bridge (out-the-window)
$RADAR = '800,0,640,480' # Head C -> win0
$MFD3 = '1440,0,640,480' # Head B, 3-color (UL/UC/UR) -> win4
$MFD2 = '2080,0,640,480' # Head A, 2-color (LL/LR) -> win3
@@ -32,7 +37,7 @@ $env:VPX_WIN4 = $MFD3 # 3-color MFD head (HEAD B)
$env:VPX_WIN3 = $MFD2 # 2-color MFD head (HEAD A)
# To swap the two MFD heads, just swap the x-coords in the $MFD3/$MFD2 lines
# above (1440 <-> 2080) -- the RECTS block is the single source of truth.
# Main render window size = the main display's native res.
# Main render window size = the board-native 832x512 (see RECTS comment).
$env:BRIDGE_W = "$MAIN_W"
$env:BRIDGE_H = "$MAIN_H"
+58
View File
@@ -0,0 +1,58 @@
# Red Planet 4.10 BARE -egg run (quick drop-in, no console/netnub) on the
# namedpipe vRIO/vPLASMA transport. Derived from net_rp.conf; networking =
# slirp (the dev-tree exe has no pcap backend -- pcap lives in the packaged
# static build only) with the ODI stack loaded as production did.
[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=slirp
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
rate=44100
blocksize=1024
prebuffer=60
[serial]
serial1=namedpipe pipe:vrio rxpollus:100 rxburst:16
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 (slirp, 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, bare -egg (test.egg) ===
32rtm.exe -x
rpl4opt.exe -egg test.egg > nn.log
32rtm.exe -u
echo === RP-EGG-RUN-DONE ===
pause
+26
View File
@@ -17,6 +17,13 @@ version control because the DOSBox-X source tree itself
- **`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.
## Applying to a DOSBox-X source checkout
@@ -40,6 +47,25 @@ Tested against DOSBox-X `v2026.06.02`, MSYS2 mingw64.
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`).
4. Build:
```
cd emulator/src
+379
View File
@@ -0,0 +1,379 @@
/*
* VWE fork: serial-over-named-pipe backend. See serialnamedpipe.h for the
* wire contract. RX pacing is a faithful copy of the (patched) directserial
* state machine so the RIO's validated rxpollus/rxburst tuning carries over.
*/
#include "dosbox.h"
#if defined(WIN32)
#include "logging.h"
#include "pic.h"
#include "setup.h"
#include "serialport.h"
#include "serialnamedpipe.h"
bool getBituSubstring(const char* name, Bitu* data, CommandLine* cmd);
#define PIPE_RETRY_MS 500.0
enum { P_RX_IDLE = 0, P_RX_WAIT, P_RX_BLOCKED, P_RX_FASTWAIT };
// frame types (identical both directions)
enum { FRAME_DATA = 0x00, FRAME_LINES = 0x01 };
CSerialNamedPipe::CSerialNamedPipe(Bitu id, CommandLine* cmd) : CSerial(id, cmd) {
InstallationSuccessful = false;
std::string tmp;
if (!cmd->FindStringBegin("pipe:", tmp, false)) {
LOG_MSG("Serial%d: namedpipe requires pipe:<name>", (int)COMNUMBER);
return;
}
// pipe:vrio -> \\.\pipe\vrio; a full \\... path passes through
if (tmp.size() >= 2 && tmp[0] == '\\' && tmp[1] == '\\')
pipename = tmp;
else
pipename = std::string("\\\\.\\pipe\\") + tmp;
Bitu rx_poll_us = 0;
if (getBituSubstring("rxpollus:", &rx_poll_us, cmd)) {
if (rx_poll_us < 50) rx_poll_us = 50;
if (rx_poll_us > 1000) rx_poll_us = 1000;
rx_poll_ms = (float)rx_poll_us / 1000.0f;
}
Bitu rx_burst = 0;
if (getBituSubstring("rxburst:", &rx_burst, cmd)) {
if (rx_burst < 1) rx_burst = 1;
if (rx_burst > 64) rx_burst = 64;
rx_burst_div = (float)rx_burst;
}
if (getBituSubstring("rxdelay:", &rx_retry_max, cmd)) {
if (!(rx_retry_max <= 10000)) rx_retry_max = 0;
}
LOG_MSG("Serial%d: namedpipe client of %s (poll %.2fms burst x%g)",
(int)COMNUMBER, pipename.c_str(), rx_poll_ms, rx_burst_div);
CSerial::Init_Registers();
// unplugged cable until the peer's 0x01 arrives
setRI(false);
setCD(false);
setDSR(false);
setCTS(false);
InstallationSuccessful = true;
rx_state = P_RX_IDLE;
tryConnect(); // eager first attempt
setEvent(SERIAL_POLLING_EVENT, rx_poll_ms); // receive/reconnect tick
}
CSerialNamedPipe::~CSerialNamedPipe() {
if (pipe != INVALID_HANDLE_VALUE) {
CancelIo(pipe);
CloseHandle(pipe);
}
// events are cleared by the framework on port destruction
}
void CSerialNamedPipe::tryConnect() {
next_retry_ms = PIC_FullIndex() + PIPE_RETRY_MS;
HANDLE h = CreateFileA(pipename.c_str(), GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (h == INVALID_HANDLE_VALUE) return; // no server yet: cable stays unplugged
DWORD mode = PIPE_READMODE_BYTE;
SetNamedPipeHandleState(h, &mode, NULL, NULL);
pipe = h;
inbuf.clear();
txq.clear();
tx_inflight = false;
LOG_MSG("Serial%d: namedpipe connected to %s", (int)COMNUMBER, pipename.c_str());
sendLineState(); // contract: one 0x01 on connect
}
void CSerialNamedPipe::dropConnection(const char* why) {
if (pipe != INVALID_HANDLE_VALUE) {
CancelIo(pipe);
CloseHandle(pipe);
pipe = INVALID_HANDLE_VALUE;
}
tx_inflight = false;
txq.clear();
inbuf.clear(); // partial frame is meaningless now
// disconnect = all lines low (rxq keeps already-delivered wire bytes)
setCTS(false);
setDSR(false);
setCD(false);
next_retry_ms = PIC_FullIndex() + PIPE_RETRY_MS;
LOG_MSG("Serial%d: namedpipe disconnected (%s)", (int)COMNUMBER, why);
}
// TX is queue + single in-flight overlapped write, reaped from the poll
// tick. The emu thread NEVER waits on the pipe: a peer that isn't reading
// (e.g. both sides sending their connect hello before either reads --
// first live vRIO run, 2026-07-13) wedges the queue, not the emulator;
// after TX_WEDGE_MS of no progress the connection drops (unplugged cable).
#define TXQ_MAX 8192
#define TX_WEDGE_MS 2000.0
void CSerialNamedPipe::queueSend(const uint8_t* data, DWORD len) {
if (pipe == INVALID_HANDLE_VALUE) return; // unplugged: discard
if (txq.size() + len > TXQ_MAX) {
dropConnection("tx queue overflow (peer not reading)");
return;
}
txq.insert(txq.end(), data, data + len);
kickTx();
}
void CSerialNamedPipe::kickTx() {
if (pipe == INVALID_HANDLE_VALUE || tx_inflight || txq.empty()) return;
tx_pend.swap(txq);
txq.clear();
ZeroMemory(&tx_ov, sizeof(tx_ov));
DWORD done = 0;
if (WriteFile(pipe, tx_pend.data(), (DWORD)tx_pend.size(), &done, &tx_ov)) {
return; // completed synchronously
}
if (GetLastError() == ERROR_IO_PENDING) {
tx_inflight = true;
tx_wedge_ms = PIC_FullIndex() + TX_WEDGE_MS;
return;
}
dropConnection("write failed");
}
void CSerialNamedPipe::pollTx() {
if (pipe == INVALID_HANDLE_VALUE || !tx_inflight) return;
DWORD done = 0;
if (GetOverlappedResult(pipe, &tx_ov, &done, FALSE)) {
tx_inflight = false;
kickTx();
return;
}
DWORD err = GetLastError();
if (err == ERROR_IO_INCOMPLETE) {
if (PIC_FullIndex() >= tx_wedge_ms)
dropConnection("write wedged (peer not reading)");
return;
}
tx_inflight = false;
dropConnection("write failed");
}
void CSerialNamedPipe::sendLineState() {
uint8_t f[2] = { FRAME_LINES,
(uint8_t)((cur_dtr ? 1 : 0) | (cur_rts ? 2 : 0)) };
queueSend(f, 2);
}
void CSerialNamedPipe::pumpPipe() {
if (pipe == INVALID_HANDLE_VALUE) return;
for (;;) {
DWORD avail = 0;
if (!PeekNamedPipe(pipe, NULL, 0, NULL, &avail, NULL)) {
dropConnection("peer closed");
return;
}
if (avail == 0) break;
uint8_t buf[512];
DWORD want = avail < sizeof(buf) ? avail : (DWORD)sizeof(buf);
DWORD got = 0;
ZeroMemory(&rx_ov, sizeof(rx_ov));
// overlapped handle needs an OVERLAPPED; Peek guaranteed the bytes
// are buffered, so the wait below completes immediately.
if (!ReadFile(pipe, buf, want, &got, &rx_ov)) {
if (GetLastError() != ERROR_IO_PENDING ||
!GetOverlappedResult(pipe, &rx_ov, &got, TRUE)) {
dropConnection("read failed");
return;
}
}
if (got == 0) {
dropConnection("read failed");
return;
}
inbuf.insert(inbuf.end(), buf, buf + got);
}
// slice complete frames
size_t pos = 0;
while (pos < inbuf.size()) {
uint8_t type = inbuf[pos];
if (type == FRAME_DATA) {
if (pos + 2 > inbuf.size()) break; // need len
uint8_t len = inbuf[pos + 1];
if (len == 0) { // contract: len >= 1
dropConnection("zero-length data frame");
return;
}
if (pos + 2 + len > inbuf.size()) break; // incomplete
for (uint8_t i = 0; i < len; i++)
rxq.push_back(inbuf[pos + 2 + i]);
pos += 2 + (size_t)len;
} else if (type == FRAME_LINES) {
if (pos + 2 > inbuf.size()) break;
uint8_t lines = inbuf[pos + 1];
// null-modem cross: their DTR -> our DSR+CD, their RTS -> our CTS
setDSR((lines & 1) != 0);
setCD((lines & 1) != 0);
setCTS((lines & 2) != 0);
pos += 2;
} else {
LOG_MSG("Serial%d: namedpipe unknown frame type 0x%02x -- protocol bug",
(int)COMNUMBER, type);
dropConnection("unknown frame type");
return;
}
}
inbuf.erase(inbuf.begin(), inbuf.begin() + pos);
}
bool CSerialNamedPipe::doReceive() {
if (rxq.empty()) pumpPipe();
if (rxq.empty()) return false;
uint8_t b = rxq.front();
rxq.pop_front();
receiveByteEx(b, 0);
return true;
}
void CSerialNamedPipe::handleUpperEvent(uint16_t type) {
switch (type) {
case SERIAL_POLLING_EVENT: {
setEvent(SERIAL_POLLING_EVENT, rx_poll_ms);
if (pipe == INVALID_HANDLE_VALUE) {
if (PIC_FullIndex() >= next_retry_ms) tryConnect();
} else {
pollTx(); // reap / restart the overlapped write
pumpPipe(); // keep line-state frames fresh even when idle
}
// directserial's RX state machine (rxpollus/rxburst semantics)
switch (rx_state) {
case P_RX_IDLE:
if (CanReceiveByte()) {
if (doReceive()) {
rx_state = P_RX_WAIT;
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
}
} else {
rx_state = P_RX_BLOCKED;
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
}
break;
case P_RX_BLOCKED:
if (!CanReceiveByte()) {
rx_retry++;
if (rx_retry >= rx_retry_max) {
rx_retry = 0;
removeEvent(SERIAL_RX_EVENT);
if (doReceive()) {
while (doReceive()); // overrun, like directserial
rx_state = P_RX_WAIT;
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
} else {
rx_state = P_RX_IDLE;
}
}
} else {
removeEvent(SERIAL_RX_EVENT);
rx_retry = 0;
if (doReceive()) {
rx_state = P_RX_FASTWAIT;
setEvent(SERIAL_RX_EVENT, bytetime * 0.65f / rx_burst_div);
} else {
rx_state = P_RX_IDLE;
}
}
break;
case P_RX_WAIT:
case P_RX_FASTWAIT:
break;
}
break;
}
case SERIAL_RX_EVENT: {
switch (rx_state) {
case P_RX_IDLE:
LOG_MSG("internal error in serialnamedpipe");
break;
case P_RX_BLOCKED:
case P_RX_WAIT:
case P_RX_FASTWAIT:
if (CanReceiveByte()) {
rx_retry = 0;
if (doReceive()) {
if (rx_state == P_RX_WAIT)
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
else {
rx_state = P_RX_FASTWAIT;
setEvent(SERIAL_RX_EVENT, bytetime * 0.65f / rx_burst_div);
}
} else {
rx_state = P_RX_IDLE;
}
} else {
setEvent(SERIAL_RX_EVENT, bytetime * 0.65f / rx_burst_div);
rx_state = P_RX_BLOCKED;
}
break;
}
break;
}
case SERIAL_TX_EVENT: {
if (rx_state == P_RX_IDLE && CanReceiveByte()) {
if (doReceive()) {
rx_state = P_RX_WAIT;
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
}
}
ByteTransmitted();
break;
}
case SERIAL_THR_EVENT: {
ByteTransmitting();
setEvent(SERIAL_TX_EVENT, bytetime * 1.1f);
break;
}
}
}
void CSerialNamedPipe::updatePortConfig(uint16_t divider, uint8_t lcr) {
(void)divider; (void)lcr; // a pipe has no baud rate
}
void CSerialNamedPipe::updateMSR() {
// modem-in lines are event-driven (0x01 frames); nothing to poll
}
void CSerialNamedPipe::transmitByte(uint8_t val, bool first) {
if (first) setEvent(SERIAL_THR_EVENT, bytetime / 10);
else setEvent(SERIAL_TX_EVENT, bytetime);
uint8_t f[3] = { FRAME_DATA, 1, val };
queueSend(f, 3); // unconnected pipe swallows it (unplugged cable)
}
void CSerialNamedPipe::setBreak(bool value) {
(void)value; // not in the contract; RIO/plasma don't use break
}
void CSerialNamedPipe::setRTSDTR(bool rts, bool dtr) {
if (rts == cur_rts && dtr == cur_dtr) return;
cur_rts = rts;
cur_dtr = dtr;
sendLineState();
}
void CSerialNamedPipe::setRTS(bool val) {
if (val == cur_rts) return;
cur_rts = val;
sendLineState();
}
void CSerialNamedPipe::setDTR(bool val) {
if (val == cur_dtr) return;
cur_dtr = val;
sendLineState();
}
#endif // WIN32
+95
View File
@@ -0,0 +1,95 @@
/*
* VWE fork: serial-over-named-pipe backend (Windows only).
*
* Replaces com0com virtual COM pairs for the vRIO cockpit board and the
* vPLASMA score display: DOSBox is the pipe CLIENT with background retry;
* the peripheral app (vRIO / vPLASMA) is the pipe SERVER. An unconnected
* pipe behaves like an unplugged cable: the UART exists, modem-in lines
* (CTS/DSR/CD) are low, writes are discarded.
*
* Wire format (single duplex byte-mode pipe, typed frames, identical in
* both directions -- contract pinned with the vRIO session 2026-07-12):
* 0x00 <len:u8> <bytes> serial data, len >= 1 (batching allowed)
* 0x01 <lines:u8> sender's own OUTPUT lines: bit0 DTR, bit1 RTS;
* the receiver applies the null-modem cross
* (their DTR -> our DSR+CD, their RTS -> our CTS)
* On connect each side sends one 0x01 with its current state. An unknown
* frame type is a protocol bug: log + drop the connection, no resync.
*
* Conf: serial1=namedpipe pipe:vrio [rxpollus:100] [rxburst:16] [rxdelay:n]
* (pipe:<name> maps to \\.\pipe\<name>; a full \\... path also works;
* rx options have directserial semantics)
*/
#ifndef DOSBOX_SERIALNAMEDPIPE_H
#define DOSBOX_SERIALNAMEDPIPE_H
#include "dosbox.h"
#if defined(WIN32)
#include "serialport.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <deque>
#include <string>
#include <vector>
class CSerialNamedPipe : public CSerial {
public:
CSerialNamedPipe(Bitu id, CommandLine* cmd);
virtual ~CSerialNamedPipe();
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);
private:
void tryConnect();
void dropConnection(const char* why);
void pumpPipe(); // drain pipe -> parse frames -> rxq / line state
bool doReceive(); // deliver one rx byte to the UART if available
void queueSend(const uint8_t* data, DWORD len); // NEVER blocks
void kickTx(); // start the next overlapped write if idle
void pollTx(); // reap a completed / wedged overlapped write
void sendLineState(); // frame 0x01 with our current DTR/RTS
HANDLE pipe = INVALID_HANDLE_VALUE;
std::string pipename;
double next_retry_ms = 0.0; // PIC_FullIndex() of the next connect attempt
// ALL pipe I/O is overlapped: a blocking write here deadlocked the emu
// thread against a peer that also writes-first (both hellos, no reader).
OVERLAPPED tx_ov = {};
OVERLAPPED rx_ov = {};
bool tx_inflight = false;
double tx_wedge_ms = 0.0; // deadline for an unfinished write
std::vector<uint8_t> txq; // bytes waiting for the next write
std::vector<uint8_t> tx_pend; // buffer owned by the in-flight write
std::vector<uint8_t> inbuf; // partial frame reassembly
std::deque<uint8_t> rxq; // decoded serial bytes awaiting the UART
bool cur_dtr = false; // our output lines (mirrors MCR writes)
bool cur_rts = false;
// receive pacing, directserial semantics (VWE RIO low-latency knobs)
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;
};
#endif // WIN32
#endif // DOSBOX_SERIALNAMEDPIPE_H