Files
TeslaRel410/emulator/firmware-decomp/igc_gpu.py
T
CydandClaude Opus 4.8 151d4ca89e M1: the EMC tile as a GL compute shader -- CONFORMANT ON GPU (RTX 5060)
One invocation per pixel, the same 208-bit bit-addressed pixel memory as the
golden model, instruction stream as SSBO records with TREE state resolved at
pack time. Passes C2 (synthetic raster) and C3 (the real cap7 bench bar
programs) bit-for-bit against igc_exec. First step of the GPU retarget
(GPU-RETARGET.md M1); M2 = the frame loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 11:47:57 -05:00

253 lines
10 KiB
Python

"""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('<f', struct.pack('<I', vword & 0xffffffff))[0])
elif m == 'SCAintoMEM':
_, addr, ln, *rest = ins
operand = rest[-1] if rest and isinstance(rest[-1], int) else 0
emit('SCAintoMEM', addr, max(1, ln), (operand or 0))
elif m == 'CPY':
_, dst, ln, operand = ins
if operand is None:
continue
emit('CPY', dst, max(1, ln), operand & 0xff)
elif m in ('MEMintoENAB', 'MEMBARintoENAB', 'ENABandeqMEM',
'ENABandeqMEMBAR', 'ENABxoreqMEM', 'ENABintoMEM',
'MEMoreqENAB', 'MEMandeqENAB'):
emit(m, ins[1], 1)
elif m == 'MEMgeSCA':
_, addr, ln, sca = ins
emit('MEMgeSCA', addr, max(1, ln), sca & 0xffff)
elif m == 'SCMEMA':
_, addr, ln, operand = ins
emit('SCMEMA', addr, max(1, min(20, ln if ln > 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")