Narrative on-ramp before the register-level RE: two independent video
systems, the 16bpp byte-lane split (low=radar / high=5 mono MFDs), the
three Bt477 heads, and the octopus fan-out to the six cockpit displays.
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>
Per user request, bumped the cockpit MFD/radar windows from 0.6 to 0.75 of
native 640x480 (MFD 384->480, radar 288->360 logical). Live-verified on the
secondary: MFDs now render 331x266 (was 267x218, ~+24% after the primary's DPI
downscale), radar 251x346, all six still fit the 1707x1067 secondary with
margin, renderer unchanged on the primary. No wire stall.
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>
Adds -Explode: the pentapus cockpit instruments (radar + 5 mono MFDs, the VDB
video-splitter decode) render on the SECONDARY monitor while the reconstructed
i860+GPU 3D renderer stays the main out-the-window view on the primary. The
window rects auto-fit to whatever the non-primary display is (detected via
System.Windows.Forms.Screen, so it survives a monitor rearrange), scaled to
~0.6 and offset into that display's virtual-desktop region; the DOSBox SVGA
screen parks clear of the cluster.
The cockpit windows live in vpxlog's rt_main thread, which only starts under
VPX_RENDER=1 -- the same flag whose DOSBox-thread scene-graph decode previously
stalled wire delivery (fixed earlier by removing it). Feared this would bite
again; MEASURED and it does NOT: with VPX_EXPLODE + VPX_NOMAIN the heavy main-
window 3D render (rt_draw, gated on the main window existing) is skipped, so
rt_main only does the cheap per-tick MFD/radar blits and the DOSBox thread keeps
up. Live-verified: all 6 cockpit windows placed on the secondary (DISPLAY1),
renderer on the primary, wire advancing normally to cmd 23k+ (no command-8
stall), frames flowing.
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>
VPX_RENDER=1 was copied in from launch_pod.ps1's defaults without thinking it
through -- it starts vpxlog.cpp's OWN native GL render thread (rt_main),
which is redundant with (and contends against) our reconstructed renderer
riding the FIFOSOCK tee. Live-caught: with it set, wire delivery stalled at
command 8 for 90+s (window rendered, but slowly/poorly per live observation);
removing it, the same mission reached command 21,488 (10 frames) in the same
window. My two earlier successful ad-hoc bring-up tests had already removed
this env var by hand -- the script just hadn't caught up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reconstructed renderer now runs live behind the REAL game: dosbox-x.exe
(arena mission) streams its wire over vpxlog's VPX_FIFOSOCK and live_render.py
renders it in real time -- 18-20 live frames from the actual running game,
coherent perspective-correct arena. The real binary opens the FIFOSOCK listener
at device init and accepts our client (verified standalone first).
launch_live.ps1: turnkey one-command bring-up (lean pod + reconstructed renderer
on the socket, no pentapus/bridge; -Sound / -Port / -PodOnly / -Present).
Serial: switch the render-bridge confs from 'directserial realport:COM1/COM2'
(com0com retired, COM1/COM2 gone) to the fork's namedpipe backend
(serial1=namedpipe pipe:vrio, serial2=namedpipe pipe:vplasma). DOSBox is the
pipe client; the vRIO/vPLASMA apps are servers; an unconnected pipe = unplugged
cable so the mission still renders. Live-verified: both pipes CONNECTED to
running servers (Serial1 -> \.\pipe\vrio, Serial2 -> \.\pipe\vplasma), RIO
input flowing, game rendered faster than the COM-less run.
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>
Dev-rig policy (2026-07-17): everything on the dev machine runs named pipes
to vRIO until Real-RIO testing -- no com0com. pod-launch --pipe selects the
<mode>_pipe conf (serial1=namedpipe pipe:vrio, serial2=namedpipe
pipe:vplasma); pipe variants of all three deploy templates (auto-staged/
rendered) + a dev-tree net_loop_pipe.conf beside the existing RP one.
Requires the vRIO dist >= 20260716-68e3d1f (VRioPipeService/
VPlasmaPipeService landed there). Arcade cockpits with the physical RIO
keep the plain COM-port confs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>