fix(visualizer): blank Mix ribbon at rest + init/draw diagnostics (P10 W1)
This commit is contained in:
@@ -67,6 +67,21 @@ const RIBBON_HALF_WIDTH_FRAC = 0.92;
|
||||
*/
|
||||
const MAX_DPR = 2;
|
||||
|
||||
// ── Diagnostics ──────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Set true to surface the init/draw/datum/playback seams to the browser console
|
||||
// (all prefixed `[MixVisualizer]`). The error/warn paths fire regardless of this
|
||||
// flag — they only trigger on the abnormal path. The verbose `log` paths (datum
|
||||
// received/uploaded, first-draw dimensions, GL error after first draw) are gated
|
||||
// here so they can be silenced once the renderer is confirmed healthy. Leave it on
|
||||
// while the runtime fix is being verified through the browser.
|
||||
const DEBUG = true;
|
||||
|
||||
const TAG = '[MixVisualizer]';
|
||||
function debugLog(...args: unknown[]): void {
|
||||
if (DEBUG) console.log(TAG, ...args);
|
||||
}
|
||||
|
||||
// ── Theme: the gradient stop colours, read live from the active MudBlazor palette. ─
|
||||
//
|
||||
// The shader cannot resolve `var(--mud-palette-*)` directly — uniforms are plain
|
||||
@@ -90,10 +105,19 @@ interface ResolvedTheme {
|
||||
edge: [number, number, number];
|
||||
}
|
||||
|
||||
/** Read a CSS custom property off an element, falling back if it is empty/undefined. */
|
||||
/**
|
||||
* Read a CSS custom property off an element, falling back if it is empty/undefined.
|
||||
* An empty value means the var did not inherit onto the canvas (e.g. the palette is
|
||||
* scoped to a wrapper the canvas isn't under), which would silently swap the ribbon
|
||||
* colour for the hardcoded default — so warn on it when diagnosing.
|
||||
*/
|
||||
function readVar(el: Element, name: string, fallback: string): string {
|
||||
const v = getComputedStyle(el).getPropertyValue(name).trim();
|
||||
return v.length > 0 ? v : fallback;
|
||||
if (v.length === 0) {
|
||||
if (DEBUG) console.warn(`${TAG} CSS var '${name}' did not resolve off the canvas — using fallback '${fallback}'; ribbon colour may be wrong.`);
|
||||
return fallback;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,6 +302,12 @@ void main() {
|
||||
// per-frame CPU blur. (No CSS backdrop-filter, no shadowBlur — spec §5.2.)
|
||||
float feather = 1.5;
|
||||
float inside = 1.0 - smoothstep(halfWidth - feather, halfWidth + feather, distFromCentre);
|
||||
// With no datum, amp is 0 everywhere and halfWidth collapses to 0 — but the
|
||||
// feathered smoothstep still lights the ~feather-wide column at the exact centre,
|
||||
// drawing a thin vertical line down the middle of an otherwise-empty backdrop.
|
||||
// That is the at-rest "vertical bar" artifact. The predecessor drew nothing at
|
||||
// rest; match it by forcing the silhouette empty whenever there is no datum.
|
||||
if (uHasDatum < 0.5) inside = 0.0;
|
||||
|
||||
// Vertical gradient: brightest at the now line, dimming toward both edges. Same
|
||||
// luminosity cue the predecessor drew (edge -> accent -> edge by screen Y).
|
||||
@@ -349,6 +379,7 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
// No WebGL2 (old engine / disabled): hand back a no-op handle so the
|
||||
// component still functions as a plain backdrop (mirrors the predecessor's
|
||||
// no-2d-context fallback, now guarding against no-WebGL2).
|
||||
console.error(`${TAG} getContext('webgl2') returned null — WebGL2 unavailable; rendering a plain backdrop.`);
|
||||
return noopHandle();
|
||||
}
|
||||
// Non-null binding so the closures below keep the narrowing (TS does not carry
|
||||
@@ -361,7 +392,7 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
} catch (err) {
|
||||
// A compile/link failure on an exotic driver should degrade to the plain
|
||||
// backdrop, not crash the page. Log for diagnosis; return the no-op handle.
|
||||
console.warn(err);
|
||||
console.error(`${TAG} shader compile/link failed; rendering a plain backdrop.`, err);
|
||||
return noopHandle();
|
||||
}
|
||||
|
||||
@@ -369,7 +400,12 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
// shader sources its positions from gl_VertexID, so no attribute buffers.
|
||||
const vao = gl.createVertexArray();
|
||||
|
||||
// Cache uniform locations once.
|
||||
// Cache uniform locations once. A null here for a uniform we actually upload
|
||||
// means either the name is misspelled or the GLSL compiler dead-stripped it
|
||||
// (it isn't reachable in the shader) — both of which silently break a uniform's
|
||||
// effect, so surface them. `uTimeSeconds` is reserved for Wave 3 and currently
|
||||
// unused by the fragment shader, so the compiler is free to strip it; we exempt
|
||||
// it from the warning to avoid a false alarm.
|
||||
const u = {
|
||||
resolution: gl.getUniformLocation(program, 'uResolution'),
|
||||
playheadSeconds: gl.getUniformLocation(program, 'uPlayheadSeconds'),
|
||||
@@ -381,6 +417,11 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
hasDatum: gl.getUniformLocation(program, 'uHasDatum'),
|
||||
datum: gl.getUniformLocation(program, 'uDatum'),
|
||||
};
|
||||
for (const [name, loc] of Object.entries(u)) {
|
||||
if (loc === null && name !== 'timeSeconds') {
|
||||
console.warn(`${TAG} uniform '${name}' resolved to null — it will have no effect (misspelled or dead-stripped from the shader).`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mutable state, fed by the component through the handle. ──────────────────
|
||||
let datum: Datum | null = null;
|
||||
@@ -389,12 +430,14 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
|
||||
/** Resolve the gradient stops from the live palette vars on the canvas. */
|
||||
function readTheme(): ResolvedTheme {
|
||||
return {
|
||||
const resolved: ResolvedTheme = {
|
||||
// Brightest stop at the "now" line — the bespoke themes' primary accent.
|
||||
accent: parseColor(readVar(canvas, '--mud-palette-primary', '#b08d57')),
|
||||
// Dim stop at the edges — the surface colour so the ribbon fades into the page.
|
||||
edge: parseColor(readVar(canvas, '--mud-palette-surface', '#1a1a1a')),
|
||||
};
|
||||
debugLog(`theme resolved — accent=[${resolved.accent.map((c) => c.toFixed(2)).join(', ')}], edge=[${resolved.edge.map((c) => c.toFixed(2)).join(', ')}].`);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
let theme: ResolvedTheme = readTheme();
|
||||
@@ -403,6 +446,12 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
let disposed = false;
|
||||
const startTimeMs = performance.now();
|
||||
|
||||
// One-shot diagnostics: log the canvas dimensions + a post-draw gl.getError() the
|
||||
// first time we actually draw at a non-degenerate size. A 1×1 (or 300×150 default)
|
||||
// backing store here means the canvas had no layout box when the first draw ran —
|
||||
// the ResizeObserver will correct it, but the first paint would be degenerate.
|
||||
let firstRealDrawLogged = false;
|
||||
|
||||
// Backing-store size in device pixels, tracked so we only resize the canvas
|
||||
// (which clears it) when the CSS box actually changed.
|
||||
let cssWidth = 0;
|
||||
@@ -495,6 +544,21 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
// One full-screen triangle (3 vertices), positions from gl_VertexID.
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
gl.bindVertexArray(null);
|
||||
|
||||
// First draw at a real (laid-out) size: report dimensions and any accumulated
|
||||
// GL error. We hold this log until cssWidth/cssHeight are populated so the
|
||||
// dimensions Daniel sees are the meaningful ones, not a pre-layout 1×1.
|
||||
// gl.getError() is a pipeline stall, so we only call it once, never per frame.
|
||||
if (!firstRealDrawLogged && cssWidth > 0 && cssHeight > 0) {
|
||||
firstRealDrawLogged = true;
|
||||
debugLog(
|
||||
`first draw — backing store ${canvas.width}x${canvas.height} px (css ${cssWidth}x${cssHeight} @ dpr ${dpr}), hasDatum=${datum ? 1 : 0}`,
|
||||
);
|
||||
const glErr = gl.getError();
|
||||
if (glErr !== gl.NO_ERROR) {
|
||||
console.error(`${TAG} gl.getError() after first draw: 0x${glErr.toString(16)} — the draw did not complete cleanly.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── rAF loop lifecycle (spec §E: cool when paused/backgrounded). ─────────────
|
||||
@@ -564,9 +628,18 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
* CLAMP_TO_EDGE. Returns the Datum, or null on an empty/invalid input.
|
||||
*/
|
||||
function uploadDatum(samplesBase64: string, durationSeconds: number): Datum | null {
|
||||
if (durationSeconds <= 0 || !samplesBase64) return null;
|
||||
if (durationSeconds <= 0 || !samplesBase64) {
|
||||
// Expected before the player reports a duration: the bridge pushes an empty
|
||||
// datum until then. Not an error, but worth seeing while diagnosing.
|
||||
debugLog(`uploadDatum skipped — durationSeconds=${durationSeconds}, base64 length=${samplesBase64?.length ?? 0}.`);
|
||||
return null;
|
||||
}
|
||||
const samples = decodeSamples(samplesBase64);
|
||||
if (samples.length === 0) return null;
|
||||
if (samples.length === 0) {
|
||||
console.warn(`${TAG} uploadDatum: decoded 0 samples from a non-empty base64 string — datum will not render.`);
|
||||
return null;
|
||||
}
|
||||
debugLog(`uploadDatum — ${samples.length} samples for ${durationSeconds.toFixed(2)}s mix (${(samples.length / durationSeconds).toFixed(1)} samples/s).`);
|
||||
|
||||
const texture = gl.createTexture();
|
||||
if (!texture) return null;
|
||||
@@ -594,6 +667,7 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
|
||||
return {
|
||||
setDatum(samplesBase64: string, durationSeconds: number): void {
|
||||
debugLog(`setDatum received — base64 length ${samplesBase64?.length ?? 0}, durationSeconds ${durationSeconds}.`);
|
||||
// Free the previous datum's GPU texture before replacing it (no leak
|
||||
// across re-pushes / mix changes — spec §5.11).
|
||||
if (datum) {
|
||||
@@ -612,6 +686,7 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
|
||||
|
||||
if (isPlaying && !wasPlaying) {
|
||||
// Transition paused/stopped → playing: start the rAF loop.
|
||||
debugLog(`playback started — position ${positionSeconds.toFixed(2)}s, datum ${datum ? 'present' : 'ABSENT'}; starting rAF loop.`);
|
||||
startLoop();
|
||||
} else if (!isPlaying && wasPlaying) {
|
||||
// Transition playing → paused/stopped: the in-flight frame draws the
|
||||
|
||||
Reference in New Issue
Block a user