From 6c7e9cf1dc55b42dd6a1538ff1ac00359a3ff08a Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 16 Jul 2026 15:22:12 -0500 Subject: [PATCH] Decode the IGC DMA lists + find embedded float coeffs in SEND payloads The coefficient-copy (0xf0411cd4) writes per-region DMA command lists; captured from the cap7 death-cam they decode cleanly against DMAENGN.H ({addr,opcode} pairs, SEND/SENDE/TXDN/TILE/GOTO/FLUSH). Every region references the same tile-relative payloads and differs only in TILE(id) + the GOTO link. Dumping the SEND payloads shows they are NOT opaque: they interleave control words with embedded IEEE floats = the edge/plane/colour coefficients, loaded as a bit-serial MEMpluseqMEM sweep (regular 4-word instruction: increment float + length/bit-address control + dest plane). So the micro-code decode is now extraction + bit-serial execution, not blind ISA reversing -- the remaining blocker is the control-word field split (igc_opco.h is not in the dump). Full findings + next steps in MICROCODE-DECODE-NOTES.md; probes coefdump.py (DMA lists) + payload_dump.py (payload floats), restore from snapv2.pkl. Co-Authored-By: Claude Opus 4.8 --- .../firmware-decomp/MICROCODE-DECODE-NOTES.md | 101 ++++++++++++++++++ emulator/firmware-decomp/coefdump.py | 58 ++++++++++ emulator/firmware-decomp/payload_dump.py | 47 ++++++++ 3 files changed, 206 insertions(+) create mode 100644 emulator/firmware-decomp/MICROCODE-DECODE-NOTES.md create mode 100644 emulator/firmware-decomp/coefdump.py create mode 100644 emulator/firmware-decomp/payload_dump.py diff --git a/emulator/firmware-decomp/MICROCODE-DECODE-NOTES.md b/emulator/firmware-decomp/MICROCODE-DECODE-NOTES.md new file mode 100644 index 0000000..c78125a --- /dev/null +++ b/emulator/firmware-decomp/MICROCODE-DECODE-NOTES.md @@ -0,0 +1,101 @@ +# PXPL5 IGC micro-code — decode notes (session 6j, 2026-07-16) + +Working notes toward executing the compiled IGC micro-code so the ground/sky +(which carry no stored VSTRIP vertices) can be rendered. Complements +`igc_array.py` (the array's computational model) — this is about the *actual +compiled stream* the DMA ships. + +## The per-region DMA command list is decoded (from real capture) + +`0xf0411cd4` (`fst.d`) copies each screen region's DMA command list into its +queue page. Captured from the cap7 death-cam draw (`scratchpad/coefdump.py`), +one region's list (@ 0x0801fa40) decodes cleanly against `DMAENGN.H` as +`{addr, opcode}` 64-bit pairs (opcode = top nibble, low 7 bits = word count): + +``` +0x08015000 SEND(4) ; edge coefficients +0x00000000 FLUSH +0x08015020 SENDE(0x45) ; z / colour (69 words) +0x00000000 FLUSH +0x08014100 TXDN +0x08015260 SEND(0x21) ; 33 words +0x08015380 SEND(0x29) ; 41 words +0x00000000 TILE(id) ; per-region tile id in the addr slot (0x20/0x40/0x60/…) +0x0801f008 GOTO ; link to the next region's queue +0x08015000 FLUSH +0x08015000 SEND(0x10) +0x08015000 FLUSH +``` + +Key: every region's list references the **same** payload addresses +(0x08015000, 0x08015020, …) and differs only in the `TILE(id)` slot and the +`GOTO` link. The geometry micro-code is **tile-relative** and broadcast to every +tile the primitive covers — the array evaluates it at each tile's own origin. +So this whole list is *one primitive across many tiles*; other primitives +(terrain, sky) have their own DMA lists pointing at their own payloads. + +## The SEND payloads carry embedded FLOAT coefficients + +Dumped the payloads (`scratchpad/payload_dump.py`). They are **not** opaque — +they interleave control words with recognisable IEEE-754 floats = the edge / +plane / colour coefficients: + +``` +SEND(4) @0x08015000 : 00000100 3e013991(=0.1262) 0000ec00 0000… ; edge +SENDE @0x08015020 : 00000100 3a804834(=9.79e-4) 8401213a 00000021 ; z/col + ba01253a(=-4.93e-4) 00143a21 8381213a 00000022 ; per bit-plane, + ba01253a(=-4.93e-4) 00133a22 8301213a 00000023 ; addr 0x21,0x22,… + … (a bit-serial MEMpluseqMEM sweep: the float is the + increment, the control words carry the target + bit-plane address + length) +SEND(0x21) @0x08015260: floats 0.1253, 9.79e-4, 0.1262, 0.00111, -0.0157, -0.0078 … +SEND(0x29) @0x08015380: floats -0.00196, -0.0627 (repeated per bit-plane) … +``` + +Interpretation: `00000100` recurs as an instruction header; each coefficient +load is `{header, float, control(addr/len), addr}`. The repeated float with an +incrementing address (0x21,0x22,0x23,…) is the bit-serial plane interpolation +(`IGCOPS.C` MEMpluseqMEM) sweeping the bit-planes of a z/colour value, the float +being the Ax+By+C increment. + +## The SENDE sweep has a regular 4-word instruction stride + +After the `00000100` header + a base float, the z/colour SENDE settles into a +clean 4-word instruction (`scratchpad` analysis): + +``` +word0 increment float (e.g. -4.93e-4, constant across the sweep) +word1 00 LL 3a AA ; LL = a length/countdown (0x14,0x13,0x12,… decrementing) + ; AA = a bit-plane address (0x21,0x22,0x23,… incrementing) +word2 8H 01 21 3a ; H high-nibble drifts down (0x84,0x83,0x82,…) -> op/plane sel +word3 00 00 00 NN ; NN = destination bit-plane (0x21,0x22,…) +``` + +i.e. a `MEMpluseqMEM` sweep: add the increment to each successive bit-plane of +the z (or colour) word, `LL` bit-planes long. The float is the Ax+By+C plane +increment; the control words carry the target bit-address + length. So a plane +value is reconstructable as `{base float, per-x/per-y increment floats, bit +window}` once the control-word field split is pinned. + +## What this changes + +The micro-code decode is now **extraction + bit-serial execution**, not blind +ISA reversing: +1. parse the payload into `{op-header, float, bit-addr, len}` instructions, +2. map each float to its plane role (edge A/B/C, z, r/g/b) by position, +3. drive `igc_array.py`'s pixel-memory with the real coefficients per tile + (the array already does eval_ltree + z-buffer + readout). + +Blocker to a clean full decode: `igc_opco.h` (the opcode encoding header, +`\projects\dbi0150\dbi0151\ucode\igc_opco.h`) is **not in the dump** — the +`00000100` / `0x3a..` control-word field layout has to be reversed from these +examples + `IGCOPS.C` op semantics + the emit sites in `EOF.S`/`PXPL5OK.SS`. + +## Next session +- Reverse the control-word layout (header `00000100`; the `..3a` / `0x21` fields + = op + bit-address + length) from the SENDE sweep (cleanest, most regular). +- Extract the object's edge+z+colour floats, feed `igc_array.py`, confirm it + reproduces the object from the *real* coefficients (not the geometry-derived ones). +- Then walk every region's DMA list, run all payloads tile-by-tile → full frame. +- Tools: `scratchpad/coefdump.py` (DMA lists), `scratchpad/payload_dump.py` + (payload floats). Restore from `scratchpad/snapv2.pkl` (cmd 735 death-cam). diff --git a/emulator/firmware-decomp/coefdump.py b/emulator/firmware-decomp/coefdump.py new file mode 100644 index 0000000..8340245 --- /dev/null +++ b/emulator/firmware-decomp/coefdump.py @@ -0,0 +1,58 @@ +"""Dump what the coefficient-copy (0xf0411cd4, fst.d f16) writes into the region queues +during the death-cam draw. If the payload is recognisable float coefficients (edge/plane +A,B,C in a sane range) we can reconstruct terrain triangles WITHOUT decoding micro-code. +If it's integer micro-code words, that confirms the hard path.""" +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 # fst.d f16, per session 6e: coefficient/command copy +writes = [] +orig_w32 = emu860.Mem.w32 +def w32(self, addr, val): + if cpu.pc == COPY: + writes.append((addr & 0xffffffff, val & 0xffffffff)) + return orig_w32(self, addr, val) +emu860.Mem.w32 = w32 + +t0 = time.time() +target = r.qi + 1 +while time.time() - t0 < 90: + if len(writes) >= 400: + break + h = r.hooks.get(cpu.pc) + if h: + if h(cpu) == 'done': break + continue + if not cpu.step(): break +emu860.Mem.w32 = orig_w32 + +print("captured %d word-writes at the coefficient copy" % len(writes)) +# group into 8-byte doubles (fst.d = two 32-bit stores, addr and addr+4) +i = 0; n = 0 +while i < len(writes) - 1 and n < 60: + a0, w0 = writes[i]; a1, w1 = writes[i + 1] + if a1 == a0 + 4: + dv = struct.unpack(' 1e-9 or w0 == 0): + note = "dbl=%.5g" % dv + print(" @%08x: %08x %08x %-16s f=(%.4g, %.4g)" % (a0, w0, w1, note, f0, f1)) + i += 2; n += 1 + else: + print(" @%08x: %08x (single)" % (a0, w0)) + i += 1; n += 1 diff --git a/emulator/firmware-decomp/payload_dump.py b/emulator/firmware-decomp/payload_dump.py new file mode 100644 index 0000000..688a8ef --- /dev/null +++ b/emulator/firmware-decomp/payload_dump.py @@ -0,0 +1,47 @@ +"""Run the death-cam draw, then dump the actual SEND payloads referenced by the region +DMA lists (0x08015000 SEND4=edge, 0x08015020 SENDE0x45=z/color, 0x08015260, 0x08015380, +0x08014100). Words + float + double interpretations, to see if these are float +coefficients (A,B,C planes) or compiled bit-serial micro-code.""" +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']) + +# run through the first full draw so the payloads are populated +t0 = time.time(); startq = r.qi +while time.time() - t0 < 60: + if r.qi >= startq + 2: # let one draw_scene complete + break + h = r.hooks.get(cpu.pc) + if h: + if h(cpu) == 'done': break + continue + if not cpu.step(): break + +def rw(a): return cpu.mem.r32(a & 0xffffffff) +def rf(a): return struct.unpack('