020a945843
Probes UNMASKED_RENDERER_WEBGL once per page via a throwaway WebGL context; defaults the lava subsystem off on a positive software-renderer match or total WebGL failure; releases the throwaway context via WEBGL_lose_context after reading the renderer string to avoid exhausting the browser's per-page context limit.
109 lines
5.9 KiB
TypeScript
109 lines
5.9 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
}
|