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>
272 lines
11 KiB
Python
272 lines
11 KiB
Python
"""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('<f', struct.pack('<I', w & 0xffffffff))[0]
|
|
|
|
|
|
# ---------------- constructors (the derived formulas, for verification) -------
|
|
def ix_memlttree_l3(addr, ln):
|
|
return (0x422100 | (addr & 0xff) | (((ln + 2 + 115) & 0xff) << 23)
|
|
| (2 << 18) | (2 << 20))
|
|
|
|
|
|
def ix_treeintomem_l0(addr, ln, flag20=0):
|
|
return (0x24300 | (addr & 0xff) | (((ln + 115) & 0xff) << 23)
|
|
| (2 << 18) | (flag20 << 20))
|
|
|
|
|
|
def ix_treeclmpintomem_c1(addr, ln):
|
|
return (0x80435a00 | (addr & 0xff) | (((ln + 115) & 0xff) << 23)
|
|
| (1 << 18) | (1 << 20))
|
|
|
|
|
|
def ix_scaintomem_s1(addr, ln):
|
|
return (0xf700 | (addr & 0xff) | (((ln + 116) & 0x7f) << 23) | (1 << 20))
|
|
|
|
|
|
def ix_memgesca_s1(ln):
|
|
return (0x80016c00 | (((ln + 116) & 0x7f) << 23) | (1 << 20))
|
|
|
|
|
|
# ---------------- the parser ----------------
|
|
def parse(words):
|
|
"""Parse a payload into (mnemonic, args...) tuples. Returns (instrs, unknowns)."""
|
|
out, unk = [], []
|
|
i, n = 0, len(words)
|
|
while i < n:
|
|
w = words[i] & 0xffffffff
|
|
op = (w >> 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))
|