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>
59 KiB
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 writeOutput Data. - in byte: spin until
Input Status bit0, then readInput Data. altRecord()/inputReady()= readInput 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
resetstrobe, 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 1994VR_COMMS.Csource — a shipped-vs-snapshot delta found by static analysis (patha/verify_framing.py:or ebp,0x40000000in the transmit path, plus a global initializedmov [..],0x40000000). Exact receive-side meaning is still TBD (routing/epoch flag?); confirm in the dynamic capture. We mirror it on replies (vrboard.LW_FLAG) so our board is byte-identical to the real one; our decoder ignores it (masks low-16 for count, tests bit31 for iserver). - bits 16–23 = node id;
0xff= broadcast (host→board render commands always use0xff). - low 16 bits (
count) = number of bytes that follow the length word =4 (action) + N. Max 1024; the host caps payloads and fragments (seevelocirender_packetize, 508-byte chunks).
action= avr_actionenum value (§3), int32 LE.payload= command-specific bytes (§3). Multi-byte fields are LE on the wire; thedpl_little_endianpath sends host memory verbatim (endian_fixonly 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_sceneis async: the board replies with a barevr_draw_scene_actionframe-ack later; the host tracksdpl_frame_repliedand picks it up invelocirender_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:
setLA(0x150); initLA();reset()— the analyse/reset strobe sequence on ports 0x160/0x161.boot_xputer()— streams the transputer.BTLfile raw in 2 KB blocks out the link.- 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 iniserver_action. Comment in source: "relies on the knowledge that the C run-time boots up and does 3 iserver transactions before closing down." boot_HSP()— 128-byte HSP microcode blocks viavr_hspcode.boot_860()— reads the.MNG: a 7×int32 header (csize, dsize, bsize, cstart, dstart, bstart, entry) sent viavr_860code, then code/data/bss segments viavr_860code/860data/860bss, plus argv viavr_860args.vr_initwith 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_readpixelspayload; 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_flushstruct decoders (mappingDPLTYPES.Hstructs to our scene objects), and boot-handshake emulation.VR_REMOT.Cis 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/.SVTfiles.
Why Path A is now cheap
The two things that usually make "run the original binary" expensive — reversing an undocumented protocol, and rebuilding the renderer — are both already solved here: the protocol is in source (this doc), and the renderer is the project we've been building. Path A is mostly plumbing the existing renderer behind a wire protocol, plus a tiny emulated link card.
Relationship to the reimplementation track
Roadmap items 6–7 (port the geometry pipeline; GPU rasterizer) reimplement DPL3 as a
standalone viewer and discard the EXE. Path A instead keeps the original host binary
(its scene scripting, .EVT event timing, game logic, input, frame pacing) and
substitutes only the renderer. The two share the same rendering core; Path A adds the
link device + protocol server on top.
8. Open items / next steps
- Confirm the readback display path in
FLYK.EXE(§6) — decides whether the board returns pixels or self-displays. Highest priority; everything else assumes readback. - Pull the exact
vr_readpixelsrequest/reply layout from the shipped lib (DPL.Creadpixels path) — the only payload not fully fixed by the snapshot. - Lock struct sizes for
vr_flushbodies and the vertex/connection blocks againstDPLTYPES.Hon this exact build (watch Watcom struct packing/alignment). - Prototype the DOSBox-X device headless first: log the boot handshake +
vr_initfrom a realFLYK.EXErun, verify our assembler frames the messages, then add rendering. - Decide host: DOSBox-X custom device (C++ in the emulator) vs. a shim DLL — DOSBox-X is the right home because the app does direct port I/O, with no driver hook point.
9. Validation status
Static (patha/verify_framing.py, vs the shipped FLYK.EXE code object). Found, as
literals in the transmit/link code: the 0x00FF0000 broadcast mask, the 0x80000000
iserver bit, the 0x1FC (508) packetize chunk, the 0x150 link base, and C012 port I/O.
Disassembly of the length-word construction confirmed the structure — and revealed the
shipped-build bit30 0x40000000 flag absent from the 1994 source (§2), now mirrored.
Dynamic (real FLYK.EXE under stock DOSBox-X 2026.07.02, no responder). FLYK boots,
drives the link at 0x150, streams the transputer boot, then blocks in the handshake
awaiting the board. The unmapped port reads 0xFFFFFFFF, and FLYK prints:
Protocol error : length 65535 too big, length_word 0xffffffff
+++ERROR : rcv_protocol fail during iserver handling, xmits=0
This is exactly receive_protocol (nb = length_word & 0xffff = 65535 > 1040 → reject)
called from handle_iserver_stuff during startup_handshake — confirming, against the
real binary, that the length word is 4 bytes with the count in the low 16 bits, and that
the only thing missing is a board that answers. Two more benign shipped-vs-snapshot
deltas surfaced: the shipped error strings additionally print length_word and xmits
(same framing). A responder that satisfies the 3-transaction handshake (§4) will let FLYK
proceed into the framed HSP/860/init/scene traffic.
Live (custom DOSBox-X build + our C012 bridge + real FLYK.EXE). A custom DOSBox-X was
built with src/hardware/vrlink.cpp (the C012 bridge, ports 0x150–0x161 → TCP 127.0.0.1:8620,
enabled by env VRLINK=1; init hooked next to GLIDE_Init in sdlmain.cpp). Running real
FLYK against a quiet capture board (patha/vrcapture.py) captured 80,909 bytes:
00 52 53 45 54 ×3 (FLYK's reset() strobing port 0x160 three times) + the full 80,894-byte
transputer boot stream (f0 b4 d1 d1 d1 24 … = VRENDMON.BTL verbatim). FLYK then printed:
timeout in inRecord
velocirender_receive timed out - sends_wo_rcv=0
velocirender_input failed in start_velocirender
— a clean timeout (not the stock garbage-read), i.e. the bridge faithfully carries the protocol and FLYK is now waiting for the board to answer. The whole path is proven: custom build → C012 ports → socket → Python board. Iteration from here needs no rebuild (board logic is pure Python).
Next blocker (precisely located): the post-BTL boot handshake. After the boot download the
shipped start_velocirender calls velocirender_input expecting the board to send the first
message (the transputer/i860 C-runtime startup handshake — the 1994 source's "3 iserver
transactions", but the shipped flow differs and uses velocirender_input/sends_wo_rcv). An
unknown iserver tag makes the host exit(666), so the exact expected bytes should be nailed by
disassembling shipped FLYK's velocirender_input path (or careful empirical bring-up), not
guessed. Once the handshake passes, FLYK proceeds to the framed vr_860* MNG download → vr_init
→ scene traffic → draw_scene/readpixels.
10. END-TO-END ACHIEVED — FLYK.EXE runs its full render loop through the board
Custom DOSBox-X (src/hardware/vrlink.cpp, env VRLINK=1) + patha/vrrun.py board +
real FLYK.EXE. FLYK now runs unmodified through the entire protocol into its main loop,
with no errors:
- Transputer
.BTLboot streamed (byte writes to 0x151); board sends one vr_net frame to satisfyvelocirender_input(§4). - i860
.MNGfirmware downloaded as framedvr_860code/data/bss/args(833 msgs) — discarded. vr_init(real argv) → board replies.- 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. - Main loop:
draw_sceneper frame (board async-acks) + per-frame0x1d(56B).0x2a(8B) is the velocirender_sync barrier (echo-ack it);0x1c/0x1dare 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 outswwords (padded to even; the length word'scountis 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
.BTLstreams byte-wise to 0x151 (no body pending → raw passthrough).vrlink.cppdemuxes 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.
createfire-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
0x1danimation 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:
HPDAVE\SHARKS.SCNsearches..\geometry,..\sharks,..\texture— none of which exist besidesda4\HPDAVE. FLYK fails object loads silently (the "Failed to load object" print only fires whendpl_NewObjectreturns NULL, not when the file is missing) and builds an empty world: every instance flushed withobject=NULL, no geometry/texture traffic. That was the capture §11 analyzed.- The shipped FLYK registers object-load extensions
.bgf .bmf .bsl .tga .vtx(strings in the EXE) — not.b2z/.svt. But itsdpl_bgfReadvalidates the sameDIV-BIZ2container the.B2Zfiles hold ("dpl_bgfRead "%s" is not a valid biz file"). - Fix = staging (
patha/stage_assets.py→c:\temp\flykc, mounted as C: bypatha/flyk_vr.conf): copyHPDAVE, copyDPL3\GEOMETRYexposing*.B2Zas*.BGF, decodeDPL3\TEXTURE\*.SVT(raw xBGR 32-bit) to*.TGA. With that, FLYK loads the reef and uploads everything over the link. (fishes,banner, and thesharks:*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) aftern_verts*stride*4bytes. 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], thenn_conns*perint32 indices; one ack. (Ocean quad =[1][4]+0,1,3,2; fish pmesh =[70][3]+ 70 triangles.) - Graph refs in flush bodies (wire-payload word indices, where word0=remote):
instance word6 = object handle; geogroup words16/17 = f/b material; DCS matrix words4–19
(4×4 row-major, translation in row 3), word20 = parent DCS (instance DCSs → world DCS 0x3;
camera tree uses explicit
dcs_link0x1→0x2, 0x1→0x3 instead). object→lod→geogroup→geometry attach vialist_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 bypatha/heading_test.py: across ~7000 records, model −Z tracks the swim velocity (dot ≈ −1, 95% of samples); a transpose mirrors the yaw (user-visible as reversed turning). Corollary: the spline animation is built for nose-along−Z models. The original..\sharksshark (lost) was authored that way; the surviving stand-inDPL3\GEOMETRY\SHARK.B2Zis nose-along-+X, so the renderer applies a +90° yaw model correction (X→−Z) to dynamically animated instances only. - Camera = inverse of (M_dcs0x2 · M_dcs0x1); look −Z, row-vector convention (p′ = p·M), child→parent composition. 0x1d on 0x1 = head motion (joystick when a pilot is present).
Board-side real-time window (Path A/B complete)
patha/vrview.py (numpy rasterizer + pygame, ~18–30 fps at 640×400) renders straight from the
live board state: wire geometry, DCS graph, VIEW projection + fog + back color, 0x1d animation.
python vrrun.py <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)
python patha/stage_assets.py(once; rebuildsc:\temp\flykc)python patha/vrrun.py cap --view$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.CdN_pxpl5_fx_texels). - mode 1 = 8-bit intensity (grayscale replicated across B,G,R) — decodes clean.
- mode 0 = BSL bit-slice pack: the page holds up to eight 4-bit monochrome textures,
one per nibble plane of the 32-bit texel word (verified: planes of SDEMO's texmap 0x3e are
distinct coherent sub-interior textures; a 256×256 page holds the VW logo + a mural in
different byte lanes). Each dpl_TEXTURE node sharing the texmap carries a small-int slice
selector late in its flush body (values 0x14–0x1a observed) — exact value→nibble mapping
not yet pinned. Matches the b2z
bitslicetag (0x018) and the.BSLfiles 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 zparses z wrong (camera got y duplicated into z — authentic shipped behavior);TRACKER KEYBOARDinput path not yet working under DOSBox (keys don't move the camera — investigate how the tracker reads the keyboard). - MAPS*.SCN files are a different key=value dialect (
viewangle=60.0) that hangs FLYK's parser — stage SCENES\ versions, never MAPS. - BLADE.SCN runs: 770 geometry uploads, 29 texture pages, 200+ instances — the Mars canal city renders fully textured (RPDAVE pages are full-color, no BSL packing).
Completeness batch (session 2 close-out) — all five scenes regression-PASS
Implemented and validated by patha/regress.py (replays every scene capture, renders,
reports; zero handler errors across SHARKS/SDEMO/KLNGVID/BLADE/FXTEST):
- Wire lighting: lmodel (0x6) / light (0xe) bodies =
[dcs@2][light_type@3][r g b @4..6](stored words); type 2 = ambient, type 3 = directional with aim = the light-DCS's Z row (SHARKS: sun (0,1,0) from rx=−90 ✓). Renderer uses scene ambient + suns, double-sided. - LOD range selection: lod body stored words 15/16 = switch_in/switch_out; renderer picks by eye distance (fallback LOD 0).
- SCROLL: board-side autonomous texture animation from the texture body's u0/v0/du/dv (stored floats 8..11), du/dv per second.
- BILLBOARD: instance stored word 3 == 2 (vs 3 normal) — camera-facing spherical
billboard (FXTEST
flamebig). - Sphere lists (vtype 0x41,
[x y z r]): shaded screen-space discs. - sect_vector: real Möller–Trumbore picking over live world triangles; reply = int32 instance handle. Self-test: an upward underwater ray correctly hits the ocean surface quad. (sect_pixel still needs camera unprojection of board 832×512 coords.)
- vr_readpixels: experimental reply —
[pad,B,G,R]pixel words from the last rendered frame, 508B fragments; no shipped binary has exercised it yet. - Action 0x22 = name binding
[handle][0x8000xxxx name id](board createName; fire-and-forget; stored in board.names — GEOMETRIZE 0x8000005a-style refs resolve here). - BSL slice mapping: rank-based (sorted texture-node selectors → content nibble planes in order). No more rainbow; exact selector→nibble encoding still unproven.
SPECIALFX (action 0x1c = install_sfx) — decoded + ambient sim implemented
Payload (80B): [code][texture ref][type][velocity][size][off_y][bias][cook_r g b] [variance][gravity][o_cool][cool][frags][rpt][4 host ptrs] — exactly the .SCN
SPECIALFX columns. Defs are installed at scene load; two trigger models:
- Ambient (SHARKS bubbles/marine snow): the board steps installed defs
autonomously. Implemented in vrview
_particles: per-def pools (frags×rpt), spawned in a camera-centred volume at bias/off_y height, rise atvelocitywithvariancejitter andgravity, fade byo_cool^age, lifetime 1/cool; soft-blended discs, no z-write. Regression stays green on all five scenes. - Event-triggered (
PSFX \fx\*.pfxlines 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öller–Trumbore) for telemetry; FLYK's own hit/FX consequences must be computed host-side or armed by a mechanism still undecoded (the .EVTPSFXchain 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,0x18into 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.pyis the reusable hunt tool.get_geom_numconnsexists 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 —
_meshalready 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 * vin GLSL ISp @ M. Projection is a row-vector frustum built from the VIEW body (x0/x1/y0/y1 at zeye, near=hither, far=yon·1.05) matching the software mapping exactly; instance matrices (chain, FIX yaw-90, billboard, HUD/NOVIEWMATRIX, LOD select by ‖T‖) are computed CPU-side identically to the software path. - Shaders replicate the software formulas 1:1: per-vertex lighting (wire ambient + Σ|n·L|·suncol double-sided, per-sun specular rgb+exp, 0.7/sun no-normal fallback, 0.35/0.65 default rig when no wire lights), vertex-color modulate, clamp, texture×(base·1.275) (= software's texel·base/200), SCROLL uv offset (u0+du·t from the texture body, per frame), alpha cutout (texel sum ≤ 24/255 discard), DITHER 2×2 screen-door discard vs opacity, per-vertex linear fog mixed after texturing, sphere lists + SPECIALFX pools as depth-tested point sprites (particles: blend (ONE, 1−SRC_ALPHA) with rgb=cook·0.75·fade, a=0.6·fade ≡ the software soft blend; no depth write).
- Linear FBO + gamma present pass: the scene renders linear into an offscreen
832×512 FBO; a fullscreen present pass applies the Division DAC gamma
(out = in^(1/1.25)) — same single-application point as the software path.
last_frame(readpixels replies) reads the FBO back post-gamma on demand, cached per rendered frame. Headless (SDL_VIDEODRIVER=dummy, regress) uses a standalone WGL context — no window needed. - Textures: nearest + repeat (matches the software point sampler); uploaded once per SceneCache texture array (BSL slices included), cached by array identity.
Validation (RTX 3080 Ti):
regress.pynow runs both backends: software 45–447 ms/frame vs GL 0.6–8.7 ms/frame at 832×512 (SHARKS 1509 fps, SDEMO 272, KLNGVID 446, BLADE 115, FXTEST 1431) — 60 Hz with ≥2× headroom on the heaviest scene. PNGsregress_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 inboard.anim_abs(replace) vsanim(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=autoin 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.