payload_scan_mid.py scans a mid-mission frame (snapfull1, cmd 19,889): identical 9 coordinates as cmd 735 (65.5/72/181/236, all x[66,236]). So cap7 redraws the same static surface patch every frame across its whole length -- no terrain, no battle, ever. Corrects the earlier "mid-mission frames have battle scenes" guess: a full battle scene lives in a different capture entirely. Readout last-mile updated. The decode + array render fully account for cap7's content. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
734 lines
44 KiB
HTML
734 lines
44 KiB
HTML
<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 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 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>
|
||
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>
|
||
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 plane <b>constants</b> sit alongside as fixed-point screen coordinates
|
||
(<span class="mono">0x0000ec00 = 236.0</span>, a vertex x). 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 geometry takes on the wire to the
|
||
card. 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>. And scanning
|
||
<b>all 14,223 payload writes</b> of this draw, every screen coordinate lands in the object's own
|
||
range (<span class="mono">x 66–236</span>) — this death-cam frame carries <b>no geometry beyond
|
||
the object</b>; the rest is a background clear. So the array's render above isn't one primitive of
|
||
many: for this frame, it <b>is</b> the geometry.</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 Newton–Raphson 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>Coefficients <b>verified to 0.2%</b> against the geometry; for this frame, the object <b>is</b> the geometry (§05)</li>
|
||
<li>cap7 redraws <b>this one object every frame</b> — confirmed identical at cmd 735 and cmd 19,889, same 9 coordinates. A battle scene with terrain is a different capture entirely</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer>
|
||
<span>VWE 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>
|