Lighting: stride-8/9 vertices carry a normal (floats 3-5, uv at 6-7). The backend transforms normals by the instance rotation and lights with a single directional sun (GL_LIGHT0, smooth, two-sided, GL_COLOR_MATERIAL). Terrain and hulls shade instead of reading flat. LOD: the lod flush has no switch distances -- the host keeps the active LOD at the child-list head and re-orders over the wire, so children[0] is right and LOD changes come through as the sim runs. The crash that halted the game once the RIO drove the sim was not ours: fault at 0047E1D1 writing 0xFFFFFFFF is the RIO driver's DISABLE_AND_DIE debug macro (PCSPAK.ASM, DIE_ON_ERROR=1), which deliberately faults on a serial-tx anomaly -- error 3 = body char >0x7F during a RIO packet, i.e. a glitch on a board reset. Release build (DIE_ON_ERROR=0) compiles it out and recovers (and al,07Fh). patch_btl4opt.py NOPs all 12 sites by signature (with .orig backup) = release behavior; game then survives RIO resets (1200+ frames). (ALPHA_1 is git-ignored, so the tool ships, not the patched binary.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
Phase 3 — Render Backend: Progress
Status (2026-07-03): Phase 3a + 3b + 3c complete. 3a — the render command
stream is fully captured and decoded, and a captured DIVRGB frame
reconstructed to pixels offline. 3b — a live OpenGL window built into the
emulated board draws each frame in real time inside DOSBox-X. 3c — the
production vr_sync abort is fixed and the full BattleTech v4.10 game now runs
indefinitely through the emulated board, drawing 1000s of frames (was hard-
capped at 200). First images ever produced from the Rel 4.10 VPX protocol
without a real board. Next (3d): traverse the game's DPL hierarchy + DCS
transforms so its mechs actually appear.
That is flyk divrgb.scn — Division's SMPTE color-bar calibration scene —
rendered entirely from the FIFO command stream captured by the emulated VPX
board: geometry, connectivity, materials, camera, viewport and background all
come from the wire, none from the scene files. divrgb-frame0.png is the same
capture drawn with the actual frame-0 spline camera the app sent.
3a. Full FIFO capture
vpxlog.cpp now records every FIFO burst when VPX_FIFODUMP=<path> is set
(records: 'VPXM' magic, u32 length, raw bytes; one record per burst between
outputData tag writes). The capture used here: divrgb.fifodump
(1297 messages) from divrgb.conf.
Tools:
decode_fifodump.py <dump> [--hex N] [--action A]— action census + payload hexdumps.render_capture.py <dump> [-o out.png] [--frame N] [--eye x,y,z]— reconstructs the scene graph and software-renders the frame eachdraw_scenecommits.
The Rel 4.10 wire protocol (established from this capture)
One burst per message: [action:4][payload], packetized at 508 bytes.
The i860 image download itself rides the FIFO in this build (actions 18–20,
VREND.MNG). The DPL3 vr_action enum holds for 0–23 but Rel 4.10 extends
and re-purposes the tail:
| action | meaning | payload |
|---|---|---|
| 0 | init | args string (/device~0x150~/video~svga~…) |
| 1 | create | [type][name] — the host assigns node names (1,2,3…); the board's create reply node value is ignored |
| 3 | flush | [name][type][node struct] (see node types below) |
| 7 | dcs_link | [parent][child] |
| 9 | draw_scene | [view?][0][0][1][1.0f] — commits the frame |
| 11 | list_add | [parent][child] |
| 18–21 | 860code/data/bss/args | i860 download (FIFO in this build) |
| 23 | set_geom_verts (Rel4.10; DPL3 had 22) | hdr [name][0][n_verts][3][n_blocks][1][5][n_verts][1.0f], then float32 x,y,z per vertex |
| 25 | set_geom_conns (new) | hdr [name][n_polys][loop_len][0], then indices; each poly a closed loop (last=first) |
| 31 | camera (new) | [?][view][3×3 rotation, row-major][eye x,y,z] per frame |
| 45 (0x2D) | sync | token ping (see PHASE2) |
Node types (from create/flush pairs in this capture):
| type | node | flush payload highlights |
|---|---|---|
| 2 | texture/ramp? | one word, 0xFFFFFFFF / 0x0FFFFFFF |
| 3 | view | window l,b,r,t (±1 × ±0.6154), window-plane distance 1.3, viewport 832×512, near 2, far 12000, background RGB |
| 4 | light | |
| 5 | dcs | 4×4 matrix |
| 6 | material (old-style) | RGB floats |
| 7 | object | |
| 8 | lod | 2×4 bounds + lists |
| 9 | geogroup | bounds; payload int 14 = material node name |
| 10 | geometry | [geo_type][n_polys][n_verts]…[radius] |
| 11 | material | floats 10–12 (payload) = diffuse RGB |
Graph: list_add links object→lod→geogroup→geometry (and zone-level nodes to
node 0); geogroups bind materials by name. The 13 DIVRGB bars decode to the
textbook SMPTE pattern — 7 color bars, the reverse strip, and the −I/white/+Q
PLUGE row — which validates vertices, connectivity, materials, camera and
window mapping in one image. (Division screen x runs opposite to a GL-style
eye space; without negating x the pattern comes out mirrored.)
3b. Live OpenGL backend (DONE)
VPX_RENDER=1 turns on a live render backend built into vpxlog.cpp
(Windows/WGL). The same burst assembler that feeds VPX_FIFODUMP now also
feeds an in-process scene store (scene_burst() → struct VScene), and each
vr_draw_scene publishes a frame snapshot to a dedicated OpenGL window thread
(rt_main) that draws it with fixed-function GL. No build-system change was
needed — opengl32 was already linked, and vpxlog.cpp is already in
hardware/.
Validated: flyk divrgb.scn (divrgb.conf with VPX_RENDER=1) opens the
"VPX VelociRender (emulated)" window and draws the SMPTE bars live, framed by
the real camera.spl spline camera the app sends — matching the offline
render_capture.py decode. The window runs on its own thread, so it keeps
displaying the last frame after the DOS app exits.
Design notes:
- Multi-burst payloads (
set_geom_verts/connscontinuations) are tracked withgeom_active/conn_activestate, same as the offline decoder. - Projection:
glFrustumfrom the view-node window rect scaled bynear/window_distance; aglScalef(-1,1,1)handles Division's mirrored screen-x. Camera is the row-major 3×3 from action 31 loaded as the modelview rotation, thenglTranslatef(-eye). - Frame handoff is a critical-section-guarded
VFrame+ auto-reset event; the emulator thread never blocks on GL.
Game path (alpha1.conf, full BattleTech v4.10): the window opens and draws
the view background, but the run hits the pre-existing production-path sync
timeout (velocirender_receive timed out — sends_wo_rcv=3, then
failed in vr_sync) — the btdpl.ini/netnub launch bursts several sends
before its first receive, which the POLL_THRESHOLD gating stalls. This is not
a 3b issue (the flyk clean-launch path renders fine); it is the same
production-sync item still open from Phase 2.
3d. The game's own world decodes and renders — LIVE
The live backend now draws the game's actual out-the-window view in real time: sky, the arena floor receding to the horizon, and the player's own gun barrels at frame bottom (static scene — without a RIO the sim doesn't advance). Same frame decoded offline below.
That is an enemy mech (object 1048, 488 verts) standing in the mission arena —
reconstructed entirely from the game's captured FIFO stream by
render_game.py (real hull/armor/cockpit-glass materials from the wire; the
half-buried look is the offline painter's-algorithm artifact, which the live
GL depth buffer doesn't have). game-cockpit-decoded.png is the actual
cockpit camera: the arena floor to the horizon with the player's own gun
barrels rising at frame bottom.
What the game adds over flyk's flat scene (all now handled, offline + live backend):
- Stride-aware vertices.
set_geom_vertsheader word 3 = floats per vertex: 3 (xyz), 4, 5 (xyz+uv), 8 (xyz+normal+uv), 9. Mech meshes carry normals and texture coordinates — lighting/texturing data is on the wire. - Full DPL hierarchy. Instances are
list_addchildren of DCS nodes (dcs→instance); instance flush field 4 references the object (type 7); object→lod→geogroup→geometry via list_add.dcs_link(action 7) builds the articulation tree (mech torso/arms/legs), each DCS a 4×4 at payload floats 4–19 (row-major, row 3 = translation). - The world is y-down (the DCS matrices carry a y reflection; game coords vs Division's). The renderer flips both x (Division mirror) and y.
- Mission scene scale: 10 km arena of 1000-unit ground tiles, 246 placed instances, 330 geometries, 280 materials; the player's Thor (644 verts, articulated sub-parts as sibling instances) sits exactly at the camera; six enemy mechs stand ~1.5 km north.
- Camera: action 31 (rotation + eye at the cockpit position); with the sim stalled (no RIO) it arrives once and the scene is static.
Offline tool: render_game.py <fifodump> (near-plane clipping, hierarchy
traversal). Fixture: a 954-frame capture. The live backend (vpxlog.cpp)
gained the same traversal: stride-aware geometry, dcs/instance/object link
decode, cached world transforms per DCS, y-down projection.
3e. Texturing (DONE) — the texture chain
The wire texture model is Division's intensity+ramp scheme (no palette ever crosses the link):
- action 26 uploads texels: header
[node][nbytes][w][h]then 8-bit intensity rows (64×64…128×128; node type 13 = texture). - texmap (type 12) flush references its texture node; material (type 11) flush references its texmap and a ramp (type 14): two RGBs (lo/hi), e.g. dark red-brown → light tan for mech hulls.
- Final texel color =
lerp(ramp_lo, ramp_hi, intensity/255).
The live backend bakes each textured material's RGBA once (texture × ramp),
uploads it to GL keyed by material, and maps it with the wire UVs (stride-5
verts: floats 3–4; stride-8/9: floats 6–7). Result: the arena renders with
the ravine's actual brown dirt textures, live (game-live-textured.png).
3f. Lighting + the shipped-in debug crash
Lighting: stride-8/9 vertices carry a normal at floats 3–5 (uv at 6–7).
The backend transforms them by the instance world matrix (rotation only) and
lights with a single directional "sun" (GL_LIGHT0, smooth shading,
two-sided, GL_COLOR_MATERIAL so texture/material color survives). Terrain
and hulls now shade instead of reading flat.
LOD note: the lod flush carries no switch distances — the host game
keeps the active LOD at the head of the object's child list and re-orders it
over the wire, so children[0] is correct and LOD changes as the sim runs
come through for free.
The crash that halted the game with the RIO in sync was not ours. Fault
Exception 0E at 0047E1D1 writing to 0xFFFFFFFF is a debug landmine the
build shipped with: the RIO serial driver's DISABLE_AND_DIE macro
(PCSPAK.ASM, gated by DIE_ON_ERROR equ 1) deliberately faults on any
serial-transmit anomaly. Error code 3 = a body char > 0x7F during a RIO
packet (PCSPAK.ASM:1630) — i.e. a serial glitch during a board reset. The
release build (DIE_ON_ERROR equ 0) compiles it out and takes the recovery
path (and al,07Fh). patch_btl4opt.py finds all 12 sites by their 14-byte
signature and NOPs them — exactly the release behavior — with a .orig
backup. After patching, the game survives RIO resets (1200+ frames).
3c. The full game runs through the live renderer (sync abort fixed)
The production vr_sync abort is fixed, and BattleTech v4.10 now runs
indefinitely through the emulated board + live renderer. Root cause was a
Phase-2 bring-up guard left in the device: vpx_max_postboot_acks = 200
capped how many post-boot replies the device would ever feed. A real game
session issues an unbounded stream of sync/frame/render replies; the game
aborted with velocirender_receive timed out — sends_wo_rcv the instant the
device fell silent — at exactly the 200th ack (measured: 143 sync + 55
frame-ack + 2 render = 200). The cap is now effectively unlimited
(0x7fffffff, override with VPX_MAX_ACKS). Same run after the fix: 2500+
syncs, 1264+ frame-acks, no abort. The game progresses past sync into
L4VIDEO.cpp content/entity setup (loading the Thor mech and terrain) and
draws continuously.
Contrary to the earlier note, the content is on the production image: 841
.bgf models under ALPHA_1/REL410/BT/VIDEO/GEO (incl. BUTTEE.BGF,
MSLR.BGF, the THR* Thor mech). test.egg is only mission parameters (as
the pod owner said); the geometry loads from VIDEO/GEO regardless.
Game render config: game.conf (RIO disabled so the missing cockpit board
doesn't stall the control manager; cycles=max).
What the game's wire stream looks like (vs. flyk)
The game geometry path is heavier and different from flyk's flat DIVRGB:
- action 26 (0x1A) dominates — texture texel upload. 32-byte headers
[node][size][w][h](0x1000=64×64,0x4000=128×128 tiles) then 256-byte texel rows. 2880+ of these = the mission texture set streaming in. - action 28 (0x1C): material/light parameter blocks.
- actions 23/25 (verts/conns) still carry mesh geometry but are a small fraction of the stream.
- The scene graph is the full DPL hierarchy — zone → dcs → instance → object → lod → geogroup → geometry — with DCS transform matrices positioning each mech/prop. flyk's DIVRGB put geometry directly in geogroups at world coordinates.
Remaining (Phase 3d: game geometry on screen)
The window currently shows only the view background for the game because the
live backend's scene_publish_frame() walks the flat flyk layout
(geogroup→geometry at world coords). To draw the game's mechs it must:
- Traverse the full hierarchy (instance/object/lod/geogroup) and apply DCS transform matrices (accumulate the 4×4s down the graph) so each mech lands at its world position; pick a LOD.
- Arena content search path: the game logs
couldn't load object thr_tshd.bgf— that object lives inVIDEO/GEO/ARENA/and…/POLAR/(arena-specific), not the topGEOdir; the geometry search path needs the selected arena subdir (asetenv/config detail, not protocol). - Texturing (action 26 texel maps + SVT), lighting, depth/material. Current backend is flat-shaded untextured polys.
- DOS/4GW sync variant for the CYCLE
flyk yip.scnfixture (Red Planet geometry) — the older action-check sync.




