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

59 KiB
Raw Blame History

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.


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 setiserver 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 memcpys 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.

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 (y230, 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.pyc:\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 n32rtm -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_added 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.