Files
deepdrft/DeepDrftPublic/Interop/visualizer/hwAccel.test.ts
T
daniel-c-harvey 020a945843 Detect HW acceleration; default lava off on software renderer; release probe WebGL context
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.
2026-06-26 10:41:07 -04:00

138 lines
5.6 KiB
TypeScript

/**
* hwAccel classifier tests — the pure software-renderer signature matching and the
* uncertainty/failure policy that drives the lava default-off decision.
*
* These cover the code-PROVABLE half of the feature: given a renderer string (or its absence, or a
* total WebGL failure), is the browser classified "accelerated" (lava on) or not (lava off)? The
* impure probe (detectHardwareAcceleration → real getContext) is browser-confirmed, not unit-tested.
*
* Same harness convention as decodePressure.test.ts — no test runner in this repo; Node 22+ strips TS
* types natively. Run a copy from the COMPILED output so the `./hwAccel.js` import specifier resolves:
*
* dotnet build DeepDrftPublic/DeepDrftPublic.csproj
* cp DeepDrftPublic/Interop/visualizer/hwAccel.test.ts DeepDrftPublic/wwwroot/js/visualizer/
* node DeepDrftPublic/wwwroot/js/visualizer/hwAccel.test.ts
*
* A thrown error / non-zero exit signals failure; "ALL <n> TESTS PASSED" signals success.
* Excluded from the production tsc build via tsconfig `exclude: Interop/ ** /*.test.ts`.
*/
import {
classifyHardwareAcceleration,
isSoftwareRenderer,
SOFTWARE_RENDERER_SIGNATURES,
} from './hwAccel.js';
// --- tiny inline harness (no dependencies) ---------------------------------------------------
let passed = 0;
const failures: string[] = [];
function test(name: string, fn: () => void): void {
try {
fn();
passed++;
} catch (e) {
failures.push(`FAIL: ${name}\n ${(e as Error).message}`);
}
}
function assertTrue(actual: boolean, msg?: string): void {
if (actual !== true) throw new Error(`${msg ?? 'assertTrue'}: expected true, got ${String(actual)}`);
}
function assertFalse(actual: boolean, msg?: string): void {
if (actual !== false) throw new Error(`${msg ?? 'assertFalse'}: expected false, got ${String(actual)}`);
}
// --- isSoftwareRenderer: positive matches -----------------------------------------------------
// Real-world software renderer strings, as reported by UNMASKED_RENDERER_WEBGL on accel-off configs.
const SOFTWARE_STRINGS = [
'Google SwiftShader',
'ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0) (0x0000C0DE)), SwiftShader driver)',
'llvmpipe (LLVM 12.0.0, 256 bits)',
'Gallium 0.4 on llvmpipe (LLVM 17.0.6, 256 bits)',
'softpipe',
'Microsoft Basic Render Driver',
'Mesa OffScreen',
'Software Rasterizer',
];
for (const s of SOFTWARE_STRINGS) {
test(`isSoftwareRenderer matches software string: "${s}"`, () => {
assertTrue(isSoftwareRenderer(s), `"${s}" should match a software signature`);
});
}
// --- isSoftwareRenderer: hardware (GPU) strings must NOT match ---------------------------------
const HARDWARE_STRINGS = [
'ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0, D3D11)',
'ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)',
'ANGLE (AMD, AMD Radeon RX 6800 XT Direct3D11 vs_5_0 ps_5_0, D3D11)',
'Apple GPU',
'Mali-G78',
'Adreno (TM) 650',
];
for (const s of HARDWARE_STRINGS) {
test(`isSoftwareRenderer rejects hardware string: "${s}"`, () => {
assertFalse(isSoftwareRenderer(s), `"${s}" should NOT match any software signature`);
});
}
// --- case-insensitivity -----------------------------------------------------------------------
test('isSoftwareRenderer is case-insensitive', () => {
assertTrue(isSoftwareRenderer('SWIFTSHADER'), 'upper-case must still match');
assertTrue(isSoftwareRenderer('LlVmPiPe'), 'mixed-case must still match');
});
test('every declared signature self-matches (sanity on the list)', () => {
for (const sig of SOFTWARE_RENDERER_SIGNATURES) {
assertTrue(isSoftwareRenderer(sig), `signature "${sig}" must match itself`);
}
});
// --- classifyHardwareAcceleration: the full policy --------------------------------------------
test('positive software match → NOT accelerated (lava off)', () => {
assertFalse(
classifyHardwareAcceleration(true, 'Google SwiftShader'),
'a working context with a software renderer must classify as not accelerated',
);
});
test('real GPU renderer → accelerated (lava on)', () => {
assertTrue(
classifyHardwareAcceleration(true, 'ANGLE (NVIDIA GeForce RTX 3080, D3D11)'),
'a working context with a GPU renderer must classify as accelerated',
);
});
// Uncertainty / default-on case: context works but the renderer string is masked or absent.
test('masked renderer (null) with a working context → accelerated (default on)', () => {
assertTrue(
classifyHardwareAcceleration(true, null),
'an unknown renderer must favor the HW-accel majority',
);
});
test('empty/whitespace renderer with a working context → accelerated (default on)', () => {
assertTrue(classifyHardwareAcceleration(true, ''), 'empty string is unknown, not software');
assertTrue(classifyHardwareAcceleration(true, ' '), 'whitespace is unknown, not software');
});
// Total-WebGL-failure case: no context at all → lava can't run → not accelerated.
test('no WebGL context at all → NOT accelerated (lava off), regardless of renderer arg', () => {
assertFalse(classifyHardwareAcceleration(false, null), 'no context → lava off');
assertFalse(
classifyHardwareAcceleration(false, 'ANGLE (NVIDIA GeForce RTX 3080, D3D11)'),
'no context dominates even a GPU-looking string',
);
});
// --- report ----------------------------------------------------------------------------------
if (failures.length > 0) {
console.error(failures.join('\n'));
throw new Error(`${failures.length} test(s) failed, ${passed} passed`);
}
console.log(`ALL ${passed} TESTS PASSED`);