diff --git a/emulator/firmware-decomp/igc_exec.py b/emulator/firmware-decomp/igc_exec.py index de63398..6a923c0 100644 --- a/emulator/firmware-decomp/igc_exec.py +++ b/emulator/firmware-decomp/igc_exec.py @@ -1,70 +1,78 @@ -"""igc_exec.py -- Tier-1 IGC instruction-level executor. +"""igc_exec.py -- Tier-1 IGC instruction-level executor (rev 2: NAMED op table). -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. +Sources of truth: PXPL5SUP/ADDR (igc_opco.h fragment), PXPL5SUP/DUMP (live op +table + 2 reference packets), the expanded compiler output (PXPL5TRI.S etc.), +and the firmware's own emitters (builder_trace). See IGC-ENCODING-DERIVATION.md. -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 layout: addr(bits0-7) | op(bits8-15) | aux(bits16-22) | lenf(bits23-30) | S1(bit31) +lenf is (len+115..117) for the length-bearing ops. - word = op<<8 | aux<<16 | addr | (len+115..117)<<23 | flags(bits18-21) | S1<<31 +Named single-word ops (low16 forms): NOOP=0, SETENABS=0x100, CLRENABS=0x200, +MEMintoENAB=0x300|a, ENAB(and/or)eqMEM=0x500|a, ENABandeqMEMBAR=0x800|a, +CRYintoMEM=0xb300|a, ENABintoMEM=0xb500|a, MEMoreqENAB=0xb700|a, +MEMandeqENAB=0xb900|a, ENABxoreqMEM=0x10a00|a, MEMBARintoENAB=0x4800|a. -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 +4-word tree ops (hdr + A,B,C floats): + op 0x42 TREEltZERO (0x3ae94200; L0 variant aux 0x41), op 0x0d TREEgeZERO + (0x3ae80d00; L0 aux 0x40), op 0x21 MEMltTREE_L3 (0x44ea21xx), + op 0x43 TREEintoMEM_L3 (0x3daa43xx / 0x3d2a43xx; L0 = aux 0x8a/0x02/0x06, 1 word). +2/3-word mem ops: op 0x5a TREEclmpintoMEM (+P-word +val), op 0xf7 SCAintoMEM +(+val), op 0x6c MEMgeSCA/pluseq-family (+operand [+val]), op 0x4c CPY +(+operand), op 0x25/0x21-aux1 sweep pair (+operand), 0x97/0x1f/0xfa/0xf2/ +0x05/0x49/0x30/0x75/0xdb/0x7c/0xcf/0x62 eof-family (structurally parsed; +semantics best-effort or stubbed -- see EXOTIC below). """ import struct TILE_W, TILE_H = 64, 128 PIXBYTES = 26 +NPIX = TILE_W * TILE_H def f32(w): return struct.unpack('> 24) == 0 and ((w >> 8) & 0xff) in range(0, 0xff) -def ix_treeintomem_l0(addr, ln, flag20=0): - return (0x24300 | (addr & 0xff) | (((ln + 115) & 0xff) << 23) - | (2 << 18) | (flag20 << 20)) +# (op, aux) -> (mnemonic, n_extra_words) [aux=None matches any] +OPTAB = {} +def _reg(op, aux, name, extra): + OPTAB[(op, aux)] = (name, extra) + +_reg(0x42, 0x69, 'TREEltZERO_L3', 3) +_reg(0x42, 0x41, 'TREEltZERO_L0', 0) +_reg(0x0d, 0x68, 'TREEgeZERO_L3', 3) +_reg(0x0d, 0x40, 'TREEgeZERO_L0', 0) +_reg(0x21, 0x6a, 'MEMltTREE_L3', 3) +_reg(0x21, 0x01, 'SWEEP21', 1) +_reg(0x25, 0x01, 'SWEEP25', 1) +_reg(0x43, 0x2a, 'TREEintoMEM_L3', 3) +_reg(0x43, 0x0a, 'TREEintoMEM_L0', 0) +_reg(0x43, 0x02, 'TREEintoMEM_L0', 0) +_reg(0x43, 0x06, 'TREEintoMEM_L0', 0) +_reg(0x5a, None, 'TREEclmpintoMEM', 2) +_reg(0xf7, None, 'SCAintoMEM', 1) +_reg(0x6c, None, 'MEMSCA6C', 1) +_reg(0x4c, None, 'CPY', 1) +_reg(0x48, None, 'OP48', 1) # w/ len-field: stipple/enable-cond (operand optional) +_reg(0x7c, None, 'OP7C', 1) +_reg(0x1f, None, 'OP1F', 0) +_reg(0xfa, None, 'OPFA', 0) +_reg(0xf2, None, 'OPF2', 1) +_reg(0x97, None, 'OP97', 0) +_reg(0x05, None, 'OP05', 0) +_reg(0x49, None, 'OP49', 1) +_reg(0x30, None, 'OP30', 1) +_reg(0x75, None, 'OP75', 1) +_reg(0xdb, None, 'OPDB', 0) -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: @@ -73,63 +81,53 @@ def parse(words): 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)) + ln = lnf - 115 i += 1 + if w == 0: + out.append(('NOOP',)); continue + if w == 0x100: + out.append(('SETENABS',)); continue + if w == 0x200: + out.append(('CLRENABS',)); continue + if w == 0x80000000 and i < n and (words[i] >> 24) == 0x48: + out.append(('FBITS', 15)); i += 1; continue + lowops = {0x03: 'MEMintoENAB', 0x05: 'ENABandeqMEM', 0x08: 'ENABandeqMEMBAR', + 0xb3: 'CRYintoMEM', 0xb5: 'ENABintoMEM', 0xb7: 'MEMoreqENAB', + 0xb9: 'MEMandeqENAB'} + if (w >> 16) == 0 and op in lowops: + out.append((lowops[op], addr)); continue + if (w >> 16) == 1 and op == 0x0a: + out.append(('ENABxoreqMEM', addr)); continue + if (w >> 16) == 0 and op == 0x48: + out.append(('MEMBARintoENAB', addr)); continue + ent = OPTAB.get((op, aux)) or OPTAB.get((op, None)) + if ent: + name, extra = ent + if extra == 3 and i + 2 < n + 1: + out.append((name, addr, ln, f32(words[i]), f32(words[i+1]), f32(words[i+2]))) + i += 3; continue + if extra == 2 and i + 1 < n + 1: + out.append((name, addr, ln, words[i], words[i+1])); i += 2; continue + if extra == 1: + # take the operand only if it looks like one + if i < n and _is_operand(words[i]): + out.append((name, addr, ln, words[i])); i += 1 + else: + out.append((name, addr, ln, None)) + continue + out.append((name, addr, ln)); continue + unk.append((i - 1, w)) + out.append(('UNK', w)) 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 + self.mem = [bytearray(PIXBYTES) for _ in range(NPIX)] + self.enab = [1] * NPIX + self.tree = (0.0, 0.0, 0.0) - # bit-field access (LSB-first within the 208-bit pixel memory) @staticmethod def _rd(pix, bit0, bits): v = 0 @@ -147,98 +145,145 @@ class Tile: else: pix[b >> 3] &= ~(1 << (b & 7)) - def _treeval(self, x, y): + def _tv(self, x, y): A, B, C = self.tree - return int(A * (x + self.ox) + B * (y + self.oy) + C) + return A * (x + self.ox) + B * (y + self.oy) + C - def run(self, instrs, trace=False): + def run(self, instrs): + unhandled = set() for ins in instrs: m = ins[0] - if m in ('NOOP', 'FBITS', 'OPAC_INTRO_A', 'OPAC_INTRO_B', 'UNK'): + if m in ('NOOP', 'FBITS', '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': + elif m == 'SETENABS': + self.enab = [1] * NPIX + elif m == 'CLRENABS': + self.enab = [0] * NPIX + elif m in ('TREEltZERO_L3', 'TREEgeZERO_L3'): _, addr, ln, A, B, C = ins self.tree = (A, B, C) + lt = (m[4] == 'l') # ltZERO for y in range(TILE_H): + ybase = y * TILE_W for x in range(TILE_W): - i = x + y * TILE_W + i = x + ybase if self.enab[i]: - if not (self._rd(self.mem[i], addr, ln) > self._treeval(x, y)): + v = self._tv(x, y) + ok = (v < 0) if lt else (v >= 0) + if not ok: self.enab[i] = 0 - continue - if m == 'TREEintoMEM_L3': + elif m == 'MEMltTREE_L3': _, addr, ln, A, B, C = ins self.tree = (A, B, C) for y in range(TILE_H): + ybase = y * TILE_W for x in range(TILE_W): - i = x + y * TILE_W + i = x + ybase 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 + if not (self._rd(self.mem[i], addr, max(1, ln)) < int(self._tv(x, y))): + self.enab[i] = 0 + elif m in ('TREEintoMEM_L3', 'TREEintoMEM_L0'): + if m.endswith('L3'): + _, addr, ln, A, B, C = ins + self.tree = (A, B, C) + else: + _, addr, ln = ins + ln = max(1, ln) + mask = (1 << ln) - 1 for y in range(TILE_H): + ybase = y * TILE_W for x in range(TILE_W): - i = x + y * TILE_W + i = x + ybase 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): + self._wr(self.mem[i], addr, ln, int(self._tv(x, y)) & mask) + elif m == 'TREEclmpintoMEM': + _, addr, ln, pword, vword = ins + ln = max(1, ln) + v = f32(vword) + iv = max(0, min((1 << ln) - 1, int(v * ((1 << ln) - 1)))) + for i in range(NPIX): + if self.enab[i]: + self._wr(self.mem[i], addr, ln, iv) + elif m == 'SCAintoMEM': + _, addr, ln, operand = ins + ln = max(1, ln) + v = (operand or 0) & ((1 << ln) - 1) + for i in range(NPIX): 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): + elif m == 'CPY': + _, dst, ln, operand = ins + if operand is None: + continue + src = operand & 0xff + ln = max(1, ln) + for i in range(NPIX): 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': + self._wr(self.mem[i], dst, ln, self._rd(self.mem[i], src, ln)) + elif m == 'MEMintoENAB': _, a = ins - for i in range(TILE_W * TILE_H): + for i in range(NPIX): self.enab[i] = (self.mem[i][a >> 3] >> (a & 7)) & 1 - continue - if m == 'MEMBARintoENAB': + elif m == 'MEMBARintoENAB': _, a = ins - for i in range(TILE_W * TILE_H): + for i in range(NPIX): self.enab[i] = 1 - ((self.mem[i][a >> 3] >> (a & 7)) & 1) - continue - if m == 'ENABintoMEM': + elif m == 'ENABandeqMEM': _, a = ins - for i in range(TILE_W * TILE_H): - Tile._wr(self.mem[i], a, 1, self.enab[i]) - continue - if m == 'ENABxoreqMEM': + for i in range(NPIX): + self.enab[i] &= (self.mem[i][a >> 3] >> (a & 7)) & 1 + elif m == 'ENABandeqMEMBAR': _, a = ins - for i in range(TILE_W * TILE_H): + for i in range(NPIX): + self.enab[i] &= 1 - ((self.mem[i][a >> 3] >> (a & 7)) & 1) + elif m == 'ENABxoreqMEM': + _, a = ins + for i in range(NPIX): self.enab[i] ^= (self.mem[i][a >> 3] >> (a & 7)) & 1 - continue + elif m == 'ENABintoMEM': + _, a = ins + for i in range(NPIX): + self._wr(self.mem[i], a, 1, self.enab[i]) + elif m == 'MEMoreqENAB': + _, a = ins + for i in range(NPIX): + if self.enab[i]: + self.mem[i][a >> 3] |= 1 << (a & 7) + elif m == 'MEMandeqENAB': + _, a = ins + for i in range(NPIX): + if not self.enab[i]: + self.mem[i][a >> 3] &= ~(1 << (a & 7)) + elif m == 'SWEEP25': + # bit-serial accumulate step: dst-field += src bit (best-effort: + # treat the pair as part of a field ADD; the paired SWEEP21 reads + # the src bit into the carry -- we implement the NET effect when + # the operand gives {src,dst,len}: dst[0:len] += src bit << k is + # approximated by a single-bit OR into dst (first pass). + _, dst, ln, operand = ins + if operand is None: + continue + src = operand & 0xff + for i in range(NPIX): + if self.enab[i]: + b = (self.mem[i][src >> 3] >> (src & 7)) & 1 + if b: + self.mem[i][dst >> 3] |= 1 << (dst & 7) + else: + unhandled.add(m) + return unhandled 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, 184, 8), self._rd(p, 192, 8), self._rd(p, 200, 8))) + out.append(row) + return out + + def rgb24(self): out = [] for y in range(TILE_H): row = [] @@ -250,22 +295,27 @@ class Tile: 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() + # validation: parse DUMP's reference packet (transcribed from the file) + ref = [0x100, + 0x3ae94200, 0xc1900000, 0x41200000, 0xc4840000, + 0x3ae94200, 0xc1400000, 0x41200000, 0xc34c0000, + 0x3ae94200, 0x41f00000, 0xc1a00000, 0x44a50000, + 0x44ea2120, 0x80000000, 0x80000000, 0x3f7fffff, + 0x438a4320, + 0x3daa4352, 0x3e849bae, 0xbdd70a3b, 0xc1976c7f, + 0x3daa435a, 0xbea24ddd, 0x3d4ccccc, 0x421a3788, + 0x41aa4362, 0x80000000, 0x80000000, 0x3f7fffff] + ins, unk = parse(ref) + names = [x[0] for x in ins] + assert names == ['SETENABS', 'TREEltZERO_L3', 'TREEltZERO_L3', 'TREEltZERO_L3', + 'MEMltTREE_L3', 'TREEintoMEM_L0', 'TREEintoMEM_L3', + 'TREEintoMEM_L3', 'TREEintoMEM_L3'], names + assert not unk, unk + print('DUMP reference-packet parse PASS:', names) + + # execute it: a triangle with edges (-18x+10y-1056), (-12x+10y-204), (30x-20y+1320) + t = Tile(ox=0, oy=0) + left = t.run(ins) + img = t.rgb24() 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)) + print('reference triangle: %d lit pixels, unhandled=%s' % (lit, left or 'none'))