Files
TeslaRel410/emulator/firmware-decomp/render-readout.html
T
CydandClaude Opus 4.8 09ea5f9802 Verify the edge decode: payload coeff matches geometry to 0.2%
edge_verify.py: the edge SEND payload float 0.12527 matches the object's own
screen-space edge normal (0.12555, computed from the captured vertices) to
0.2%, and 0.1262 to 0.5%. Edges are pure screen geometry (no z-scale ambiguity),
so this is a hard confirmation that the compiled micro-code carries the real
coefficients. Readout §02 states the 0.2% match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 16:09:07 -05:00

729 lines
43 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)}
.arr{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:18px;align-items:start}
@media(max-width:640px){.arr{grid-template-columns:1fr}}
.arr figure{margin:0;border:1px solid var(--line2);border-radius:10px;overflow:hidden;
background:linear-gradient(180deg,#05090d,#070d13)}
.arr canvas{display:block;width:100%;height:auto}
.arr figcaption{padding:10px 13px;font-family:var(--mono);font-size:11px;color:var(--mute);
line-height:1.5;border-top:1px solid var(--line)}
.arr figcaption b{color:var(--phos)}
.arrinfo{display:flex;flex-direction:column;gap:14px;padding-top:4px}
.pmlab{font-family:var(--mono);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
color:var(--faint)}
.pmbar{display:flex;gap:2px;height:34px;font-family:var(--mono);font-size:10px;font-weight:600}
.pmbar span{display:flex;align-items:center;justify-content:center;border-radius:3px;
color:#04070a;letter-spacing:.02em;white-space:nowrap;overflow:hidden}
.pmbar .z{flex:24;background:var(--cyan)}
.pmbar .r{flex:8;background:#ff6b6b}
.pmbar .g{flex:8;background:var(--phos)}
.pmbar .b{flex:8;background:#6ba8ff}
.pmbar .free{flex:160;background:#12202a;color:var(--faint)}
.mini{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px}
.mini li{position:relative;padding-left:18px;font-size:13.5px;color:var(--mute);line-height:1.45}
.mini li::before{content:"";position:absolute;left:2px;color:var(--cyan);font-weight:700}
.mini li b{color:var(--ink)}
.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.sim .v{color:var(--cyan)}
.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)}
.stage.sim .dot{background:var(--cyan);box-shadow:0 0 8px var(--cyan)}
/* 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 sim"><span class="dot"></span><div class="k">px</div><div class="v">Pixel-Planes</div><div class="d">array model simulated · §05</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>). Both panels below are captured verbatim from the
emulator. Left: a real per-region command list — every region references the same
tile-relative payloads and differs only in its <span class="mono">TILE</span> id and the
<span class="mono">GOTO</span> link. Right: inside a SEND — the payload isn't opaque, it
interleaves control words with the actual <b>float coefficients</b>, swept bit-plane by
bit-plane into pixel memory.</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 0x0801fa40 · DMA list</div>
<pre><span class="cm"># addr opcode</span>
<span class="ad">0x08015000</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">0x08015020</span> <span class="op">SENDE</span>(0x45) <span class="cm">; z / colour</span>
<span class="ad">0x00000000</span> <span class="am">FLUSH</span>
<span class="ad">0x08014100</span> <span class="op">TXDN</span>
<span class="ad">0x08015260</span> <span class="op">SEND</span>(0x21)
<span class="ad">0x08015380</span> <span class="op">SEND</span>(0x29)
<span class="ad">0x00000020</span> <span class="am">TILE</span> <span class="cm">; tile id</span>
<span class="ad">0x0801f008</span> <span class="am">GOTO</span> <span class="cm">; → next region</span></pre>
</div>
<div class="term">
<div class="bar"><span class="d g"></span><span class="d"></span><span class="d"></span>
&nbsp;SENDE target 0x08015020 · payload</div>
<pre><span class="cm"># bit-serial sweep (MEMpluseqMEM)</span>
+000 <span class="ad">00000100</span> <span class="cm">; header</span>
+004 <span class="op">3a804834</span> <span class="cm">; = 9.79e-4 float</span>
+008 <span class="ad">8401213a</span>
+00c <span class="ad">00000021</span> <span class="cm">; bit-plane 0x21</span>
+010 <span class="op">ba01253a</span> <span class="cm">; = 4.93e-4 += A</span>
+018 <span class="ad">8381213a</span>
+01c <span class="ad">00000022</span> <span class="cm">; bit-plane 0x22 …</span></pre>
</div>
</div>
<p class="sub" style="margin-top:14px">Pull the floats out of those payloads and they
line up into clean <b>×2 chains</b>
<span class="mono">0.0079 · 0.016 · 0.032 · 0.063 · 0.126 · 0.252 · 0.504 · 1.009</span>
a coefficient stored as its binary place values <span class="mono">C·2ᵏ</span>, one per
bit-plane, exactly how a bit-serial adder holds a number. And the recovered values are the
object's own geometry: a payload edge coefficient (<span class="mono">0.12527</span>) lands on
the edge normal computed from the captured vertices (<span class="mono">0.12555</span>) to
<b>0.2%</b>. The coefficients feeding the array simulator (§05) are
<b>the ones the hardware actually shipped</b>.</p>
</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> The Pixel-Planes array, simulated</h2>
<p class="sub">This is the final stage in silicon — and it now runs in software. The
<span class="mono">PXPL5 IGC</span> is a region rasteriser: the screen splits into
<b>64 × 128 tiles</b>; every pixel carries a 26-byte bit-memory and an enable bit; all
pixels evaluate one linear tree in lockstep —
<span class="mono">eval_ltree(x,y,A,B,C) = (int)(x·A + y·B + C)</span>. A triangle becomes
three <b>edge trees</b> into the enable register, then <b>Z and R/G/B planes</b>
interpolated per pixel and z-buffered in pixel memory (<span class="mono">IGCOPS.C</span>).
Fed the captured surface, the model lights the tiles below and renders pixels that match our
reference rasteriser to within edge anti-aliasing — the array reproduced faithfully.</p>
<div class="arr">
<figure>
<canvas id="depth" width="560" height="500" aria-label="the depth buffer the array computes"></canvas>
<figcaption><b>the depth buffer</b>, read straight out of pixel memory — the 24-bit
Z plane the <span class="mono">SENDE</span> z-sweep interpolates (near = bright)</figcaption>
</figure>
<figure>
<canvas id="tiles" width="500" height="360" aria-label="the object's footprint across the 64x128 tile array"></canvas>
<figcaption><b>the object's footprint</b> — 18 of 50 <b>64×128</b> tiles lit,
each a region of pixel processors</figcaption>
</figure>
</div>
<div class="arrinfo" style="margin-top:16px">
<div class="pmlab">pixel memory · 26 bytes = 208 bits</div>
<div class="pmbar">
<span class="z">Z · 24b</span><span class="r">R·8</span><span class="g">G·8</span>
<span class="b">B·8</span><span class="free">enable + working bits</span>
</div>
<ul class="mini">
<li>three edge trees → the <b>enable register</b> (the inside test)</li>
<li>Z / R / G / B are <b>planes</b>, evaluated per pixel by the linear tree</li>
<li>z-buffer via <span class="mono">MEM2geMEM2</span>; every write gated by enable</li>
<li>the depth image (left) is that Z plane, read back out of pixel memory</li>
<li><b>validated</b> pixel-faithful to the reference — ~1%, at edges only</li>
</ul>
</div>
<p class="sub" style="margin-top:16px">What this is <b>not</b>, yet: a full from-scratch run of
the compiled <b>micro-code</b> the DMA ships (§02) — the form the ground and sky take, since they
carry no stored vertices. But that stream is now largely decoded: the command lists read cleanly,
the <span class="mono">SENDE</span> z-sweep resolves into a regular 4-word instruction, and its
coefficients are the same ones driving this depth buffer — the array's inputs are
<b>cross-validated against the hardware's own compiled stream</b>. What remains is numeric
reconstruction across every region, not reversing an opaque binary.</p>
</section>
<section>
<h2><span class="idx">06</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>
<li><b>Pixel-Planes array simulated</b> (§05) — 64×128 tiles, per-pixel memory, edge trees + z-buffer, validated pixel-faithful</li>
</ul>
</div>
<div>
<h3>The last mile to pixels</h3>
<ul class="todo">
<li>The array's <b>computational model</b> runs and matches the reference — the piece left is executing the board's own <b>compiled micro-code</b></li>
<li>The per-region <b>DMA command lists decode cleanly</b> (§02) — SEND / SENDE / TXDN / TILE / GOTO, tile-relative</li>
<li>The SEND payloads carry <b>real float coefficients</b> in a regular bit-serial sweep — recoverable, not opaque</li>
<li>What's left: pin the control-word fields, run the sweep per tile → the <b>ground and sky</b> that carry no stored vertices</li>
</ul>
</div>
</div>
</div>
<footer>
<span>VWE&nbsp;TESLA REL410 · firmware-decomp · emu860</span>
<span>Tier 0 complete · Tier 1 array simulated · micro-code decode 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);
}
}
/* ---- depth buffer: the same surface, coloured by camera Z (the array's z-plane) ---- */
(function(){
var sc=document.getElementById('depth'); if(!sc) return;
var dx=sc.getContext('2d');
var CW=sc.width, CH=sc.height, SS=2, W=CW*SS, H=CH*SS;
var XS=GRID.xs, ZS=GRID.zs, R=GRID.rows, NX=XS.length, NZ=ZS.length;
var cx=8, cz=-4, cy=0;
(function(){var s=0,n=0;for(var j=0;j<NZ;j++)for(var i=0;i<NX;i++){s+=R[j][i][0];n++;}cy=s/n;})();
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];}
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);
P[j][i]={X:p[0],Y:p[1],Z:p[2]};
}}
var mnx=1e9,mxx=-1e9,mny=1e9,mxy=-1e9,mnz=1e9,mxz=-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;
if(p.Z<mnz)mnz=p.Z;if(p.Z>mxz)mxz=p.Z;}
var pad=0.1*W, s=Math.min((W-2*pad)/(mxx-mnx),(H-2*pad)/(mxy-mny));
var ox=(W-(mxx-mnx)*s)/2, oy=(H-(mxy-mny)*s)/2, zsp=(mxz-mnz)||1;
function SXp(p){return (p.X-mnx)*s+ox;} function SYp(p){return (mxy-p.Y)*s+oy;}
var dep=new Float32Array(W*H); dep.fill(-2); var zb=new Float32Array(W*H); for(var k=0;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; dep[idx]=(z-mnz)/zsp;}
}
}
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=dx.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=dep[(y*SS+yy)*W+(xx*SS+xs2)];if(v>=0){acc+=v;cnt++;}}
var o=(y*CW+xx)*4;
if(cnt>0){var vv=1-acc/cnt,cov=cnt/(SS*SS); // near = bright
dd[o]=30+vv*120; dd[o+1]=90+vv*150; dd[o+2]=120+vv*130; dd[o+3]=Math.round(cov*255);}
else dd[o+3]=0;
}
dx.putImageData(img,0,0);
})();
/* ---- IGC tile footprint: which 64x128 tiles the array lit for the object ---- */
var TILES={ntx:10,nty:5,touched:[[0,2],[1,1],[1,2],[2,1],[2,2],[2,3],[3,1],[3,2],[3,3],[4,1],[4,2],[4,3],[5,1],[5,2],[6,1],[6,2],[7,1],[8,1]]};
(function(){
var tc=document.getElementById('tiles'), tx=tc.getContext('2d');
var W=tc.width, H=tc.height, NX=TILES.ntx, NY=TILES.nty;
var lit={}; TILES.touched.forEach(function(t){lit[t[0]+'_'+t[1]]=1;});
var gap=3;
var cw=(H-2*gap)/(NY*2); // enforce 64:128 -> cellH = 2*cellW
var ch=cw*2;
var gw=NX*cw, gh=NY*ch;
var ox=(W-gw)/2, oy=(H-gh)/2;
tx.clearRect(0,0,W,H);
for(var ty2=0;ty2<NY;ty2++)for(var tx2=0;tx2<NX;tx2++){
var px=ox+tx2*cw, py=oy+ty2*ch;
var on=lit[tx2+'_'+ty2];
if(on){
tx.fillStyle='rgba(65,255,142,0.16)'; tx.fillRect(px,py,cw-gap,ch-gap);
tx.strokeStyle='rgba(65,255,142,0.85)'; tx.lineWidth=1.2;
tx.strokeRect(px+0.5,py+0.5,cw-gap-1,ch-gap-1);
tx.fillStyle='rgba(65,255,142,0.9)';
tx.fillRect(px+cw/2-1.5,py+ch/2-1.5,3,3);
} else {
tx.strokeStyle='rgba(84,105,122,0.28)'; tx.lineWidth=1;
tx.strokeRect(px+0.5,py+0.5,cw-gap-1,ch-gap-1);
}
}
// caption-ish label of tile dimensions along the top-left cell
tx.fillStyle='rgba(95,208,255,0.7)'; tx.font='9px ui-monospace,monospace';
tx.fillText('64×128',ox+2,oy-4>8?oy-4:10);
})();
})();
</script>