mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-12 22:42:10 +02:00
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>
114 lines
4.2 KiB
TypeScript
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;
|
|
}
|
|
}
|