Files
TeslaRel410/dpl3-revive/spec/VELOCIRENDER_PROTOCOL.md
T
CydandClaude Fable 5 afc3fd839e Vendor dpl3-revive: the Division/DPL3 renderer, now ours
Bring the graphics-dev collaborator's dpl3-revive into the repo as first-class
project code (they've handed it off; it's ours now). This is the proven
Division renderer that our in-process rt_draw has been trying to be.

What's here:
- parser/  B2Z/V2Z/SVT/SCN/SPL/BGF/BMF/BSL decoders (pure Python).
- spec/    reverse-engineered format + the definitive VelociRender wire
           protocol (from the original DIVISION source, matches our live
           VPX node/action tables exactly).
- source-ref/  read-only copies of the original DIVISION C (BIZREAD.C,
           DPLTYPES.H, DPL.H) that define the formats.
- patha/   the "virtual VelociRender board": vrboard.py (24-action protocol
           server), vrview.py (numpy software rasterizer, the reference),
           vrview_gl.py (moderngl GPU backend, 832x512@60Hz), plus the
           run/replay/regress tooling and evidence renders. Drives FLYK/BLADE/
           Star Trek demos AND our btl4opt/rpl4opt game binaries.
- viewer/  WebGL archive generators (.py); prebuilt HTML/data regeneratable.
- samples/ small test models/textures.
- bt*.raw.bin  real BTL4OPT arena wire captures (kept for offline renderer
           testing/regression against OUR game).

.gitignore keeps the multi-hundred-MB demo capture dumps + debug logs +
regeneratable viewer bundles out of history (they stay on disk).

Phase 0 of the integration is validated: their board decodes our bt8 capture
with zero errors (3748 nodes, 507 instances, 4 mechs) and renders our arena
(terrain/dome/sky, correct Division DAC gamma). Plan + status in memory;
integration continues in emulator/RENDERER-COLLAB.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:06:25 -05:00

915 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 1623 = 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 67 (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 0x1500x161 → 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 words419
(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-alongZ 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, ~1830 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 <prefix> --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 <SCENE>` 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 0x140x1a 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öllerTrumbore 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 <gain> <gain>` 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öllerTrumbore) 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, 1SRC_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 45447 ms/frame vs **GL 0.68.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>.egg` (netnub -f runs
the game, -a forwards args). Netnub hosts wattcp, launches the game with
`-net <shared_mem_addr>`, 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=<ip>`,
`[<ip>] 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.