vestige/apps/dashboard/src/lib/observatory/firewall-renderer.ts
Sam Valladares a046c620c8 feat(observatory): raw-WebGPU living cognitive field with 5 deterministic demos
The Observatory is a full-bleed, zero-library WebGPU surface that renders
the memory graph as a living cognitive field. Five deterministic demo
moments driven by a URL contract (?demo=<name>&seed=...&frame=N):
recall-path, engram-birth, salience-rescue, forgetting-horizon, firewall.
Capture mode (?frame=N) freezes the sim deterministically so the same URL
produces identical pixels, the viral-clip primitive.

Architecture: bare-metal WebGPU engine (no Three.js), seeded demo clock,
per-demo plan + renderer modules, WGSL shaders (simulate, nodes, edges,
path, birth particles, rescue, forgetting, firewall) plus a post-processing
chain (tone mapping, MIP). DOM is instrument overlays only (telemetry strip,
timeline spine, rescue verdict); the layout gives /observatory the same
full-bleed bypass as marketing routes so recordings stay clean.

Reads the real memory graph. Verified live: svelte-check 939 files 0 errors,
96 observatory unit tests green, all 5 demos load at 108-119fps with zero
console errors against the live brain.

Known follow-up: engram-birth capture-mode particle cluster needs a render
fix pass before it is camera-ready; the other 4 demos are camera-ready.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 10:49:18 -07:00

114 lines
4.2 KiB
TypeScript

/**
* Cognitive Observatory — firewall choreography pass.
*
* Compute-only FramePass (no render(): nodes and probe beams draw via the
* NodeRenderer's existing pipelines). Uploads the per-node packed fire word
* once (static), then each frame extends the choreography INTO the NodeState
* demo lanes as a pure function of (frame, role, shockDelay) — no stateful
* integration, so capture mode (?frame=N) works with zero special-casing.
*
* PASS ORDER IS LOAD-BEARING: this pass MUST be constructed AFTER the
* NodeRenderer (the route guarantees it: handleReady creates NodeRenderer,
* the upload $effect creates FirewallRenderer) so firewall_choreo encodes
* AFTER recall_sim in the same encoder and its demo-lane overwrite wins.
* recall_sim rewrites demo.x every frame with an afterglow window of
* bf+40..bf+200 — the k=5 sever beam at bf=450 would otherwise carry residual
* ignition into the verdict window.
*
* Three independent walls keep OTHER demos pixel-identical:
* (a) the route constructs this renderer only in the firewall branch,
* (b) compute() gates on params[9] === 4 ('firewall' demo index),
* (c) the demo-4 vertex/fragment terms in render-nodes.wgsl are themselves
* gated on params.demo_id == 4.0.
*/
import type { ObservatoryEngine, FramePass } from './engine';
import type { NodeRenderer } from './node-renderer';
import type { FirewallPlan } from './firewall-plan';
import { firewallWGSL } from './shaders/firewall.wgsl';
/** DEMO_MODES.indexOf('firewall') — types.ts, verified index 4. */
const FIREWALL_DEMO_ID = 4;
export interface FirewallRendererOptions {
engine: ObservatoryEngine;
nodeRenderer: NodeRenderer;
plan: FirewallPlan;
}
export class FirewallRenderer implements FramePass {
private engine: ObservatoryEngine;
private nodeRenderer: NodeRenderer;
private plan: FirewallPlan;
private pipeline: GPUComputePipeline | null = null;
private bindGroup: GPUBindGroup | null = null;
private fireBuffer: GPUBuffer | null = null;
constructor(opts: FirewallRendererOptions) {
this.engine = opts.engine;
this.nodeRenderer = opts.nodeRenderer;
this.plan = opts.plan;
this.engine.addPass(this);
}
/** Create the fire buffer + compute pipeline. Call after NodeRenderer.upload(). */
upload(): void {
const device = this.engine.gpuDevice;
if (!device || !this.engine.paramsBuffer) return;
if (!this.plan.viable) return;
if (!this.nodeRenderer.nodeStateBuffer || this.nodeRenderer.nodeCountValue === 0) return;
this.fireBuffer?.destroy();
this.fireBuffer = device.createBuffer({
label: 'observatory-firewall-fire',
size: Math.max(4, this.plan.fireData.byteLength),
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
});
device.queue.writeBuffer(this.fireBuffer, 0, this.plan.fireData.buffer as ArrayBuffer);
const module = device.createShaderModule({
label: 'observatory-firewall-choreo',
code: firewallWGSL
});
this.pipeline = device.createComputePipeline({
label: 'observatory-firewall-choreo',
layout: 'auto',
compute: { module, entryPoint: 'firewall_choreo' }
});
// EXACTLY the 3 declared bindings — auto layout strips unused bindings
// and binding anything extra invalidates the group (the BirthRenderer
// lesson, birth-renderer.ts createComputePipeline).
this.bindGroup = device.createBindGroup({
label: 'observatory-firewall-bind',
layout: this.pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: this.engine.paramsBuffer } },
{ binding: 1, resource: { buffer: this.nodeRenderer.nodeStateBuffer } },
{ binding: 2, resource: { buffer: this.fireBuffer } }
]
});
}
/** FramePass — overwrite the four demo lanes for this frame (pure of frame). */
compute(encoder: GPUCommandEncoder): void {
if (this.engine.params[9] !== FIREWALL_DEMO_ID) return;
if (!this.pipeline || !this.bindGroup) return;
const n = this.nodeRenderer.nodeCountValue;
if (n === 0) return;
const pass = encoder.beginComputePass({ label: 'observatory-firewall-choreo' });
pass.setPipeline(this.pipeline);
pass.setBindGroup(0, this.bindGroup);
pass.dispatchWorkgroups(Math.ceil(n / 64));
pass.end();
}
dispose(): void {
this.fireBuffer?.destroy();
this.fireBuffer = null;
this.pipeline = null;
this.bindGroup = null;
}
}