From d1e83c34455a4ec1dcd0460aacc2238fbd17ab8a Mon Sep 17 00:00:00 2001
From: Cyd
Date: Thu, 16 Jul 2026 16:49:38 -0500
Subject: [PATCH] Map the full-frame micro-code structure: double-buffered
primitives
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
frame_primitives.py decodes every coeff-copy word over the draw: 1051 SEND,
334 SENDE, 384 TILE, 327 GOTO, 231 STOP across 105 tiles -- but only 2 distinct
SEND payload addresses (0x08015xxx/0x08017xxx). That's double-buffering: the
firmware compiles each primitive into an alternating coeff block and SENDs it,
so the content changes over time while the addresses don't. A from-scratch full
frame must snapshot each primitive's payload at its SEND, then run all of them.
Readout §05 + notes updated.
Co-Authored-By: Claude Opus 4.8
---
.../firmware-decomp/MICROCODE-DECODE-NOTES.md | 17 +++++
emulator/firmware-decomp/frame_primitives.py | 66 +++++++++++++++++++
emulator/firmware-decomp/render-readout.html | 6 +-
3 files changed, 87 insertions(+), 2 deletions(-)
create mode 100644 emulator/firmware-decomp/frame_primitives.py
diff --git a/emulator/firmware-decomp/MICROCODE-DECODE-NOTES.md b/emulator/firmware-decomp/MICROCODE-DECODE-NOTES.md
index af67cbb..92a00c7 100644
--- a/emulator/firmware-decomp/MICROCODE-DECODE-NOTES.md
+++ b/emulator/firmware-decomp/MICROCODE-DECODE-NOTES.md
@@ -155,6 +155,23 @@ the float list. SEND(0x29) is a long single-coefficient sweep (`-0.0626` repeate
piece for edge reconstruction: A/B slope (float, verified 0.2%) + C (fixed-point
coord). Remaining is pairing them per triangle + all-regions assembly.
+## Full-frame structure: double-buffered primitives across 105 tiles
+
+`scratchpad/frame_primitives.py` captures every coefficient-copy word over the
+draw and decodes the opcode mix: **1051 SEND, 334 SENDE, 384 TILE, 327 GOTO,
+231 STOP** over 105 distinct TILE ids. But only **two** distinct SEND payload
+addresses appear (0x08015xxx and 0x08017xxx), each with the identical 4/69/33/41
+size profile. That is **double-buffering**: the firmware compiles one primitive's
+micro-code into block A, SENDs it to every tile it covers, compiles the next
+primitive into block B, SENDs that, and reuses A for the third — so the *content*
+at those two addresses changes over time while the addresses don't.
+
+Consequence for a from-scratch full frame: enumerating payload *addresses* is not
+enough (only 2). Each primitive must be captured **at the moment its SEND fires**
+(hook the SEND, snapshot the payload before the buffer is overwritten), then all
+of them run tile-by-tile through `igc_array.py`. The frame is many primitives, not
+two — object + terrain, z-buffered across ~105 tiles.
+
## What this changes
The micro-code decode is now **extraction + bit-serial execution**, not blind
diff --git a/emulator/firmware-decomp/frame_primitives.py b/emulator/firmware-decomp/frame_primitives.py
new file mode 100644
index 0000000..efa2359
--- /dev/null
+++ b/emulator/firmware-decomp/frame_primitives.py
@@ -0,0 +1,66 @@
+"""Enumerate the death-cam frame's primitives: capture every DMA-list word written by
+the coefficient-copy (0xf0411cd4) across the whole draw, decode {addr,opcode} pairs,
+and collect the distinct SEND/SENDE payload addresses = distinct primitives. Tells us
+how many pieces (object + terrain + sky) a from-scratch full frame must render."""
+import sys, time, struct, pickle
+sys.path.insert(0, r'C:\VWE\TeslaRel410\emulator\firmware-decomp')
+import emu860, dis860, emu_main
+emu860.Mem.log = lambda self, *a, **k: None
+S = r'C:\Users\cyd\AppData\Local\Temp\claude\c--VWE-TeslaRel410\4e848c76-6e89-4034-8047-d8d491cb32d8\scratchpad'
+snap = pickle.load(open(S + r'\snapv2.pkl', 'rb'))
+r = emu_main.MainRunner(r'C:\VWE\TeslaRel410\dpl3-revive\patha\cap7.raw.bin', fw='capfw7', max_cmds=6000)
+cpu = r.cpu
+cpu.mem.pages = {k: bytearray(v) for k, v in snap['pages'].items()}
+cpu.ctrl.clear(); cpu.ctrl.update(snap['ctrl'])
+cpu.r = list(snap['r']); cpu.f = list(snap['f']); cpu.cr = dict(snap['cr']); cpu.pc = snap['pc']
+cpu._apipe = list(snap['apipe']); cpu._mpipe = list(snap['mpipe']); cpu._fp_pipes()
+cpu._lpipe = list(snap['lpipe']); cpu._gpipe = list(snap['gpipe'])
+cpu._kr, cpu._ki, cpu._t = snap['kr'], snap['ki'], snap['t']
+cpu.lcc = snap['lcc']; r.qi = snap['qi']; r.heap = list(snap['heap'])
+
+COPY = 0xf0411cd4
+pairs = [] # (addr_word, opcode_word) consecutive
+buf = []
+orig = emu860.Mem.w32
+def w32(self, addr, val):
+ if cpu.pc == COPY:
+ buf.append(val & 0xffffffff)
+ return orig(self, addr, val)
+emu860.Mem.w32 = w32
+
+t0 = time.time(); startq = r.qi; seen_copy = False
+while time.time() - t0 < 150:
+ if len(buf) >= 8000: break
+ # stop shortly after the copy activity ends (draw done)
+ if seen_copy and len(buf) and cpu.pc != COPY and (time.time() - t0) > 8 and len(buf) % 2 == 0:
+ pass
+ if len(buf): seen_copy = True
+ h = r.hooks.get(cpu.pc)
+ if h:
+ if h(cpu) == 'done': break
+ continue
+ if not cpu.step(): break
+emu860.Mem.w32 = orig
+print("(ran to cmd %d)" % r.qi)
+
+OPS = {0x1: 'SEND', 0x9: 'SENDE', 0x2: 'TILE', 0x3: 'TXDN', 0x6: 'FLUSH',
+ 0x0: 'GOTO', 0xf: 'STOP', 0x8: 'WAIT', 0x7: 'RETE'}
+sends = {} # payload addr -> set of sizes
+op_counts = {}
+tiles = set()
+for i in range(0, len(buf) - 1, 2):
+ a, op = buf[i], buf[i + 1]
+ code = (op >> 28) & 0xf
+ name = OPS.get(code, '?%x' % code)
+ op_counts[name] = op_counts.get(name, 0) + 1
+ if name in ('SEND', 'SENDE'):
+ sends.setdefault(a, set()).add(op & 0x7f)
+ if name == 'TILE':
+ tiles.add(a)
+
+print("coeff-copy words captured: %d (%d pairs)" % (len(buf), len(buf) // 2))
+print("opcode counts:", op_counts)
+print("\ndistinct SEND/SENDE payload addresses (= primitives' coeff blocks): %d" % len(sends))
+for a in sorted(sends):
+ print(" %#010x sizes=%s" % (a, sorted(sends[a])))
+print("\ndistinct TILE ids referenced: %d ->" % len(tiles), sorted(hex(t) for t in tiles)[:24])
diff --git a/emulator/firmware-decomp/render-readout.html b/emulator/firmware-decomp/render-readout.html
index 531bedb..0224ed9 100644
--- a/emulator/firmware-decomp/render-readout.html
+++ b/emulator/firmware-decomp/render-readout.html
@@ -354,8 +354,10 @@
carry no stored vertices. But that stream is now largely decoded: the command lists read cleanly,
the SENDE z-sweep resolves into a regular 4-word instruction, and its
coefficients are the same ones driving this depth buffer — the array's inputs are
- cross-validated against the hardware's own compiled stream. What remains is numeric
- reconstruction across every region, not reversing an opaque binary.
+ cross-validated against the hardware's own compiled stream. The full frame, we now know,
+ cycles its primitives through two double-buffered coefficient blocks — 1051 SENDs, 384
+ TILE ops across 105 tiles — so what remains is capturing each primitive as its SEND fires
+ and running them all, not reversing an opaque binary.