/** * Hardware-acceleration probe for the lava-lamp visualizer. * * WHY: with hardware acceleration OFF the WebGL2 lava field software-renders on the main thread and * starves WebCodecs Opus decode → playback struggles. The decodePressure auto-throttle alone is not * enough — even throttled, software-rendered lava is too expensive. So when there is no HW-accel * support we default the LAVA subsystem OFF (the expensive part) while keeping the WAVEFORM ON. With * HW accel present (the common case) nothing changes — lava defaults on, full quality. * * The probe creates a throwaway WebGL context, reads the unmasked renderer string via * WEBGL_debug_renderer_info, and matches it against known software-renderer signatures. * * UNCERTAINTY POLICY (favor the HW-accel majority): lava is disabled ONLY on a positive * software-renderer match or a total failure to obtain any WebGL context (lava can't run at all). If * the renderer string is unavailable/masked (some privacy configs strip * WEBGL_debug_renderer_info) but a context otherwise succeeds, we default to "accelerated" — we do not * disable lava on absence of evidence, only on positive evidence of software rendering. * * LIMIT (browser-confirmed, not code-provable): UNMASKED_RENDERER_WEBGL can be masked, and a given * browser running with HW accel OFF may report a string none of these signatures match — in which * case this probe reports "accelerated" and lava stays on. The signature list below is the only * tunable; if a real software-renderer string slips through, add it here. */ /** * Case-insensitive substrings that positively identify a software (non-GPU) WebGL renderer. Matching * any one of these means the browser is software-rendering WebGL → lava off. Order is irrelevant. */ export const SOFTWARE_RENDERER_SIGNATURES: readonly string[] = [ 'swiftshader', // Chrome's software GL fallback (also "Google SwiftShader") 'llvmpipe', // Mesa software rasterizer (Linux) 'softpipe', // Mesa software rasterizer (older/gallium) 'microsoft basic render', // Windows "Microsoft Basic Render Driver" 'mesa offscreen', // Mesa headless/offscreen software path 'software', // generic catch-all ("... Software ...") ]; /** * Pure predicate: does this renderer string positively identify a software renderer? Case-insensitive * substring match against {@link SOFTWARE_RENDERER_SIGNATURES}. Empty/whitespace is NOT a match — a * masked/absent string is "unknown", not "software" (see {@link classifyHardwareAcceleration}). */ export function isSoftwareRenderer(renderer: string): boolean { const r = renderer.toLowerCase(); return SOFTWARE_RENDERER_SIGNATURES.some((sig) => r.includes(sig)); } /** * Pure classifier mapping probe observations to "is hardware accelerated?". Split out from the * DOM-touching {@link detectHardwareAcceleration} so the policy is unit-testable without a browser. * * • no WebGL context at all → false (lava can't run — total failure) * • renderer masked/absent → true (favor the HW-accel majority — absence of evidence) * • positive software match → false (positive evidence of software rendering) * • otherwise → true (a real GPU renderer string) */ export function classifyHardwareAcceleration(hasWebglContext: boolean, renderer: string | null): boolean { if (!hasWebglContext) return false; if (renderer === null || renderer.trim() === '') return true; return !isSoftwareRenderer(renderer); } /** Read the unmasked renderer string, or null when the debug extension is unavailable/masked. */ function readUnmaskedRenderer(gl: WebGLRenderingContext | WebGL2RenderingContext): string | null { const ext = gl.getExtension('WEBGL_debug_renderer_info'); if (!ext) return null; const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); return typeof renderer === 'string' ? renderer : null; } // Probe once per page — the renderer is a constant for the lifetime of the document. Cached so the // scoped C# control-state's one-time default-set never pays for a second throwaway context. let cached: boolean | undefined; /** * Probe the browser for WebGL hardware acceleration. Returns true when the lava subsystem should * default ON (HW accel present or renderer unknown), false when it should default OFF (positive * software-renderer match or no WebGL context at all). Cached after the first call; never throws. */ export function detectHardwareAcceleration(): boolean { if (cached !== undefined) return cached; cached = probe(); return cached; } function probe(): boolean { try { const canvas = document.createElement('canvas'); const gl = (canvas.getContext('webgl2') ?? canvas.getContext('webgl')) as | WebGLRenderingContext | WebGL2RenderingContext | null; if (!gl) return classifyHardwareAcceleration(false, null); const result = classifyHardwareAcceleration(true, readUnmaskedRenderer(gl)); // Release the throwaway context — WebGL contexts are a scarce per-page resource (~16 in // Chrome before force-eviction). The renderer string is already captured in `result` above // so this is safe to call before returning. Inner try/catch ensures a rogue loseContext // implementation (or a browser that surfaces it incorrectly) cannot silently swallow the // result or re-throw out of probe() and trigger the defensive `return true` fallback. try { gl.getExtension('WEBGL_lose_context')?.loseContext(); } catch { /* defensive */ } return result; } catch { // getContext/createElement do not throw in practice; this guard is purely defensive. An // unexpected probe failure should NOT regress the HW-accel majority, so default to // accelerated (lava on) — only the clean "no context" path above disables lava. return true; } }