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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user