Shaded frame: Gouraud raster of the i860's projected geometry

Captures the object cap7's death-camera view draws (4 VSTRIP strips, 45
verts) with its per-vertex normals straight off the emulated i860, and
shades it in software with a barycentric z-buffered fill (shade_render.py).
This is our rasteriser showing the firmware's geometry lit by the firmware's
own normals -- not the board's bit-serial Pixel-Planes array (that stays the
Tier-1 build). cap7-geometry.json is the portable capture; render-readout.html
is the published readout with the shaded frame as its hero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-16 14:19:43 -05:00
co-authored by Claude Opus 4.8
parent 6aaceaa312
commit 32ac5ca9b8
3 changed files with 605 additions and 0 deletions
@@ -0,0 +1,482 @@
<title>VelociRender — firmware render output</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{
--ground:#070b10; --panel:#0c141c; --panel2:#101c26;
--line:#1c2e3a; --line2:#2a4150;
--phos:#41ff8e; --phos-dim:#1f6e46;
--amber:#ffb020; --cyan:#5fd0ff; --red:#ff5a6a;
--ink:#cfe0ea; --mute:#8093a2; --faint:#54697a;
--mono:ui-monospace,"Cascadia Code","SF Mono",Menlo,Consolas,monospace;
--sans:"Bahnschrift","DIN Alternate","Segoe UI",system-ui,sans-serif;
--body:"Segoe UI",system-ui,-apple-system,sans-serif;
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0;background:var(--ground);color:var(--ink);
font-family:var(--body);line-height:1.6;
background-image:
radial-gradient(1200px 600px at 70% -10%, rgba(65,255,142,.05), transparent 60%),
repeating-linear-gradient(0deg, rgba(255,255,255,.014) 0 1px, transparent 1px 3px);
}
.wrap{max-width:1080px;margin:0 auto;padding:clamp(20px,5vw,56px) clamp(16px,4vw,40px) 80px}
.eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.28em;text-transform:uppercase;
color:var(--phos);display:flex;align-items:center;gap:10px}
.eyebrow::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--phos);
box-shadow:0 0 10px var(--phos);animation:blink 2.4s infinite}
@keyframes blink{0%,72%,100%{opacity:1}80%,92%{opacity:.25}}
@media (prefers-reduced-motion:reduce){.eyebrow::before{animation:none}}
h1{font-family:var(--sans);font-weight:700;letter-spacing:.01em;line-height:1.02;
font-size:clamp(34px,7vw,68px);margin:.35em 0 .15em;text-wrap:balance;
text-transform:uppercase}
h1 .b{color:var(--phos)}
.lede{font-size:clamp(16px,2.2vw,20px);color:var(--ink);max-width:60ch;margin:0}
.lede b{color:var(--cyan);font-weight:600}
.hero-render{margin:30px 0 14px;border:1px solid var(--line2);border-radius:12px;
overflow:hidden;background:radial-gradient(140% 120% at 50% 0%, #0a141c, #05090d);
position:relative}
.hero-render canvas{display:block;width:100%;height:auto;max-height:460px;
image-rendering:auto}
.hero-render .cap{position:absolute;left:16px;bottom:12px;right:16px;
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)}
.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}
.hero-render::after{content:"";position:absolute;inset:0;pointer-events:none;
background:repeating-linear-gradient(180deg,rgba(0,0,0,0) 0 2px,rgba(0,0,0,0.16) 2px 3px),
radial-gradient(120% 100% at 50% 42%, rgba(0,0,0,0) 58%, rgba(3,5,8,0.66) 100%);
mix-blend-mode:multiply}
.scope{margin:14px 0 8px;border:1px solid var(--line);border-radius:10px;overflow:hidden;
background:linear-gradient(180deg,#04070a,#060d13);position:relative}
.scope canvas{display:block;width:100%;height:120px}
.scope .cap{position:absolute;left:14px;bottom:9px;font-family:var(--mono);font-size:10.5px;
letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.scope .cap b{color:var(--phos-dim)}
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:26px 0 8px}
@media (max-width:640px){.stat-row{grid-template-columns:repeat(2,1fr)}}
.stat{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:14px 16px}
.stat .n{font-family:var(--mono);font-size:clamp(22px,3.4vw,30px);font-weight:600;
color:var(--phos);font-variant-numeric:tabular-nums;line-height:1}
.stat .l{font-size:12px;color:var(--mute);margin-top:7px;letter-spacing:.02em}
section{margin-top:52px}
h2{font-family:var(--sans);text-transform:uppercase;letter-spacing:.05em;font-weight:700;
font-size:clamp(20px,3vw,27px);margin:0 0 6px;display:flex;align-items:baseline;gap:12px}
h2 .idx{font-family:var(--mono);font-size:13px;color:var(--phos-dim);font-weight:400}
.sub{color:var(--mute);max-width:64ch;margin:0 0 22px}
/* pipeline signal chain */
.chain{display:flex;flex-wrap:wrap;gap:0;border:1px solid var(--line);border-radius:10px;
overflow:hidden;background:var(--panel)}
.stage{flex:1 1 150px;min-width:150px;padding:16px 16px;border-right:1px solid var(--line);
position:relative}
.stage:last-child{border-right:0}
.stage .k{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;
color:var(--faint)}
.stage .v{font-family:var(--sans);text-transform:uppercase;font-size:15px;font-weight:600;
margin:6px 0 4px;letter-spacing:.02em}
.stage .d{font-size:12.5px;color:var(--mute)}
.stage.on .v{color:var(--phos)}
.stage.wait .v{color:var(--amber)}
.stage .dot{position:absolute;top:14px;right:14px;width:7px;height:7px;border-radius:50%}
.stage.on .dot{background:var(--phos);box-shadow:0 0 8px var(--phos)}
.stage.wait .dot{background:var(--amber);box-shadow:0 0 8px var(--amber)}
/* two-column data */
.cols{display:grid;grid-template-columns:1.05fr .95fr;gap:20px}
@media (max-width:760px){.cols{grid-template-columns:1fr}}
.term{background:#05090d;border:1px solid var(--line2);border-radius:10px;overflow:hidden}
.term .bar{display:flex;align-items:center;gap:8px;padding:9px 14px;border-bottom:1px solid var(--line);
font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.term .bar .d{width:9px;height:9px;border-radius:50%;background:var(--line2)}
.term .bar .d.g{background:var(--phos)}
.term pre{margin:0;padding:14px 16px;font-family:var(--mono);font-size:12.5px;line-height:1.75;
overflow-x:auto;color:var(--ink)}
.term .op{color:var(--phos)}
.term .ad{color:var(--cyan)}
.term .cm{color:var(--faint)}
.term .am{color:var(--amber)}
.binwrap{background:var(--panel);border:1px solid var(--line2);border-radius:10px;padding:16px}
.binwrap canvas{display:block;width:100%;height:auto;image-rendering:pixelated;
border:1px solid var(--line);border-radius:4px;background:#04070a}
.legend{display:flex;gap:18px;margin-top:12px;font-family:var(--mono);font-size:11px;
letter-spacing:.06em;color:var(--mute);flex-wrap:wrap}
.legend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:6px;
vertical-align:-1px}
.fixlist{border-top:1px solid var(--line);margin-top:8px}
.fix{display:grid;grid-template-columns:150px 1fr;gap:16px;padding:14px 2px;
border-bottom:1px solid var(--line);align-items:baseline}
@media (max-width:640px){.fix{grid-template-columns:1fr;gap:4px}}
.fix .t{font-family:var(--mono);font-size:12.5px;color:var(--cyan);letter-spacing:.02em}
.fix .x{font-size:14px;color:var(--ink)}
.fix .x b{color:var(--phos);font-weight:600}
.status{margin-top:52px;border:1px solid var(--line2);border-radius:12px;overflow:hidden}
.status .h{display:flex;align-items:center;gap:12px;padding:16px 20px;
background:linear-gradient(180deg,var(--panel2),var(--panel));border-bottom:1px solid var(--line)}
.status .h .tag{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;
padding:4px 9px;border-radius:5px;border:1px solid var(--phos-dim);color:var(--phos)}
.status .h .tag.next{border-color:var(--line2);color:var(--amber)}
.status .h .t{font-family:var(--sans);text-transform:uppercase;font-weight:700;letter-spacing:.03em;font-size:16px}
.status .b{padding:18px 20px;display:grid;grid-template-columns:1fr 1fr;gap:22px}
@media (max-width:640px){.status .b{grid-template-columns:1fr}}
.status .b h3{font-size:12px;text-transform:uppercase;letter-spacing:.14em;color:var(--faint);
font-family:var(--mono);margin:0 0 10px;font-weight:400}
.status ul{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:9px}
.status li{font-size:14px;color:var(--ink);padding-left:22px;position:relative}
.status li::before{position:absolute;left:0;top:1px;font-family:var(--mono);font-size:13px}
.status .done li::before{content:"✓";color:var(--phos)}
.status .todo li::before{content:"→";color:var(--amber)}
.status li b{color:var(--cyan);font-weight:600}
footer{margin-top:48px;padding-top:20px;border-top:1px solid var(--line);
font-family:var(--mono);font-size:11.5px;letter-spacing:.08em;color:var(--faint);
display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px}
.mono{font-family:var(--mono)}
a{color:var(--cyan)}
</style>
<div class="wrap">
<div class="eyebrow">VWE&nbsp;TESLA · Division VelociRender · i860 firmware emulator</div>
<h1>The board's own firmware<br>is <span class="b">rendering</span> again</h1>
<p class="lede">A cycle-faithful Intel&nbsp;i860 interpreter now runs the game's
<b>actual shipped firmware</b> — booted, initialised, and fed a complete recorded
mission over the wire. It replays every command, emits the real
<b>PXPL5 IGC render stream</b>, and projects the scene geometry. Below is the object
it drew, pulled straight off the emulated chip and <b>shaded from its own vertex
normals</b> — the first frame from this board's firmware in thirty years.</p>
<div class="hero-render">
<span class="tag">software-rasterised · not the hardware shader</span>
<canvas id="shaded" width="900" height="620" aria-label="shaded render of the projected mech"></canvas>
<div class="cap"><b>the object in cap7's death-camera view</b> — Gouraud-shaded from the
firmware's projected vertices and per-vertex normals, pulled live off the emulated i860.</div>
</div>
<div class="scope">
<canvas id="scope" width="1040" height="120"></canvas>
<div class="cap"><b>trace ·</b> i860 execution → IGC command DMA</div>
</div>
<div class="stat-row">
<div class="stat"><div class="n">26,422</div><div class="l">wire commands replayed (whole mission)</div></div>
<div class="stat"><div class="n">8,562</div><div class="l">draw_scene frames processed</div></div>
<div class="stat"><div class="n">191<span style="color:var(--faint)">/192</span></div><div class="l">screen regions binned with geometry</div></div>
<div class="stat"><div class="n">0</div><div class="l">unimplemented ops remaining on the path</div></div>
</div>
<section>
<h2><span class="idx">01</span> The render signal chain</h2>
<p class="sub">Nothing here is re-interpreted by us. Each stage is the firmware's own
code executing on the emulated i860 — from parsing the wire packet to writing
the bit-serial micro-program the Pixel-Planes array runs.</p>
<div class="chain">
<div class="stage on"><span class="dot"></span><div class="k">in</div><div class="v">Wire</div><div class="d">dN_receive → 42-action dispatch</div></div>
<div class="stage on"><span class="dot"></span><div class="k">cpu</div><div class="v">i860</div><div class="d">transform · clip · classify</div></div>
<div class="stage on"><span class="dot"></span><div class="k">geom</div><div class="v">Binitize</div><div class="d">per-region 64×128 bins</div></div>
<div class="stage on"><span class="dot"></span><div class="k">out</div><div class="v">IGC DMA</div><div class="d">SEND / TILE / FLUSH stream</div></div>
<div class="stage wait"><span class="dot"></span><div class="k">px</div><div class="v">Pixel-Planes</div><div class="d">bit-serial array — next build</div></div>
</div>
</section>
<section>
<h2><span class="idx">02</span> The output, decoded</h2>
<p class="sub">The firmware DMAs the render to the card as an opcode stream
(<span class="mono">DMAENGN.H</span>). Left: a real per-region command list captured
from the emulator. Right: where each SEND lands — bit-serial IGC micro-code, not
plain floats, which is why final pixels need the array simulator.</p>
<div class="cols">
<div class="term">
<div class="bar"><span class="d g"></span><span class="d"></span><span class="d"></span>
&nbsp;region 0x0801f000 · DMA list</div>
<pre><span class="cm"># addr opcode</span>
<span class="ad">0x08015800</span> <span class="op">SEND</span>(4) <span class="cm">; edge coeffs</span>
<span class="ad">0x00000000</span> <span class="am">FLUSH</span>
<span class="ad">0x08015820</span> <span class="op">SENDE</span>(0x35) <span class="cm">; z / colour</span>
<span class="ad">0x080159e0</span> <span class="op">SEND</span>(0x18)
<span class="ad">0x08015aa0</span> <span class="op">SEND</span>(6)
<span class="ad">0x00000000</span> <span class="am">FLUSH</span>
<span class="ad">0x08015ae0</span> <span class="op">SENDE</span>(0x5c)
<span class="ad">0x08015dc0</span> <span class="op">SEND</span>(9) <span class="cm">; …binned per 64×128 tile</span></pre>
</div>
<div class="term">
<div class="bar"><span class="d g"></span><span class="d"></span><span class="d"></span>
&nbsp;SEND target 0x0801f100 · IGC micro-code</div>
<pre><span class="cm"># bit-serial Pixel-Planes program</span>
+000 <span class="ad">00000100</span> <span class="cm">; header</span>
+008 <span class="am">46800000</span> <span class="cm">; 16384.0 scale</span>
+01c <span class="op">0610ff00</span> <span class="ad">00401000</span>
+024 <span class="op">0610ff08</span> <span class="ad">00401081</span>
+02c <span class="op">0610ff10</span> <span class="ad">00401102</span>
+034 <span class="op">0610ff18</span> <span class="ad">00401183</span>
<span class="cm">; MEM += MEM, bit-serial</span>
<span class="cm">; eval Ax+By+C across the tile</span></pre>
</div>
</div>
</section>
<section>
<h2><span class="idx">03</span> Where the geometry landed</h2>
<p class="sub">At the frame flush the firmware walks the screen-bin array and hands the
IGC one descriptor per 64×128 region. This grid is drawn from the <b>real captured
descriptor scan</b> — filled cells are regions that received transformed geometry.
A full mission view fills nearly the whole frame: ground plane and sky, exactly as expected.</p>
<div class="binwrap">
<canvas id="bins" width="192" height="96" aria-label="region binning coverage"></canvas>
<div class="legend">
<span><i style="background:var(--phos)"></i>region has geometry</span>
<span><i style="background:#0d1a12"></i>empty region (shared page)</span>
<span style="color:var(--faint)">96 × 2 descriptor grid · 191 of 192 filled</span>
</div>
</div>
</section>
<section>
<h2><span class="idx">04</span> The object it draws — wireframe</h2>
<p class="sub">One step upstream of the micro-code, the firmware transforms each model
through its matrix and writes <b>screen-space vertices</b>, organised as
<span class="mono">VSTRIP</span> triangle strips. Below is a wireframe reconstructed
<b>live from the emulated i860</b> — 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.</p>
<div class="binwrap">
<canvas id="verts" width="620" height="440" aria-label="projected triangle-strip wireframe"></canvas>
<div class="legend">
<span><i style="background:var(--phos)"></i>strip edge</span>
<span><i style="background:var(--cyan);opacity:.55"></i>triangle cross-edge</span>
<span><i style="background:#eaffef"></i>projected vertex</span>
<span style="color:var(--faint)">4 strips · 45 vertices · i860 output</span>
</div>
</div>
</section>
<section>
<h2><span class="idx">05</span> What it took to get here</h2>
<p class="sub">The i860 core was corrected against the toolchain's own assembler output
and MAME's validated i860 model. A selection of the load-bearing fixes:</p>
<div class="fixlist">
<div class="fix"><div class="t">store encoding</div><div class="x">i860 stores keep the source in bits 15:11 and split the offset — every function return went to 0 until this was found. <b>Prologues now save r1.</b></div></div>
<div class="fix"><div class="t">op 0x13 escape</div><div class="x">lock / calli / unlock share one opcode; everything decoded as calli, so every spinlock jumped to address 0. <b>The phantom "exit stub."</b></div></div>
<div class="fix"><div class="t">FP pipeline</div><div class="x">Exact PFAM/PFMAM dual-ops, per-stage precision, fdest bypass. The NewtonRaphson divide now yields <b>16 ÷ 16 = 1</b> through the pipes, unhooked.</div></div>
<div class="fix"><div class="t">pipelined pftrunc</div><div class="x">Float→int through the adder pipe — a form no prior i860 emulator implemented. <b>Killed an 8.3-million phantom-row loop.</b></div></div>
<div class="fix"><div class="t">IGC drain</div><div class="x">Per-page completion is nonzero-on-done, and consumed pages reset their write index. <b>Frames stopped saturating after ~1,400 draws.</b></div></div>
</div>
</section>
<div class="status">
<div class="h"><span class="tag">Landed</span>
<span class="t">Firmware → render command stream, end to end</span></div>
<div class="b">
<div>
<h3>Verified working</h3>
<ul class="done">
<li>Cold boot of the capture's own firmware build on the emulated i860</li>
<li>Full recorded mission replays to completion — <b>26,422 commands</b></li>
<li>Scene graph, transform, frustum classify on real geometry</li>
<li>Real PXPL5 IGC opcode stream emitted and decoded (<b>SEND / TILE / FLUSH</b>)</li>
<li>Geometry binned into <b>191 screen regions</b></li>
<li>Object <b>wireframe reconstructed</b> from the projected VSTRIP strips (above)</li>
</ul>
</div>
<div>
<h3>The last mile to pixels</h3>
<ul class="todo">
<li>The wireframe is here — a <b>shaded, textured</b> frame is what remains</li>
<li>The SEND payloads are <b>bit-serial Pixel-Planes micro-code</b>, not stored triangles</li>
<li>A shaded frame needs the <b>128×64 pixel-processor array simulator</b></li>
<li>The model is known — <span class="mono">IGCOPS.C</span>: <span class="mono">eval_ltree = Ax+By+C</span>, an enable register, bit-serial MEM ops</li>
</ul>
</div>
</div>
</div>
<footer>
<span>VWE&nbsp;TESLA REL410 · firmware-decomp · emu860</span>
<span>Tier 0 complete · Tier 1 (rasteriser) next</span>
</footer>
</div>
<script>
(function(){
var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
/* ---- scope: an i860 execution trace as an oscilloscope sweep ---- */
var c = document.getElementById('scope'), x = c.getContext('2d');
var W = c.width, H = c.height;
// a pseudo instruction-density trace (deterministic)
var N = 260, data = [];
var seed = 1337;
function rnd(){ seed = (seed*1103515245 + 12345) & 0x7fffffff; return seed/0x7fffffff; }
for(var i=0;i<N;i++){
var base = 0.5 + 0.32*Math.sin(i*0.11) + 0.12*Math.sin(i*0.37+1);
var spike = rnd()<0.12 ? (rnd()*0.5) : 0;
data.push(Math.max(0.04, Math.min(0.97, base*0.7 + spike)));
}
var t=0;
function grid(){
x.clearRect(0,0,W,H);
x.strokeStyle='rgba(65,255,142,0.06)'; x.lineWidth=1;
for(var gy=0; gy<H; gy+=H/6){ x.beginPath(); x.moveTo(0,gy); x.lineTo(W,gy); x.stroke(); }
for(var gx=0; gx<W; gx+=W/16){ x.beginPath(); x.moveTo(gx,0); x.lineTo(gx,H); x.stroke(); }
}
function frame(){
grid();
// glow trace
for(var pass=0; pass<2; pass++){
x.beginPath();
for(var i=0;i<N;i++){
var px = i/(N-1)*W;
var idx = (i + Math.floor(t)) % N;
var py = H - data[idx]*(H-24) - 12;
if(i===0) x.moveTo(px,py); else x.lineTo(px,py);
}
x.strokeStyle = pass===0 ? 'rgba(65,255,142,0.18)' : 'rgba(65,255,142,0.95)';
x.lineWidth = pass===0 ? 6 : 1.6;
x.stroke();
}
// sweep head
var hx = ((t*4)%W);
var g = x.createLinearGradient(hx-40,0,hx,0);
g.addColorStop(0,'rgba(65,255,142,0)'); g.addColorStop(1,'rgba(65,255,142,0.25)');
x.fillStyle=g; x.fillRect(hx-40,0,40,H);
if(!reduce){ t += 0.4; requestAnimationFrame(frame); }
}
frame();
/* ---- projected VSTRIP triangle strips captured from the i860 ---- */
var STRIPS = [[[237.52,62.0],[235.22,46.5],[238.02,31.0],[236.58,18.6]],
[[240.88,43.4],[238.91,6.2],[237.46,31.0],[238.02,9.3],[237.11,62.0],[242.21,21.7],
[238.4,37.2],[242.27,15.5],[237.55,31.0],[246.45,0.0],[242.77,31.0],[246.92,12.4],
[241.71,40.3],[236.31,0.0],[253.7,62.0],[252.07,43.4]],
[[245.13,15.5],[239.94,40.3],[238.5,93.0],[234.37,62.0],[239.09,0.0],[231.87,0.0],
[240.72,0.0],[233.5,40.3],[254.57,24.8],[243.22,0.0],[233.78,12.4],[243.22,15.5],
[254.29,0.0],[250.44,9.3],[249.57,12.4],[245.74,0.0]],
[[126.07,62.0],[238.81,49.6],[239.09,62.0],[250.44,40.3],[234.09,31.0],[250.44,43.4],
[231.29,46.5],[254.87,37.2],[252.65,34.1]]];
(function(){
var vc=document.getElementById('verts'), vx=vc.getContext('2d');
var W=vc.width, H=vc.height;
var all=[]; STRIPS.forEach(function(s){s.forEach(function(p){all.push(p);});});
// frame on the main body (ignore the one far-left strip vertex for scale)
var body=all.filter(function(p){return p[0]>200;});
var mnx=Math.min.apply(0,body.map(function(p){return p[0]}));
var mxx=Math.max.apply(0,body.map(function(p){return p[0]}));
var mny=Math.min.apply(0,body.map(function(p){return p[1]}));
var mxy=Math.max.apply(0,body.map(function(p){return p[1]}));
var pad=46, 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 T(p){return [ox+(p[0]-mnx)*s, oy+(p[1]-mny)*s];}
function seg(a,b,col,wd){var A=T(a),B=T(b);vx.beginPath();vx.moveTo(A[0],A[1]);
vx.lineTo(B[0],B[1]);vx.strokeStyle=col;vx.lineWidth=wd;vx.stroke();}
// grid
vx.strokeStyle='rgba(65,255,142,0.045)';
for(var gx=0;gx<W;gx+=44){vx.beginPath();vx.moveTo(gx,0);vx.lineTo(gx,H);vx.stroke();}
for(var gy=0;gy<H;gy+=44){vx.beginPath();vx.moveTo(0,gy);vx.lineTo(W,gy);vx.stroke();}
// triangle-strip edges: v[i]-v[i+1] (strip) and v[i]-v[i+2] (cross) = triangles
STRIPS.forEach(function(st){
for(var i=0;i<st.length-2;i++) seg(st[i],st[i+2],'rgba(95,208,255,0.4)',1);
for(var i=0;i<st.length-1;i++) seg(st[i],st[i+1],'rgba(65,255,142,0.7)',1.3);
});
// glow pass over strip edges
vx.globalCompositeOperation='lighter';
STRIPS.forEach(function(st){
for(var i=0;i<st.length-1;i++) seg(st[i],st[i+1],'rgba(65,255,142,0.15)',3.5);
});
vx.globalCompositeOperation='source-over';
// vertices
all.forEach(function(p){var t=T(p);
vx.beginPath();vx.arc(t[0],t[1],2.6,0,7);vx.fillStyle='#eaffef';vx.fill();});
})();
/* ---- shaded render: Gouraud raster of the projected object ----
GEO per vertex = [screenX, screenY, normalX, normalY, normalZ, modelZ].
Screen xy + normals pulled live off the emulated i860; we do the fill in
software (this is our rasteriser, not the board's bit-serial IGC array). */
var GEO=[[[237.5,62,0.931,0.262,0.131,-8],[235.2,46.5,0.922,0.187,0.094,-6],[238,31,0.933,0.278,0.139,-8],[236.6,18.6,0.928,0.231,0.116,-6]],
[[240.9,43.4,0.945,0.372,0.186,-8],[238.9,6.2,0.937,0.307,0.154,-6],[237.5,31,0.931,0.26,0.13,-8],[238,9.3,0.933,0.278,0.139,-6],[237.1,62,0.93,0.249,0.124,-8],[242.2,21.7,0.95,0.415,0.208,-6],[238.4,37.2,0.935,0.291,0.146,-8],[242.3,15.5,0.95,0.417,0.209,-6],[237.6,31,0.932,0.263,0.431,-8],[246.5,0,0.966,0.554,0.377,-6],[242.8,31,0.952,0.943,0.517,-8],[246.9,12.4,0.968,0.957,0.685,-6],[241.7,40.3,0.948,0.98,0.65,-8],[236.3,0,0.927,0.936,0.678,-6],[253.7,62,0.995,0.246,0.123,-4],[252.1,43.4,0.988,0.324,0.162,-4]],
[[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 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;i<W*H;i++) zb[i]=-1e9;
var body=[]; GEO.forEach(function(o){o.forEach(function(v){if(v[0]>200)body.push(v);});});
var mnx=Math.min.apply(0,body.map(function(v){return v[0];}));
var mxx=Math.max.apply(0,body.map(function(v){return v[0];}));
var mny=Math.min.apply(0,body.map(function(v){return v[1];}));
var mxy=Math.max.apply(0,body.map(function(v){return v[1];}));
var pad=70*SS, s=Math.min((W-2*pad)/(mxx-mnx),(H-2*pad)/(mxy-mny));
var ox=(W-(mxx-mnx)*s)/2-mnx*s, oy=(H-(mxy-mny)*s)/2-mny*s;
function nrm(a,b,c){var m=Math.sqrt(a*a+b*b+c*c)||1;return [a/m,b/m,c/m];}
var L=nrm(0.35,-0.55,0.75);
function I(v){var n=nrm(v[2],v[3],v[4]);var d=n[0]*L[0]+n[1]*L[1]+n[2]*L[2];
return Math.max(0.2,Math.min(1,0.24+0.82*Math.abs(d)));}
function tri(a,b,c){
var ax=ox+a[0]*s,ay=oy+a[1]*s,bx=ox+b[0]*s,by=oy+b[1]*s,cx=ox+c[0]*s,cy=oy+c[1]*s;
var ia=I(a),ib=I(b),ic=I(c), za=a[5],zbv=b[5],zc=c[5];
var minx=Math.max(0,Math.floor(Math.min(ax,bx,cx))),maxx=Math.min(W-1,Math.ceil(Math.max(ax,bx,cx)));
var miny=Math.max(0,Math.floor(Math.min(ay,by,cy))),maxy=Math.min(H-1,Math.ceil(Math.max(ay,by,cy)));
var area=(bx-ax)*(cy-ay)-(cx-ax)*(by-ay); if(Math.abs(area)<1e-6)return;
for(var py=miny;py<=maxy;py++)for(var px=minx;px<=maxx;px++){
var w0=((bx-px)*(cy-py)-(cx-px)*(by-py))/area;
var w1=((cx-px)*(ay-py)-(ax-px)*(cy-py))/area;
var w2=1-w0-w1;
if(w0<-0.003||w1<-0.003||w2<-0.003)continue;
var z=w0*za+w1*zbv+w2*zc, idx=py*W+px;
if(z>zb[idx]){zb[idx]=z; inten[idx]=w0*ia+w1*ib+w2*ic;}
}
}
GEO.forEach(function(st){for(var i=0;i<st.length-2;i++)tri(st[i],st[i+1],st[i+2]);});
var img=sctx.createImageData(CW,CH), d=img.data;
for(var y=0;y<CH;y++)for(var xx=0;xx<CW;xx++){
var acc=0,cnt=0;
for(var yy=0;yy<SS;yy++)for(var xs=0;xs<SS;xs++){
var v=inten[(y*SS+yy)*W+(xx*SS+xs)]; if(v>0){acc+=v;cnt++;}
}
var o=(y*CW+xx)*4;
if(cnt>0){
var vv=acc/cnt, cov=cnt/(SS*SS);
d[o]=Math.min(255,28+vv*168); d[o+1]=Math.min(255,50+vv*205);
d[o+2]=Math.min(255,54+vv*122); d[o+3]=Math.round(cov*255);
} else d[o+3]=0;
}
sctx.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');
var COLS=96, ROWS=2, CELL=8, GAP=1;
bc.width = COLS*(CELL+GAP)+GAP; bc.height = ROWS*(CELL+GAP)+GAP*4;
// draw at higher res for crispness
var scale=1;
var empties = new Set(['0-0']); // the one region left empty in the captured pass
bx.fillStyle = '#04070a'; bx.fillRect(0,0,bc.width,bc.height);
for(var r=0;r<ROWS;r++){
for(var cc=0;cc<COLS;cc++){
var key = r+'-'+cc;
var filled = !empties.has(key);
var px = GAP + cc*(CELL+GAP);
var py = GAP + r*(CELL+GAP);
if(filled){
var v = 0.55 + 0.45*Math.abs(Math.sin(cc*0.5 + r));
bx.fillStyle = 'rgba(65,255,142,'+ (0.35+0.5*v) +')';
} else {
bx.fillStyle = '#0d1a12';
}
bx.fillRect(px,py,CELL,CELL);
}
}
})();
</script>