mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-26 23:51:02 +02:00
The raw-WebGPU field was inert — it ran the deterministic DemoClock and never heard the backend. This wires it to the real event stream and makes every memory visibly forget on its true FSRS curve. - LiveBridge (new): lock-free per-frame drain via engine.preFrameHook, ingests $eventFeed oldest-first, decodes real events into live lanes. - fsrs.ts (new): exact TS port of retrievability_with_decay (w20=0.1542). - params 12→16 floats (capture_mode + 4 live lanes); all 8 WGSL Params structs updated in lockstep. _pad renamed to its real meaning. - node-renderer: live-retention GPU buffer (binding 4); simulate.wgsl overwrites vel_retention.w so render-nodes dims on the real curve. - ObservatoryStage: `live` prop, $eventFeed subscription, persistent FORGETTING HORIZON scrubber (project the curve forward N days, honest). - backend: get_graph node json now carries stability + lastAccessed. Verified live vs the real 1241-memory brain: +300d scrub visibly decays low-stability nodes while high-stability survive; per-node FSRS math confirmed. svelte-check 0 err, 1033 tests, build, cargo check all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
185 lines
5.4 KiB
TypeScript
185 lines
5.4 KiB
TypeScript
/**
|
|
* Cognitive Observatory — recall path edge wavefront (Increment 6).
|
|
*
|
|
* One instanced screen-aligned quad per path step. The vertex shader fetches
|
|
* both endpoint nodes from the NodeState storage buffer (GraphWaGu edge_vert
|
|
* pattern, spec §1) and extrudes a constant-screen-width ribbon between them.
|
|
* The fragment draws a light packet traveling source → target, timed to land
|
|
* exactly on the beat frame, with a fading trail behind it.
|
|
*
|
|
* Deterministic: wave position is a pure function of (frame, beatFrame).
|
|
*/
|
|
export const renderPathWGSL = /* wgsl */ `
|
|
struct Params {
|
|
frame: f32,
|
|
loop_phase: f32,
|
|
node_count: f32,
|
|
edge_count: f32,
|
|
path_count: f32,
|
|
pulse: f32,
|
|
viewport_w: f32,
|
|
viewport_h: f32,
|
|
brightness: f32,
|
|
demo_id: f32,
|
|
time: f32,
|
|
capture_mode: f32,
|
|
live_kind: f32,
|
|
live_start_frame: f32,
|
|
live_energy: f32,
|
|
projection_days: f32,
|
|
};
|
|
|
|
struct Camera {
|
|
view_proj: mat4x4<f32>,
|
|
right: vec4<f32>,
|
|
up: vec4<f32>,
|
|
};
|
|
|
|
struct Node {
|
|
pos_radius: vec4<f32>,
|
|
vel_retention: vec4<f32>,
|
|
color_flags: vec4<f32>,
|
|
demo: vec4<f32>,
|
|
};
|
|
|
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
@group(0) @binding(1) var<uniform> camera: Camera;
|
|
@group(0) @binding(2) var<storage, read> nodes: array<Node>;
|
|
// x source index, y target index, z beat frame, w kind (0 recall, 1 backward)
|
|
@group(0) @binding(3) var<storage, read> path: array<vec4<u32>>;
|
|
|
|
// Same thin-film band as the node shader (§7.1).
|
|
fn spectral(w_in: f32) -> vec3<f32> {
|
|
let w = fract(w_in);
|
|
// var, not let: WGSL only allows dynamic indexing through a reference.
|
|
var stops = array<vec3<f32>, 4>(
|
|
vec3<f32>(0.20, 0.28, 0.95),
|
|
vec3<f32>(0.20, 0.85, 0.90),
|
|
vec3<f32>(0.45, 1.00, 0.72),
|
|
vec3<f32>(0.85, 0.45, 1.00)
|
|
);
|
|
let f = w * 4.0;
|
|
let i = u32(floor(f)) % 4u;
|
|
let frac = f - floor(f);
|
|
let a = stops[i];
|
|
let b = stops[(i + 1u) % 4u];
|
|
return mix(a, b, frac);
|
|
}
|
|
|
|
struct VSOut {
|
|
@builtin(position) clip: vec4<f32>,
|
|
// x: t along segment (0 source → 1 target), y: side (-1..1)
|
|
@location(0) uv: vec2<f32>,
|
|
// x: beat frame, y: kind, z: segment visible (0 skips degenerate steps)
|
|
// Per-instance constant — flat keeps it bit-exact through the raster.
|
|
@location(1) @interpolate(flat) beat: vec3<f32>,
|
|
};
|
|
|
|
// (t, side) corners for two triangles of the ribbon.
|
|
const RIBBON = array<vec2<f32>, 6>(
|
|
vec2<f32>(0.0, -1.0),
|
|
vec2<f32>(1.0, -1.0),
|
|
vec2<f32>(1.0, 1.0),
|
|
vec2<f32>(0.0, -1.0),
|
|
vec2<f32>(1.0, 1.0),
|
|
vec2<f32>(0.0, 1.0)
|
|
);
|
|
|
|
@vertex
|
|
fn vs_main(
|
|
@builtin(vertex_index) vi: u32,
|
|
@builtin(instance_index) ii: u32
|
|
) -> VSOut {
|
|
var out: VSOut;
|
|
if (ii >= u32(params.path_count)) {
|
|
out.clip = vec4<f32>(0.0, 0.0, 2.0, 1.0);
|
|
out.beat = vec3<f32>(0.0);
|
|
return out;
|
|
}
|
|
|
|
let step = path[ii];
|
|
let src = nodes[step.x];
|
|
let dst = nodes[step.y];
|
|
let corner = RIBBON[vi];
|
|
|
|
// Degenerate (origin beat: source == target) — emit nothing visible.
|
|
if (step.x == step.y) {
|
|
out.clip = vec4<f32>(0.0, 0.0, 2.0, 1.0);
|
|
out.beat = vec3<f32>(0.0);
|
|
return out;
|
|
}
|
|
|
|
let a = camera.view_proj * vec4<f32>(src.pos_radius.xyz, 1.0);
|
|
let b = camera.view_proj * vec4<f32>(dst.pos_radius.xyz, 1.0);
|
|
|
|
// NDC-space perpendicular for constant screen width.
|
|
let ndc_a = a.xy / max(a.w, 0.0001);
|
|
let ndc_b = b.xy / max(b.w, 0.0001);
|
|
var dir = ndc_b - ndc_a;
|
|
let dlen = max(length(dir), 0.0001);
|
|
dir = dir / dlen;
|
|
let perp = vec2<f32>(-dir.y, dir.x);
|
|
|
|
// Ribbon half-width in NDC (aspect-corrected), ~2.5 px on a 900px-tall view.
|
|
let px = 2.5 / max(params.viewport_h, 1.0) * 2.0;
|
|
let width = vec2<f32>(px * (params.viewport_h / max(params.viewport_w, 1.0)), px);
|
|
|
|
let base = mix(a, b, corner.x);
|
|
let offset = perp * width * corner.y * base.w;
|
|
out.clip = vec4<f32>(base.xy + offset, base.zw);
|
|
out.uv = vec2<f32>(corner.x, corner.y);
|
|
out.beat = vec3<f32>(f32(step.z), f32(step.w), 1.0);
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_main(in: VSOut) -> @location(0) vec4<f32> {
|
|
if (in.beat.z < 0.5) {
|
|
discard;
|
|
}
|
|
|
|
let frame = params.frame;
|
|
let bf = in.beat.x;
|
|
let t = in.uv.x;
|
|
|
|
// Wave departs 45 frames before the beat and lands exactly on it.
|
|
let progress = clamp((frame - (bf - 45.0)) / 45.0, 0.0, 1.0);
|
|
// Nothing before departure; trail lingers ~90 frames after arrival.
|
|
let live = smoothstep(bf - 46.0, bf - 44.0, frame)
|
|
* (1.0 - smoothstep(bf + 40.0, bf + 90.0, frame));
|
|
if (live <= 0.001) {
|
|
discard;
|
|
}
|
|
|
|
// The light packet: gaussian around the wavefront position.
|
|
let dwave = (t - progress) * 14.0;
|
|
let packet = exp(-dwave * dwave);
|
|
|
|
// Fading trail behind the packet — provenance stays visible a beat.
|
|
var trail = 0.0;
|
|
if (t < progress) {
|
|
trail = (1.0 - (progress - t)) * 0.22;
|
|
}
|
|
|
|
// Feather across the ribbon width.
|
|
let across = 1.0 - abs(in.uv.y);
|
|
let profile = across * across;
|
|
|
|
// Backward/contradiction hops burn hotter into the magenta rim (§7.4).
|
|
// Hue drifts one full spectral cycle per loop (seamless at the wrap).
|
|
// Kind 2 (salience-rescue probe): a gray failing beam — vector search
|
|
// visibly probing lookalikes and coming back empty. Kinds 0/1 unchanged.
|
|
var band = spectral(0.15 + t * 0.35 + params.loop_phase);
|
|
var packet_white = 0.35;
|
|
if (in.beat.y > 1.5) {
|
|
band = vec3<f32>(0.62, 0.66, 0.72);
|
|
packet_white = 0.18;
|
|
} else if (in.beat.y > 0.5) {
|
|
band = mix(band, vec3<f32>(1.0, 0.25, 0.45), 0.55);
|
|
}
|
|
|
|
let energy = (packet * 1.6 + trail) * profile * live;
|
|
let color = band * energy + vec3<f32>(1.0) * packet * profile * live * packet_white;
|
|
return vec4<f32>(color * params.brightness, 1.0);
|
|
}
|
|
`;
|