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>
253 lines
10 KiB
Python
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")
|