What it measures
The signal reads the unmasked GPU vendor and renderer strings exposed by the WEBGL_debug_renderer_info WebGL extension. These raw strings are normalised to remove ANGLE wrappers, trademark symbols, and trailing suffixes, then bucketed into a coarse GPU family (such as apple silicon, intel integrated, nvidia, or amd). The hash is computed from the family bucket alone, not from the granular normalised string.
The granular vendor and renderer strings are preserved in value.normalised for diagnostics and are available to callers, but they do not influence the hash. Per-browser entropy from GPU caps and extension lists is captured separately by the engine-bound webgl_params signal.
Why a coarse family bucket
The three browser engines report the same GPU at different levels of granularity on Apple Silicon Macs. Chromium reports 'ANGLE (Apple, Apple M2 Pro, OpenGL 4.1)', Safari reports 'Apple GPU', and Firefox reports 'Apple M1' regardless of the actual chip. Hashing the granular renderer string produces three different hashes on the same machine. Hashing the coarse family bucket — apple silicon — produces one hash across all three engines, which is required for hardwareFingerprint to be cross-browser stable.
The M2-vs-M3-vs-M4 granularity within Chromium is sacrificed for stability, but that granularity was never recoverable cross-browser anyway. It remains available in value.normalised.renderer for callers that run only within Chromium.
GPU family buckets
| Bucket | Matches |
|---|---|
| apple silicon | Apple M-series chips, Safari's generic 'Apple GPU', Firefox's generic 'Apple M1' |
| apple legacy | Pre-M1 Apple GPUs (older iOS / Intel Mac PowerVR) |
| intel integrated | Intel HD, UHD, Iris, Iris Pro, Iris Plus, Iris Xe |
| nvidia | Any NVIDIA card — GeForce, RTX, GTX, Quadro, Tesla |
| amd | AMD Radeon and RX series |
| adreno | Qualcomm Adreno (mobile) |
| mali | ARM Mali (mobile) |
| powervr | Imagination PowerVR (mobile) |
| swiftshader | Chromium CPU fallback renderer (also caught by headless_gpu_detected flag) |
| llvmpipe | Mesa software fallback (also caught by headless_gpu_detected flag) |
| unknown | Empty vendor and renderer strings |
| <renderer-string> | Any unclassified GPU — falls back to the normalised renderer string |
Confidence rules
| Confidence | Trigger |
|---|---|
| normal | WEBGL_debug_renderer_info extension was available and data was collected |
| absent | WebGL context could not be created, or any unhandled exception |
Why hardware-bound
The GPU installed in a device does not change between browser sessions or between browser engines. While the string representation of that GPU differs by engine — Chromium wraps it in an ANGLE prefix, Safari reports a generic Apple GPU, Firefox may append 'or similar' — the normalisation pipeline canonicalises all of these to the same lowercase family bucket. The underlying hardware is what the bucket describes, making the hash stable across browsers on the same machine.
Context creation is fast (sub-millisecond) because only a 1x1 surface is needed to query the extension. The context is explicitly released via WEBGL_lose_context.loseContext() immediately after data collection to free GPU resources before garbage collection.
Things worth knowing
- getWebGLData() is not cached at module level. Each invocation creates a fresh context to prevent a concurrent getFingerprint() call from observing skewed timeMs values.
- When WEBGL_debug_renderer_info is absent but a WebGL context was obtained, the raw strings are empty and the collector still returns normal with a hash of the empty-string family bucket.
- The normaliseGpuRenderer function strips ANGLE wrappers, 'or similar' suffixes, trailing 'GPU' suffixes, and trademark symbols (R) and (TM) before lowercasing.
- Total collection time is typically under 5 ms. No timeouts or yield points are used.

