fix(dashboard): correct WebGPU shader coordinate + frame-rate bugs (MiniMax M3 audit)

MiniMax M3 (max-depth audit, fresh eyes on the shader MATH that Claude+Kimi's
lifecycle-focused passes missed) found 4 pre-existing criticals, all verified:

- C1 aspect-ratio inversion: vertex shader divided particle X by aspect, which
  SQUASHED the whole field into the center ~56% of a 16:9 screen (~22% empty
  bars each side). Fix: position now uses the full [-1,1] clip box (no divide);
  roundness comes from a proper px2ndc = 2/viewport corner offset so quads are
  sizePx x sizePx device px on any aspect ratio (also kills the 0.01 magic number).
- C3 firewall flare landed at the wrong X (distance measured in unit-box space
  while the particle rendered in divided space). Fixed-for-free by C1; the crimson
  distance is now aspect-corrected so the halo is circular, centered on the catch.
- C4 frame-rate-dependent flow absorption: the velocity lerp used a fixed 0.06
  per-frame factor (not dt-scaled), so the field was 2x snappier at 120Hz
  ProMotion vs 60Hz. Fix: absorb = 1 - exp(-dt * 3.7) — identical feel at 60fps,
  correct everywhere else.

VERIFIED: svelte-check 0 errors / 967 files; zero WGSL validation errors in the
browser (a broken shader struct/syntax would throw at pipeline creation — none).
NOTE: the on-screen full-width widening is not yet visually captured this session
— the preview backend returns API 500 so the (app) layout error-states and the
field doesn't mount; pending eyeball on a wide window against a live backend.
Memory Cinema / Graph3D untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-06-27 14:52:45 -05:00
parent 85650f02fd
commit deb3b82134

View file

@ -113,7 +113,12 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
let rm = u.viewport.w;
let speed = mix(1.0, 0.18, rm) * u.params.z;
let flow = curlFlow(pr.pos.xyz * 1.1);
pr.vel = vec4f(mix(pr.vel.xyz, flow * 0.6, 0.06), pr.vel.w);
// Frame-rate-independent absorption toward the flow. 0.06/frame at 60fps =>
// rate 3.7/s; exp form keeps the feel identical at 60fps but correct on
// 120Hz ProMotion / 30fps throttle (was a fixed per-frame lerp -> 2x snappier
// at 120Hz). u.params.y is dt.
let absorb = 1.0 - exp(-u.params.y * 3.7);
pr.vel = vec4f(mix(pr.vel.xyz, flow * 0.6, absorb), pr.vel.w);
// --- reactive event impulse (scoped to the event origin) -----------------
let ecode = u.event.x;
@ -169,12 +174,17 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VsOut
);
let c = corners[vi];
let p = parts[ii];
let aspect = u.viewport.x / max(u.viewport.y, 1.0);
// gentle parallax depth so the cloud reads volumetric
let depth = 1.7 + p.pos.z * 0.5;
let sizePx = (2.6 + p.pos.w * 3.4) / depth;
var ndc = vec2f(p.pos.x / aspect, p.pos.y);
ndc = ndc + c * vec2f(sizePx * 0.01 / aspect, sizePx * 0.01);
// Position fills the FULL clip box [-1,1]^2 — do NOT divide x by aspect
// (that squashed the field into the center ~56% on 16:9). Roundness comes
// from the corner offset instead: convert sizePx (device px) to NDC per
// axis via px2ndc = 2/viewport, so the quad is sizePx x sizePx px and round
// on any aspect ratio.
var ndc = vec2f(p.pos.x, p.pos.y);
let px2ndc = vec2f(2.0 / max(u.viewport.x, 1.0), 2.0 / max(u.viewport.y, 1.0));
ndc = ndc + c * (px2ndc * sizePx);
let speed = length(p.vel.xyz);
var out: VsOut;
out.clip = vec4f(ndc, 0.0, 1.0);
@ -185,7 +195,12 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VsOut
// "angry" block. Crimson is reserved ONLY for this; nothing else turns red.
var crimson = 0.0;
if (u.event.x > 0.5 && u.event.x < 1.5) {
let d = length(p.pos.xy - vec2f(u.event.z, u.event.w));
// p.pos.xy and the event origin (u.event.zw) are now in the SAME clip
// space (no aspect divide on position), so the flare lands exactly on
// the catch origin. Aspect-correct the distance so the halo is circular
// on wide screens rather than an x-stretched ellipse.
let aspect = u.viewport.x / max(u.viewport.y, 1.0);
let d = length((p.pos.xy - vec2f(u.event.z, u.event.w)) * vec2f(aspect, 1.0));
crimson = exp(-d * 1.6) * u.event.y;
}
out.crimson = clamp(crimson, 0.0, 1.0);