# VelociRender wire protocol (Path A — run the original binary unmodified) Reverse-engineered from the original DIVISION source on the archive drive (`sda4/DPL3/`), **not** from disassembly — both ends of the link are present in C: | Role | File | What it is | |------|------|-----------| | Host framing | `sda4/DPL3/VR_COMMS.C` / `VR_COMMS.H` | length-prefixed messaging, byte-sex, boot | | Host link driver | `sda4/DPL3/LINKIO.C` / `LINKIO.H` | INMOS C012 link-adapter port I/O | | Host command builder | `sda4/DPL3/DPL_HOST.C` | turns DPL nodes into `vr_action` messages | | Protocol enum | `sda4/DPL3/VR_PROT.H` | the 24 `vr_action` command tags | | **Board reference impl** | `sda4/DPL3/VRENDER/VR_REMOT.C` | the i860 message loop we are replacing | | Board raster/texture | `sda4/DPL3/VRENDER/DNC.C`, `PAZPL5.C` | PixelPlanes-5 backend | Author throughout: Phil Atkin (PJA), DIVISION Ltd, 1994. Node struct layouts are in `source-ref/DPLTYPES.H` (already used by the `dpl3-revive` parsers). **Goal of Path A:** stand up a *virtual VelociRender board* that speaks this protocol, so `FLYK.EXE` (and the other HPDAVE/DPL3 binaries) run **unmodified** under DOSBox-X. The board's rendering guts are the pipeline `dpl3-revive` already built. --- ## 1. Transport — the link adapter (what DOSBox-X must emulate) The host talks to the board through an **INMOS C012 link adapter, IMS-B004 register layout**, at I/O base `0x150` (default from `getXputer()` / `TRANSPUTER` env var). `LINKIO.C` `setLA()` fixes the map: | Port | Register | Access | Semantics | |------|----------|--------|-----------| | `0x150` | Input Data | R | next received byte | | `0x151` | Output Data | W | byte to transmit | | `0x152` | Input Status | R | **bit0 = 1** → a byte is available to read | | `0x153` | Output Status | R | **bit0 = 1** → ready to accept a byte | | `0x160` | Reset / fifo-ok | W / R | reset (write); `fifo_ok_status` reads bit0 | | `0x161` | Analyse | W | transputer analyse strobe | Byte primitives (`LINKIO.C`): - **out byte:** spin until `Output Status bit0`, then write `Output Data`. - **in byte:** spin until `Input Status bit0`, then read `Input Data`. - `altRecord()` / `inputReady()` = read `Input Status bit0` (non-blocking poll). - `reset()` = analyse=0, reset=0, delay, reset=1, delay, clear both status, reset=0. **Emulation contract for the DOSBox-X device:** - Output Status bit0 → always ready (we consume host bytes into a message assembler). - Input Status bit0 → set whenever we have reply bytes queued; host drains via Input Data. - On `reset` strobe, clear both directions and re-arm the boot handshake. That is the *entire* hardware surface: six ports, no DMA, no IRQ used by the polled DOS path. No i860, no transputer, no PixelPlanes silicon is emulated. --- ## 2. Envelope — framing on the wire Every message, both directions (`velocirender_transmit` / `receive_protocol`), is: ``` +----------------+----------------+---------------------------+ | length_word:4 | action:4 | payload : N bytes | +----------------+----------------+---------------------------+ LE LE (little-endian structs) ``` - `length_word = flags | (id << 16) | count`, little-endian. - **bit31 set** → *iserver* control message (C-runtime service; see §4). **clear** → render command. - **bit30 (`0x40000000`)** → set on **every** length word by the shipped Aug-1995 HPDAVE build (`FLYK.EXE`), on both vr_net and iserver messages (iserver ⇒ `0xC0000000`). It is **absent from the 1994 `VR_COMMS.C` source** — a shipped-vs-snapshot delta found by static analysis (`patha/verify_framing.py`: `or ebp,0x40000000` in the transmit path, plus a global initialized `mov [..],0x40000000`). Exact receive-side meaning is still TBD (routing/epoch flag?); confirm in the dynamic capture. **We mirror it** on replies (`vrboard.LW_FLAG`) so our board is byte-identical to the real one; our decoder ignores it (masks low-16 for count, tests bit31 for iserver). - bits 16–23 = node id; `0xff` = broadcast (host→board render commands always use `0xff`). - low 16 bits (`count`) = number of bytes that **follow the length word** = `4 (action) + N`. Max 1024; the host caps payloads and fragments (see `velocirender_packetize`, 508-byte chunks). - `action` = a `vr_action` enum value (§3), int32 LE. - `payload` = command-specific bytes (§3). Multi-byte fields are LE on the wire; the `dpl_little_endian` path sends host memory verbatim (`endian_fix` only matters on big-endian hosts, which we are not). **Reply envelope is identical.** The board handler returns `ret` bytes; `reply()` re-frames them with the **echoed action** as the first int32. The host (`velocirender_receive`) reads `action`, then `count-4` payload bytes, and matches the action to know the reply is for the command it sent. Synchronous vs. async: - Most commands that expect data back (`create`, `delete`, `set_geom_verts`, `set_texmap_texels`, `init`) do **transmit → receive** and block for the reply. - `draw_scene` is **async**: the board replies with a bare `vr_draw_scene_action` frame-ack later; the host tracks `dpl_frame_replied` and picks it up in `velocirender_frameack` / `wait_draw_scene_complete`. --- ## 3. Command set (`vr_action`, VR_PROT.H) and payloads Enum ordinals are positional (0-based, in declaration order). Payload columns are the bytes **after** the 4-byte action. "Reply" is what the board sends back (0 = none). | # | action | payload (host→board) | reply | source | |--:|--------|----------------------|-------|--------| | 0 | `vr_init` | init arg string (leftover `DPLARG` opts joined by `\|`, NUL-term) | `int32 = 1` | DPL_HOST.C `ghost_dpl_init` | | 1 | `vr_create` | `int32 type_check` | `int32 remote` (board handle) | `velocirender_create` | | 2 | `vr_delete` | `int32 remote, int32 type` | echo | `velocirender_delete` | | 3 | `vr_flush` | **the node struct body** (see §5) | none | `velocirender_flush` | | 4 | `vr_sect_pixel` | `dpl_POINT + float x,y` | inst handle (stubbed here) | `ghost_dpl_sectpixel` | | 5 | `vr_sect_vector` | `dpl_POINT + float x0..z1` | inst handle (stubbed here) | `ghost_dpl_sectvector` | | 6 | `vr_dcs_nest` | `int32 parent_remote, int32 node_remote` | none | `ghost_dpl_nest_dcs` | | 7 | `vr_dcs_link` | `int32 bro_remote, int32 sis_remote` | none | `ghost_dpl_link_dcs` | | 8 | `vr_dcs_prune` | `int32 parent_remote, int32 child_remote` | none | `ghost_dpl_prune_dcs` | | 9 | `vr_draw_scene` | `int32 double_buffered` | **async** `vr_draw_scene` frame-ack | `ghost_dpl_draw_scene` | | 10 | `vr_draw_scene_complete` | (poll only, via input status) | — | host-side only | | 11 | `vr_list_add` | `int32 head_remote, int32 node_remote` | none | `ghost_dpl_add_list_item` | | 12 | `vr_list_remove` | `int32 head_remote, int32 node_remote` | none | `ghost_dpl_remove_list_item` | | 13 | `vr_morph` | `int32 morphed,a,b_remote, float32 alpha` | (stub) | `ghost_dpl_morph_object` | | 14 | `vr_version` | — | version fields (host fills locally) | `ghost_dpl_version` | | 15 | `vr_statistics` | — | stats record | `velocirender_statistics` | | 16 | `vr_readpixels` | `x, y, n_pixels` | **pixel data** (see §6) | see §6 | | 17 | `vr_hspcode` | link addr + 128-byte HSP blocks | — | boot only | | 18 | `vr_860code` | i860 .text (7-word header + segment) | — | boot only | | 19 | `vr_860data` | i860 .data segment | — | boot only | | 20 | `vr_860bss` | i860 .bss size | — | boot only | | 21 | `vr_860args` | i860 argv string | — | boot only | | 22 | `vr_set_geom_verts` | header + vertex/connection blocks (§5.2) | `int32` ack | `ghost_dpl_set_geometry_vertices` | | 23 | `vr_set_texmap_texels` | header + 64-int32 texel blocks (§5.3) | `int32` ack | `ghost_dpl_set_texmap_texels` | Board dispatch that a virtual board mirrors: `VR_REMOT.C:remote_velocirender()` — receive message, `action = data[0]`, `param_data = &data[1]`, switch on action, and if the handler returns `ret>0`, `reply(ret,...)`. --- ## 4. Boot sequence (must be satisfied before any rendering) `start_velocirender()` (VR_COMMS.C) runs, in order: 1. `setLA(0x150); initLA();` 2. `reset()` — the analyse/reset strobe sequence on ports 0x160/0x161. 3. `boot_xputer()` — streams the transputer `.BTL` file raw in 2 KB blocks out the link. 4. **3-transaction iserver handshake per i860** (`startup_handshake`): the (real) i860 C-runtime boots and issues 3 iserver requests (getenv-style); the host services them in `iserver_action`. Comment in source: *"relies on the knowledge that the C run-time boots up and does 3 iserver transactions before closing down."* 5. `boot_HSP()` — 128-byte HSP microcode blocks via `vr_hspcode`. 6. `boot_860()` — reads the `.MNG`: a 7×int32 header (csize, dsize, bsize, cstart, dstart, bstart, entry) sent via `vr_860code`, then code/data/bss segments via `vr_860code/860data/860bss`, plus argv via `vr_860args`. 7. `vr_init` with the init arg string; waits for the reply. **Virtual-board behaviour:** accept and **discard** all boot payloads (BTL/HSP/MNG); we don't run that firmware. The only thing we must *reproduce* is the handshake shape so the host proceeds: emit the 3 iserver transactions the host services in step 4, and reply to `vr_init`. The `iserver_action` cases the host implements (and therefore the kinds of request it expects from us) are: 40=commandline, 42=version(1.50), 32=getenv, 24=fputblock, 13=fwrite, 16=fflush. For getenv we can reply with empty strings; the goal is only to advance past the 3-transaction gate. --- ## 5. The node / handle / flush model (the heart of the renderer contract) DPL is a scene graph of typed nodes (`DPLTYPES.H`): `dpl_DCS` (transform), `dpl_GEOMETRY`, `dpl_GEOGROUP`, `dpl_LOD`, `dpl_OBJECT`, `dpl_INSTANCE`, `dpl_MATERIAL`, `dpl_TEXTURE`, `dpl_TEXMAP`, `dpl_LIGHT`, `dpl_LMODEL`, `dpl_RAMP`, `dpl_VIEW`, `dpl_ZONE`. **Handles.** Each node has a `remote` field = its board-side identity. `vr_create` sends the node type; the board allocates the object and returns a handle; the host stores it in `remote`. All later references use handles, not host pointers. *A virtual board just hands out unique 32-bit ids and keeps an id→object table.* **Pointer→handle rewrite (`remotize`).** Before flushing a node, the host swaps every embedded child pointer for that child's `remote` handle (`DPL_HOST.C` macro `remotize`), flushes, then restores local pointers. So **on the wire, all inter-node links are handles** — directly usable by our board's object table. ### 5.1 `vr_flush` — node body upload `velocirender_flush` sends the node struct starting at the `remote` field for `sizeof(struct) - delta` bytes (`delta = sizeof(dpl_node) - sizeof(dpl_remote_node)`, i.e. it skips the local-only header). The board `memcpy`s it over its copy. This is how transforms (the DCS 4×4 matrix), material colours, LOD ranges, instance refs, view params, light params, etc. reach the board. **Field layouts come straight from `DPLTYPES.H`** — the same structs `dpl3-revive/parser` already reads. ### 5.2 `vr_set_geom_verts` — geometry Header record (5×int32 used): `remote, vert_blox, conn_blox, vertices, connections`, then `vert_blox` × `sizeof(dpl_VERTEX_LIST)` blocks, then `conn_blox` × `sizeof(dpl_CONNECTION_LIST)` blocks (each a linked-list node sent whole). Board reassembles into its geometry. Reply = int32 ack. ### 5.3 `vr_set_texmap_texels` — textures Header (5×int32 used of 8 sent): `remote, n_texels(=u*v), u_size, v_size, mode`, then texels streamed **64 int32 at a time** (`endian_fix=0`, raw). `mode`: `1`=8-bit (bilinear path, `dN_pxpl5_texture8`), `2`=24-bit FX (`dN_pxpl5_fx_texture24`), else 24-bit (`dN_pxpl5_texture24`). Texel word format matches the SVT `0BGR` convention already decoded in `spec/SVT_FORMAT.md`. Reply = int32 ack. --- ## 6. Display path — the one item to pin against `FLYK.EXE` In **this DPL3 source snapshot**, `velocirender_readpixels`, `sectpixel`, `sectvector`, and `morph` all `return 0` (stubs): the board rendered to its own PixelPlanes video output (NTSC/SVGA off the card), so the host never read pixels back. But the **shipped `FLYK.EXE`** contains active pixel-readback machinery — strings `_rframebuffer/_gframebuffer/_bframebuffer`, `dump_frame_buffer`, `vr_read_pixels`, `velocirender received only %d bytes`, `VelociRender flush on 2d display 0x%x`. So the HPDAVE build is a **later/derived** library where readback *is* implemented and the host paints its own SVGA/VESA framebuffer from the returned pixels. This is the ideal seam for Path A and must be confirmed against the binary: - **If FLYK uses readback (expected):** our virtual board renders a frame (GPU or the existing CPU rasterizer) and returns it as `vr_readpixels` payload; the host's own SVGA path displays it. We never emulate a video encoder — the host shows the pixels. - **If any content instead relied on board-direct video:** the virtual board opens its own output window rather than returning pixels. (Applies at most to binaries lacking the readback strings.) **Action:** trace `vr_readpixels` in `FLYK.EXE` (its `dpl_readpixels` → the `DPL.C:3804 ghost_dpl_readpixels` call site, reimplemented in the shipped lib) to nail the exact request args (`x, y, n_pixels`) and the reply pixel packing (word order, chunking — likely the same 64-word fragmentation as texels). This is the only payload whose shipped form isn't fully fixed by the snapshot. --- ## 7. Virtual-board architecture ``` FLYK.EXE (unmodified, 32-bit Watcom/DOS4G) │ port I/O 0x150-0x161 (polled C012) ▼ DOSBox-X custom device "virtual C012 link adapter" ← NEW, small │ byte stream ⇄ message assembler (§1,§2) ▼ VelociRender protocol server (§3-§6) ← NEW, the real work │ id→object table, flush→struct decode, scene graph ▼ Renderer = dpl3-revive pipeline (transform/light/cull + raster/GPU) ← REUSE │ rendered RGBA frame ▼ vr_readpixels reply ─────────────────────────────────► host SVGA display ``` Work split: - **New, mechanical:** the DOSBox-X C012 device (six ports + a byte FIFO) and the message-envelope assembler. Bounded and testable in isolation. - **New, substantive:** the protocol server — handle table, the `vr_flush` struct decoders (mapping `DPLTYPES.H` structs to our scene objects), and boot-handshake emulation. `VR_REMOT.C` is the line-by-line reference. - **Reused:** geometry/texture ingestion, the DPL instance transform, materials + gamma, and rasterization already exist in `dpl3-revive/parser` + `viewer`. The protocol server feeds them the *same* data the file parsers produce — it arrives over the wire instead of from `.BGF`/`.SVT` files. ### Why Path A is now cheap The two things that usually make "run the original binary" expensive — reversing an undocumented protocol, and rebuilding the renderer — are both already solved here: the protocol is in source (this doc), and the renderer is the project we've been building. Path A is mostly *plumbing the existing renderer behind a wire protocol*, plus a tiny emulated link card. ### Relationship to the reimplementation track Roadmap items 6–7 (port the geometry pipeline; GPU rasterizer) reimplement DPL3 as a standalone viewer and discard the EXE. Path A instead keeps the **original host binary** (its scene scripting, `.EVT` event timing, game logic, input, frame pacing) and substitutes only the renderer. The two share the same rendering core; Path A adds the link device + protocol server on top. --- ## 8. Open items / next steps 1. **Confirm the readback display path in `FLYK.EXE`** (§6) — decides whether the board returns pixels or self-displays. Highest priority; everything else assumes readback. 2. **Pull the exact `vr_readpixels` request/reply layout** from the shipped lib (`DPL.C` readpixels path) — the only payload not fully fixed by the snapshot. 3. **Lock struct sizes** for `vr_flush` bodies and the vertex/connection blocks against `DPLTYPES.H` on this exact build (watch Watcom struct packing/alignment). 4. **Prototype the DOSBox-X device** headless first: log the boot handshake + `vr_init` from a real `FLYK.EXE` run, verify our assembler frames the messages, then add rendering. 5. Decide host: DOSBox-X custom device (C++ in the emulator) vs. a shim DLL — DOSBox-X is the right home because the app does **direct port I/O**, with no driver hook point. --- ## 9. Validation status **Static (patha/verify_framing.py, vs the shipped `FLYK.EXE` code object).** Found, as literals in the transmit/link code: the `0x00FF0000` broadcast mask, the `0x80000000` iserver bit, the `0x1FC` (508) packetize chunk, the `0x150` link base, and C012 port I/O. Disassembly of the length-word construction confirmed the structure — and revealed the shipped-build **bit30 `0x40000000`** flag absent from the 1994 source (§2), now mirrored. **Dynamic (real `FLYK.EXE` under stock DOSBox-X 2026.07.02, no responder).** FLYK boots, drives the link at 0x150, streams the transputer boot, then blocks in the handshake awaiting the board. The unmapped port reads `0xFFFFFFFF`, and FLYK prints: ``` Protocol error : length 65535 too big, length_word 0xffffffff +++ERROR : rcv_protocol fail during iserver handling, xmits=0 ``` This is exactly `receive_protocol` (`nb = length_word & 0xffff` = 65535 > 1040 → reject) called from `handle_iserver_stuff` during `startup_handshake` — confirming, against the real binary, that the length word is 4 bytes with the count in the low 16 bits, and that the *only* thing missing is a board that answers. Two more benign shipped-vs-snapshot deltas surfaced: the shipped error strings additionally print `length_word` and `xmits` (same framing). A responder that satisfies the 3-transaction handshake (§4) will let FLYK proceed into the framed HSP/860/init/scene traffic. **Live (custom DOSBox-X build + our C012 bridge + real FLYK.EXE).** A custom DOSBox-X was built with `src/hardware/vrlink.cpp` (the C012 bridge, ports 0x150–0x161 → TCP 127.0.0.1:8620, enabled by env `VRLINK=1`; init hooked next to `GLIDE_Init` in `sdlmain.cpp`). Running real FLYK against a **quiet capture board** (patha/vrcapture.py) captured 80,909 bytes: `00 52 53 45 54` ×3 (FLYK's `reset()` strobing port 0x160 three times) + the **full 80,894-byte transputer boot stream** (`f0 b4 d1 d1 d1 24 …` = VRENDMON.BTL verbatim). FLYK then printed: ``` timeout in inRecord velocirender_receive timed out - sends_wo_rcv=0 velocirender_input failed in start_velocirender ``` — a *clean* timeout (not the stock garbage-read), i.e. the bridge faithfully carries the protocol and FLYK is now waiting for the board to answer. **The whole path is proven: custom build → C012 ports → socket → Python board.** Iteration from here needs **no rebuild** (board logic is pure Python). **Next blocker (precisely located): the post-BTL boot handshake.** After the boot download the shipped `start_velocirender` calls `velocirender_input` expecting the board to send the first message (the transputer/i860 C-runtime startup handshake — the 1994 source's "3 iserver transactions", but the shipped flow differs and uses `velocirender_input`/`sends_wo_rcv`). An unknown iserver tag makes the host `exit(666)`, so the exact expected bytes should be nailed by disassembling shipped FLYK's `velocirender_input` path (or careful empirical bring-up), not guessed. Once the handshake passes, FLYK proceeds to the framed `vr_860*` MNG download → `vr_init` → scene traffic → `draw_scene`/`readpixels`. --- ## 10. END-TO-END ACHIEVED — FLYK.EXE runs its full render loop through the board Custom DOSBox-X (`src/hardware/vrlink.cpp`, env `VRLINK=1`) + `patha/vrrun.py` board + real `FLYK.EXE`. FLYK now runs unmodified through the entire protocol into its main loop, with **no errors**: 1. Transputer `.BTL` boot streamed (byte writes to 0x151); board sends one vr_net frame to satisfy `velocirender_input` (§4). 2. i860 `.MNG` firmware downloaded as framed `vr_860code/data/bss/args` (833 msgs) — discarded. 3. `vr_init` (real argv) → board replies. 4. **Full SHARKS scene graph built** (~69 nodes): `create` (fire-and-forget, host-assigned handle in payload word 2 — not the 1994 reply model), `flush` (struct bodies), `dcs_link`, `list_add`; loads geometry/texture/material search paths, sfx, fog. 5. **Main loop**: `draw_scene` per frame (board async-acks) + per-frame `0x1d` (56B). `0x2a` (8B) is the **velocirender_sync barrier** (echo-ack it); `0x1c`/`0x1d` are fire-and-forget. ### C012 FIFO wire protocol (definitive, from vrlink port logging) Framed message = two channels, **body first**: - **Body** (action+payload) → **port 0x154**, 16-bit `rep outsw` words (padded to even; the length word's `count` is the true unpadded length). - **Trailing length word** (`0x40ff00NN`, 4 bytes) → **port 0x151**, byte writes (commits it). - Replies read **byte-wise from port 0x150** as serial `[length_word][body]`. - Boot `.BTL` streams byte-wise to 0x151 (no body pending → raw passthrough). `vrlink.cpp` demuxes and re-emits length-first `[length_word][body(count)]` to the board. ### Shipped-vs-1994 deltas - Length word always sets **bit30 (0x40000000)**. - **FIFO transport** (0x154 word body + 0x151 trailing length) vs source's serial byte path. - `create` **fire-and-forget**, **host-assigned handle** (payload word 2), no reply. - New actions: **0x2a** = `velocirender_sync` (echo-ack), **0x1c/0x1d** = fire-and-forget data / per-frame ops (geometry/camera — not yet decoded). ### Remaining = RENDERING (not protocol) FLYK spins `draw_scene` with no wait (instant ack) and issues **no `vr_readpixels`** in this scene. To see the demo: decode the geometry/material carried by `flush` bodies + `0x1c/0x1d`, feed the dpl3-revive renderer, present frames (return via `vr_readpixels` if FLYK pulls them, else a board-side window), and pace `draw_scene` acks to ~60 Hz for real-time animation. --- ## 11. Rendering phase — live scene-graph decode (patha/analyze_scene.py) What FLYK streams (decoded from the framed capture of a SHARKS.SCN run): | Data | Carrier | Notes | |------|---------|-------| | Camera | `flush` body, type 3 (view) | matrix + 832×512 screen + clip + fog + back_color(0.4,0.6,0.9) | | Transforms | `flush` bodies, type 5 (dcs) ×26 | 4×4 `dpl_MATRIX` each | | Placements | `flush` bodies, type 4 (instance) ×22 | `dcs`,`object`,`f_material`… handles (`dpl_INSTANCE`) | | Materials | `flush` body, type 12 ×1 | emissive/ambient/diffuse/opacity/specular (`dpl_MATERIAL`) | | **Per-frame animation** | **action 0x1d** (2×/frame, 56B) | transform/camera update — drives the flythrough | | **SPECIALFX particles** | **action 0x1c** (80B) | matches SHARKS.SCN SPECIALFX params (11.0,360,0.9,-19,0.99…) | | Geometry meshes | **referenced by name → files** | NOT on the wire — no `set_geom_verts`/object/geometry creates; loaded board-side from the geometry/texture search paths. dpl3-revive already parses these (`.BGF`/`.SVT`). | Flush body layout = `[remote:4][type_check:4][struct fields after dpl_node]` (flush starts at `&node->remote`). Struct field offsets per `source-ref/DPLTYPES.H` (mind the `#if REMOTE` fields). **Key finding:** the whole *scene graph and animation* come over the wire; only the *geometry* is by-reference (file-loaded). And FLYK issues **no `vr_readpixels`** — the real board drove video directly — so frames must be presented board-side, not returned to FLYK. ### Rendering approaches (design fork) - **A — Hybrid (fastest to visible):** take the live camera + per-frame `0x1d` animation from the protocol; render the SHARKS scene's geometry/placements from files via the existing dpl3-revive pipeline; present in a board-side window. FLYK drives camera/timing; sidesteps the instance→object→file handle mapping (which isn't on the wire). - **B — Faithful (purist):** additionally decode instance→object references and reproduce exact placements/materials/sfx from the wire, using files only for the raw meshes. More decode work (resolve how object handles map to geometry files board-side). Either way the renderer core is the dpl3-revive pipeline; pace `draw_scene` acks to ~60 Hz so FLYK animates at real speed instead of spinning. ### Hybrid render achieved (patha/hybrid_render.py -> patha/flyk_render.png) First visible frame: the SHARKS.SCN reef FLYK.EXE is running, assembled from files by the dpl3-revive pipeline and viewed from an in-scene (diver) camera — a textured shark in the reef. The fish cluster (-220,225,-350) matches FLYK's live 0x1d translations (y~230, z~-200), confirming the wire animation targets the same content. Camera *projection/fog/back_color* are taken from the decoded VIEW body (§11); the *pose* is a DCS (animated by 0x1d). --- ## 12. FULL PATH B ACHIEVED — geometry on the wire, live board-side window (2026-07-04, session 2) **The §11 "geometry is file-loaded board-side" conclusion was wrong — it was an artifact of a broken run.** Root cause chain, proven by live iteration: 1. `HPDAVE\SHARKS.SCN` searches `..\geometry`, `..\sharks`, `..\texture` — none of which exist beside `sda4\HPDAVE`. FLYK **fails object loads silently** (the "Failed to load object" print only fires when `dpl_NewObject` returns NULL, not when the file is missing) and builds an empty world: every instance flushed with `object=NULL`, no geometry/texture traffic. That was the capture §11 analyzed. 2. The shipped FLYK registers object-load extensions `.bgf .bmf .bsl .tga .vtx` (strings in the EXE) — **not** `.b2z/.svt`. But its `dpl_bgfRead` validates the same `DIV-BIZ2` container the `.B2Z` files hold ("`dpl_bgfRead "%s" is not a valid biz file`"). 3. **Fix = staging** (`patha/stage_assets.py` → `c:\temp\flykc`, mounted as C: by `patha/flyk_vr.conf`): copy `HPDAVE`, copy `DPL3\GEOMETRY` exposing `*.B2Z` as `*.BGF`, decode `DPL3\TEXTURE\*.SVT` (raw xBGR 32-bit) to `*.TGA`. With that, FLYK loads the reef and **uploads everything over the link**. (`fishes`, `banner`, and the `sharks:*` textures have no surviving source files anywhere on the drive — those entities stay object-less; the i860 firmware has no file I/O, so nothing was ever board-loaded.) ### Shipped wire protocol, geometry phase (all decoded live) - **Shipped node type ids differ from 1994 from 7 up**: 2=zone 3=view 4=instance 5=dcs 6=lmodel **7=object 8=lod 9=geogroup 0xa=geometry 0xb=material 0xc=texmap** (1994 order was object=8…material=12). - **Action 0x17** (1994 `set_texmap_texels`) = **bulk vertex upload**. Header 36B = `[geom_handle][0][n_verts][stride_words][n_conns][vert_type][geo_type][conn_sz][GEOMSCALE:f32]`, then framed data fragments (≤508B); **one ack** (echo action, int32 1) after `n_verts*stride*4` bytes. Vertex strides: `vtype 0x13` = `[x y z nx ny nz u v]`, `vtype 0x15` = `[x y z r g b a u v]`. `gtype 5` = pmesh, `gtype 2` = implicit single polygon (no connectivity follows). **Wire positions are divided by GEOMSCALE; the header's float restores model units** (verified against raw .B2Z bounds). - **Action 0x19** = **add connections**. Header 16B = `[geom_handle][n_conns][indices_per_conn] [flags]`, then `n_conns*per` int32 indices; one ack. (Ocean quad = `[1][4]` + `0,1,3,2`; fish pmesh = `[70][3]` + 70 triangles.) - **Graph refs in flush bodies** (wire-payload word indices, where word0=remote): instance word6 = object handle; geogroup words16/17 = f/b material; DCS matrix words4–19 (4×4 row-major, translation in row 3), word20 = parent DCS (instance DCSs → world DCS 0x3; camera tree uses explicit `dcs_link` 0x1→0x2, 0x1→0x3 instead). object→lod→geogroup→geometry attach via `list_add`. Repeated STATICs share objects (host object file cache). - **VIEW body** (104B, projection only, no matrix): wire float idx 6..17 = `x0 y0 x1 y1 zeye x_size y_size hither yon back_rgb[3]`, idx18 = fog_enable, idx19..23 = fog `[near far r g b]`. SHARKS: ±1 × ±0.615 image plane at zeye=1.3, 832×512, clip 8..4500, back (0.4,0.6,0.9), fog 500..4000 (0.05,0.1,0.12). - **Action 0x1d** = per-frame DCS transform (shipped `flush_artics`): `[dcs_type=1 (matrix)][dcs_handle][3×3][tx ty tz]`, fire-and-forget, 2/frame in SHARKS: handle 0x1 (root, constant identity+t(0,5,5) when idle) and the DYNAMIC shark's DCS (spline path from SHARK1.SPL, scale 8 baked into the 3×3). **The 0x1d 3×3 is row-vector convention, same as the flush matrices** — proven by `patha/heading_test.py`: across ~7000 records, model −Z tracks the swim velocity (dot ≈ −1, 95% of samples); a transpose mirrors the yaw (user-visible as reversed turning). Corollary: **the spline animation is built for nose-along−Z models.** The original `..\sharks` shark (lost) was authored that way; the surviving stand-in `DPL3\GEOMETRY\SHARK.B2Z` is nose-along-+X, so the renderer applies a +90° yaw model correction (X→−Z) to dynamically animated instances only. - **Camera** = inverse of (M_dcs0x2 · M_dcs0x1); look −Z, row-vector convention (p′ = p·M), child→parent composition. 0x1d on 0x1 = head motion (joystick when a pilot is present). ### Board-side real-time window (Path A/B complete) `patha/vrview.py` (numpy rasterizer + pygame, ~18–30 fps at 640×400) renders straight from the live board state: wire geometry, DCS graph, VIEW projection + fog + back color, 0x1d animation. `python vrrun.py --view` presents each `draw_scene` and paces the ack (≤60 Hz), so the unmodified FLYK.EXE runs its demo in real time against the virtual board — swimming shark, fish cluster, kelp, ocean planes, all from the wire. Offline proof/debug: `patha/test_view.py` (replay a capture → PNG; `flyk_live_render2.png` = the shark mid-swim), `patha/census.py`, `patha/decode_anim.py`. ### Run recipe (session 2) 1. `python patha/stage_assets.py` (once; rebuilds `c:\temp\flykc`) 2. `python patha/vrrun.py cap --view` 3. `$env:VRLINK='1'; G:\DOSBox-X\dosbox-x-vrlink.exe -conf patha\flyk_vr.conf` ### SDEMO findings (2026-07-04, session 2 continued — the full Hull Pressure demo) Nearly all HPDAVE scenes point their GEOMETRY path at `.\video`, which **survives intact** (201 files) — SHARKS.SCN was the worst-provisioned scene on the drive. `stage_assets.py` now stages `HPDAVE\VIDEO`; `run_demo.py ` selects scene/exe via `C:\RUNSCN.BAT`. SDEMO (144 entities, 41 objects, 12 splines) runs live end-to-end and decoded more of the protocol: - **Action 0x1a = set_texmap_texels** (1994 layout): header 32B `[texmap][n_texels][u][v][mode][3 junk words]`, then 64-int32 (256B) chunks; ONE ack. Texel word = `[pad,B,G,R]` (confirmed against DNC.C `dN_pxpl5_fx_texels`). - **mode 1** = 8-bit intensity (grayscale replicated across B,G,R) — decodes clean. - **mode 0** = **BSL bit-slice pack**: the page holds up to eight 4-bit monochrome textures, one per nibble plane of the 32-bit texel word (verified: planes of SDEMO's texmap 0x3e are distinct coherent sub-interior textures; a 256×256 page holds the VW logo + a mural in different byte lanes). Each dpl_TEXTURE node sharing the texmap carries a small-int slice selector late in its flush body (values 0x14–0x1a observed) — exact value→nibble mapping not yet pinned. Matches the b2z `bitslice` tag (0x018) and the `.BSL` files in VIDEO. - **More vertex formats** on 0x17 uploads: vtype 0x01 stride 3 `[xyz]`, vtype 0x11 stride 5 `[xyz uv]`, vtype 0x41 stride 4 (gtype 0xa, spheres/points?), in addition to 0x13/0x15. - **Offset vertex updates**: 0x17 header word1 = first-vertex index, word7 = total vertex count (word1+count ≤ word7) — `dpl_UpdateGeometryVertices`, used for vertex animation. The board must merge uploads into the full array, not replace. - **Geometry flush body references its texmap directly** (stored word 3), simpler than the geogroup→material→texture→texmap chain. - **Corrected type ids: 0xc = texture, 0xd = texmap** (earlier §12 guess had them swapped). - Per-frame traffic: heavy **vr_morph** (action 13 — fish-school vertex morphing, .V2Z), **0x1b** (72B, periodic), **0x22** (8B), 0x1c SPECIALFX — all currently discarded. ### Cross-product runs (session 2 continued): KLNGVID (Star Trek), BLADE (Red Planet) Other products stage at the mount root (their scenes use absolute `\STDAVE\VIDEO`-style paths) with the HPDAVE FLYK runner copied in (`stage_assets.py stage_product`). Decoded: - **Material flush body** (0xb, wire words): texture@2, **emissive@5-7, ambient@8-10, diffuse@11-13, opacity@14-16, specular@17-20** (stored-body idx −1). Applied in vrview. **Exact ambient=(1,0,0) AND diffuse=(1,0,0) is the shipped build's UNSET-material marker, not a colour** — every .B2Z-default material (all of SHARKS) and the Trek star materials carry it; render as white. (Found as a red-reef regression during the SHARKS re-validation run.) - **dpl_TEXTURE flush body word 14 (stored 13) = BSL bit-slice selector** (0x14…0x1a). - **0x1d COMPOSES with the flushed DCS matrix** (M_flush · M_anim, row-vector): the DCS holds model scale (KLNGVID klngn 0.1), the spline pose rides on top. SHARKS' identity base matrix had made replace-vs-compose indistinguishable. - **Action 13 (vr_morph)** = `[morphed][geomA][geomB][alpha:f][flags]` — board-side vertex blend a→b into the morphed geometry (SDEMO fish schools). Implemented. - **Action 0x1b** = `[zone][dcs][4×4]` occasional zone-root matrix refresh; identity 3×3 + uninitialized row-3 junk when the host DCS is identity-flagged — ignore those. - **SCHILD/DCHILD effect attachments** (Trek shield bubbles, invisible-until-hit) carry a nonzero hierarchy pointer at wire word 21 of their DCS body — vrview skips them. - **Shipped FLYK quirks**: KLNGVID's `START x y z` parses z wrong (camera got y duplicated into z — authentic shipped behavior); `TRACKER KEYBOARD` input path not yet working under DOSBox (keys don't move the camera — investigate how the tracker reads the keyboard). - **MAPS\*.SCN files are a different key=value dialect** (`viewangle=60.0`) that hangs FLYK's parser — stage SCENES\ versions, never MAPS. - **BLADE.SCN runs**: 770 geometry uploads, 29 texture pages, 200+ instances — the Mars canal city renders fully textured (RPDAVE pages are full-color, no BSL packing). ### Completeness batch (session 2 close-out) — all five scenes regression-PASS Implemented and validated by `patha/regress.py` (replays every scene capture, renders, reports; zero handler errors across SHARKS/SDEMO/KLNGVID/BLADE/FXTEST): - **Wire lighting**: lmodel (0x6) / light (0xe) bodies = `[dcs@2][light_type@3][r g b @4..6]` (stored words); type 2 = ambient, type 3 = directional with aim = the light-DCS's Z row (SHARKS: sun (0,1,0) from rx=−90 ✓). Renderer uses scene ambient + suns, double-sided. - **LOD range selection**: lod body stored words 15/16 = switch_in/switch_out; renderer picks by eye distance (fallback LOD 0). - **SCROLL**: board-side autonomous texture animation from the texture body's u0/v0/du/dv (stored floats 8..11), du/dv per second. - **BILLBOARD**: instance stored word 3 == 2 (vs 3 normal) — camera-facing spherical billboard (FXTEST `flamebig`). - **Sphere lists** (vtype 0x41, `[x y z r]`): shaded screen-space discs. - **sect_vector**: real Möller–Trumbore picking over live world triangles; reply = int32 instance handle. Self-test: an upward underwater ray correctly hits the ocean surface quad. (sect_pixel still needs camera unprojection of board 832×512 coords.) - **vr_readpixels**: experimental reply — `[pad,B,G,R]` pixel words from the last rendered frame, 508B fragments; no shipped binary has exercised it yet. - **Action 0x22** = name binding `[handle][0x8000xxxx name id]` (board createName; fire-and-forget; stored in board.names — GEOMETRIZE 0x8000005a-style refs resolve here). - **BSL slice mapping**: rank-based (sorted texture-node selectors → content nibble planes in order). No more rainbow; exact selector→nibble encoding still unproven. ### SPECIALFX (action 0x1c = install_sfx) — decoded + ambient sim implemented Payload (80B): `[code][texture ref][type][velocity][size][off_y][bias][cook_r g b] [variance][gravity][o_cool][cool][frags][rpt][4 host ptrs]` — exactly the .SCN SPECIALFX columns. Defs are *installed* at scene load; two trigger models: - **Ambient** (SHARKS bubbles/marine snow): the board steps installed defs autonomously. Implemented in vrview `_particles`: per-def pools (frags×rpt), spawned in a camera-centred volume at bias/off_y height, rise at `velocity` with `variance` jitter and `gravity`, fade by `o_cool^age`, lifetime 1/`cool`; soft-blended discs, no z-write. Regression stays green on all five scenes. - **Event-triggered** (`PSFX \fx\*.pfx` lines in .EVT scripts, MORPH_OBJ/MORPH_MTL events): confirmed in FXTEST that *nothing* fires without the input path — not even the t=0.2 morph — so runtime FX triggering is **blocked on the keyboard tracker**, not on the protocol. The .PFX emitter format is self-documented in the files (position/velocity/radius/colour-in-out/duration + variances). ### Input SOLVED — joystick flight via the game-port (THRUSTMASTER) tracker FLYK's `TRACKER KEYBOARD` is inert under DOSBox (no key ever moved the camera), but `TRACKER THRUSTMASTER ` reads the DOS game port, which DOSBox maps from the physical joystick — **verified live: BLADE flown with a Logitech Extreme 3D Pro**, camera 0x1d translations tracking the stick on the wire. `run_demo.py` now rewrites any scene's TRACKER line to `THRUSTMASTER 4.0 0.3` before launch (`--no-joystick` to keep the original). FLYK's input layer is joystick-first throughout (`joystick_read`, DIVISION game-port calibration, `joystick.cal`). ### Fire control (action 0x23) + calibration notes - **Stale joystick.cal = constant-velocity camera drift** (stick center offset reads as permanent deflection). Fix: delete the staged cal; FLYK runs its RE-CALIBRATION flow (wiggle to full scale) and writes a fresh one matched to the DOSBox-mapped stick. - **Action 0x23** = fire/pick event on joystick trigger: `[u:f][v:f][host ptr][-1]`, (0.5, 0.5) = crosshair center. **CONFIRMED fire-and-forget** — replying desyncs the host stream (tested both ways: unanswered 0x23s are harmless; one reply stalled the run). The board still resolves the pick internally (pick_screen: VIEW-frustum unproject → Möller–Trumbore) for telemetry; FLYK's own hit/FX consequences must be computed host-side or armed by a mechanism still undecoded (the .EVT `PSFX` chain never fired in any FXTEST run). ### Protocol edges closed (session 3) - **Action 0x18 = get_geom_vertices** — found by static hunt (LE fixup-table parse + capstone over shipped FLYK: `mov eax,0x18` into velocirender_transmit, 0x28-byte request, reply must echo action 0x18 into a 0x100-byte receive buffer). Board replies with the geometry's merged current vertex floats (request word0 = handle, words 1/2 honored as vertex0/count when sane) in ≤240B echoed-action frames. Regression: byte-exact round-trip of an uploaded quad. `patha/hunt_actions.py` is the reusable hunt tool. `get_geom_numconns` exists only in the game binaries (RPL4OPT etc.), which are Borland **PE** images (32RTM DPMI), not Watcom LE — needs a PE-based hunt, filed under the MUNGA campaign. - **sect_pixel** — wired to pick_screen (auto-normalizes board-pixel vs 0..1 coords); crosshair self-test picks a live instance. - **statistics** — 1994 reply (int32 1) per VR_REMOT.C. **version** confirmed board-silent (host-local; not in the board dispatch switch). - **readpixels** — self-tested: correct echoed-action fragmentation (100 px → 1 frame, 400B). Still awaiting a real caller for final validation. - Regression suite extended with all of the above; 5 scenes + 6 protocol self-tests PASS. ### Rendering fidelity batch (session 3) - **BSL slice encoding DECODED EXACTLY** (`patha/bsl_pair.py`): wire selector 0 = file bitslice 0 (unsliced), selector 0x13+b = file bitslice b (1..8) — proven by the GENS pack whose nine wire selectors {0,0x14..0x18,0x1a,0x1b,+dup 0} match its file bitslices {0,1,2,3,4,5,0,7,8} one-for-one, duplicate included. **Nibble plane = bitslice + 2** (pad byte owns planes 0-1; GENH's six textures = exactly content planes 2..7). b ≥ 6 clamps to plane 7 (only the 9-texture packs; residual). On a sliced pack, selector-0 textures read plane 2; on plain pages selector 0 = the full-colour texture. b2z tag 0x018 (`bitslice`) is the authoritative file-side value. - **Transparency**: texture-body alpha flag (stored word 4) → near-black texel cutout; material opacity < 1 (`DITHER n` = n%) → 2×2 ordered screen-door. - **Specular** from material specular[4] (rgb + exponent), per-sun highlight. - **NOVIEWMATRIX / HUD zones**: instance DCS whose parent word references a ZONE node renders in camera space (view matrix skipped) — SHARKS banner, SDEMO fcard. - **RETRACE n** honored: run_demo parses the scene divider → board paces draw_scene acks at 60/n Hz (SHARKS 30 Hz, FXTEST 20 Hz — authentic animation speed). - Regression: 5 scenes + 6 protocol self-tests all PASS; SDEMO interior now renders as coherent monochrome sub panels under scene lighting. ### DCS topology corrected — camera-motion cancellation bug (session 3) `vr_dcs_link` is a **SIBLING** link (1994 payload `[bro, sis]`) and the DCS flush-body word at stored offset 76 is the **sibling pointer**, not a parent: FLYK's top-level DCSs (head 0x1, view-holder 0x2, and every instance DCS) form a flat sibling ring. Treating either as parentage had chained the whole world through the tracker-animated head DCS, so camera motion cancelled exactly (title-bar pose moved, view didn't — also the fxjoy "frozen screen while drifting to (−1036,605)" symptom). Correct model: instance chains follow **vr_dcs_nest edges only** (flat roots in all scenes so far); camera pose = M(view's DCS) · M(head 0x1). Verified: the drift capture now renders dramatically different frames along the flight path. Ambient SPECIALFX sim is now **opt-in** (`run_demo --sfx` / VRVIEW_SFX=1): most scenes install their defs for event use only — simulating them ambiently put bubbles in scenes that never show them. Launchers enable it for the underwater HPDAVE set. ### Software-rasterizer optimization pass (session 3) Vectorized pre-culling: whole-mesh early-out (nothing in front / screen bbox miss), per-triangle batch rejection (clip, off-screen, degenerate) before the Python loop, and **far-plane (yon) culling** — the big one for open scenes (BLADE's yon is 600 in a 6 km canal). Measured: BLADE 769→368 ms, SDEMO 883→466 ms (~2×), KLNGVID 249→224, small scenes unchanged. `VRVIEW_SIZE=WxH` env (e.g. 384x240) trades resolution for another ~1.7×. The remaining cost is per-visible-triangle Python overhead + numpy fill — the endpoint is the **moderngl GPU backend** (mesh cache maps directly to static VBOs; only matrices change per frame), targeted at 832×512 @ 60 Hz. ### GPU backend (session 4, 2026-07-04) — moderngl, 832×512 @ 60 Hz ACHIEVED `patha/vrview_gl.py` = `GLRenderer(vrview.Renderer)`: SceneCache, DCS chain math, picking, and view/material/SCROLL parsing are inherited; only the draw path is replaced with moderngl (GL 3.3 core). The software rasterizer stays intact as the debugging reference — `vrrun --view --soft` / `run_demo --soft` / `VRVIEW_SOFT=1` select it; any GL failure auto-falls-back with a message. Architecture facts: - **SceneCache._mesh() results = static VBOs.** The VBO cache is keyed on the mesh dict's *identity* — `_mesh` already version-caches per handle (uploads, offset vertex updates, morph alpha), so a new dict means re-upload, same dict means zero per-frame work. Interleaved layout `[pos3 nrm3 col3 uv2]` + uint32 index buffer (fan-triangulated connections, unchanged). Morphing geometries re-upload per alpha change (small meshes; negligible). - **Row-vector convention needs no transposes**: numpy row-major mat4 bytes are read column-major by GLSL, i.e. the shader sees Mᵀ, so `u_m * v` in GLSL IS `p @ M`. Projection is a row-vector frustum built from the VIEW body (x0/x1/y0/y1 at zeye, near=hither, far=yon·1.05) matching the software mapping exactly; instance matrices (chain, FIX yaw-90, billboard, HUD/NOVIEWMATRIX, LOD select by ‖T‖) are computed CPU-side identically to the software path. - **Shaders replicate the software formulas 1:1**: per-vertex lighting (wire ambient + Σ|n·L|·suncol double-sided, per-sun specular rgb+exp, 0.7/sun no-normal fallback, 0.35/0.65 default rig when no wire lights), vertex-color modulate, clamp, texture×(base·1.275) (= software's texel·base/200), SCROLL uv offset (u0+du·t from the texture body, per frame), alpha cutout (texel sum ≤ 24/255 discard), DITHER 2×2 screen-door discard vs opacity, per-vertex linear fog mixed after texturing, sphere lists + SPECIALFX pools as depth-tested point sprites (particles: blend (ONE, 1−SRC_ALPHA) with rgb=cook·0.75·fade, a=0.6·fade ≡ the software soft blend; no depth write). - **Linear FBO + gamma present pass**: the scene renders linear into an offscreen 832×512 FBO; a fullscreen present pass applies the Division DAC gamma (out = in^(1/1.25)) — same single-application point as the software path. `last_frame` (readpixels replies) reads the FBO back post-gamma on demand, cached per rendered frame. Headless (`SDL_VIDEODRIVER=dummy`, regress) uses a standalone WGL context — no window needed. - Textures: nearest + repeat (matches the software point sampler); uploaded once per SceneCache texture array (BSL slices included), cached by array identity. Validation (RTX 3080 Ti): - `regress.py` now runs both backends: software 45–447 ms/frame vs **GL 0.6–8.7 ms/frame at 832×512** (SHARKS 1509 fps, SDEMO 272, KLNGVID 446, BLADE 115, FXTEST 1431) — 60 Hz with ≥2× headroom on the heaviest scene. PNGs `regress_gl_*.png`; mean-abs-diff vs the software reference: BLADE 4.5/255, KLNGVID 6.5/255 (near-identical). SHARKS/SDEMO/FXTEST diff higher for a KNOWN benign reason: the software pre-cull drops any triangle with a vertex behind hither/beyond yon, which kills huge ground/ocean planes; **GL clips properly and renders them** (FXTEST arena floor, SHARKS sea floor + surface horizon appear only in GL — GL is the more correct image). - Live (real FLYK.EXE through DOSBox): SDEMO ran 3300+ frames (zero errors, RETRACE 2 → 30 Hz pacing honored, wire rate ~37 fps burst); **BLADE ran live at 65.7 fps** (900 frames / 13.7 s, 2400+ frames total, zero errors, a joystick-trigger 0x23 fire resolved a pick against the live scene) — full 60 Hz on the heaviest scene. Windowed replay paces at 60.0 fps with 9 ms render. Adaptive frame-skip now never engages (skip stays 1 below 50 ms/frame) — as predicted, it's vestigial on GPU. ## 13. MUNGA GAME BRING-UP — the real Red Planet runs (2026-07-05, session 5) **`rpl4opt.exe -egg test.egg` (Red Planet v1.2.0.1) runs its full engine loop against the virtual board**: 892 geometry uploads, 22 texture pages, 308 instances, 63 objects, 4 lights — the whole Mars canal city + the player's starting hangar — with the attract-mode flight rendering live at ~90 fps in the GL window. Staging: `stage_assets.py --game RPDAVE` (adds MAPS/MODELS/SOLIDS/ CAMERAS/GAUGE/SCENES + `32RTM.EXE` **and `DPMI32VM.OVL`** borrowed from BTDAVE — 32rtm refuses to start without its DPMI overlay). Launcher: `run_game.py` (mirrors RP.BAT: `SETENV.BAT t s n n` → `32rtm -x` → exe → `32rtm -u`). `hunt_pe.py` = the PE-format static hunt (Borland PE sections + capstone), counterpart of the LE hunt_actions.py. ### The MUNGA wire dialect (all proven live + by disasm) - **Dialect detection: the first vr_sync action id.** The init argv is BYTE- IDENTICAL between shipped FLYK and rpl4opt (`/device~0x150~/video~svga~/pipes ~1~/qual~0x14~/system_tex~0~`) — sniffing it misclassified FLYK and yawed every demo camera (caught by regress diffs; the numbers, not the PASS flag). 0x2d = MUNGA, 0x2a = FLYK; the flag flips on the first 0x2d. - **vr_sync = action 0x2d** (FLYK's 0x2a moved). Disasm of rpl4opt's vr_sync (0x497080): the reply ACTION is never checked; the reply PAYLOAD word0 must echo the request's word0 (a cookie; mismatch → "unexpected action %d returned in velocirender_sync" printing the unrelated action + exit 9). FLYK's 0x2a cookie is 0, so the old zero-payload echo was accidentally correct. - **The sync cookie is the action id being synchronized** (create → cookie 1, texel upload → cookie 0x1a): **MUNGA bulk ops are ACK-LESS** — 0x17/0x19/0x1a completions must NOT be acked (the sync echo is the ack; an extra upload ack desyncs the receive stream by one frame). FLYK keeps its per-upload acks. - **create stays fire-and-forget** (host-assigned handle in payload word2; the 1994 "unexpected action %d in velocirender_create" strings are absent from the PE — an experimental create-ack desynced the stream). - **Action 0x1f = batched flush_artics**, replacing 0x1d entirely: `[n_records]` then n × `[handle][payload]` where a DCS handle carries 3×3 row-major + t (12 floats) and non-DCS handles (HUD gauge nodes) carry 5 floats (sin/cos-like; skipped). **Poses are ABSOLUTE** — the first record duplicates the flushed matrix exactly; composing (the FLYK 0x1d rule) doubles the transform. Stored in `board.anim_abs` (replace) vs `anim` (compose). - **Camera rig via dcs_link parentage** (MUNGA ONLY): vehicle DCS —link→ cockpit —link→ head DCS, view re-`list_add`ed onto the head (0xb5c); world pose lives in the 0x1f-animated vehicle DCS (0xaa1). The camera chain follows link edges in MUNGA; FLYK's links are flat sibling rings (following them relocated every demo camera). Instance chains stay nest-only in both. - **MUNGA vehicles fly nose-along-+Z** — straight-fast-flight samples of the RAW vehicle 0x1f records put local velocity 3.8° off +Z (459 samples). An earlier composed-chain heading test read +X (+0.79) — that was BIAS from the attract head-yaw swinging ±54° in the cockpit DCS; measure the vehicle DCS alone. The renderer applies a camera-only yaw(180) (`cam_matrix`) mapping render-forward (−Z) onto vehicle +Z (the raw chain rendered 180° backward; the interim yaw(−90) guess showed as "flying sideways", user-confirmed). - **Material body is 88B (FLYK 84B): every stored offset shifts +1** (emissive@5-7, ambient@8-10, diffuse@11-13, opacity@14-16, specular@17-20 + exponent). Misparse rendered the whole world green with a global DITHER screen-door (opacity read 0.815). lmodel (0x6) light bodies parse with the FLYK layout unchanged; the four type-0xe 32B bodies (vehicle lamps?) fail the sanity filter and are ignored. - **Fire-and-forget unknowns** (don't block anything): 0x29 (8B, one word ≈ a high node handle) and 0x2b (140B, `[handle][1][2][0x1d]...` + floats — HUD/ gauge displaylist-shaped). Undecoded; game runs without replies. - First-boot trap: `32rtm error: Can't find DPMI32VM.OVL` — the overlay must sit beside 32RTM.EXE on the path. Regression: all five FLYK scenes byte-identical to baseline after the dialect fix (36.8/33.6/6.5/4.5/89.5 diff signature); MUNGA runs live: hangar + canal render in correct Mars tans/reds, camera tracks the attract flight. ### BattleTech (BTL4OPT v1.1.0.4) bring-up — session 5 continued Runs the same MUNGA dialect end-to-end (arena + mechs on the board, 1800+ frames, ~100-130 fps GL). Asset chain findings: - **BTDAVE's VIDEO tree is empty of the video\geo|mat|tex sets** its BTDPL.INI searches — GETVID.BAT fetched video.zip from a dead resource server. Symptom: "Unable to cache tsphere.bgf", "couldn't find texture btfx:...", then "L4VIDEO.cpp couldn't load object plit.bgf" → NULL instance → exit. - **BTLIVE** (the pod's live snapshot) has the complete 1585-file VIDEO (GEO/MAT/TEX subdirs, .BSL packs) but its BTL4.RES is REJECTED by its own exe ("btl4.res v1.0.1.2 is obsolete!"). Working combination = **BTDAVE build + BTLIVE VIDEO graft** (stage_assets.py --game BTDAVE does this; --game also now copies product root files + VIDEO recursively; ANIMS staged: 634 files). - Keyboard/controls: same keyjoy.map Thrustmaster synthesis applies. **0x1f record structure CORRECTED (BT broke the fixed-size guess):** `[n_records]` then n × `[dcs handle][payload]` where payload = 12f full pose (absolute 3×3+t) OR a **joint angle record [sin, cos]** — 2 floats in BTL4 v1.1, 5 floats (sin,cos,0,0,0) in RPL4 v1.2. Version-dependent sizes → the board parses by BACKTRACKING (each record starts with a valid DCS handle; float bit patterns don't collide with small handle ints; misparse symptom was det=0 matrices corrupting the camera chain — "landscape rendered, then flipped to garbage" when per-frame updates began). Joint records = the mech articulation stream (torso/leg joints, e.g. BT camera chain runs 11 DCS deep through the mech rig: torso 0xdf1 world pose → ... → cockpit 0xe4a). Joint sin/cos → local axis semantics still undecoded; flushed matrices stand, so mech limbs don't articulate yet. New fire-and-forget unknowns in BT: 0x24 (frequent), 0x20, 0x26 (once each). Rendering findings from the BT bring-up (apply to all products): - **Per-geometry texture refs**: MUNGA geometry bodies reference their TEXTURE node directly (word 2; some also their material) — resolving only through the geogroup→material chain assigned one shared page (every BT terrain tile got the cloud texture). SceneCache now prefers per-geometry texture/material refs, geogroup chain as fallback. Side effect: SDEMO's interior sub-panels now resolve their own BSL slices (visibly richer, wood/teal panels). - **vtype 0x15 field 6 is NOT alpha**: values run −3.75..1.0 across products (a blend experiment made SHARKS' kelp ghostly). Stored as mesh['alpha'] data; semantics undecoded. How the BT cloud/sky layers blended on the real board is an open question (their 64×64 gray cloud page + SCROLL dv=−1.3 confirmed). - **BT's parked view is fog-dominated**: its VIEW fog is near=150 far=1250 color (0.32,0.30,0.65) — the purple "walls" are ground tiles (Y=75, camera 11 above) and the 4000×4000 sky deck (Y=150) fogging to saturation. The view-body layout is IDENTICAL across FLYK/RP/BT (fog_enable word17=0x5). - **"Waiting for translocation!"** — BTL4OPT parks the mech at the map edge in a LOBBY state until the pod network translocates it into the arena (NETNUB.EXE / BTNET.BAT are the network hub). This is BT's mission-start path; the RP equivalent is still undecoded. The arena itself (snow/rock textured terrain, mountains, structures) renders correctly — verified with the debug chase camera. - **VRVIEW_CHASE=1|2** (env): debug chase camera in vrview.cam_matrix — follows the most-traveled 0x1f-animated DCS from behind-above; =1 turns with the target's nose, =2 keeps a world-locked offset (no spin). Use to see the world when the cockpit is fog-bound or a rig is undecoded. - **MUNGA texture-body floats 10/11 are a STATIC uv offset**, not FLYK's du/dv-per-second SCROLL — animating them drifted the BT terrain texture across its mesh "like clouds" (user-observed). Board renders them as a fixed offset in the MUNGA dialect (semantics inference; FLYK SCROLL unchanged and regression-validated). - **Instance stored word 3 = DISPLAY MODE: 3 normal, 2 billboard, 1/0 HIDDEN until armed.** Census: RP's exactly-4 w3=1 instances are the vehicle's effect attachments (rendered, they block the cockpit lens); BT's 11 are the PLAYER MECH awaiting translocation; SHARKS' single w3=0 is the null-object 'fishes'. The renderer skips w3 ∉ {2,3} in all dialects (regression green; SDEMO −1 instance). Corollary: the mech is SUPPOSED to be invisible until translocation re-arms it — expect a re-flush with w3=3 as part of the translocation sequence (a decode hook for game-start). - **MUNGA instance chains follow dcs_link parentage** (like the camera): mech/ vehicle part instances hang off the 0x1f-animated rig via link edges — nest-only chains rendered them at world origin. FLYK stays nest-only. - **SCHILD skip is FLYK-only**: MUNGA rig DCSs carry host pointers in the same stored-word-20 slot (skipping on it hid legit vehicle parts); MUNGA hiding is governed by instance w3 instead. - **Texture filtering: the board POINT-SAMPLED** — proven in the i860 source: the texture inner loop (VRENDER/AS860/SCANLINE.SS, .txinner_loop) issues exactly one `fld.l texbase(reg)` per pixel from the interpolated u,v; no four-tap fetch exists. The GL backend's NEAREST default is authentic; VRVIEW_FILTER=linear opts into bilinear for modern taste. ### Translocation / mission-start (session 5 close) — network stack UP, needs a peer BattleTech's "Waiting for translocation!" is a **multi-node network gate**, not a local flag. The full 1996 pod LAN now runs inside DOSBox-X (run_game.py --netnub): - `[ne2000] ne2000=true nicbase=300 nicirq=3 backend=auto` in flyk_vr.conf. - `Z:\SYSTEM\NE2000.COM 0x60 3 0x300` — DOSBox-X's embedded Crynwr packet driver (registered under Z:\SYSTEM\ only when ne2000=true). The drive's era ODI stack (LSL+NE2000 MLID+ODIPKT) does NOT work — "An MLID could not be found" — use the built-in one. WATTCP.CFG (my_ip=200.0.0.86, from the egg) beside the exe. - BTNET.BAT wrapper: `NETNUB.EXE -f btl4opt.exe -a -egg .egg` (netnub -f runs the game, -a forwards args). Netnub hosts wattcp, launches the game with `-net `, prints "Communicating through interrupt vector 0x61". - Result: game becomes a live network client (further than standalone ever got) but still draws 0 frames — no peer sends the translocation message. **Egg format decoded** (TEST.EGG = plain INI): `[mission] adventure/map=polar3/ scenario=freeforall/time/weather/temperature/length`, `[pilots] pilot=`, `[] hostType=0 vehicle=thor dropzone=one name=Milo bitmapindex badge...`, `[ordinals]`/`[largebitmap]` = HUD digit + pilot-portrait bitmaps (hex rows). hostType 1/2/3 variants tested standalone — all still park (translocation is the gate, not hostType). BTL4OPT strings confirm the ladder from AUDIO/SYMBOLS.SCP: **BTPlayer states DropZoneAcquired(1) → VehicleTranslocated(2) → MissionStarting(3)**. Translocation is the network event moving the mech from staging into the arena (POVTranslocateRenderable) — expect the hidden w3=1 mech instances to re-flush w3=3 when it fires. Two paths forward (user decision): (A) reconstruct the mission HOST — BTL4D2S.EXE (spooling/server build) or `netnub -t` master mode, two networked DOSBox nodes; (B) hunt_pe.py the state check in BTL4OPT and patch past DropZoneAcquired → MissionStarting (single-node bypass). RP's mission start is the analogous undecoded gate. hunt_pe.py located the wait at 0x45d1e0 (state byte at [ebx+0x88], values 2/9 branch to the print). ### Remaining to "100%" - ramp colorization when a scene uses one (unseen on wire so far). - MUNGA: joint-record axis semantics (mech articulation!), 0x29/0x2b HUD gauges, 0x24/0x20/0x26, get_geom_numconns (hunt_pe.py ready), mission start (translocation — host node or binary patch; see above). - SDEMO VIEWSPLINE camera attach; .EVT arming; .SKL mech articulation test; validation vs RENDERS/pod video.