From c54d92076106a87619addc3bdbf3c59799f62158 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 17 Jul 2026 22:38:57 -0500 Subject: [PATCH] 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 --- .../IGC-ENCODING-DERIVATION.md | 138 +++++++++ emulator/firmware-decomp/igc_exec.py | 271 ++++++++++++++++++ 2 files changed, 409 insertions(+) create mode 100644 emulator/firmware-decomp/IGC-ENCODING-DERIVATION.md create mode 100644 emulator/firmware-decomp/igc_exec.py diff --git a/emulator/firmware-decomp/IGC-ENCODING-DERIVATION.md b/emulator/firmware-decomp/IGC-ENCODING-DERIVATION.md new file mode 100644 index 0000000..5751a27 --- /dev/null +++ b/emulator/firmware-decomp/IGC-ENCODING-DERIVATION.md @@ -0,0 +1,138 @@ +# IGC instruction-word encoding — derivation from EOF.C ↔ EOF.S (2026-07-17) + +Goal: the Tier-1 decoder (coefficient stream → pixels). `igc_opco.h` (the macro +definitions) is NOT in the dump — but `sda4/DPL3/VRENDER/PXPL5SUP/` has both +**EOF.C** (ordered `IGC_*` macro calls) and **EOF.S** (its PGC-compiled output with +every emitted literal visible). Aligning them derives each macro's word template. +This supersedes blind reversing; MICROCODE-DECODE-NOTES.md has the value layer +(x2 doubling chains, 4-word SENDE stride, fixed-point constants) already decoded. + +## Derived so far (from _init_screenbin, EOF.S lines 330-510 ↔ EOF.C 269-360) + +| macro | emitted words | encoding | +|---|---|---| +| `IGC_SETENABS` | 1 | `0x00000100` | +| `IGC_MEMintoENAB(ix)` | 1 | `0x0300 \| ix` | +| `IGC_ENABxoreqMEM(ix)` | 1 | `0x10A00 \| ix` (bit16 set) | +| `IGC_ENABintoMEM(ix)` | 1 | `0xB500 \| ix` | +| `IGC_TREEintoMEM_L3(dst,len,A,B,C)` | 4 | `{hdr, A.f32, B.f32, C.f32}` | + +TREE header samples: `(dst=57,len=7)` → `0x3d2a4339`; `(dst=64,len=15)` → `0x412a4340`. +- bits 0-7 = dst bit-address (0x39=57 ✓, 0x40=64 ✓) +- bits 8-15 = op code **0x43** (cf. `0x3a` in the captured SENDE sweep = the + MEMpluseqMEM-family op; `0x03`/`0xB5`/`0x0A+bit16` above — op byte is bits 8-15) +- bits 16-23 = `0x2a` in both (probably format/variant field for `_L3`) +- bits 24-31 = `0x3d` vs `0x41` for len 7 vs 15 — length-derived, exact formula + needs more samples (Δ=4 for Δlen=8 → maybe (len>>1)+base? PIN WITH MINER). + +Also from the same region: the coeff stream is written via `st.l`/`fst.l` through +the rolling pointer; float args go through `.C00741`-style data literals (= the +float constants, e.g. -1.0f, 1.0f) — EOF.S shows them as `fld.l l%.C00741`; read +the .data section at the end of EOF.S for their values. + +## The captured-stream correspondence (already verified earlier) + +- SENDE z/colour sweep instruction (4 words): `{inc.f32, 00 LL 3a AA, place-value.f32, 000000NN}` + → op `0x3a` at bits 8-15 of word1, LL=countdown at bits 16-23, AA=src addr bits 0-7. + This is consistent with the header field layout above (op byte = bits 8-15). +- `0x00000100` recurs as "header" in captured payloads = SETENABS (or the 1-word + op family with op=0x01 at bits 8-15). + +## Key sources +- `sda4/DPL3/VRENDER/PXPL5SUP/EOF.C` — macro call sequences (init_screenbin @224, + linterp @396 = MEMpluseqMEM samples, multuu_unc @451, send_em @499, + perspective_divides @592 has TREEgeZERO/ltZERO/TREEintoMEM_C1/SCAintoMEM_S1/ + MEMintoENAB/CLEAR samples, texture path further down). +- `sda4/DPL3/VRENDER/PXPL5SUP/EOF.S` — compiled, all literals visible + (`_init_screenbin` @330, `_send_em` @1442, `_configEMCs` @221/@58). +- `DIVPXMAP.H` — dvpx_* bit map: io=0(32b), texz=32(20b), opacity=52(5b), + texu=57(20b), texv=77(20b), zbuf=97(20b), r24=117 g24=125 b24=133, + scalar=141(13b: texsize@141/2,texid@143/6,rampsel@149/2,texmode@151/3), + enblpush=154, eof block @160+ (fog=160/8, subu=168/5, subv=173/5, ramp=178/2, + mode=180/3...). +- `GOODEQNS.C` — edgeize/planarize/binitize math + binchunk DMA layout + (BIN_FULL=63*2, {addr, count|opcode} 64-bit pairs, GOTO chaining). +- `NUREGMAP.SS` — dpl_REMOTE_VERTEX (80B: pos@48, normcol@64, tex@32) and + dpl_REMOTE_CONNECTION (next@0, n_verts@4, indices[6]@8, planeEqn@32, rgb@48). +- `FOOTER.SS` — scales: .Czscale=0x497fffff≈2^20, .Ctexscale=0x477fffff≈2^16, + Cturn_z_to_tex(=5.1118e5, /4, /8 variants). +- `IGCOPS.C` — op vocabulary + pixel-memory semantics (read/write_pixmem_word, + eval_ltree=int(Ax+By+C)). +- PXPL5001/2.DOC (WinWord2) — design notes: flat-shaded textured quad packet = + edge×4, plane Z/S/T/MIP, const colour/type (~25 regs); texturing math. + +## Miner results (eofs_mine.py, round 1 — scratchpad, uses EOF.S `// lineno:` markers) + +| macro | words | encoding (op byte = bits 8-15 unless noted) | +|---|---|---| +| `IGC_NOOP` | 1 | `0x00000000` | +| `IGC_FBITS(15)` | 2 | `{0x80000000, 0x48000000}` (n=15 packing TBD — need a 2nd sample) | +| `IGC_SETENABS` | 1 | `0x00000100` | +| `IGC_MEMintoENAB(x)` | 1 | `0x0300 \| x` (confirmed symbolically: `rN&0xff\|0x0300`) | +| `IGC_MEMBARintoENAB(x)` | 1 | `0x4800 \| x` (NEW — linterp line 426) | +| `IGC_ENABxoreqMEM(x)` | 1 | `0x10A00 \| x` | +| `IGC_ENABintoMEM(x)` | 1 | `0xB500 \| x` | +| `IGC_TREEintoMEM_L3(d,l,A,B,C)` | 4 | `{hdr, A.f32, B.f32, C.f32}`; hdr bits0-7=d, bits8-15=0x43, bits16-23=0x2a, bits24-31=0x39+ceil(l/2) (fits l=5:0x3c, 7:0x3d, 15:0x41) | +| `IGC_CPY(d,s,l)` | 2 | `{0x80\|((0x39+ceil(l/2))<<24) \| 0x4c00 \| d, s}` (sample d=0x34,s=0x40,l=5 → `0xbc004c34, 0x40`) | +| `IGC_MEMgeSCA_S1(d,l,sca)` | 2 | sample → `{0xbc916c00, 0x34}` (op 0x6c; hdr low byte NOT dst here — per-op layouts differ) | +| `IGC_SCAintoMEM_S1(d,l,sca)` | 2 | sample → `{0x3c90f734, 0x1d}` (dst 0x34 bits0-7, operand word = scalar 29) | +| DMA `SEND(n)` | — | `0x10000000 \| n`, chunked at 127 longwords (send_em) | + +Byte 24-31 = `0x39+ceil(len/2)` looks like the bit-serial cycle count (2 bits/clock) ++ 0x80 flag on the enable-conditioned (`S1`?) forms. Bits 16-23 vary per op +(0x2a TREE / 0x00 CPY / 0x91 geSCA / 0x90 SCAinto) — likely aux operand/format. +Word-constructor macros also exist (`Ix_TBLENTRY_S1`, `P_TBLENTRY` — EOF.C:1104) = +more single-word samples in EOF.S. + +## THE ENCODING FORMULA (cracked 2026-07-17 — PXPL5OPT.S:1649, expanded macro in the clear) + +`_zbuffer_fn` (PXPL5SUP/PXPL5OPT.S line 1649) contains the fully-expanded +constructor: + +```c +( ( 0x422100 | ((addr & 0xff) << 0) | (((len+2)+115 & 0xff) << 23) ) |(2<<18)|(2<<20) ) +``` + +**Universal IGC instruction-word template:** +- bits 0-7 = operand bit-plane address +- bits 8-15 = opcode (0x21=MEMltTREE, 0x43=TREEintoMEM, 0x03=MEMintoENAB, + 0x48=MEMBARintoENAB, 0xB5=ENABintoMEM, 0x4c=CPY, 0x6c=MEMgeSCA, 0x01=SETENABS...) +- bits 16-22 = aux/format field (0x2a for TREE_L3, 0x42 for MEMltTREE(+flag bits + 18-21 e.g. (2<<18)|(2<<20)), 0x00 CPY, ...) +- bits 23-30 = (len + 115) [zbuffer uses len+2+115] — VERIFIED: TREE l=7→0x3D..., + l=15→0x41..., CPY l=5→0x3C... +- bit 31 = flag on the S1/enable-conditioned forms (CPY sample had it) + +**Edge headers are tiny constants: 0x601, 0x602, 0x603** (edgize_tri_fn: edge 1/2/3, +each followed by A,B,C floats — 4-word instructions). Suffix system: +**_L3 = +3 coeff floats; _L0 = 0 args (reuses the live tree state); _C1 = +1 const; +_S1 = +1 scalar.** The tree is loaded by one instruction and REUSED by the next +(z-compare loads the z plane; z-write is a bare 1-word TREEintoMEM_L0). + +**The triangle packet (tri_zb_f, PXPL5OK.SS:1063):** 3× edge{hdr,A,B,C} → +z-test {Ix_MEMltTREE_L3(texz? zbuf,20), zA,zB,zC} → z-write {Ix_TREEintoMEM_L0} → +3× colour {Ix_TREEclmpintoMEM_C1(r/g/b24,8), P_TREEclmpintoMEM(...), col.f32} → +{Ix_SCAintoMEM_S1(scalar,13), material.f32} → 0-pad. C-callable wrappers +(PXPL5TRI.S) call edgize_tri_fn + zbuffer_fn + planarize_fn(0x666, vert-field-idx). + +**Harvest sources (expanded, numeric):** PXPL5SUP/PXPL5OPT.S (edgize@1492, +zbuffer@1646, planarize@1743, preplanarize@1137) + PXPL5SUP/PXPL5TRI.S + parent +PXPL5OPT.S (rev differs, both useful) + EOF.S. Grep pattern: `0x[0-9a-f]{4,6} \|` +parenthesized expressions — eval them with the known (op,addr,len) from the .SS +source lines to fill the whole op table. + +## NEXT (the miner) +Write `eofs_mine.py`: parse EOF.S per function; track `st.l`/`fst.l` stores +through the coeff pointer; recover, in order, each emitted word (int literals +from or/orh pairs; floats from the fld of .data literals — parse EOF.S's .data +tail for values). Align against EOF.C's macro sequence per function (start with +init_screenbin, perspective_divides, linterp — together they cover: FBITS, NOOP, +TREEintoMEM_L3/_C1, TREEgeZERO_L3, TREEltZERO_L3, MEMintoENAB, ENABxoreqMEM, +ENABintoMEM, SETENABS, CPY, MEMgeSCA_S1, SCAintoMEM_S1, CLEAR, MEMpluseqMEM, +MEMintoENAB, SHIFTL-family, send_em's DMA emit). Output: the op table +(op byte, word count, arg-field packing). Then: +1. parse captured payloads (fxtest_frame.pkl / cap7 bench) into instruction + sequences with the table — every word must decode (acceptance: 0 unknown ops); +2. execute per-tile via igc_array.py (pixel-memory model exists there); +3. acceptance 1 = SMPTE bars from the cap7 bench stream; acceptance 2 = the + fxtest asset frame. diff --git a/emulator/firmware-decomp/igc_exec.py b/emulator/firmware-decomp/igc_exec.py new file mode 100644 index 0000000..de63398 --- /dev/null +++ b/emulator/firmware-decomp/igc_exec.py @@ -0,0 +1,271 @@ +"""igc_exec.py -- Tier-1 IGC instruction-level executor. + +Parses the compiled IGC coefficient stream (the SEND payload words the i860 +firmware ships to the PXPL5 array) and executes it on a modelled 64x128 tile: +26-byte-per-pixel bit memory + enable register + the shared linear-expression +tree, per IGCOPS.C semantics. + +Encoding (derived from the expanded compiler output -- see +IGC-ENCODING-DERIVATION.md; constructors verified word-exact against +PXPL5TRI.S/PXPL5OPT.S/EOF.S and the captured streams): + + word = op<<8 | aux<<16 | addr | (len+115..117)<<23 | flags(bits18-21) | S1<<31 + +Instruction templates (word counts): + 0x00000000 NOOP + 0x00000100 SETENABS + 0x0000060N EDGE N (N=1..3) + A,B,C floats (4 words) + 0x422100-family (op 0x21) MEMltTREE_L3 + A,B,C floats (4 words) + 0x024300-family (op 0x43, aux 0x02/0x06) TREEintoMEM_L0 (1 word) + 0x2a4300-family (op 0x43, aux 0x2a) TREEintoMEM_L3 + A,B,C (4 words) + 0x80435a00-family (op 0x5a) TREEclmpintoMEM_C1 + P-word + value (3 words) + 0x0000f700-family (op 0xf7) SCAintoMEM_S1 + value word (2 words) + 0x80016c00-family (op 0x6c) MEMgeSCA_S1 + addr word + value (3 words) + 0x0300|x MEMintoENAB(x) + 0x4800|x MEMBARintoENAB(x) + 0xb500|x ENABintoMEM(x) + 0x10a00|x ENABxoreqMEM(x) + 0x414200 / 0x400d00 (len 2) opacity-intro ops (enable set + mode) + 0x80000000,0x48000000 FBITS pair +""" +import struct + +TILE_W, TILE_H = 64, 128 +PIXBYTES = 26 + + +def f32(w): + return struct.unpack('> 8) & 0xff + addr = w & 0xff + aux = (w >> 16) & 0x7f + lnf = (w >> 23) & 0xff + if w == 0: + out.append(('NOOP',)); i += 1; continue + if w == 0x100: + out.append(('SETENABS',)); i += 1; continue + if w in (0x601, 0x602, 0x603) and i + 3 < n: + out.append(('EDGE', w & 0xf, f32(words[i+1]), f32(words[i+2]), f32(words[i+3]))) + i += 4; continue + if w == 0x80000000 and i + 1 < n and (words[i+1] >> 24) == 0x48: + out.append(('FBITS', 15)); i += 2; continue + if op == 0x21 and i + 3 < n: # MEMltTREE_L3 (z test) + out.append(('MEMltTREE', addr, lnf - 117, f32(words[i+1]), f32(words[i+2]), f32(words[i+3]))) + i += 4; continue + if op == 0x43 and aux == 0x2a and i + 3 < n: # TREEintoMEM_L3 + out.append(('TREEintoMEM_L3', addr, lnf - 115, f32(words[i+1]), f32(words[i+2]), f32(words[i+3]))) + i += 4; continue + if op == 0x43 and aux != 0x2a: # TREEintoMEM_L0 + out.append(('TREEintoMEM_L0', addr, lnf - 115)); i += 1; continue + if op == 0x5a and i + 2 < n: # TREEclmpintoMEM_C1 + P + val + out.append(('TREEclmpintoMEM', addr, lnf - 115, (words[i+1] >> 16) & 0x7f, f32(words[i+2]))) + i += 3; continue + if op == 0xf7 and i + 1 < n: # SCAintoMEM_S1 + val + out.append(('SCAintoMEM', addr, lnf - 116, f32(words[i+1]), words[i+1])) + i += 2; continue + if op == 0x6c and i + 2 < n: # MEMgeSCA_S1 + addr + val + out.append(('MEMgeSCA', words[i+1] & 0xff, lnf - 116, words[i+2])) + i += 3; continue + if op == 0x03 and (w >> 16) == 0: + out.append(('MEMintoENAB', addr)); i += 1; continue + if op == 0x48 and (w >> 16) == 0: + out.append(('MEMBARintoENAB', addr)); i += 1; continue + if op == 0xb5 and (w >> 16) == 0: + out.append(('ENABintoMEM', addr)); i += 1; continue + if op == 0x0a and (w >> 16) == 1: + out.append(('ENABxoreqMEM', addr)); i += 1; continue + if op == 0x42 and aux in (0x41,): # opacity-intro A + out.append(('OPAC_INTRO_A',)); i += 1; continue + if op == 0x0d: # opacity-intro B + out.append(('OPAC_INTRO_B',)); i += 1; continue + unk.append((i, w)) + out.append(('UNK', w)) + i += 1 + return out, unk + + +# ---------------- the tile ---------------- +class Tile: + """64x128 pixels, 26-byte bit-addressed memory each, 1-bit enable, and the + shared linear-expression tree evaluated per-pixel at (tile_x+x, tile_y+y).""" + + def __init__(self, ox=0, oy=0): + self.ox, self.oy = ox, oy + self.mem = [bytearray(PIXBYTES) for _ in range(TILE_W * TILE_H)] + self.enab = [1] * (TILE_W * TILE_H) + self.tree = (0.0, 0.0, 0.0) # A, B, C + self.frac = 15 + + # bit-field access (LSB-first within the 208-bit pixel memory) + @staticmethod + def _rd(pix, bit0, bits): + v = 0 + for k in range(bits): + b = bit0 + k + v |= ((pix[b >> 3] >> (b & 7)) & 1) << k + return v + + @staticmethod + def _wr(pix, bit0, bits, val): + for k in range(bits): + b = bit0 + k + if (val >> k) & 1: + pix[b >> 3] |= 1 << (b & 7) + else: + pix[b >> 3] &= ~(1 << (b & 7)) + + def _treeval(self, x, y): + A, B, C = self.tree + return int(A * (x + self.ox) + B * (y + self.oy) + C) + + def run(self, instrs, trace=False): + for ins in instrs: + m = ins[0] + if m in ('NOOP', 'FBITS', 'OPAC_INTRO_A', 'OPAC_INTRO_B', 'UNK'): + continue + if m == 'SETENABS': + self.enab = [1] * (TILE_W * TILE_H); continue + if m == 'EDGE': + _, n, A, B, C = ins + self.tree = (A, B, C) + for y in range(TILE_H): + for x in range(TILE_W): + i = x + y * TILE_W + if self.enab[i] and self._treeval(x, y) < 0: + self.enab[i] = 0 + continue + if m == 'MEMltTREE': + _, addr, ln, A, B, C = ins + self.tree = (A, B, C) + for y in range(TILE_H): + for x in range(TILE_W): + i = x + y * TILE_W + if self.enab[i]: + if not (self._rd(self.mem[i], addr, ln) > self._treeval(x, y)): + self.enab[i] = 0 + continue + if m == 'TREEintoMEM_L3': + _, addr, ln, A, B, C = ins + self.tree = (A, B, C) + for y in range(TILE_H): + for x in range(TILE_W): + i = x + y * TILE_W + if self.enab[i]: + self._wr(self.mem[i], addr, ln, self._treeval(x, y) & ((1 << ln) - 1)) + continue + if m == 'TREEintoMEM_L0': + _, addr, ln = ins + for y in range(TILE_H): + for x in range(TILE_W): + i = x + y * TILE_W + if self.enab[i]: + self._wr(self.mem[i], addr, ln, self._treeval(x, y) & ((1 << ln) - 1)) + continue + if m == 'TREEclmpintoMEM': + _, addr, ln, slen, val = ins + # colour write: the value is the flat colour (0..1 float) -> len-bit + v = max(0, min((1 << ln) - 1, int(val * ((1 << ln) - 1)))) + for i in range(TILE_W * TILE_H): + if self.enab[i]: + self._wr(self.mem[i], addr, ln, v) + continue + if m == 'SCAintoMEM': + _, addr, ln, fval, raw = ins + v = raw & ((1 << ln) - 1) + for i in range(TILE_W * TILE_H): + if self.enab[i]: + self._wr(self.mem[i], addr, ln, v) + continue + if m == 'MEMgeSCA': + _, addr, ln, sca = ins + s = sca & ((1 << ln) - 1) + for i in range(TILE_W * TILE_H): + if self.enab[i] and not (self._rd(self.mem[i], addr, ln) >= s): + self.enab[i] = 0 + continue + if m == 'MEMintoENAB': + _, a = ins + for i in range(TILE_W * TILE_H): + self.enab[i] = (self.mem[i][a >> 3] >> (a & 7)) & 1 + continue + if m == 'MEMBARintoENAB': + _, a = ins + for i in range(TILE_W * TILE_H): + self.enab[i] = 1 - ((self.mem[i][a >> 3] >> (a & 7)) & 1) + continue + if m == 'ENABintoMEM': + _, a = ins + for i in range(TILE_W * TILE_H): + Tile._wr(self.mem[i], a, 1, self.enab[i]) + continue + if m == 'ENABxoreqMEM': + _, a = ins + for i in range(TILE_W * TILE_H): + self.enab[i] ^= (self.mem[i][a >> 3] >> (a & 7)) & 1 + continue + + def rgb(self): + """Read out r24/g24/b24 (8 bits each at 117/125/133).""" + out = [] + for y in range(TILE_H): + row = [] + for x in range(TILE_W): + p = self.mem[x + y * TILE_W] + row.append((self._rd(p, 117, 8), self._rd(p, 125, 8), self._rd(p, 133, 8))) + out.append(row) + return out + + +if __name__ == '__main__': + # self-test: constructors reproduce captured/compiled words + assert ix_memlttree_l3(97, 20) == 0x44ea2161, hex(ix_memlttree_l3(97, 20)) + assert ix_scaintomem_s1(52, 5) == 0x3c90f734, hex(ix_scaintomem_s1(52, 5)) + assert ix_memgesca_s1(5) == 0xbc916c00, hex(ix_memgesca_s1(5)) + assert ix_treeclmpintomem_c1(117, 8) == 0xbdd75a75, hex(ix_treeclmpintomem_c1(117, 8)) + print('constructor self-tests PASS') + # smoke: one triangle on a tile + tri = [('SETENABS',), + ('EDGE', 1, 1.0, 0.0, -8.0), # x >= 8 + ('EDGE', 2, -1.0, 0.0, 40.0), # x <= 40 + ('EDGE', 3, 0.0, 1.0, -20.0), # y >= 20 + ('TREEclmpintoMEM', 117, 8, 8, 0.9), + ('TREEclmpintoMEM', 125, 8, 8, 0.4), + ('TREEclmpintoMEM', 133, 8, 8, 0.1)] + t = Tile() + t.run(tri) + img = t.rgb() + lit = sum(1 for row in img for px in row if px != (0, 0, 0)) + print('smoke: %d lit pixels (expect (40-8)x(128-20)=%d)' % (lit, 32 * 108))