import { componentInterface } from '../../factory' import { hash } from '../../utils/hash' import { getCommonPixels } from '../../utils/commonPixels'; import { getBrowser } from '../system/browser'; const _RUNS = (getBrowser().name !== 'SamsungBrowser') ? 1 : 3; const _USE_CACHE = getBrowser().name !== 'Brave'; // Canvas and viewport dimensions — part of the fingerprint signal, do not change. const _CANVAS_W = 200; const _CANVAS_H = 100; const _NUM_SPOKES = 137; // Shader sources are constant — hoist to module scope to avoid per-call string allocation. const _VERTEX_SHADER_SRC = ` attribute vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); } `; const _FRAGMENT_SHADER_SRC = ` precision mediump float; void main() { gl_FragColor = vec4(0.812, 0.195, 0.553, 0.921); // Set line color } `; // Precompute the spoke vertices once at module load. The values are determined // solely by _NUM_SPOKES, _CANVAS_W, and _CANVAS_H — all module constants — // so they are identical to what the old per-call computation produced. const _VERTICES: Float32Array = (() => { const v = new Float32Array(_NUM_SPOKES * 4); const angleIncrement = (2 * Math.PI) / _NUM_SPOKES; for (let i = 0; i < _NUM_SPOKES; i++) { const angle = i * angleIncrement; v[i * 4] = 0; // Center X v[i * 4 + 1] = 0; // Center Y v[i * 4 + 2] = Math.cos(angle) * (_CANVAS_W / 2); // Endpoint X v[i * 4 + 3] = Math.sin(angle) * (_CANVAS_H / 2); // Endpoint Y } return v; })(); interface WebGLCache { canvas: HTMLCanvasElement; gl: WebGLRenderingContext; program: WebGLProgram; buffer: WebGLBuffer; } // Module-scope cache — only populated when _USE_CACHE is true (non-Brave). let _cache: WebGLCache | null = null; /** Test-only: reset the module-scope cache between test runs. */ export function __resetWebGLCache(): void { _cache = null; } /** Test-only: read the current cache reference without mutating it. */ export function __getWebGLCache(): WebGLCache | null { return _cache; } /** * Create a canvas, compile shaders, link program, and upload vertex data. * Returns null if any step fails so callers can degrade gracefully. */ function setupWebGL(): WebGLCache | null { try { if (typeof document === 'undefined') return null; const canvas = document.createElement('canvas'); canvas.width = _CANVAS_W; canvas.height = _CANVAS_H; const gl = canvas.getContext('webgl'); if (!gl) return null; // When the browser invalidates GPU resources it fires webglcontextlost. // Null the cache so the next getOrInitCache() rebuilds via a fresh setupWebGL(). // { once: true } ensures the listener self-removes after firing so the old // canvas does not keep the WebGLCache object alive after recovery. canvas.addEventListener('webglcontextlost', (event) => { event.preventDefault(); _cache = null; }, { once: true }); const vertexShader = gl.createShader(gl.VERTEX_SHADER); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); if (!vertexShader || !fragmentShader) return null; gl.shaderSource(vertexShader, _VERTEX_SHADER_SRC); gl.compileShader(vertexShader); if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) return null; gl.shaderSource(fragmentShader, _FRAGMENT_SHADER_SRC); gl.compileShader(fragmentShader); if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) return null; const program = gl.createProgram(); if (!program) return null; gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return null; const buffer = gl.createBuffer(); if (!buffer) return null; gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, _VERTICES, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); return { canvas, gl, program, buffer }; } catch (_) { return null; } } /** * For non-Brave browsers: lazily initialise once and reuse. * For Brave: always create fresh (Brave farbles WebGL per-context, preserving * the noise signal that today's per-call setup drives). */ function getOrInitCache(): WebGLCache | null { if (_USE_CACHE) { if (!_cache) _cache = setupWebGL(); return _cache; } // Brave path: fresh context every call, byte-identical behaviour to pre-cache code. return setupWebGL(); } /** * Execute one render pass on the shared (or fresh) WebGL context and return * the raw pixel data as an ImageData. Returns a 1×1 blank ImageData on error. */ function renderImage(cache: WebGLCache): ImageData { const { canvas, gl, program, buffer } = cache; try { gl.useProgram(program); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); const positionAttribute = gl.getAttribLocation(program, 'position'); gl.enableVertexAttribArray(positionAttribute); gl.vertexAttribPointer(positionAttribute, 2, gl.FLOAT, false, 0, 0); gl.viewport(0, 0, canvas.width, canvas.height); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.LINES, 0, _NUM_SPOKES * 2); const pixelData = new Uint8ClampedArray(canvas.width * canvas.height * 4); gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixelData); return new ImageData(pixelData, canvas.width, canvas.height); } catch (_) { // Belt-and-suspenders: any render failure (context loss, GPU driver glitch, etc.) // invalidates the cache so the next call rebuilds rather than retrying with a stale context. _cache = null; return new ImageData(1, 1); } finally { // Reset WebGL state to match pre-cache behaviour. gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.useProgram(null); gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.clearColor(0.0, 0.0, 0.0, 0.0); } } export default async function getWebGL(): Promise { const cache = getOrInitCache(); if (!cache) { return { 'webgl': 'unsupported' }; } try { const imageDatas: ImageData[] = Array.from({ length: _RUNS }, () => renderImage(cache)); const commonImageData = getCommonPixels(imageDatas, cache.canvas.width, cache.canvas.height); return { 'commonPixelsHash': hash(commonImageData.data.toString()).toString(), }; } catch (_) { return { 'webgl': 'unsupported' }; } }