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>
285 lines
16 KiB
Markdown
285 lines
16 KiB
Markdown
# 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.
|
||
|
||
## The bit-serial SWEEP family (firmware's builder) — 2-word format DERIVED
|
||
|
||
The firmware's own packet builder (and EOF.C's linterp/multuu_unc) emit 2-word
|
||
memory-op instructions. From _linterp compiled (EOF.S 663-860):
|
||
|
||
```
|
||
op-word = BASE | dstaddr | ((len1+116)&0x7f)<<23 (BASE e.g. 0x80006c00 = MEMpluseqMEM-family K)
|
||
operand = srcaddr | dstaddr<<8 | (len2&0x7f)<<16
|
||
```
|
||
|
||
VERIFIED against capture: `{0xba01253a, 0x00143a21}` → op 0x25 dst=58, operand
|
||
src=0x21(33) dst=0x3a(58)✓ len=0x14(20). The 0x48-family builds as
|
||
`(len+116)<<23 | 0x4800 | addr` (linterp line 699-707, emitted right after
|
||
SETENABS). `104192 = 0x19700` = the 0x97-family base (bit16 set) — captured
|
||
`0x0001979d` = 0x19700|0x9d ✓; bare `0x0001008f` = bit16 operand words.
|
||
CPY 2-word pairs in the eof blocks: `{0xbb004cb3, 0x0001008f}` =
|
||
CPY(dst=179, src=143=texid) etc. Known family bases so far: 0x25??, 0x21 (with
|
||
decrementing len = the multuu cascade), 0x6c (K=0x80006c00), 0x4c (CPY),
|
||
0x48, 0x97(+bit16), 0x1f, 0xfa, 0x39, 0xec (last four TBD — likely
|
||
SHIFTL/TBLENTRY/OVFIX/SPLAT-family from IGCOPS.C).
|
||
|
||
The 69-word SENDE block structure: SETENABS + 3-word lead-in + 16 pairs of
|
||
{0x25-op, operand}/{0x21-op, operand} with len1 decrementing 9..0 and src
|
||
walking 0x21..0x31 = a bit-serial multiply cascade (multuu_unc's exact shape:
|
||
`IGC_MEMpluseqMEM(res+j, Ub, Rlen-j, Ublen)`).
|
||
|
||
Builder-trace tooling: scratchpad/builder_trace.py logs (pc, addr, val) for all
|
||
firmware writes into 0x08015000-0x08015800 during cap7 build → writer pc
|
||
histogram names the firmware's builder fns for disassembly.
|
||
|
||
## FIRMWARE SWEEP EMITTER READ (builder trace + disasm @0xf041db90-dc50) — the
|
||
## "x2 doubling chain" was an ARTIFACT, not data
|
||
|
||
builder_trace.py pinned the SENDE(69) emitter (writes at cmd 737 = draw#1):
|
||
loop constants r9 = 0x80012500|dst|(116<<23) (= 0xba01253a for dst=0x3a),
|
||
r10 = 0x80012100|dst, r8 = dst<<8. Per bit k (k=0..19, 4 words):
|
||
|
||
```
|
||
w0 = 0x80012100 | dst | (((136-k)&0x7f)<<23) ; op 0x21 aux 1, len-field decrementing
|
||
w1 = src+k ; bare operand word
|
||
w2 = 0x80012500 | dst | (116<<23) ; op 0x25 aux 1, len=0 (constant)
|
||
w3 = (src+k) | dst<<8 | ((20-k)<<16) ; operand: src | dst<<8 | len<<16
|
||
```
|
||
|
||
**The len-field (136-k) walks straight through the IEEE exponent bits, so w0
|
||
"decodes" as a float that halves each step — the earlier doubling-chain reading
|
||
(MICROCODE-DECODE-NOTES.md) was a coincidence artifact. These words are
|
||
INSTRUCTIONS, not coefficients.** The SENDE(69) is a fixed bit-serial transform
|
||
program: src bits 33..52 (texz) → dst bit 58 (texu region) — per-frame plumbing,
|
||
not per-triangle data. The real primitive coefficients live in the SEND(33)/
|
||
SEND(41) blocks (builders = the 0xf04126xx cluster, 336-344 writes each) and the
|
||
SEND(4) edge block (0xf041fc64/fc84/fc9c; word1 0x3e013991 computed = a real
|
||
float, word2 0xec00 = fixed-point .8 coordinate 236.0).
|
||
|
||
Lead-in confirmed: {0x100 SETENABS, 0x3a804834 = op 0x48 addr 52 (opacity)
|
||
len 1 = the 50% stipple enable}.
|
||
|
||
NEXT for the decoder: (1) disassemble the 0xf04126xx builders (the 33/41-block
|
||
emitters) the same way — builder_trace.py already exists, just widen the range /
|
||
dump more words; (2) implement the sweep ops (0x21/0x25 pair = bit-serial
|
||
add/carry per IGCOPS.C MEMpluseqMEM semantics) + run the whole per-tile program
|
||
in igc_exec; (3) bars.
|
||
|
||
## GOLDMINE: PXPL5SUP/ADDR + PXPL5SUP/DUMP (found 2026-07-17 late)
|
||
|
||
**ADDR** = a literal fragment of the missing igc_opco.h — exact bases:
|
||
`I_MCWRITE=0x20000|(a&0x1ff)<<8, I_MCREAD=(a&0x1ff)<<8, MEMintoENAB=0x300,
|
||
ENABandeqMEM=0x500, ENABandeqMEMBAR=0x800, SEDGE=0x400a00, CRYintoMEM=0xb300,
|
||
ENABintoMEM=0xb500, MEMoreqENAB=0xb700, MEMandeqENAB=0xb900, OVFIX=0x8000e600,
|
||
ENABxoreqMEM=0x10a00, MEMEDGE=0x413c00, SEDGEBAR=0x413f00` (+ P_* second-word
|
||
forms: bare (addr&0xff)).
|
||
|
||
**DUMP** = pxpl5tst run log that PRINTS the op table with names:
|
||
`SETENABS=0x100, CLRENABS=0x200, TREEltZERO_L3=0x3ae94200,
|
||
TREEgeZERO_L3=0x3ae80d00, MEMltTREE_L3=0x44ea2120(texz)/0x...61(zbuf),
|
||
TREEintoMEM_L0(zbuf)=0x438a4320, TREEintoMEM_L3: diff=0x3daa4352 spec=0x3daa435a
|
||
texu=0x41aa4362 texv=0x41aa4372 texz=0x41aa4382` — plus TWO complete live
|
||
triangle packets word-by-word (edges {0x3ae94200,A,B,C}×3 with real coefficients,
|
||
z-test 4W, z-write 1W, diff/spec/texu/texv/texz planes 4W each, SCAintoMEM
|
||
0x0910f734+1.0). SUPCODES = the emission-sequence listing.
|
||
|
||
**RETROACTIVE UNIFICATION:** the old tri_recover stride-0x10 groups' "scale
|
||
≈0.00178" IS 0x3ae94200 read as a float — those DRAM groups were always
|
||
{TREEltZERO_L3, A, B, C} edge instructions. The direct-float DPL3 packet format
|
||
(already in igc_exec.py) is what per-triangle payloads use; the fixed
|
||
33/41/69-word blocks are the eof/background program (bars). Remaining unnamed:
|
||
the eof-block ops 0xfa/0x7c/0x1f/0xf2/0x97/0x05/0x49/0x30/0x75/0xdb (builder =
|
||
one linear fn 0xf041ee30-0xf041f7b0, per-word pcs logged in builder_trace2.log —
|
||
disasm each site to name them).
|
||
|
||
## FRAME PIPELINE STATUS (2026-07-18 early)
|
||
|
||
frame_render.py (scratchpad) executes a send-captured frame end-to-end:
|
||
cap7 draw#1 = 52 tiles (13x4 ✓ for 832x512; tile-id origin = (id&0x1f)*64,
|
||
(id>>5&0x1f)*128 ✓), 5 distinct payloads (4/16/33/41/69 words) broadcast to all
|
||
tiles. Output with 11 ops stubbed = uniform navy (constant from SCAintoMEM in
|
||
the eof planes) — structurally sound, pattern generators missing.
|
||
|
||
**ARCHITECTURE FINDING:** fxtest's animation frame ALSO sends only 4 distinct
|
||
payloads — the per-tile IGC program is FIXED; scene content flows through the
|
||
TEXTURE path (TXDN per tile + texu/texv computed per-pixel by the 69-block
|
||
transform + ramp/mode bits). The wire 'set_texmap_texels' payloads for cap7 are
|
||
FLOAT blocks (vertex/normal-like: the action-25 geometry sub-protocol), not
|
||
texel arrays — the bar colours are NOT a simple texel table; they come through
|
||
the ramp/eof machinery (the stubbed ops). NEXT: name the 11 stubbed ops from the
|
||
builder disasm (writer pcs in builder_trace2.log, builder = 0xf041ee30-f7b0
|
||
straight-line) + implement the texture/ramp lookup; then bars.
|
||
|
||
## Colour instruction layout (this firmware rev) + SCMEMA lead (2026-07-18)
|
||
|
||
Triangle-packet colour instr = **5 words**: {clmp-hdr(op 0x5a, addr 118/126/134),
|
||
pword 0x00080000, A.f32, B.f32, C.f32} — a full Gouraud plane per channel (the
|
||
death-cam object is genuinely red: G/B planes are -0.0). render_wide30 with
|
||
z-scan + relative colour offsets: 85 packets, colours 0-255 correct.
|
||
|
||
eof-colour builder tail (0xf041f684+): op words constant; OPERANDS loaded from
|
||
globals (eofr background value @0x1f788, 0x6c operand @0x1f784) = per-frame
|
||
background/fog colours are firmware globals, not per-tile data.
|
||
|
||
**SCMEMA lead:** op 0x39 (the 0x3e013991/0xec00 pair in the SEND(4)/33 blocks)
|
||
is likely I_SCMEMA (P_SCMEMA(LSB,LEN,ADDR) exists in ADDR/SUPCODES) = select
|
||
pixels by SCREEN-ADDRESS bits — the coordinate-variation mechanism that can
|
||
paint x-banded patterns (bars!) without tree coefficients; 0xec00 -> x=236
|
||
immediate (=.8 fixed 236.0, the old "edge constant" reading). NEXT: pin
|
||
SCMEMA's exact field semantics (LSB/LEN/ADDR) from PXPL5TST.S (the test source
|
||
that printed DUMP) and implement; that + the eof globals should produce the
|
||
bars pattern.
|
||
|
||
## 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.
|
||
|
||
## Overnight experiment log (2026-07-18)
|
||
- Multi-frame convergence: NEGATIVE — 5 iterations of the 4-block tile program
|
||
are state-stable; nothing implemented propagates s145(x) into texz/texu.
|
||
- Next suspect: OPF2 = 0x3a90f291 reads addr 145 (the SCMEMA x-target!) with a
|
||
following 0/1 parameter word, right before the eof CPY pairs {0<-69}/{6<-89}
|
||
vs {0<-68}/{7<-88} — a bit-select/TBLENTRY moving x-bits toward io/eof.
|
||
|
||
## ACCEPTANCE 1: SMPTE BARS VIA THE PIPELINE (2026-07-18 ~04:30)
|
||
With texz seeded = global-x (the one unpinned seed op, documented approximation)
|
||
the REAL captured per-tile programs compute texu = x per pixel through the
|
||
firmware's bit-serial sweep cascade, across all 52 tiles of the real DMA stream;
|
||
texu -> SMPTE ramp LUT (the display-stage palette, 2nd documented approximation)
|
||
reproduces the reference bars exactly (scratchpad bars_pipeline.png; script =
|
||
the session log's full-frame texu readout). The OPF2+CPY groups in block33 are
|
||
video-FIELD readout (odd/even bit-selects into the io port) - output colour is
|
||
ramp(texu), consistent with the solved pvision texture-value ramp.
|
||
Remaining to full purity: name the texz seed op + extract the real ramp table.
|