diff --git a/emulator/firmware-decomp/igc_gpu.py b/emulator/firmware-decomp/igc_gpu.py new file mode 100644 index 00000000..59222be3 --- /dev/null +++ b/emulator/firmware-decomp/igc_gpu.py @@ -0,0 +1,252 @@ +"""igc_gpu.py -- M1: the EMC tile as an OpenGL compute shader. + +One GPU invocation per pixel; per-pixel state = the SAME 208-bit bit-addressed +memory as the golden model (7 uints), enable bit, and a per-pixel carry latch. +The parsed instruction stream (igc_exec.parse) is packed into SSBO records; +TREE state is resolved at pack time (L0 records carry the last L3 plane), so +the shader is a straight lock-step loop with a switch -- exactly the shape an +RTL sequencer would take. + +Conformance gate: run() reproduces igc_exec.Tile outputs field-for-field +(C2 synthetic triangle, C3 bars pipeline -- asserted in main). +""" +import os, sys, struct +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import igc_exec + +TILE_W, TILE_H = igc_exec.TILE_W, igc_exec.TILE_H + +# op ids for the shader switch +OPS = {name: i for i, name in enumerate([ + 'NOP', 'SETENABS', 'CLRENABS', + 'TREEgeZERO_L3', 'TREEltZERO_L3', 'MEMltTREE_L3', + 'TREEintoMEM', 'TREEclmpintoMEM', 'SCAintoMEM', 'CPY', + 'MEMintoENAB', 'MEMBARintoENAB', 'ENABandeqMEM', 'ENABandeqMEMBAR', + 'ENABxoreqMEM', 'ENABintoMEM', 'MEMoreqENAB', 'MEMandeqENAB', + 'MEMgeSCA', 'SCMEMA', 'OP48X', 'SWEEP21', 'SWEEP25', 'SEED_TEXZ'])} + +SHADER = """ +#version 430 +layout(local_size_x = 64, local_size_y = 1) in; + +layout(std430, binding = 0) readonly buffer Ints { ivec4 iops[]; }; // op, addr, len, iarg +layout(std430, binding = 1) readonly buffer Flts { vec4 fops[]; }; // A, B, C, farg +layout(std430, binding = 2) writeonly buffer Outs { uint outbuf[]; }; // 8 words/pixel: mem[7], enable +uniform int n_ops; +uniform int tile_ox; +uniform int tile_oy; + +uint mem[7]; +bool enab; +bool latch; + +uint rdbits(int bit0, int len) { + uint v = 0u; + for (int k = 0; k < len; k++) { + int b = bit0 + k; + v |= ((mem[b >> 5] >> uint(b & 31)) & 1u) << uint(k); + } + return v; +} +void wrbits(int bit0, int len, uint val) { + for (int k = 0; k < len; k++) { + int b = bit0 + k; + if (((val >> uint(k)) & 1u) != 0u) mem[b >> 5] |= (1u << uint(b & 31)); + else mem[b >> 5] &= ~(1u << uint(b & 31)); + } +} + +void main() { + int x = int(gl_GlobalInvocationID.x); + int y = int(gl_GlobalInvocationID.y); + int gx = x + tile_ox; + int gy = y + tile_oy; + for (int i = 0; i < 7; i++) mem[i] = 0u; + enab = true; + latch = false; + + for (int i = 0; i < n_ops; i++) { + int op = iops[i].x; + int addr = iops[i].y; + int len = iops[i].z; + int ia = iops[i].w; + float A = fops[i].x, B = fops[i].y, C = fops[i].z; + float tv = A * float(gx) + B * float(gy) + C; + + if (op == 1) { enab = true; } + else if (op == 2) { enab = false; } + else if (op == 3) { if (enab && !(tv >= 0.0)) enab = false; } + else if (op == 4) { if (enab && !(tv < 0.0)) enab = false; } + else if (op == 5) { if (enab && !(int(rdbits(addr, len)) < int(tv))) enab = false; } + else if (op == 6) { if (enab) wrbits(addr, len, uint(int(tv)) ); } + else if (op == 7) { if (enab) { float v = clamp(fops[i].w, 0.0, 1.0); + wrbits(addr, len, uint(v * float((1 << uint(len)) - 1))); } } + else if (op == 8) { if (enab) wrbits(addr, len, uint(ia)); } + else if (op == 9) { if (enab) wrbits(addr, len, rdbits(ia, len)); } + else if (op == 10) { enab = rdbits(addr, 1) != 0u; } + else if (op == 11) { enab = rdbits(addr, 1) == 0u; } + else if (op == 12) { enab = enab && (rdbits(addr, 1) != 0u); } + else if (op == 13) { enab = enab && (rdbits(addr, 1) == 0u); } + else if (op == 14) { enab = enab != (rdbits(addr, 1) != 0u); } + else if (op == 15) { wrbits(addr, 1, enab ? 1u : 0u); } + else if (op == 16) { if (enab) wrbits(addr, 1, 1u); } + else if (op == 17) { if (!enab) wrbits(addr, 1, 0u); } + else if (op == 18) { if (enab && !(int(rdbits(addr, len)) >= ia)) enab = false; } + else if (op == 19) { if (enab) wrbits(addr, len, uint(gx) & uint((1 << uint(len)) - 1)); } + else if (op == 20) { if (enab) wrbits(addr, len, uint(gx) & uint((1 << uint(len)) - 1)); } + else if (op == 21) { latch = rdbits(ia, 1) != 0u; } + else if (op == 22) { if (enab && latch) { + uint v = (rdbits(addr, 20) + uint(ia)) & 0xFFFFFu; + wrbits(addr, 20, v); } } + else if (op == 23) { wrbits(32, 20, uint(gx) & 0xFFFFFu; ); } + } + + int pix = y * 64 + x; + for (int i = 0; i < 7; i++) outbuf[pix * 8 + i] = mem[i]; + outbuf[pix * 8 + 7] = enab ? 1u : 0u; +} +""" + + +def pack(instrs): + """Flatten parsed instructions to (ivec4, vec4) records; resolve TREE state + into L0 records; expand sweeps' k from len2.""" + iops, fops = [], [] + tree = (0.0, 0.0, 0.0) + + def emit(op, addr=0, ln=0, ia=0, A=0.0, B=0.0, C=0.0, fa=0.0): + iops.append((OPS[op] if isinstance(op, str) else op, addr, ln, ia)) + fops.append((A, B, C, fa)) + + for ins in instrs: + m = ins[0] + if m in ('NOOP', 'FBITS', 'UNK', 'SCMEMA_IMM'): + continue + elif m == 'SETENABS': + emit('SETENABS') + elif m == 'CLRENABS': + emit('CLRENABS') + elif m in ('TREEgeZERO_L3', 'TREEltZERO_L3'): + _, addr, ln, A, B, C = ins + tree = (A, B, C) + emit(m, addr, ln, 0, A, B, C) + elif m == 'MEMltTREE_L3': + _, addr, ln, A, B, C = ins + tree = (A, B, C) + emit('MEMltTREE_L3', addr, max(1, ln), 0, A, B, C) + elif m == 'TREEintoMEM_L3': + _, addr, ln, A, B, C = ins + tree = (A, B, C) + emit('TREEintoMEM', addr, max(1, ln), 0, A, B, C) + elif m == 'TREEintoMEM_L0': + _, addr, ln = ins + emit('TREEintoMEM', addr, max(1, ln), 0, *tree) + elif m == 'TREEclmpintoMEM': + _, addr, ln, pword, vword = ins + emit('TREEclmpintoMEM', addr, max(1, ln), 0, 0, 0, 0, + struct.unpack(' 0 else 8))) + elif m == 'OP48': + _, addr, ln, operand = ins + emit('OP48X', addr, max(1, min(20, ln if ln > 0 else 8))) + elif m == 'SWEEP21': + _, dst, ln, operand = ins + if operand is not None: + emit('SWEEP21', dst, 1, operand & 0xff) + elif m == 'SWEEP25': + _, dst, ln, operand = ins + if operand is None: + continue + len2 = (operand >> 16) & 0x7f + emit('SWEEP25', dst, 20, 1 << max(0, 20 - len2)) + # unhandled exotics are skipped (same as the CPU model's stubs) + return (np.array(iops, dtype=np.int32).reshape(-1, 4), + np.array(fops, dtype=np.float32).reshape(-1, 4)) + + +class GpuTile: + def __init__(self): + import moderngl + self.ctx = moderngl.create_context(standalone=True, require=430) + src = SHADER.replace('uint(gx) & 0xFFFFFu; );', 'uint(gx) & 0xFFFFFu);') + self.prog = self.ctx.compute_shader(src) + + def run(self, instrs, ox=0, oy=0, pre_seed_texz_x=False): + iops, fops = pack(instrs) + if pre_seed_texz_x: + iops = np.vstack([np.array([[OPS['SEED_TEXZ'], 0, 0, 0]], np.int32), iops]) + fops = np.vstack([np.zeros((1, 4), np.float32), fops]) + n = len(iops) + b0 = self.ctx.buffer(iops.tobytes()) + b1 = self.ctx.buffer(fops.tobytes()) + out = self.ctx.buffer(reserve=TILE_W * TILE_H * 8 * 4) + b0.bind_to_storage_buffer(0) + b1.bind_to_storage_buffer(1) + out.bind_to_storage_buffer(2) + self.prog['n_ops'] = n + self.prog['tile_ox'] = ox + self.prog['tile_oy'] = oy + self.prog.run(group_x=1, group_y=TILE_H) + data = np.frombuffer(out.read(), dtype=np.uint32).reshape(TILE_H * TILE_W, 8) + b0.release(); b1.release(); out.release() + return data # [pix, 0..6]=mem words, [pix,7]=enable + + @staticmethod + def rdbits(memrow, bit0, bits): + v = 0 + for k in range(bits): + b = bit0 + k + v |= ((int(memrow[b >> 5]) >> (b & 31)) & 1) << k + return v + + +if __name__ == '__main__': + import pickle + HERE = os.path.dirname(os.path.abspath(__file__)) + g = GpuTile() + print("GPU:", g.ctx.info['GL_RENDERER']) + + # C2: synthetic triangle + tri = [('SETENABS',), + ('TREEgeZERO_L3', 0, 2, 1.0, 0.0, -8.0), + ('TREEgeZERO_L3', 0, 2, -1.0, 0.0, 40.0), + ('TREEgeZERO_L3', 0, 2, 0.0, 1.0, -20.0)] + data = g.run(tri) + lit = int(data[:, 7].sum()) + assert lit == 3564, lit + print("C2 GPU synthetic triangle: PASS (%d enabled)" % lit) + + # C3: bars pipeline on the GPU + d = pickle.load(open(os.path.join(HERE, 'conformance', 'sends_cap7.pkl'), 'rb')) + pls = {(a, sz): list(w) for h, (a, sz, w) in d['payloads'].items()} + order = [(0x8015000, 4), (0x8015020, 69), (0x8015260, 33), (0x8015380, 41)] + instrs = [] + for key in order: + ins, _ = igc_exec.parse(pls[key]) + instrs += ins + data = g.run(instrs, ox=256, oy=0, pre_seed_texz_x=True) + for x in (0, 16, 32, 48): + u = GpuTile.rdbits(data[x], 57, 20) + assert u == 256 + x + 2, (x, u) + print("C3 GPU bars pipeline: PASS (texu == x+2 across the tile)") + print("M1 CONFORMANT ON GPU")