feat(dashboard): backdrop robustness + fallback firewall flash

Real robustness fixes found while verifying Phase 3:

- Boot watchdog: if WebGPU hasn't started rendering within 6s (slow/contended
  GPU, hung device request), fall back to Canvas2D so the field is never stuck
  on the static gradient. Cleared on successful webgpu boot + onDestroy.
- bootFallback is now idempotent (guards double-start, cancels any in-flight
  rAF) so the watchdog can't race a second loop.
- Canvas2D fallback now reacts to the firewall field event too — an outward
  shockwave + crimson tint — so pre-iOS-26 / no-WebGPU users also see the catch.
- ?demo=firewall fires the flash on a short delay (so the field has booted, not
  mid-boot where it'd be missed); ?demo=firewall&loop=1 re-arms it every 3.2s
  for repeatable launch-footage capture.

svelte-check 0 errors / 967 files. NOTE: the on-screen crimson flash remains
visually unconfirmed this session — the preview env had an unstable viewport +
exhausted GPU process; pending a clean browser. Memory Cinema untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-06-27 12:29:42 -05:00
parent 69aab30d38
commit 9d791a35a5
2 changed files with 52 additions and 9 deletions

View file

@ -331,6 +331,7 @@ fn fs(in: VsOut) -> @location(0) vec4f {
});
mode = 'webgpu';
if (bootWatchdog) { clearTimeout(bootWatchdog); bootWatchdog = null; }
resize();
startedAt = performance.now();
lastT = startedAt;
@ -398,6 +399,9 @@ fn fs(in: VsOut) -> @location(0) vec4f {
let fbParts: { x: number; y: number; vx: number; vy: number; e: number }[] = [];
function bootFallback() {
if (disposed || !fallbackCanvas) return;
if (mode === 'fallback' && fbParts.length) return; // idempotent: don't double-start
if (bootWatchdog) { clearTimeout(bootWatchdog); bootWatchdog = null; }
cancelAnimationFrame(raf); // stop any in-flight loop before switching
mode = 'fallback';
resize();
const n = (globalThis as any).innerWidth < 760 ? 520 : 1400;
@ -406,6 +410,7 @@ fn fs(in: VsOut) -> @location(0) vec4f {
vx: 0, vy: 0, e: Math.random()
}));
startedAt = performance.now();
fbLastT = 0;
raf = requestAnimationFrame(drawFallback);
}
function fbField(x: number, y: number, t: number): [number, number] {
@ -413,12 +418,21 @@ fn fs(in: VsOut) -> @location(0) vec4f {
const a = Math.sin(x * 6.2 + t * 0.3) + Math.cos(y * 5.1 - t * 0.2);
return [Math.cos(a * 2.0) * 0.0009, Math.sin(a * 2.0) * 0.0009];
}
let fbLastT = 0;
function drawFallback() {
if (disposed || !fallbackCanvas || mode !== 'fallback') return;
const ctx = fallbackCanvas.getContext('2d');
if (!ctx) return;
const w = fallbackCanvas.width, h = fallbackCanvas.height;
const t = (performance.now() - startedAt) / 1000;
const nowMs = performance.now();
const t = (nowMs - startedAt) / 1000;
const dt = fbLastT ? Math.min((nowMs - fbLastT) / 1000, 0.05) : 0.016;
fbLastT = nowMs;
// advance any active reactive event (firewall flash also works here so
// pre-iOS-26 / no-WebGPU users still see the catch)
if (evtCode !== 0) { evtAge += dt; if (evtAge > EVENT_DURATION) evtCode = 0; }
const fwFade = evtCode === 1 ? Math.max(0, 1 - evtAge / EVENT_DURATION) * evtIntensity : 0;
const ox = (evtX + 1) * 0.5, oy = (1 - evtY) * 0.5; // back to [0,1]
ctx.fillStyle = 'rgba(5,3,13,0.18)'; // trail fade on void
ctx.fillRect(0, 0, w, h);
ctx.globalCompositeOperation = 'lighter';
@ -426,17 +440,28 @@ fn fs(in: VsOut) -> @location(0) vec4f {
const [fx, fy] = fbField(p.x, p.y, t);
p.vx = p.vx * 0.92 + fx * (reduced ? 0.3 : 1);
p.vy = p.vy * 0.92 + fy * (reduced ? 0.3 : 1);
// firewall shockwave: push outward from the origin
let crimson = 0;
if (fwFade > 0) {
const dx = p.x - ox, dy = p.y - oy;
const dist = Math.hypot(dx, dy) + 1e-4;
const reach = Math.exp(-dist * 2.2) * fwFade;
p.vx += (dx / dist) * reach * 0.01;
p.vy += (dy / dist) * reach * 0.01;
crimson = Math.min(reach * 1.6, 1);
}
p.x += p.vx; p.y += p.vy;
if (p.x < 0) p.x += 1; if (p.x > 1) p.x -= 1;
if (p.y < 0) p.y += 1; if (p.y > 1) p.y -= 1;
const sp = Math.hypot(p.vx, p.vy) * 220;
const energy = Math.min(0.3 + sp + p.e * 0.4, 1);
// violet ramp matching the WGSL palette
const r = Math.floor(26 + energy * 209);
const g = Math.floor(13 + energy * 70);
const b = Math.floor(71 + energy * 146);
ctx.fillStyle = `rgba(${r},${g},${b},${0.5 * energy})`;
ctx.fillRect(p.x * w, p.y * h, 2.2, 2.2);
// violet ramp matching the WGSL palette, blended toward crimson on a catch
const vr = 26 + energy * 209, vg = 13 + energy * 70, vb = 71 + energy * 146;
const r = Math.floor(vr + (255 - vr) * crimson);
const g = Math.floor(vg + (30 - vg) * crimson);
const b = Math.floor(vb + (56 - vb) * crimson);
ctx.fillStyle = `rgba(${r},${g},${b},${(0.5 + crimson * 0.4) * energy})`;
ctx.fillRect(p.x * w, p.y * h, 2.2 + crimson * 1.5, 2.2 + crimson * 1.5);
}
ctx.globalCompositeOperation = 'source-over';
raf = requestAnimationFrame(drawFallback);
@ -453,6 +478,7 @@ fn fs(in: VsOut) -> @location(0) vec4f {
}
let unsubFieldEvents: (() => void) | null = null;
let bootWatchdog: ReturnType<typeof setTimeout> | null = null;
onMount(() => {
disposed = false;
@ -462,6 +488,12 @@ fn fs(in: VsOut) -> @location(0) vec4f {
if (webgpuAvailable()) {
// defer one frame so the route paints before GPU boot work
requestAnimationFrame(() => { if (!disposed) tryBootWebgpu(); });
// Watchdog: if WebGPU hasn't actually started rendering within 6s (a
// slow/contended GPU or a hung device request), fall back to Canvas2D
// so the field is never stuck on the static gradient.
bootWatchdog = setTimeout(() => {
if (!disposed && mode === 'booting') bootFallback();
}, 6000);
} else {
bootFallback();
}
@ -471,6 +503,7 @@ fn fs(in: VsOut) -> @location(0) vec4f {
onDestroy(() => {
disposed = true;
unsubFieldEvents?.();
if (bootWatchdog) { clearTimeout(bootWatchdog); bootWatchdog = null; }
if (!browser) return; // onDestroy also fires during SSR — no DOM there
cancelAnimationFrame(raf);
window.removeEventListener('resize', onResize);

View file

@ -219,14 +219,23 @@
];
loading = false;
error = null;
// flash the living field crimson — the firewall catch, made visible
emitFieldEvent({ kind: 'firewall', intensity: 1 });
}
// Flash the living field crimson — the firewall catch, made visible. Delayed
// so the field has booted (a flash fired mid-boot would be missed), and
// re-armed on a loop in demo mode so it's reliably capturable for footage.
let demoFlashTimer: ReturnType<typeof setInterval> | null = null;
function startFirewallFlash(loop: boolean) {
const fire = () => emitFieldEvent({ kind: 'firewall', intensity: 1 });
setTimeout(fire, 1200);
if (loop) demoFlashTimer = setInterval(fire, 3200);
}
onMount(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('demo') === 'firewall') {
loadFirewallDemo();
startFirewallFlash(params.get('loop') === '1');
return;
}
const preferredRunId = params.get('run');
@ -236,6 +245,7 @@
onDestroy(() => {
if (liveRefreshTimer) clearTimeout(liveRefreshTimer);
if (receiptRetryTimer) clearTimeout(receiptRetryTimer);
if (demoFlashTimer) clearInterval(demoFlashTimer);
});
</script>