Commit Graph
93 Commits
Author SHA1 Message Date
CydandClaude Opus 4.8 84ea322a18 Document the weapon-fire/pick-range investigation, paused per user
Records tonight's finding before stopping this thread: the user correctly
pushed back on an oversimplification (the renderer isn't a purely passive
wire tap -- the protocol has a real synchronous pick-query shape, sect_pixel/
sect_vector, where the board runs its own Moller-Trumbore ray-triangle test
and replies with range+identity). Checked directly against the REAL firmware
(not just the reference software renderer's notes):

- sect_pixel/sect_vector never appear in this capture -- unused by this build.
- Action 0x23 (the notes' fire/pick candidate) gets no reply from the real
  firmware either (checked via the h_reply hook, which fires on ANY
  firmware-issued reply) -- but its payload doesn't match the documented
  FLYK format at all (no screen coords; a counter-tagged discharge-event
  shape instead), and its wire context (list_remove + zone-color + a
  PSFX-shaped 0x1d) reads as a weapon-discharge notification with visual
  consequences, not a pick query.

Net: open question, not resolved. BT4.10/MUNGA's real pick/range mechanism
(if board-side at all) is not yet identified. Paused per the user rather
than continue an open-ended search without a specific pointer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:58:48 -05:00
CydandClaude Opus 4.8 ccc6ee6914 Fix HUD vertical flip: dpl2d Y grows DOWN, not up
User-reported live: the torso-twist tape appeared at the TOP of the reticle
(should be bottom) and the range ladder filled the wrong direction. I had
assumed dpl2d used a standard math y-up convention (y0=bottom, y1=top) and
inverted accordingly.

Found the exact, already-solved answer in vrview_gl.py's own HUD vertex
shader comment: "dpl2d Y grows DOWN (screen convention): period VHS footage
shows the twist dial UNDER the reticle and the range bar filling top-to-
bottom; mapping straight onto GL's Y-up NDC rendered the whole HUD flipped"
-- the exact same bug, already diagnosed and fixed there. y0 (the smaller
raw view-rect value) is the TOP of screen, y1 the bottom -- direct mapping,
no inversion needed.

Verified offline: the composited reticle now shows the twist tape (with its
bowtie shape) at the bottom and the range ladder/pips reading top-to-bottom,
matching BT411's documented layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:46:03 -05:00
CydandClaude Opus 4.8 3e766c13c1 Wire the HUD reticle into live_render.py
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>
2026-07-21 14:48:14 -05:00
CydandClaude Opus 4.8 859529d6f2 HUD reticle: the missing wire mechanism found + composited (verified live)
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>
2026-07-21 14:40:31 -05:00
CydandClaude Opus 4.8 a89d08e1c9 Commit the vrend410 firmware asset + RE tooling; gitignore run artifacts
- 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>
2026-07-20 11:43:46 -05:00
CydandClaude Opus 4.8 f5fc241603 Clear the renderer background to the fog color, not black
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>
2026-07-20 11:33:09 -05:00
CydandClaude Opus 4.8 66cef329d1 Implement fog: source-derived depth formula, not a guess
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>
2026-07-20 11:13:55 -05:00
CydandClaude Opus 4.8 9ebdfc4610 IG-SHADING-MODEL.md: document tonight's ground-truth findings for BT411
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>
2026-07-20 11:02:48 -05:00
CydandClaude Opus 4.8 0527022ecd Revert texture tinting by the lit-color plane -- it double-colours
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>
2026-07-20 11:01:27 -05:00
CydandClaude Opus 4.8 3473f5facf live_render: skip the per-frame PNG write once a window is showing
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>
2026-07-20 10:43:30 -05:00
CydandClaude Opus 4.8 854ec0b216 Live-view fixes: stale-frame skip, mid-mission catch-up, lit-color plane
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>
2026-07-20 10:42:51 -05:00
CydandClaude Opus 4.8 ef7db9f2e4 Fix live-render lag: bulk memory read + masked texture compositing (~2.1x)
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>
2026-07-20 10:07:55 -05:00
CydandClaude Opus 4.8 bf234842cb Commit the texel-format fix + shared dpl_sampler module (was uncommitted)
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>
2026-07-20 10:07:16 -05:00
CydandClaude Opus 4.8 e0a2073dff M4c-device: the LIVE socket seam -- proven bit-identical over VPX_FIFOSOCK
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>
2026-07-20 09:22:36 -05:00
CydandClaude Opus 4.8 b688e0e4bd M4c-raster: GPU render BIT-IDENTICAL to the CPU reference, ~50x faster
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>
2026-07-20 09:14:18 -05:00
CydandClaude Opus 4.8 695aacec30 M4b: offline end-to-end seam -- live mission -> faithful frame SEQUENCE
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>
2026-07-20 09:01:50 -05:00
CydandClaude Opus 4.8 ac4142e7c7 M5-B finding: the teal arena is FAITHFUL -- the game's texture palette is teal
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>
2026-07-20 07:50:13 -05:00
CydandClaude Opus 4.8 a24210adf0 M5 integrate: recognizable arena interior -- verified divide makes texturing land
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>
2026-07-19 22:15:44 -05:00
CydandClaude Opus 4.8 b74b0c89e8 M5-A: the faithful IGC perspective divide, verified against the divlogo oracle
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>
2026-07-19 22:13:10 -05:00
CydandClaude Opus 4.8 cb5ea73ab2 M3 texture mapping: perspective-correct sampling proven; scale/binding = the calibrated boundary
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>
2026-07-19 20:54:20 -05:00
CydandClaude Opus 4.8 60d812484e M3 sampling framework + honest calibration boundary
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>
2026-07-19 16:23:29 -05:00
CydandClaude Opus 4.8 cc2d9290be M3 texel decode: the game's own textures recovered from the wire
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>
2026-07-19 16:18:39 -05:00
CydandClaude Opus 4.8 b4c2337ab1 Battle geometry, rendered honestly: coherent arena floor + horizon + sky
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>
2026-07-19 16:10:49 -05:00
CydandClaude Opus 4.8 fecb5a5f20 M4c COMPLETE: real battle geometry from live game wire, end to end
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>
2026-07-19 15:48:31 -05:00
CydandClaude Opus 4.8 979c82b37c M4c end-to-end: production firmware runs LIVE BATTLE WIRE on the C core
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>
2026-07-19 15:44:16 -05:00
CydandClaude Opus 4.8 99f46bea78 emu_main: vrend410 map -- the production firmware (game BT/RP VREND.MNG)
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>
2026-07-19 15:42:49 -05:00
CydandClaude Opus 4.8 28785d9486 M4c: live_server -- the authentic backend for the vpxlog wire feed
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>
2026-07-19 15:03:00 -05:00
CydandClaude Opus 4.8 ff91bc675f M4b: offline end-to-end frames -- firmware -> C core -> GPU, live
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>
2026-07-19 14:23:29 -05:00
CydandClaude Opus 4.8 cff783d2ad Full-mission replay through the C core: 26,422 commands / 3.49B steps in 39s
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>
2026-07-19 13:37:38 -05:00
CydandClaude Opus 4.8 be8f72e731 M4a: emu860c -- the i860 step-core in C, 351x, checkpoint-perfect
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>
2026-07-19 13:24:43 -05:00
CydandClaude Opus 4.8 40817bee91 M2: both fixture frames render on GPU (bars 52-tile ISA path + fxtest primitive path)
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>
2026-07-19 12:31:26 -05:00
CydandClaude Opus 4.8 151d4ca89e M1: the EMC tile as a GL compute shader -- CONFORMANT ON GPU (RTX 5060)
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>
2026-07-19 11:47:57 -05:00
CydandClaude Opus 4.8 b241d70aaa op-0x2c = edge-bounded strip primitives (2 edges); preamble builder read verbatim
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 11:04:09 -05:00
CydandClaude Opus 4.8 5d29442ecb Golden-model conformance suite (C1-C4 CONFORMANT) + GPU-compute retarget plan
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>
2026-07-19 10:21:47 -05:00
CydandClaude Opus 4.8 cc7c95d738 ACCEPTANCE 2: fxtest content renders -- perspective ground + textured boxes
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>
2026-07-19 09:47:21 -05:00
CydandClaude Opus 4.8 fb04de017e TXDN hunt session 2: a26 = table uploads duplicating firmware .data; texels are transformed on receipt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 09:18:53 -05:00
CydandClaude Opus 4.8 1e6b5258b3 Texture store = the object graph (action-26 handler identified); route mapped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 08:10:24 -05:00
CydandClaude Opus 4.8 ba7c657dd9 FPGA reconstruction design note: block-by-block feasibility + the three-retarget plan
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>
2026-07-19 07:54:34 -05:00
CydandClaude Opus 4.8 599b2388a1 fxtest texture-path survey: TXDN io-port fetch protocol mapped; next mountain identified
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 05:31:35 -05:00
CydandClaude Opus 4.8 e9963a5040 ACCEPTANCE 1: SMPTE bars reproduced through the executed IGC pipeline
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>
2026-07-18 04:29:11 -05:00
CydandClaude Opus 4.8 12673c5a22 IGC decode: convergence experiment negative; OPF2(145) = next suspect
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 03:26:09 -05:00
CydandClaude Opus 4.8 6c3dc2e954 igc_exec: SCMEMA (screen-address seed) + gated sweep-pair adds
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>
2026-07-18 01:51:17 -05:00
CydandClaude Opus 4.8 6451b739bd IGC decode: colour-instr layout (5-word Gouraud planes) + SCMEMA lead for the bars
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:17:13 -05:00
CydandClaude Opus 4.8 7bac137a63 Tier-1 FIRST GEOMETRY RENDER: real captured IGC packets -> shaded z-buffered 3D
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>
2026-07-18 00:42:52 -05:00
CydandClaude Opus 4.8 ddaa63f9b0 igc_exec rev2: named op table (ADDR/DUMP ground truth) + executor semantics
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>
2026-07-18 00:10:13 -05:00
CydandClaude Opus 4.8 ecdb4b6122 IGC decode: op table named via PXPL5SUP/ADDR (igc_opco.h fragment) + DUMP (live op-table print)
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>
2026-07-17 23:40:00 -05:00
CydandClaude Opus 4.8 8ff234fe9c IGC decode: sweep-family 2-word format derived; doubling-chain myth busted
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>
2026-07-17 23:10:05 -05:00
CydandClaude Opus 4.8 c54d920761 Tier-1 IGC decoder: encoding formula cracked + instruction executor core
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>
2026-07-17 22:38:57 -05:00
CydandClaude Opus 4.8 38dd84f5ec i860: annul the delay slot of a not-taken bc.t/bnc.t (fixes >512-object captures)
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>
2026-07-17 17:24:33 -05:00
CydandClaude Opus 4.8 2a11838d74 Stream-verify the demo scenes: warp field + Klingon = DRAM-resident assets
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>
2026-07-17 12:57:25 -05:00