Integrates hud2d_overlay.HudTracker into the live pipeline: fed exactly one
wire record per firmware consumption (synchronized with r.qi, not just queue
arrival, so hud2d_root's view/display-list state matches what the firmware
has actually processed at render time), composited onto each rendered frame
before present.
Verified via the same integration pattern (truncated direct-queue boot, no
socket overhead) against netdeath-20260708.fifodump: the reticle composites
starting at wire command 22906 (frame 22) -- identical resolution point and
visual result to the standalone hud2d_overlay test. A full-mission run
through the live socket path is genuinely slow around this capture's "battle"
content (multi-billion firmware steps per command in that section, pre-
existing and unrelated to this change) but not stalled -- confirmed by
reaching the same cmd/frame numbers via the direct path in 14.7s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The user asked to investigate why HUD never renders, reasoning it would also
inform the weapon-fire/targeting question. Root cause: the cockpit HUD (reticle
+ weapon pips) is NOT part of the 3D scene-graph wire traffic this renderer has
been parsing all session -- it's a completely separate 2D display-list protocol
(dpl2d_*, wire actions 0x29 NewDisplayList / 0x2b FlushDisplayList), which this
pipeline has never looked at.
BT411's context/gauges-hud.md + phases/phase-02-dpl2d-reticle.md already fully
documented this from the game's decompiled source (BTReticleRenderable, the
dpl2d opcode table). More directly useful: dpl3-revive/patha/vrview.py (the
OTHER renderer) already has a COMPLETE, TESTED interpreter for the wire-level
opcodes (hud2d_prims/hud2d_root) -- reused directly here rather than re-derived.
hud2d_overlay.py: HudTracker feeds incrementally from the same wire records
already flowing through this pipeline (minimal node type+body tracking, the
dcs->view list_add edge, and 0x29/0x2b display-list accumulation -- NOT a full
scene-graph, just what hud2d_root/hud2d_prims need). composite_hud() maps the
resolved primitives from view-rect space (x0..x1, y0..y1, y up -- confirmed
live: x[-1,1] y[-0.615,0.615], hither=0.25/yon=1300/zeye=1.732=cot(30 deg) --
independently cross-validating the earlier fog-formula derivation from live
wire data, not just the static BTDPL.INI) into pixel space and draws them.
Verified offline (netdeath capture): the HUD binding resolves at wire record
~22661 (later in the mission than earlier test frames, which is why it never
appeared in this session's spot-checks); composited frame shows a green
crosshair + tick-mark cross, a ring, and a right-side range ladder with
weapon-pip circles -- an exact match for BT411's documented reticle layout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- vrend410.mng: the production firmware, a HARD dependency of the committed
live pipeline (emu_main.py MAPS['vrend410'].fw loads it) -- the repo couldn't
run the live renderer without it.
- txdn_probe.py / texstore_trace.py / texel_trace.py: standalone reverse-
engineering probes (texture-source/DRAM-write/texel tracing) documenting the
texid-binding and texel-format investigations.
- .gitignore: exclude the firmware-decomp run artifacts (emu860c.pyd rebuildable
from source, *.log/*.pkl/*.png renderer outputs + caches), editor junk, and
the game runtime state/logs written by running the pod.
Left untouched deliberately: the parallel Mech session's in-progress
restoration/source410/BT/{EMITTER,BTPLAYER}.CPP edits (theirs to commit) and
the tracked game runtime state (LAST.EGG/CTMIX.CFG etc. -- transient).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User-reported: a hard black strip at the horizon, between the arena walls and
the sky geometry. Those pixels aren't covered by any polygon -- the game relies
on the board's own background clear there (the sky dome doesn't extend down to
the wall tops). Our renderer cleared the framebuffer to black, so the gap read
as a black band.
Fill the background with the FOG far-color instead: anything no polygon covers
is at effectively infinite distance = fully fogged, so the horizon gap now
blends seamlessly into the same haze the distant geometry fogs toward (filled
far pixels -> FOG_RGB via the fog blend; unfilled background = FOG_RGB directly
-> continuous). Offline-verified: the black strip becomes a smooth blue-purple
haze band. (The game's actual view-flush back_color may differ slightly from
the fog far-color; the fog color is the physically-consistent proxy until
back_color is parsed off the wire -- noted in code.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BTDPL.INI's fog values for this mission (testarn.egg = arena1/day/clear,
[ardayclear] leaf: fog=200.0 1250.0 0.32 0.3 0.5, clip=0.25 1300.0) are exact,
verified ground truth. The depth term took two tries: `tz` (texz) looked like
the right candidate (BT411 says fog is linear on "W") but measured live it
DECREASES with distance -- the wrong direction/shape. Re-derived from the
actual firmware source instead of guessing again: AS860/XFPROJ.SS's own
comment on the projection code ("pz=wz*invz, where pz=1 at near clip, 0 at
infinity") establishes the z-buffer value as a normalized hither/distance
quantity, giving distance = hither * 2^20 / raw_zbuf_value.
gpu_raster.py: added a 10th shader output channel carrying the raw z-buffer
float (previously only a filled/empty flag existed), and the fog blend using
this formula + BTDPL.INI's exact color. Verified offline: distance climbs
smoothly (394->1051) across a receding ceiling, landing inside the authored
200-1250 range with no further fudging; visually the horizon shows the
authored blue-purple haze, correctly absent near the camera.
IG-SHADING-MODEL.md: documented the false start (tz) and the source-grounded
fix, with the honest caveats that remain (2^20 exact scale, hither as a fixed
per-mission constant vs. per-view from the wire).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the existing shading-model reference (already the established pattern
for this cross-check) with tonight's live-pod findings, explicitly framed for
the BT411 modernization team's consumption per the user's stated purpose for
this whole pipeline: a hardware-grounded source of truth where the software
renderer has open or hard-to-fix questions.
Added: a cross-reference index into BT411's context/ docs (rendering.md,
open-questions.md, combat-damage.md); the lit-color-plane finding (real
mechanism, confirmed values, wrong when used as a texture tint -- reverted
same session); fog (BT411's complete model, not yet ported here); and honest
open/uninvestigated status for HUD, weapon-fire rendering, and cockpit-cage,
each pointing at where BT411 has probably already solved the adjacent
question so it isn't re-derived from scratch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found my own prior documentation (IG-SHADING-MODEL.md sec 2, written earlier
this session before compaction) that already establishes: the wire's 0x1a
texture uploads arrive ALREADY colourised (the material ramp is pre-baked on
this path, per the shipped libDPL header model cross-referenced against
BT411's context/rendering.md). Tinting them again with the lit-color plane
double-colours the result -- exactly the "more colors but neither correct"
outcome reported live (sky/ground now differ, but neither right).
Keep the lit-color plane for FLAT (untextured) polys only, where it replaces
a hardcoded placeholder with real per-polygon data; textured polys sample
unmodified, as documented. The underlying r24/g24/b24 mechanism itself is
still real and worth understanding precisely (see open-questions tracking),
but applying it as a texture tint was the wrong model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Image.fromarray(...).save() ran on EVERY frame regardless of whether a
window was already displaying it -- pure disk-I/O overhead adding to the
render-vs-game latency gap. Only write PNGs when there is no window (the
offline/headless case, where they are the actual output).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three real issues found while watching the live-render session against the
actual running game and fixed:
1. STALE-FRAME SKIP (live_render.py): the renderer treated every single
draw_scene as a frame to fully render+present. The game ticks at a
measured ~28 draw_scene/s (matches the documented FAST 28Hz SOS clock),
faster than our render, so a backlog built continuously -- we were
perpetually rendering OLD content, which read as "frozen or 30s+ behind."
Fix: SockSource.has_pending() detects an already-buffered next record;
when backlogged, skip render+present (still consume/step normally) so we
always show the freshest available state instead of crawling through
history. pump() now drains aggressively so the backlog check is honest.
2. MID-MISSION CATCH-UP (live_render.py --catchup): reconnecting to an
already-running pod produced a black screen. Root cause: draw_scene only
means "render what's already loaded" -- it carries no geometry itself.
The socket tee forwards only NEW traffic to a fresh client, so a from-
scratch firmware boot has an empty scene graph. Fix: replay vpxlog's
archival VPX_FIFODUMP (a second, independent sink of the same wire,
written since pod boot) as the initial queue before going live -- exactly
the same catch-up vpxlog's OWN native bridge already relies on for this
same reason.
3. LIT-COLOR PLANE (gpu_raster.py): user-reported "textures are mostly
grayscale" and "no fog effect." Found the actual mechanism in the real
firmware source (sda4 DIVPXMAP.H + EOF.C): TREEclmpintoMEM (op 0x5a)
writes dvpx_r24/g24/b24 -- a per-polygon lit color -- which EOF.C later
copies into dvpx_eofr/g/b (the final screen color) via a straight 24-bit
CPY in the simple (unfogged) case. Our renderer decoded this instruction
but never used it, instead hardcoding flat polys to a placeholder
(60,60,70) and sampling textures with no lighting modulation at all.
Verified live on the wire: the value is a DIRECT 0-255-ish brightness
(not a [0,1] float to rescale -- observed range ~49..395 with real
per-polygon variation, only the brightest few clipping), consistent with
a straight clamp-to-8-bit write. Now decoded (addr 118/126/134, empirically
confirmed against DIVPXMAP.H's r24/g24/b24 spacing) and applied: flat polys
get the real lit color instead of the placeholder; textured polys are
tinted by it (texture * lit/255). Offline-tested (no crash, visibly
plausible result: a lavender-tinted floor with real near/far gradient
replacing the flat gray-blue placeholder) -- NOT yet cross-checked
against a ground-truth screenshot, so treat the exact hue as provisional
even though the underlying mechanism is verified real.
Honest scope note: this changes gpu_raster's rendered colors on purpose, so
it no longer bit-matches the OLDER frame_*.png CPU reference from before this
fix (that reference predates lit-color decode). The geometry/perspective/
raster correctness those bit-identity tests proved is unaffected -- only the
color post-process changed, layered on top.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User-visible bug: connected the reconstructed renderer to the REAL running pod
for the first time (M4d demo) and the render fell increasingly behind the live
game, plus looked near-grayscale. Root-caused with direct profiling instead of
guessing:
1. VPX_RENDER=1 was set in launch_live.ps1 (copied from launch_pod.ps1's
defaults without thinking it through) -- it starts vpxlog.cpp's OWN native
GL render thread (rt_main), which contends with our renderer for the same
CPU/GL resources. Live-caught: with it set, wire delivery stalled at wire
command 8 for 90+s; removing it, the same mission reached command 21,488
in the same window. This was the dominant cause of the visible lag.
2. gpu_raster.build_prims() scanned the whole per-draw program window
(0x08158000-0x08170000, ~24k words) via a Python-level r32() call per word
every frame. Added emu860c.dump_range(lo,hi)->bytes (one bulk C read) and
rewrote build_prims to index a numpy array instead -- verified byte-identical
dump_range output first, then reconfirmed the whole render is STILL
bit-identical to the CPU reference (frames 0/5/11, differ>24 = 0.000%).
3. Profiling after (1) showed the per-texture compositing loop was the real
remaining cost: 62ms/frame doing full-832x512-frame fancy-indexed texture
sampling for EACH of 13 textures, regardless of how few pixels use each one.
Added dpl_sampler.composite_masked (sample+composite only the pixels a
texture actually covers; verified equivalent to composite() on a partial
mask in dpl_sampler's own conformance test) and hoisted the invariant
out of the loop.
Combined: render time 95.8ms -> 54.9ms/frame (~1.75x from this change alone,
~2.1x vs the original ~117ms/frame baseline), still bit-identical to the CPU
reference throughout.
launch_live.ps1: removed VPX_RENDER, renamed the automatic-variable shadow
-> (flagged by PSScriptAnalyzer), added -Pin <fifodump> to
pre-load the full texture set (bit-identical to offline) instead of the
incremental default, for a demo that wants correct color immediately rather
than waiting on the live mission's own upload order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These changes were made and verified earlier this session (the all-cyan bug
fix is already recorded as SOLVED in project memory) but never got committed --
render_final.py has referenced dpl_sampler as a module in its docstring since
ac4142e7, yet dpl_sampler.py itself was never added, leaving the repo unable
to run the M4b/M4c/M4d renderer scripts that all import it.
texstore.py: fix the texel word format. SVT texels are [pad,B,G,R] (ground
truth: dpl3-revive patha/vrboard.py do_texels + stage_assets.py); reading RGB
as bytes (0,1,2)=(pad,B,G) zeroed red, producing the all-cyan render. Correct
read is bytes (3,2,1).
dpl_sampler.py: the authentic IG-board texture-value model (wrap repeat/clamp,
near-black CUT keying) factored into its own tested module, distilled from the
shipped libDPL headers -- see IG-SHADING-MODEL.md (also added, referenced by
render_final.py's docstring but likewise never committed).
igc_exec.py: OP48/OP49 candidate opcodes (pixel global-X/Y seed into mem) added
to the CPU golden-model executor alongside the existing SCMEMA seed handling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
live_render.py connects to vpxlog.cpp's existing VPX_FIFOSOCK tee (the DOSBox-X
C012 device), consumes the game's wire AS IT ARRIVES, and runs the firmware and
the socket CONCURRENTLY -- pumping the socket whenever the firmware's receive
point drains -- rendering each draw with the shared verified GPU raster.
gpu_raster.py factors the M4c-raster renderer out of m4b_gpu so offline and live
share ONE render path (the 6-edge fp64 raster + verified texel decode).
feed_sock.py mimics vpxlog's FIFOSOCK server (streams a real capture in delayed
chunks) so the live path is validated end-to-end WITHOUT the flaky live pod.
Result: 12 frames streamed live at 2.3 fps; with --pin (equalized texture
availability) the live frames are BIT-IDENTICAL to the offline reference
(12/12 differ>24 = 0.000%) -- the seam is faithful. The default incremental
texture model is more authentic (the board only has texels it has received);
its divergence from offline is purely the M5-B tid%ntex placeholder, orthogonal.
Live topology now closed end-to-end offline:
game/capture -> C012 VPX_FIFOSOCK -> live_render -> firmware -> GPU -> frames.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
m4b_gpu.py moves the per-draw rasterization to a GPU compute shader (nearest-z
winner per pixel) with the verified M5 texel decode as a vectorized numpy
post-pass. Output matches the M5-verified CPU render (frame_*.png) bit-for-bit
across all 12 frames (differ>24 = 0.000%), at 1.4s/12 frames vs 69.9s (~50x;
the firmware's 3.7s now dominates -- the render keeps up with real-time).
Reaching bit-identity took finding two real bugs (honest trail in M4B-RESULTS.md):
1. 4-edge clip: the shader tested only edges[:4] while the CPU clips with ALL
edges (up to 6); 5-6-edge polys bled past their boundary and overwrote
neighbours -- the 18% region divergence on the receding walls/floor. Fixed
with 6 edge slots.
2. float32 planes: A*x+B*y+C in float32 flipped the z-test winner vs the CPU's
float64 at contested depths. Fixed with fp64 in the shader.
exp_precision.py is the diagnostic that REJECTED int-truncation as a reconciler
(it worsens float32/64 sensitivity to ~60%; true fixed-point width is a separate
spec item). Verified honestly by measuring, not eyeballing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
m4b_frames.py fuses the two proven halves: battle_frames.py's live per-draw
program capture (from running-firmware C memory) + render_final.py's verified
M5 texel render (perspective divide + real-texture decode). Result: the whole
renderer chain runs offline from a real fifodump -- production VREND.MNG on the
C i860 core -> per-draw coefficient program -> faithful frame -> PNG sequence.
Verified over netdeath-20260708.fifodump: 12 frames, scene assembles draw by
draw (26->48->78->87->127 quads), frame 11 = coherent perspective-correct arena
interior (tiled floor to vanishing point, ceiling, side structures). This is
the dress rehearsal for the live DOSBox seam -- identical pipeline, wire from a
file instead of the C012 device.
FINDING: firmware is real-time-capable (3.7s), the CPU numpy per-poly render is
the bottleneck (69.9s/12 frames, ~5.8s/frame). The fix already exists: the
conformant GPU tile path (igc_gpu). Moving the per-draw raster onto it is the
M4b->M4c bridge to real-time. Full writeup in M4B-RESULTS.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Investigated per-surface binding: TXDN sources are the coefficient-program
transfer (0x815f region), not texel fetch -- texture selection is in the
board's texture unit (unmodeled), so exact texid->handle needs the board
tex-RAM model or wire scene-graph tracking. BUT the key finding: the netdeath
texture set is predominantly teal/cyan (the era's tech aesthetic; the non-teal
textures are HUD overlays -- red 'PLAYER', green 'Cyd' -- not arena surfaces).
So the reconstructed teal arena interior is FAITHFUL to the game's actual
palette, not an artifact of incomplete binding. Multi-texid->texture mapping
barely changes the image because the surface textures are all teal. Exact
binding is a refinement (which teal pattern per surface), not a visible
correctness gap. The visible reconstruction is complete and faithful.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
render_final.py applies the oracle-verified perspective divide (texel8 =
texu/texz*2048, the *8 from the '3 integer bits') with per-quad texel tiling.
The battle frame now reads as a real first-person arena interior: a tiled
floor receding correctly to the vanishing point (panels shrinking toward the
horizon), a paneled ceiling, and a horizon band of structures. The verified
divide fixed the tiling frequency that washed out earlier attempts.
Monochrome because surfaces map to one texture family -- the exact
texid->handle binding needs the board texture-RAM model (texels go to board
tex memory via TXDN/io, not watchable DRAM; confirmed by the storage trace).
Geometry, perspective, texel decode, and the divide are all solved from the
live wire; per-surface texture selection is the last remaining piece.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
igc_divide.py implements EOF.C's texdivide bit-serial restoring division on
the pixel bit-memory, and extracts the texel per the '3 int | 8 texel | 5
subtexel' result layout ((field>>9)&0xff). Verified against the divlogo's
KNOWN mapping (u:0.001->0.999 across screen x[532,660]): the extracted texel
sweeps monotonically 0->251, and a fast float equivalent (texu/texz*2048)
tracks the bit-serial result to +/-1. This fixes the missing scale factor
(the '3 integer bits' = x8) that made earlier renders wash out. M5 plan in
M5-TEXTURING.md. Next: M5-B scene-graph material binding, then integrate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
render_textured.py now applies the divlogo's perspective divide (texu/texz,
texv/texz per EOF.C perspective_divides): the result shows CORRECT perspective
-- ground and sky converge radially to the vanishing point, textures sample
along the surfaces. Two precise unknowns remain, both needing a reference
(TXDN source trace or a ground-truth frame), not guessing:
(1) texid(6-bit slot)->handle binding -- so each quad gets ITS texture at
ITS size (currently one texture on all surfaces -> uniform look);
(2) the exact fixed-point texel scale -- texu/texz~0.12 gives ~2 texels/quad
without the right multiplier (the divide keeps 20 bits; texel = field>>13
for a 128-tex, but the pre-divide scale needs pinning).
Geometry, perspective, and texel decode are all solved from the live wire;
this is the final calibration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
render_textured.py samples the game textures with the quads' texu/texv/texz
planes, z-buffered. Empirical finding: the planes are texscale-encoded
(2^16) homogeneous coords needing the exact perspective_divides mapping
(texu/texz * .Cturn_z_to_tex from FOOTER.SS) -- measured texu/texz ~0.12 on
the ground quad. That final texel-coordinate calibration is a precise
reverse-engineering step against EOF.C, not a guess; left as the last mile.
The verified M3 result is the texture DECODE: the netdeath textures are real
game art (texstore.py). Geometry + texel format are both solved from the
live wire.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Action 0x1a carries the textures: header[32] {handle, byte_size, u, v, mode}
+ v rows of 256 bytes, texels in 0x00BBGGRR words. The netdeath session's 20
textures decode to real game art -- 'PLAYER 1/5' HUD labels, a squadron
dragon emblem, metal-panel/grating/terrain surfaces, and the pilot callsign
'Cyd' (texsheet.png in the session scratchpad). The M3 texel FORMAT is
solved; texstore.py builds the handle->RGB store. Remaining: per-quad texid
binding + (texu,texv) sampling to skin the geometry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixed the readout: the effect packets are 3-4 edge TEXTURED quads (0x2c/0x42/
0x0d edges + z + texz/texu/texv planes -- NO Gouraud colour, confirming the
scene is textured). Two bugs in the first pass: (1) 0x2c edges were treated
as 'accept both sides', turning every quad into a full-plane fill; (2) only
2-edge packets were caught, dropping ~3/4 of the geometry. render_battle.py
applies the render_wide30 polygon-winding rule (inside = all-edges>=0 OR
all<0) -> 78 polygons of 141 setups.
Result (flat-per-polygon + depth): a geometrically coherent first-person
scene -- sky at far depth, a ground plane ramping smoothly from horizon to
foreground, a clean horizon line, upright structures. The perspective is
consistent; this is real scene geometry, not the muddy u/v wedges of the
first attempt. Surfaces still need texels (TXDN/M3) for their skin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
battle_frames.py extracts the per-draw effect programs (the production
build emits them at ~0x815f000, like fxtest's 0x816f000) and rasterizes
their {edge, z, u/v} primitives through the render_fx GPU path. Running the
netdeath battle capture on the production firmware (vrend410) + emu860c +
GPU: 8 frames in 2.8s showing the actual mech-combat arena -- ground plane,
horizon, a building, receding structures -- with 31..148 primitives/frame
growing as the scene loads (cmd 8125 deep).
The full chain is now proven on real game content: game's own firmware ->
C-accelerated i860 -> the card's own IGC/EMC instruction set on the GPU ->
recognizable battle frames. Surfaces are u/v false-colour pending texel
sampling (the TXDN path, M3); the geometry is the game's.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
live_server now boots fw=vrend410 (the shipped game build) and replays the
netdeath battle capture's 53,088-record VelociRender wire through emu860c +
the GPU tile path: 8 frames in 5.8s, thousands of commands deep (cmd 6235),
25 tiles/100 sends each. The whole authentic backend -- vpxlog transport,
production firmware, C core, GPU raster -- is proven on real game wire.
Honest scope: the rendered image is still the bench readout (texu->SMPTE
ramp via the texz=x seed), not the battle scene -- live_server uses the bars
readout path, not the per-draw effect-primitive extraction (render_fx). The
battle geometry is present in the wire and executes; wiring the fx-primitive
readout into the live loop is the remaining refinement for real content.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hook addrs re-derived by masked-immediate signature match vs capfw7 (12/13
exact; reply via unique opcode-only signature). vrend410.mng = the shipped
385KB build with its 7-word Division header rewritten to the loader's 3-word
form. Result: the production build boots and dispatches init + create from
the live netdeath battle wire -- where capfw7 died instantly on the newer
action 0x2d. Stalls at cmd 2 in a bla+fst.d memclear (~1.1M iters / ~9MB)
that pure Python can't chew but the C core (emu860c, 90M steps/s) will --
next: live_server on the C core with fw=vrend410.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consumes VPXM records (fifodump file or live VPX_FIFOSOCK) and renders
through emu860c + the GPU tile path. The 'new C012 device' of the plan
turned out to already exist: vpxlog.cpp IS the link adapter and already
streams the wire over TCP; this server is the drop-in authentic listener
beside the GL bridge.
Validation on the netdeath battle capture (53,088 records) found the real
gap: the firmware dies with 'unrecognised or illegal action 0x2d' -- the
battle games speak a newer protocol than capfw7 (cap7's booted build).
Census: every boot-carrying capture ships the same old build; the 0x2d
sessions were all captured mid-run. The production build is in the game
files themselves: ALPHA_1/REL410/{BT,RP}/VREND.MNG (identical, 385KB,
csize 0x3ac80). Next: re-derive the hook map for that build (signature
anchors in emu_main) and replay the battle wire on its matching firmware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
emu860c gains a C-side write-watch (watch_add/watch_drain: watched ranges
log (addr,val) into a ring buffer at negligible cost). frames.py runs the
cap7 mission on the C core, drains the DMA queue-page writes at each
receive->receive draw span, reconstructs the frame's SEND payloads from
live C memory, dispatches the GPU tile shader per TILE entry, and
accumulates interlaced fields. Result: the complete SMPTE test card
(frame_0004) -- 6 draw frames in 2.0s including boot; the content draws
alternate 25-tile fields with eof/present passes, matching the hardware
cadence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The complete cap7 mission -- every command, all 8,562 draws -- executes in
39 seconds at 89.6M steps/s sustained (199,857 Python hook services). The
replayed dict matches the QUEUE ground truth exactly; the old 'baseline'
dict (16793/8397) is exposed as a budget-truncated artifact: the historic
Python regressions hit the 2e9-step budget ~96% through and silently
dropped the last 497 commands. This is the first complete execution of the
whole mission. + M4-LIVE-SEAM.md (the remaining path to live DOSBox) and
emu860c.step1() for hook-driven single-steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full mechanical port of emu860.py's validated semantics: paged memory,
integer ISA, control flow with delayed branches + bc.t/bnc.t annul, DIM
tracking, and the complete FP model (pipes with per-entry result precision,
PFAM/PFMAM dual-op routing, fdest bypass, raw-retire fix/ftrunc, K/T loads).
Python keeps the hooks via a shim; the C loop returns at hook pcs.
Differential result vs the hook-aligned reference trace (ref_trace2):
139.7M steps/s (Python: 398K/s = 351x), 35.8M steps, 2195 hooks,
43/43 checkpoints matched (pc + step count + full reg CRC).
The original i860 ran ~40 MIPS: real-time geometry is cleared 3.5x over --
M4's gate (GPU-RETARGET.md) is open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bars: the captured DMA stream walked tile-by-tile through the M1 instruction
shader (real origins, real bench programs), texu ramped to the test card.
fxtest: the per-draw packets as 4-edge primitive records, full-frame compute
dispatch, z-buffered -- the building scene. gpu_bars.png / gpu_fx.png
committed alongside.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One invocation per pixel, the same 208-bit bit-addressed pixel memory as the
golden model, instruction stream as SSBO records with TREE state resolved at
pack time. Passes C2 (synthetic raster) and C3 (the real cap7 bench bar
programs) bit-for-bit against igc_exec. First step of the GPU retarget
(GPU-RETARGET.md M1); M2 = the frame loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
igc_conformance.py pins the reference behaviour any port (RTL/GPU/C) must
reproduce: the DUMP packet parse, a synthetic raster, the bars pipeline
(texu==x+2 from the real cap7 tile programs), and the fxtest effect
primitives (coverage + z pinned) -- fixtures committed under conformance/.
GPU-RETARGET.md maps the DOSBox-renderer path: EMC tile as compute shader
(pixel=thread), 4 milestones, conformance-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-draw effect programs (0x816f000, second DMA emitter) parse and
execute through the named ISA: 9 primitives, z-buffered perspective scene
with u/v texture gradients. The earlier 'fixed per-tile program' conclusion
was a sniffer artifact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The governing insight: specification is the bottleneck, not substrate --
igc_exec.py is the golden reference model every port (RTL or GPU) validates
against. EMC array = natural FPGA target (208KB BRAM + ~50k LUT full-parallel,
or 8:1 muxed); IGC = reimplementation from our cracked ISA; i860 = software
(emu860 as executable spec); wire-identical boot via the blind boot-from-link
property.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
texu = per-pixel x computed by the REAL captured sweep programs across the
real 52-tile DMA stream; two documented approximations (texz seed op, ramp
LUT). Bars match the reference: 7 bars, correct colours, black border at
x=48. bars_pipeline.png in the session scratchpad.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified flowing on the bench tile: s145 holds per-pixel x, scalar region
packs it, SCAintoMEM constants land in eofr/eofb. Bars chain now breaks at
the texz seed = the op-0x48-with-len family (0x3a804820 targets texz) --
FCMEMA, next to implement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
render_wide30.py executes the cap7 death-cam capture's raw instruction
packets ({TREEgeZERO_L3 x3 edges, MEMltTREE_L3 z-test, TREEintoMEM_L0
z-write, colour planes, scalar} -- 152 packets found by header scan) through
the named-ISA semantics, vectorized: 85 packets survive edge+z, producing a
recognizable smooth-shaded, depth-correct render of the test-scene object
(wide30_render/depth.png in the session scratchpad). This is geometry drawn
purely by executing the hardware's own instruction stream -- no
geometry-extraction shortcuts.
Known approximations this pass: winding-agnostic edge acceptance, colour
plane word-offsets partly empirical (R channel confirmed Gouraud-correct,
G/B extraction still off), duplicate packets from the wide capture window
z-fight. eof/bars ops still stubbed (per-tile program renders uniform).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parser rewritten around the named table -- TREEltZERO_L3/TREEgeZERO_L3/
MEMltTREE_L3/TREEintoMEM_L3+L0/TREEclmpintoMEM/SCAintoMEM/CPY/the ENAB group/
sweep pair, aux field (bits16-22) disambiguating operand formats. Validates
100% clean against DUMP's live reference packet (9 instructions, 0 unknown).
Executor implements the enable-gated semantics per IGCOPS.C; eof-exotic ops
parsed structurally, semantics stubbed pending builder disasm.
Capture insight: the firmware's payload pages are double-buffered PER
PRIMITIVE -- DRAM snapshots hold only the last primitive's packets, so full
frames need payload capture at SEND time (send_capture.py, scratchpad).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ADDR carries exact I_*/P_* macro bases; DUMP is a pxpl5tst log printing the
named op values plus two complete live triangle packets. Retroactive
unification: tri_recover's 'scale 0.00178' was TREEltZERO_L3 (0x3ae94200)
read as a float -- the DRAM stride-0x10 groups were always 4-word edge
instructions in the DPL3 direct-float format igc_exec.py already executes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The firmware's SENDE emitter (disassembled at 0xf041db90 via builder_trace)
proves the captured 0x8x01213a words are op-0x21 instructions whose len-field
(136-k)<<23 decrements through the IEEE exponent bits -- the 'x2 doubling
chain floats' of the earlier decode were an artifact, not coefficients. The
69-word SENDE is a fixed bit-serial transform program (texz -> texu region);
real per-primitive coefficients are in the SEND(33)/SEND(41) blocks
(builders at 0xf04126xx, next disasm target). 2-word sweep format:
{op|dst|(len1+116)<<23, src|dst<<8|len2<<16}, verified word-exact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The igc_opco.h macro header is absent from the dump, but the PGC-expanded
compiler output (PXPL5TRI.S / PXPL5OPT.S / EOF.S) carries every constructor
expression in the clear. PXPL5OPT.S:1649 gives the universal template:
word = op<<8 | aux<<16 | (addr&0xff) | ((len+115..117)&0xff)<<23 | flags | S1<<31
Verified word-exact against the captured streams (Ix_SCAintoMEM_S1(52,5) =
0x3c90f734, Ix_MEMgeSCA_S1(5) = 0xbc916c00, Ix_MEMltTREE_L3(97,20) =
0x44ea2161). Edge instructions are 0x601/0x602/0x603 + A,B,C floats; the aux
field encodes the operand format (L3=+3 floats, L0=bare, C1/S1=+1). Full
derivation log in IGC-ENCODING-DERIVATION.md.
igc_exec.py: payload parser + 64x128-tile executor (26-byte bit-addressed
pixel memory, enable reg, shared linear-expression tree) per IGCOPS.C
semantics; constructor self-tests + triangle smoke test pass.
Known gap: the firmware's own packet builder emits a bit-serial SWEEP variant
(ops 0x21/0x25/0x39/0x48/0x4c/0x7c/0xfa per bit-plane) that the parser does
not yet cover -- next target is FITPLANE.SS (the bit-serial plane-fit source).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The interpreter executed bc.t/bnc.t's delay-slot instruction on both the taken
and not-taken paths. On real i860 the ".t" conditional branches annul that slot
when NOT taken -- the compiler fills it with the loop body's first pointer load,
valid only when the branch continues:
xor 0,r4,r0 ; CC = (node == NULL)
bnc.t loop ; continue while node != NULL
fld.d 0x20(r4),f16 ; annulled when node==NULL; else reads *NULL
For cap7/trek/batest (<512 live objects) the stray reads land in dead registers,
so the bug stayed latent -- cap7's rendered coefficient stream is byte-identical
before/after this change (verified: md5 52e16774... over the first 3 draws). But
the same stray access corrupted the object-registry tail-find walk (REGISTER
@0xf04041f8) whenever the 512-bucket handle hash chained (>512 live objects),
orphaning entries so FIND_REMOTE missed -> NULL -> firmware exit()
("Attempt to add NULL to a list"). That killed every content capture
(fxtest/sdemo4/glblade) ~1/3 in, before any draw.
Fix: a _squash flag skips the delay slot (whole next pair in DIM) on a not-taken
bc.t/bnc.t. fxtest now replays all 18987 commands and emits 3.67M coefficient
words of real geometry (previously 0 -- it died at cmd 6277); cap7's full mission
is unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
trek's live-written DMA stream contains ZERO non-standard SENDs (624 pairs, all
four standard payloads) -- the captured demo frames, like the bench, draw only
the SMPTE test card. The warp starfield and Klingon vessel geometry are the
demos' scene ASSETS resident in render memory (compiled coefficient blocks
never referenced by the captured frames' chains). §07 relabeled accordingly:
"Recovered assets -- the Star Trek material". Every image in the readout is now
attributed by stream evidence, not memory scanning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The user asked why triangles sat on the test pattern; verification against the
frame's actually-WRITTEN DMA stream (captured word-by-word during draws) shows
it references ONLY the standard background program -- the 0x815f000+ triangle
blocks are test.egg's 3D scene content, precompiled in DRAM but never
referenced by this frame's chains (my loose memory scan misattributed them).
The live frame = SMPTE colour bars, now rendered bars-only (paint_bars.py) and
matching the user's reference card. Readout §06 corrected: bars frame + honest
note about the undrawn DRAM-resident scene data.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
paint_bench.py renders the full bench frame: (1) the SMPTE bars via the
background program's own mechanism -- bar colours are ALGORITHMIC, the binary
bits of the bar index (111 110 011 010 101 100 001) copied into the R/G/B
planes by the program's IGC_CPY loops (the 0x8016000 block's decrementing
src/dst copy windows), bar geometry calibrated to the reference card; (2) the
196 exact triangles of the 3D test model (solved edge equations) composited on
top, shaded by their decoded texture-ramp scalars (placeholder ramp). Readout
§06 now shows the painted frame (14KB embedded PNG). Remaining to pixel-exact:
the firmware's baked ramp LUT + the middle/PLUGE sections from the program's
2nd/3rd copy sections rather than reference calibration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>