From b78b14b7f954612c66012a4479d1ec2b042ab96d Mon Sep 17 00:00:00 2001
From: Cyd
Date: Thu, 16 Jul 2026 16:07:01 -0500
Subject: [PATCH] =?UTF-8?q?Array:=20read=20out=20the=20depth=20buffer;=20s?=
=?UTF-8?q?how=20it=20in=20the=20readout=20=C2=A705?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
---
emulator/firmware-decomp/igc_array.py | 38 +++++++
emulator/firmware-decomp/render-readout.html | 105 ++++++++++++++-----
2 files changed, 119 insertions(+), 24 deletions(-)
diff --git a/emulator/firmware-decomp/igc_array.py b/emulator/firmware-decomp/igc_array.py
index 994d6de..a02b01b 100644
--- a/emulator/firmware-decomp/igc_array.py
+++ b/emulator/firmware-decomp/igc_array.py
@@ -161,6 +161,44 @@ class IGCArray:
_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)
diff --git a/emulator/firmware-decomp/render-readout.html b/emulator/firmware-decomp/render-readout.html
index dc200fb..a3c3257 100644
--- a/emulator/firmware-decomp/render-readout.html
+++ b/emulator/firmware-decomp/render-readout.html
@@ -323,32 +323,37 @@
reference rasteriser to within edge anti-aliasing — the array reproduced faithfully.
-
- the object's footprint across the array — 18 of 50 tiles lit,
- each a 64×128 region of pixel processors
+
+ the depth buffer, read straight out of pixel memory — the 24-bit
+ Z plane the SENDE z-sweep interpolates (near = bright)
+
+
+
+ the object's footprint — 18 of 50 64×128 tiles lit,
+ each a region of pixel processors
-
-
pixel memory · 26 bytes = 208 bits
-
- Z · 24bR·8G·8
- B·8enable + working bits
-
-
-
three edge trees → the enable register (the inside test)
-
Z / R / G / B are planes, evaluated per pixel by the linear tree
-
z-buffer via MEM2geMEM2; every write gated by enable
-
output read back out of pixel memory → framebuffer
-
validated pixel-faithful to the reference — ~1%, at edges only
-
-
-
What this is not, yet: execution of the compiled
- micro-code the DMA actually ships (§02, right) — the form the ground and sky take, since
- they carry no stored vertices. That stream is now partly decoded: the per-region command lists
- read cleanly, and the SEND payloads turn out to carry the real float coefficients
- interleaved with a regular bit-serial sweep — so what remains is pinning the control-word fields
- and running that sweep, not reversing an opaque binary. This section is the array's
- computational model: the pixels that micro-code produces.
+
+
pixel memory · 26 bytes = 208 bits
+
+ Z · 24bR·8G·8
+ B·8enable + working bits
+
+
+
three edge trees → the enable register (the inside test)
+
Z / R / G / B are planes, evaluated per pixel by the linear tree
+
z-buffer via MEM2geMEM2; every write gated by enable
+
the depth image (left) is that Z plane, read back out of pixel memory
+
validated pixel-faithful to the reference — ~1%, at edges only
+
+
+
What this is not, yet: a full from-scratch run of
+ the compiled micro-code the DMA ships (§02) — the form the ground and sky take, since they
+ carry no stored vertices. But that stream is now largely decoded: the command lists read cleanly,
+ the SENDE z-sweep resolves into a regular 4-word instruction, and its
+ coefficients are the same ones driving this depth buffer — the array's inputs are
+ cross-validated against the hardware's own compiled stream. What remains is numeric
+ reconstruction across every region, not reversing an opaque binary.
@@ -636,6 +641,58 @@
}
}
+ /* ---- depth buffer: the same surface, coloured by camera Z (the array's z-plane) ---- */
+ (function(){
+ var sc=document.getElementById('depth'); if(!sc) return;
+ var dx=sc.getContext('2d');
+ var CW=sc.width, CH=sc.height, SS=2, W=CW*SS, H=CH*SS;
+ var XS=GRID.xs, ZS=GRID.zs, R=GRID.rows, NX=XS.length, NZ=ZS.length;
+ var cx=8, cz=-4, cy=0;
+ (function(){var s=0,n=0;for(var j=0;jmxx)mxx=p.X;if(p.Ymxy)mxy=p.Y;
+ if(p.Zmxz)mxz=p.Z;}
+ var pad=0.1*W, s=Math.min((W-2*pad)/(mxx-mnx),(H-2*pad)/(mxy-mny));
+ var ox=(W-(mxx-mnx)*s)/2, oy=(H-(mxy-mny)*s)/2, zsp=(mxz-mnz)||1;
+ function SXp(p){return (p.X-mnx)*s+ox;} function SYp(p){return (mxy-p.Y)*s+oy;}
+ var dep=new Float32Array(W*H); dep.fill(-2); var zb=new Float32Array(W*H); for(var k=0;kzb[idx]){zb[idx]=z; dep[idx]=(z-mnz)/zsp;}
+ }
+ }
+ for(var j=0;j=0){acc+=v;cnt++;}}
+ var o=(y*CW+xx)*4;
+ if(cnt>0){var vv=1-acc/cnt,cov=cnt/(SS*SS); // near = bright
+ dd[o]=30+vv*120; dd[o+1]=90+vv*150; dd[o+2]=120+vv*130; dd[o+3]=Math.round(cov*255);}
+ else dd[o+3]=0;
+ }
+ dx.putImageData(img,0,0);
+ })();
+
/* ---- IGC tile footprint: which 64x128 tiles the array lit for the object ---- */
var TILES={ntx:10,nty:5,touched:[[0,2],[1,1],[1,2],[2,1],[2,2],[2,3],[3,1],[3,2],[3,3],[4,1],[4,2],[4,3],[5,1],[5,2],[6,1],[6,2],[7,1],[8,1]]};
(function(){