Files
TeslaRel410/emulator/firmware-decomp/render-readout.html
T
CydandClaude Opus 4.8 e43cf24993 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 <noreply@anthropic.com>
2026-07-16 14:44:49 -05:00

561 lines
33 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)}
.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}
.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 an object
it drew, its vertices and normals pulled straight off the emulated chip and
<b>reconstructed in 3D</b> — geometry this board's firmware has not computed in thirty years.</p>
<div class="hero-render">
<span class="tag">reconstructed · firmware geometry + normals</span>
<canvas id="shaded" width="880" height="560" aria-label="the object rebuilt as a 3D height-field surface"></canvas>
<div class="cap"><b>the object cap7 was drawing</b> — 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.</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> How the death-camera saw it</h2>
<p class="sub">The surface above is the same 45 vertices, sorted back into model space —
they form an exact <b>9 × 5 height-field grid</b> (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 <b>edge-on</b>, 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 <span class="mono">VSTRIP</span> wireframe.</p>
<div class="duo">
<figure>
<canvas id="surf" width="500" height="560" aria-label="the death-camera's edge-on shaded projection"></canvas>
<figcaption><b>the board's screen-space frame</b> — Gouraud-shaded,
grazing near-edge-on angle</figcaption>
</figure>
<figure>
<canvas id="verts" width="420" height="560" aria-label="raw VSTRIP wireframe"></canvas>
<figcaption><b>raw VSTRIP wireframe</b> — 4 strips, 45 vertices,
exactly as the i860 emitted them</figcaption>
</figure>
</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>recovered and shaded</b> — a 9×5 height-field surface, lit by the firmware's own normals (above)</li>
</ul>
</div>
<div>
<h3>The last mile to pixels</h3>
<ul class="todo">
<li>Our software-shaded frame is here — the <b>board's own hardware-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('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;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);
})();
/* ---- true-3D surface: the object rebuilt as its 9x5 height-field grid ----
xs/zs = model grid axes; rows[z][x] = [heightY, nx,ny,nz]. All 45 cells real,
captured off the i860. We reconstruct the grid connectivity (2 tris/quad) and
light it with the firmware's own normals from a clean 3/4 camera. */
var GRID={"xs":[0,2,4,6,8,10,12,14,16],"zs":[-8,-6,-4,-2,0],"rows":[[[-2.167,0.948,0.98,0.65],[-1.295,0.952,0.943,0.517],[-1.439,0.932,0.263,0.431],[-1.591,0.935,0.291,0.146],[-1.87,0.93,0.249,0.124],[-2.311,0.931,0.26,0.13],[-2.014,0.945,0.372,0.186],[-2.446,0.933,0.278,0.139],[-3.021,0.931,0.262,0.131]],[[-2.158,0.927,0.936,0.678],[-1.016,0.968,0.957,0.685],[-0.584,0.966,0.554,0.377],[-1.007,0.95,0.417,0.209],[-1.016,0.95,0.415,0.208],[-1.726,0.933,0.278,0.139],[-1.735,0.937,0.307,0.154],[-2.023,0.928,0.231,0.116],[-2.589,0.922,0.187,0.094]],[[-2.158,0.944,0.984,0.685],[-1.735,0.909,0.951,0.539],[-1.87,0.938,0.645,0.424],[-1.87,0.919,0.643,0.216],[-1.448,0.935,0.446,0.223],[-1.582,0.941,0.341,0.171],[-1.16,0.961,0.384,0.192],[-1.016,0.988,0.324,0.162],[-1.448,0.995,0.246,0.123]],[[-2.446,0.964,0.939,0.723],[-2.598,0.979,0.94,0.58],[-2.733,0.982,0.64,0.501],[-2.455,0.997,0.614,0.207],[-1.735,0.954,0.461,0.231],[-1.448,0.917,0.431,0.215],[-0.872,0.954,0.461,0.231],[-0.441,0.998,0.415,0.208],[-0.719,0.916,0.263,0.132]],[[-3.021,0.991,0.926,0.663],[-3.021,0.999,0.916,0.508],[-3.165,0.907,0.639,0.37],[-2.589,0.982,0.402,0.201],[-1.87,0.918,0.432,0.216],[-1.726,0.982,0.402,0.201],[-1.151,0.938,0.448,0.224],[-0.432,0.937,0.447,0.223],[0,0.494,0.914,0.206]]]};
(function(){
var sc=document.getElementById('shaded'), sx=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; // grid centre
(function(){var s=0,n=0;for(var j=0;j<NZ;j++)for(var i=0;i<NX;i++){s+=R[j][i][0];n++;}cy=s/n;})();
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.72);
var YAW=40*Math.PI/180, PIT=27*Math.PI/180;
var cyw=Math.cos(YAW),syw=Math.sin(YAW),cp=Math.cos(PIT),sp=Math.sin(PIT);
function rot(x,y,z){var x2=x*cyw+z*syw, z2=-x*syw+z*cyw; var y2=y*cp-z2*sp, z3=y*sp+z2*cp; return [x2,y2,z3];}
// project every grid node
var P=[];
for(var j=0;j<NZ;j++){P[j]=[];for(var i=0;i<NX;i++){
var c=R[j][i];
var p=rot(XS[i]-cx,(c[0]-cy)*1.8,ZS[j]-cz);
var nn=rot(c[1],c[2],c[3]); nn=nrm(nn[0],nn[1],nn[2]);
var d=nn[0]*L[0]+nn[1]*L[1]+nn[2]*L[2];
var inten=Math.max(0.16,Math.min(1,0.22+0.85*Math.abs(d)));
P[j][i]={X:p[0],Y:p[1],Z:p[2],I:inten};
}}
var mnx=1e9,mxx=-1e9,mny=1e9,mxy=-1e9;
for(var j=0;j<NZ;j++)for(var i=0;i<NX;i++){var p=P[j][i];
if(p.X<mnx)mnx=p.X;if(p.X>mxx)mxx=p.X;if(p.Y<mny)mny=p.Y;if(p.Y>mxy)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;k<W*H;k++)zb[k]=-1e9;
function tri(a,b,c){
var ax=SXp(a),ay=SYp(a),bx=SXp(b),by=SYp(b),cx2=SXp(c),cy2=SYp(c);
var area=(bx-ax)*(cy2-ay)-(cx2-ax)*(by-ay); if(Math.abs(area)<1e-6)return;
var x0=Math.max(0,Math.floor(Math.min(ax,bx,cx2))),x1=Math.min(W-1,Math.ceil(Math.max(ax,bx,cx2)));
var y0=Math.max(0,Math.floor(Math.min(ay,by,cy2))),y1=Math.min(H-1,Math.ceil(Math.max(ay,by,cy2)));
for(var py=y0;py<=y1;py++)for(var px=x0;px<=x1;px++){
var w0=((bx-px)*(cy2-py)-(cx2-px)*(by-py))/area;
var w1=((cx2-px)*(ay-py)-(ax-px)*(cy2-py))/area;
var w2=1-w0-w1; if(w0<-0.004||w1<-0.004||w2<-0.004)continue;
var z=w0*a.Z+w1*b.Z+w2*c.Z, idx=py*W+px;
if(z>zb[idx]){zb[idx]=z; inten[idx]=w0*a.I+w1*b.I+w2*c.I;}
}
}
for(var j=0;j<NZ-1;j++)for(var i=0;i<NX-1;i++){
var a=P[j][i],b=P[j][i+1],c=P[j+1][i],d=P[j+1][i+1];
tri(a,b,c); tri(b,d,c);
}
var img=sx.createImageData(CW,CH), dd=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 xs2=0;xs2<SS;xs2++){var v=inten[(y*SS+yy)*W+(xx*SS+xs2)];if(v>0){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');
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>