Files
TeslaRel410/emulator/firmware-decomp/igc_array.py
T
CydandClaude Opus 4.8 b78b14b7f9 Array: read out the depth buffer; show it in the readout §05
igc_array.py gains readout_depth() -> the 24-bit Z stored in pixel memory as a
depth image (near=bright), i.e. the plane the decoded SENDE z-sweep interpolates.
Readout §05 now shows the depth buffer next to the tile footprint, ties the
z-decode to a visible array output, and reframes the "remaining work" as numeric
reconstruction across regions (the coefficients are already cross-validated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 16:07:01 -05:00

313 lines
13 KiB
Python

"""Tier-1: a faithful software model of the Division VelociRender PXPL5 IGC
(Pixel-Planes 5) rasteriser array, driven by geometry captured off the emulated i860.
This is the array model from sda4/DPL3/VRENDER/PXPL5SUP/IGCOPS.C + IGCTYPES.H, made
real: the screen is partitioned into 64x128 tiles (tile_x_bits=6, tile_y_bits=7); each
pixel owns a small bit-addressable memory (pxpl5_mem_chars=26 bytes) plus an enable bit.
The IGC has no per-pixel ALU program in the classic sense -- every pixel evaluates the
same linear tree in lockstep: eval_ltree(x,y,A,B,C) = (int)(x*A + y*B + C) (IGCOPS.C).
A triangle is drawn exactly as the hardware does (PXPL5GEO tri_zb_rgb):
* three EDGE trees Ei = Ai*x + Bi*y + Ci, oriented inside>=0 -> the enable register
* a Z tree and R/G/B trees, each an Ax+By+C plane through the 3 vertices
* per enabled pixel: znew = eval z-tree; if znew nearer than the stored z, the enable
survives (MEM2geMEM2) and z + rgb are written into pixel memory (writes are gated by
enable, as in hardware).
Tiles the geometry never touches are skipped (the real board bins per region first).
Finally each tile's pixel memory is read back out (read_pixmem_word) into an RGB frame.
Not a decode of the compiled bit-serial micro-code (that binary encoding is still
undecoded) -- it is the array's *computational* model, producing the pixels that
micro-code would. Validated to match shade_render.py on the same projection.
"""
import sys, struct, math, pickle, json
TILE_X_BITS, TILE_Y_BITS = 6, 7
TILE_X, TILE_Y = 1 << TILE_X_BITS, 1 << TILE_Y_BITS # 64 x 128
PXPL5_MEM_CHARS = 26
# pixel-memory bit layout we use (fits in 26 bytes = 208 bits):
Z_BIT0, Z_BITS = 0, 24 # 24-bit depth, smaller = nearer
R_BIT0, G_BIT0, B_BIT0, C_BITS = 24, 32, 40, 8
Z_FAR = (1 << Z_BITS) - 1
class Tile:
"""One 64x128 IGC tile: per-pixel enable bit + 26-byte bit memory."""
__slots__ = ('x0', 'y0', 'mem', 'enable', 'touched')
def __init__(self, x0, y0):
self.x0, self.y0 = x0, y0
self.mem = [bytearray(PXPL5_MEM_CHARS) for _ in range(TILE_X * TILE_Y)]
self.enable = bytearray(TILE_X * TILE_Y)
self.touched = False
for p in self.mem: # z-clear to far
_wword(p, Z_BIT0, Z_BITS, Z_FAR)
def _rbit(pix, bit):
return 1 & (pix[bit >> 3] >> (bit & 7))
def _wbit(pix, bit, val):
if val:
pix[bit >> 3] |= (1 << (bit & 7))
else:
pix[bit >> 3] &= ~(1 << (bit & 7))
def _rword(pix, bit0, bits):
res = 0
for i in range(bits):
res |= _rbit(pix, bit0 + i) << i
return res
def _wword(pix, bit0, bits, val):
for i in range(bits):
_wbit(pix, bit0 + i, val & 1)
val >>= 1
def plane(x0, y0, v0, x1, y1, v1, x2, y2, v2):
"""A,B,C so that A*x+B*y+C == v at each of the 3 vertices (the IGC linear tree)."""
det = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
if abs(det) < 1e-9:
return 0.0, 0.0, v0
A = ((v1 - v0) * (y2 - y0) - (v2 - v0) * (y1 - y0)) / det
B = ((v2 - v0) * (x1 - x0) - (v1 - v0) * (x2 - x0)) / det
C = v0 - A * x0 - B * y0
return A, B, C
class IGCArray:
def __init__(self, W, H):
self.W, self.H = W, H
self.ntx = (W + TILE_X - 1) // TILE_X
self.nty = (H + TILE_Y - 1) // TILE_Y
self.tiles = {}
self.tris = 0
def _tile(self, tx, ty):
key = (tx, ty)
t = self.tiles.get(key)
if t is None:
t = Tile(tx * TILE_X, ty * TILE_Y)
self.tiles[key] = t
return t
def triangle(self, v0, v1, v2):
"""Each v = (sx, sy, depth, r, g, b). depth: smaller = nearer (0..1)."""
(x0, y0, z0, r0, g0, b0) = v0
(x1, y1, z1, r1, g1, b1) = v1
(x2, y2, z2, r2, g2, b2) = v2
area = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
if abs(area) < 1e-6:
return
if area < 0: # orient CCW so inside>=0
x1, y1, z1, r1, g1, b1, x2, y2, z2, r2, g2, b2 = \
x2, y2, z2, r2, g2, b2, x1, y1, z1, r1, g1, b1
# three edge trees Ei = Ai*x + Bi*y + Ci; normalise each so the centroid is
# inside (>=0), independent of winding / screen-Y direction.
gcx, gcy = (x0 + x1 + x2) / 3.0, (y0 + y1 + y2) / 3.0
E = []
for (ax, ay), (bx, by) in (((x0, y0), (x1, y1)),
((x1, y1), (x2, y2)),
((x2, y2), (x0, y0))):
A = (by - ay); B = -(bx - ax); C = -(A * ax + B * ay)
if A * gcx + B * gcy + C < 0:
A, B, C = -A, -B, -C
E.append((A, B, C))
# z + rgb planes
zc = plane(x0, y0, z0, x1, y1, z1, x2, y2, z2)
rc = plane(x0, y0, r0, x1, y1, r1, x2, y2, r2)
gc = plane(x0, y0, g0, x1, y1, g1, x2, y2, g2)
bc = plane(x0, y0, b0, x1, y1, b1, x2, y2, b2)
self.tris += 1
minx = max(0, int(min(x0, x1, x2))); maxx = min(self.W - 1, int(max(x0, x1, x2)) + 1)
miny = max(0, int(min(y0, y1, y2))); maxy = min(self.H - 1, int(max(y0, y1, y2)) + 1)
for ty in range(miny // TILE_Y, maxy // TILE_Y + 1):
for tx in range(minx // TILE_X, maxx // TILE_X + 1):
self._raster_tile(self._tile(tx, ty), E, zc, rc, gc, bc,
minx, maxx, miny, maxy)
def _raster_tile(self, t, E, zc, rc, gc, bc, minx, maxx, miny, maxy):
(zA, zB, zC) = zc
lx0 = max(0, minx - t.x0); lx1 = min(TILE_X - 1, maxx - t.x0)
ly0 = max(0, miny - t.y0); ly1 = min(TILE_Y - 1, maxy - t.y0)
(A0, B0, C0), (A1, B1, C1), (A2, B2, C2) = E
for ly in range(ly0, ly1 + 1):
gy = t.y0 + ly
base = ly << TILE_X_BITS
for lx in range(lx0, lx1 + 1):
gx = t.x0 + lx
# enable = inside all three edge trees
if (A0 * gx + B0 * gy + C0) < 0: continue
if (A1 * gx + B1 * gy + C1) < 0: continue
if (A2 * gx + B2 * gy + C2) < 0: continue
idx = base + lx
pix = t.mem[idx]
znew = int((zA * gx + zB * gy + zC) * Z_FAR)
if znew < 0: znew = 0
elif znew > Z_FAR: znew = Z_FAR
if znew >= _rword(pix, Z_BIT0, Z_BITS): # MEM2geMEM2: nearer wins
continue
t.touched = True
_wword(pix, Z_BIT0, Z_BITS, znew)
rv = min(255, max(0, int(rc[0] * gx + rc[1] * gy + rc[2])))
gv = min(255, max(0, int(gc[0] * gx + gc[1] * gy + gc[2])))
bv = min(255, max(0, int(bc[0] * gx + bc[1] * gy + bc[2])))
_wword(pix, R_BIT0, C_BITS, rv)
_wword(pix, G_BIT0, C_BITS, gv)
_wword(pix, B_BIT0, C_BITS, bv)
def readout_depth(self, bg=(7, 11, 17)):
"""Read the 24-bit z stored in pixel memory back into a depth image (near=bright).
This is the plane the SENDE z-sweep interpolates, straight out of pixel memory."""
img = bytearray(self.W * self.H * 3)
for y in range(self.H):
o = y * self.W * 3
for x in range(self.W):
img[o] = bg[0]; img[o + 1] = bg[1]; img[o + 2] = bg[2]; o += 3
# gather the touched z range for contrast
zmin, zmax = Z_FAR, 0
for t in self.tiles.values():
if not t.touched:
continue
for pix in t.mem:
z = _rword(pix, Z_BIT0, Z_BITS)
if z != Z_FAR:
zmin = min(zmin, z); zmax = max(zmax, z)
span = (zmax - zmin) or 1
for (tx, ty), t in self.tiles.items():
if not t.touched:
continue
for ly in range(TILE_Y):
gy = t.y0 + ly
if gy >= self.H:
break
base = ly << TILE_X_BITS
for lx in range(TILE_X):
gx = t.x0 + lx
if gx >= self.W:
break
z = _rword(t.mem[base + lx], Z_BIT0, Z_BITS)
if z == Z_FAR:
continue
v = 1.0 - (z - zmin) / span # near = bright
o = (gy * self.W + gx) * 3
img[o] = int(30 + v * 120); img[o + 1] = int(90 + v * 150); img[o + 2] = int(120 + v * 130)
return img
def readout(self, bg=(7, 11, 17)):
"""Read pixel memory back out into an RGB framebuffer (row-major, RGB bytes)."""
img = bytearray(self.W * self.H * 3)
for y in range(self.H):
o = y * self.W * 3
for x in range(self.W):
img[o] = bg[0]; img[o + 1] = bg[1]; img[o + 2] = bg[2]
o += 3
for (tx, ty), t in self.tiles.items():
if not t.touched:
continue
for ly in range(TILE_Y):
gy = t.y0 + ly
if gy >= self.H: break
base = ly << TILE_X_BITS
for lx in range(TILE_X):
gx = t.x0 + lx
if gx >= self.W: break
pix = t.mem[base + lx]
if _rword(pix, Z_BIT0, Z_BITS) == Z_FAR:
continue
o = (gy * self.W + gx) * 3
img[o] = _rword(pix, R_BIT0, C_BITS)
img[o + 1] = _rword(pix, G_BIT0, C_BITS)
img[o + 2] = _rword(pix, B_BIT0, C_BITS)
return img
# ------- driver: rasterise the captured 9x5 surface through the array -------
def _n(a, b, c):
m = math.sqrt(a * a + b * b + c * c) or 1.0
return a / m, b / m, c / m
def build_from_grid(pkl, W=620, H=560, yaw=40.0, pitch=28.0):
objs = pickle.load(open(pkl, 'rb'))['objs']
allv = [v for o in objs for v in o]
xs = sorted(set(round(v['mx'], 2) for v in allv))
zs = sorted(set(round(v['mz'], 2) for v in allv))
grid = {(round(v['mx'], 2), round(v['mz'], 2)): v for v in allv}
cx = sum(xs) / len(xs); cz = sum(zs) / len(zs)
cy = sum(v['my'] for v in allv) / len(allv)
ry, rp = math.radians(yaw), math.radians(pitch)
cyw, syw, cp, sp = math.cos(ry), math.sin(ry), math.cos(rp), math.sin(rp)
def rot(x, y, z):
x, z = x * cyw + z * syw, -x * syw + z * cyw
y, z = y * cp - z * sp, y * sp + z * cp
return x, y, z
L = _n(0.35, 0.55, 0.72)
P = {}
for (x, z), v in grid.items():
X, Y, Z = rot(v['mx'] - cx, (v['my'] - cy) * 1.8, v['mz'] - cz)
nx, ny, nz = rot(v['nx'], v['ny'], v['nz']); n = _n(nx, ny, nz)
d = abs(n[0] * L[0] + n[1] * L[1] + n[2] * L[2])
it = max(0.16, min(1.0, 0.22 + 0.85 * d))
P[(x, z)] = (X, Y, Z, it)
XX = [p[0] for p in P.values()]; YY = [p[1] for p in P.values()]; ZZ = [p[2] for p in P.values()]
mnx, mxx, mny, mxy = min(XX), max(XX), min(YY), max(YY)
mnz, mxz = min(ZZ), max(ZZ)
pad = 0.1 * W
s = min((W - 2 * pad) / (mxx - mnx), (H - 2 * pad) / (mxy - mny))
ox = (W - (mxx - mnx) * s) / 2; oy = (H - (mxy - mny) * s) / 2
def sx(p): return (p[0] - mnx) * s + ox
def sy(p): return (mxy - p[1]) * s + oy
def depth(p): return (p[2] - mnz) / ((mxz - mnz) or 1) # 0 near .. 1 far
def col(it):
return (min(255, 28 + it * 168), min(255, 50 + it * 205), min(255, 54 + it * 122))
arr = IGCArray(W, H)
xsl, zsl = xs, zs
for i in range(len(xsl) - 1):
for j in range(len(zsl) - 1):
a = P[(xsl[i], zsl[j])]; b = P[(xsl[i + 1], zsl[j])]
c = P[(xsl[i], zsl[j + 1])]; d = P[(xsl[i + 1], zsl[j + 1])]
def vtx(p):
r, g, bb = col(p[3]); return (sx(p), sy(p), depth(p), r, g, bb)
arr.triangle(vtx(a), vtx(b), vtx(c))
arr.triangle(vtx(b), vtx(d), vtx(c))
return arr
def main():
S = r'C:\Users\cyd\AppData\Local\Temp\claude\c--VWE-TeslaRel410\4e848c76-6e89-4034-8047-d8d491cb32d8\scratchpad'
pkl = sys.argv[1] if len(sys.argv) > 1 else S + r'\vfull.pkl'
out = next((a for a in sys.argv[1:] if a.endswith('.ppm')), 'igc.ppm')
W, H = 620, 560
arr = build_from_grid(pkl, W, H)
active = sum(1 for t in arr.tiles.values() if t.touched)
print("IGC array: %dx%d, %d tiles (%dx%d), %d touched, %d triangles" %
(W, H, len(arr.tiles), arr.ntx, arr.nty, active, arr.tris))
img = arr.readout()
with open(out, 'wb') as f:
f.write(b'P6\n%d %d\n255\n' % (W, H)); f.write(bytes(img))
print("wrote", out)
# ASCII preview
ramp = " .:-=+*#%@"
for y in range(0, H, H // 40):
line = " "
for x in range(0, W, W // 74):
o = (y * W + x) * 3
lum = (img[o] + img[o + 1] + img[o + 2]) / 3
line += ramp[min(9, int(lum / 255 * 9.99))] if lum > 20 else ' '
print(line)
if __name__ == '__main__':
main()