Files
TeslaRel410/emulator/firmware-decomp/render-readout.html
T
CydandClaude Opus 4.8 32ac5ca9b8 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>
2026-07-16 14:19:43 -05:00

483 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>