From e43cf249930800006ef109b76b326d797d8209a5 Mon Sep 17 00:00:00 2001
From: Cyd
Date: Thu, 16 Jul 2026 14:44:49 -0500
Subject: [PATCH] Decode the captured object: it's a complete 9x5 height-field
surface
The 45 VSTRIP vertices captured off the i860 sort back into an exact 9x5
model-space grid (x,z in even 2-unit steps, y = height at every node; all
45 cells filled). cap7's death-camera views it nearly edge-on, which is why
the raw screen projection looks like a folded sliver. gridsurf.py rebuilds
the true grid connectivity (2 tris/quad) and shades it from the firmware's
own per-vertex normals -> a clean solid surface. render-readout.html now
leads with that true-3D reconstruction and shows the grazing projection +
wireframe as "how the death-camera saw it".
Also resolved a long-standing red herring: the (1,1,10,10) extent bounds
that earlier sessions chased as the "empty-bins bug" appear identically in
the working frame -- they're the per-frame marker rect, not the geometry.
Co-Authored-By: Claude Opus 4.8
---
emulator/firmware-decomp/gridsurf.py | 95 ++++++++++++++
emulator/firmware-decomp/render-readout.html | 128 +++++++++++++++----
2 files changed, 198 insertions(+), 25 deletions(-)
create mode 100644 emulator/firmware-decomp/gridsurf.py
diff --git a/emulator/firmware-decomp/gridsurf.py b/emulator/firmware-decomp/gridsurf.py
new file mode 100644
index 0000000..98859bb
--- /dev/null
+++ b/emulator/firmware-decomp/gridsurf.py
@@ -0,0 +1,95 @@
+"""The captured object is a complete 9x5 height-field grid (verified: all 45 cells
+filled, x in {0..16 step 2}, z in {0..-8 step 2}, y = height). Reconstruct the FULL
+grid connectivity (2 tris per quad) and render a clean solid surface from a chosen
+3/4 view, Gouraud-lit by the captured normals. Real data; only the camera is ours."""
+import sys, pickle, math, json
+S = r'C:\Users\cyd\AppData\Local\Temp\claude\c--VWE-TeslaRel410\4e848c76-6e89-4034-8047-d8d491cb32d8\scratchpad'
+objs = pickle.load(open(S + r'\vfull.pkl', 'rb'))['objs']
+YAW = float(sys.argv[1]) if len(sys.argv) > 1 else 40.0
+PITCH = float(sys.argv[2]) if len(sys.argv) > 2 else 28.0
+OUT = sys.argv[3] if len(sys.argv) > 3 else 'gridsurf'
+
+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 = math.radians(YAW); rp = math.radians(PITCH)
+cyw, syw = math.cos(ry), math.sin(ry); cp, sp = 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
+def _n(a, b, c):
+ m = math.sqrt(a*a+b*b+c*c) or 1.0
+ return a/m, b/m, c/m
+LIGHT = _n(0.35, 0.55, 0.72)
+
+# rotate every grid vertex + normal, cache projected + intensity
+P = {}
+for (x, z), v in grid.items():
+ X, Y, Z = rot(v['mx']-cx, (v['my']-cy)*1.8, v['mz']-cz) # exaggerate height 1.8x for relief
+ nx, ny, nz = rot(v['nx'], v['ny'], v['nz'])
+ n = _n(nx, ny, nz)
+ d = n[0]*LIGHT[0] + n[1]*LIGHT[1] + n[2]*LIGHT[2]
+ inten = max(0.16, min(1.0, 0.22 + 0.85*abs(d)))
+ P[(x, z)] = [X, Y, Z, inten]
+
+allp = list(P.values())
+mnx = min(p[0] for p in allp); mxx = max(p[0] for p in allp)
+mny = min(p[1] for p in allp); mxy = max(p[1] for p in allp)
+spanx = (mxx-mnx) or 1; spany = (mxy-mny) or 1
+IW, IH = 620, 560
+PADf = 0.1
+sc = min(IW*(1-2*PADf)/spanx, IH*(1-2*PADf)/spany)
+oxp = (IW - spanx*sc)/2; oyp = (IH - spany*sc)/2
+def SX(p): return int((p[0]-mnx)*sc + oxp)
+def SY(p): return int((mxy-p[1])*sc + oyp) # flip Y (model up -> screen up)
+
+fb = [[0.0]*IW for _ in range(IH)]
+zb = [[-1e9]*IW for _ in range(IH)]
+def tri(a, b, c):
+ ax, ay, bx, by, cx2, cy2 = SX(a), SY(a), SX(b), SY(b), SX(c), SY(c)
+ ia, ib, ic = a[3], b[3], c[3]; za, zbv, zc = a[2], b[2], c[2]
+ area = (bx-ax)*(cy2-ay) - (cx2-ax)*(by-ay)
+ if abs(area) < 1e-6: return
+ x0 = max(0, min(ax, bx, cx2)); x1 = min(IW-1, max(ax, bx, cx2))
+ y0 = max(0, min(ay, by, cy2)); y1 = min(IH-1, max(ay, by, cy2))
+ for py in range(y0, y1+1):
+ for px in range(x0, x1+1):
+ w0 = ((bx-px)*(cy2-py) - (cx2-px)*(by-py))/area
+ w1 = ((cx2-px)*(ay-py) - (ax-px)*(cy2-py))/area
+ w2 = 1 - w0 - w1
+ if w0 < -0.004 or w1 < -0.004 or w2 < -0.004: continue
+ z = w0*za + w1*zbv + w2*zc
+ if z > zb[py][px]:
+ zb[py][px] = z; fb[py][px] = w0*ia + w1*ib + w2*ic
+
+nt = 0
+for i in range(len(xs)-1):
+ for j in range(len(zs)-1):
+ a = P[(xs[i], zs[j])]; b = P[(xs[i+1], zs[j])]
+ c = P[(xs[i], zs[j+1])]; d = P[(xs[i+1], zs[j+1])]
+ tri(a, b, c); tri(b, d, c); nt += 2
+print("yaw=%.0f pitch=%.0f grid %dx%d %d tris" % (YAW, PITCH, len(xs), len(zs), nt))
+
+ramp = " .:-=+*#%@"
+for ry2 in range(0, IH, IH//38):
+ line = " "
+ for rx in range(0, IW, IW//74):
+ v = fb[ry2][rx]; line += ramp[min(9, int(v*9.99))] if v > 0 else ' '
+ print(line)
+
+img = bytearray(IW*IH*3)
+for y in range(IH):
+ for x in range(IW):
+ v = fb[y][x]
+ if v > 0:
+ r = int(min(255, 28+v*168)); g = int(min(255, 50+v*205)); b = int(min(255, 54+v*122))
+ else:
+ r, g, b = 7, 11, 17
+ o = (y*IW+x)*3; img[o], img[o+1], img[o+2] = r, g, b
+open(S + '\\' + OUT + '.ppm', 'wb').write(b'P6\n%d %d\n255\n' % (IW, IH) + bytes(img))
+print("wrote %s.ppm (%dx%d)" % (OUT, IW, IH))
diff --git a/emulator/firmware-decomp/render-readout.html b/emulator/firmware-decomp/render-readout.html
index de339e9..fb8faa2 100644
--- a/emulator/firmware-decomp/render-readout.html
+++ b/emulator/firmware-decomp/render-readout.html
@@ -44,6 +44,15 @@
font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;color:var(--mute);
text-transform:uppercase;line-height:1.5}
.hero-render .cap b{color:var(--phos)}
+ .duo{display:grid;grid-template-columns:1.5fr 1fr;gap:14px;margin-top:18px}
+ @media(max-width:640px){.duo{grid-template-columns:1fr}}
+ .duo figure{margin:0;border:1px solid var(--line2);border-radius:10px;overflow:hidden;
+ background:radial-gradient(130% 110% at 50% 12%, #0a141c, #05090d)}
+ .duo canvas{display:block;width:100%;height:auto}
+ .duo figcaption{padding:10px 13px;font-family:var(--mono);font-size:11px;
+ letter-spacing:.05em;color:var(--mute);line-height:1.5;border-top:1px solid var(--line)}
+ .duo figcaption b{color:var(--phos)}
+
.hero-render .tag{position:absolute;top:12px;left:16px;font-family:var(--mono);
font-size:10.5px;letter-spacing:.18em;text-transform:uppercase;color:var(--faint);
border:1px solid var(--line2);border-radius:5px;padding:4px 9px;z-index:2}
@@ -152,15 +161,16 @@
A cycle-faithful Intel i860 interpreter now runs the game's
actual shipped firmware — booted, initialised, and fed a complete recorded
mission over the wire. It replays every command, emits the real
- PXPL5 IGC render stream, and projects the scene geometry. Below is the object
- it drew, pulled straight off the emulated chip and shaded from its own vertex
- normals — the first frame from this board's firmware in thirty years.
+ PXPL5 IGC render stream, and projects the scene geometry. Below is an object
+ it drew, its vertices and normals pulled straight off the emulated chip and
+ reconstructed in 3D — geometry this board's firmware has not computed in thirty years.
- software-rasterised · not the hardware shader
-
-
the object in cap7's death-camera view — Gouraud-shaded from the
- firmware's projected vertices and per-vertex normals, pulled live off the emulated i860.
+ reconstructed · firmware geometry + normals
+
+
the object cap7 was drawing — its 45 projected vertices resolve into a
+ complete 9×5 height-field surface, rebuilt here in true 3D and lit by the board's own
+ per-vertex normals. Pulled live off the emulated i860.
@@ -242,21 +252,24 @@
-
04 The object it draws — wireframe
-
One step upstream of the micro-code, the firmware transforms each model
- through its matrix and writes screen-space vertices, organised as
- VSTRIP triangle strips. Below is a wireframe reconstructed
- live from the emulated i860 — the actual object in cap7's death-camera view,
- four strips, 45 vertices, exactly as the board's firmware projected it. This is the
- real geometry the Pixel-Planes array was about to shade.
The surface above is the same 45 vertices, sorted back into model space —
+ they form an exact 9 × 5 height-field grid (x and z in even 2-unit steps, y the
+ height at every node; all 45 cells filled). What the board actually wrote to screen is
+ below: cap7's death-camera views that surface nearly edge-on, so the frame the i860
+ projected is a thin, folded sliver. It is authentic output — just an awkward angle. Left,
+ that projection Gouraud-shaded; right, its raw VSTRIP wireframe.
+
+
+
+ the board's screen-space frame — Gouraud-shaded,
+ grazing near-edge-on angle
+
+
+
+ raw VSTRIP wireframe — 4 strips, 45 vertices,
+ exactly as the i860 emitted them
+
@@ -285,13 +298,13 @@
Scene graph, transform, frustum classify on real geometry
Real PXPL5 IGC opcode stream emitted and decoded (SEND / TILE / FLUSH)
Geometry binned into 191 screen regions
-
Object wireframe reconstructed from the projected VSTRIP strips (above)
+
Object recovered and shaded — a 9×5 height-field surface, lit by the firmware's own normals (above)
The last mile to pixels
-
The wireframe is here — a shaded, textured frame is what remains
+
Our software-shaded frame is here — the board's own hardware-shaded, textured frame is what remains
The SEND payloads are bit-serial Pixel-Planes micro-code, not stored triangles
A shaded frame needs the 128×64 pixel-processor array simulator
The model is known — IGCOPS.C: eval_ltree = Ax+By+C, an enable register, bit-serial MEM ops
@@ -407,7 +420,7 @@
[[245.1,15.5,0.961,0.384,0.192,-4],[239.9,40.3,0.941,0.341,0.171,-4],[238.5,93,0.935,0.446,0.223,-4],[234.4,62,0.919,0.643,0.216,-4],[239.1,0,0.938,0.645,0.424,-4],[231.9,0,0.909,0.951,0.539,-4],[240.7,0,0.944,0.984,0.685,-4],[233.5,40.3,0.916,0.263,0.132,-2],[254.6,24.8,0.998,0.415,0.208,-2],[243.2,0,0.954,0.461,0.231,-2],[233.8,12.4,0.917,0.431,0.215,-2],[243.2,15.5,0.954,0.461,0.231,-2],[254.3,0,0.997,0.614,0.207,-2],[250.4,9.3,0.982,0.64,0.501,-2],[249.6,12.4,0.979,0.94,0.58,-2],[245.7,0,0.964,0.939,0.723,-2]],
[[126.1,62,0.494,0.914,0.206,0],[238.8,49.6,0.937,0.447,0.223,0],[239.1,62,0.938,0.448,0.224,0],[250.4,40.3,0.982,0.402,0.201,0],[234.1,31,0.918,0.432,0.216,0],[250.4,43.4,0.982,0.402,0.201,0],[231.3,46.5,0.907,0.639,0.37,0],[254.9,37.2,0.999,0.916,0.508,0],[252.7,34.1,0.991,0.926,0.663,0]]];
(function(){
- var sc=document.getElementById('shaded'), sctx=sc.getContext('2d');
+ var sc=document.getElementById('surf'), sctx=sc.getContext('2d');
var CW=sc.width, CH=sc.height, SS=2, W=CW*SS, H=CH*SS;
var inten=new Float32Array(W*H), zb=new Float32Array(W*H);
for(var i=0;imxx)mxx=p.X;if(p.Ymxy)mxy=p.Y;}
+ 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;
+ function SXp(p){return (p.X-mnx)*s+ox;}
+ function SYp(p){return (mxy-p.Y)*s+oy;}
+ var inten=new Float32Array(W*H), zb=new Float32Array(W*H);
+ for(var k=0;kzb[idx]){zb[idx]=z; inten[idx]=w0*a.I+w1*b.I+w2*c.I;}
+ }
+ }
+ for(var j=0;j0){acc+=v;cnt++;}}
+ var o=(y*CW+xx)*4;
+ if(cnt>0){var vv=acc/cnt,cov=cnt/(SS*SS);
+ dd[o]=Math.min(255,28+vv*168);dd[o+1]=Math.min(255,50+vv*205);
+ dd[o+2]=Math.min(255,54+vv*122);dd[o+3]=Math.round(cov*255);}
+ else dd[o+3]=0;
+ }
+ sx.putImageData(img,0,0);
+ })();
+
/* ---- region binning grid from the real descriptor scan ---- */
// 96x2 pass, 191/192 filled: the single empty cell recorded in the capture.
var bc = document.getElementById('bins'), bx = bc.getContext('2d');