# webgl_image

> A 32 by 32 pixel WebGL drawing of one shaded triangle, read back twice and reported as a digest.

Source: https://whiskerjs.com/signals/webgl-image/ (last updated 2026-10-26)

**webgl_image at a glance (from the library code, signalsVersion 9)**

| Field | Value |
| --- | --- |
| Component name | `webgl_image` |
| In the visitorId | yes |
| structuredId group | [gpu (graphics)](https://whiskerjs.com/docs/identifiers/#structuredid) |
| Hashing | always a digest (first 128 bits of SHA-256) |
| Excluded under | Brave (brave_farbling); Firefox Fingerprinting Protection (firefox_fpp); Firefox resistFingerprinting (firefox_rfp); Tor Browser and Mullvad Browser (rfp_hardened); Safari Advanced Fingerprinting Protection (safari_afp) |
| Inner timeout | 1000 ms |
| Added in | [signalsVersion 1](https://whiskerjs.com/whats-new/) |

> **In plain words:**
>
> WhiskerJS asks the graphics chip to draw one small coloured triangle with a tiny shader program and reads the resulting pixels back. Different GPUs and drivers compute colours and edges in slightly different ways, so the pixels differ between devices. The drawing is done twice: if the two results are not identical, the browser is adding noise and the value is left out. A second check, a known colour read back from the same context, catches noise that is the same on both reads.

## What it reads

`webgl_image` uses the shared WebGL context of the collection: a detached 32 by 32 pixel canvas with a `webgl` (or `experimental-webgl`) context created with `preserveDrawingBuffer: true` and `antialias: false`. It compiles a vertex shader and a fragment shader, draws one triangle whose colour is computed per pixel from its position (with `fract` and `sin`), and reads all 32 by 32 RGBA pixels back with `readPixels()`. Those 4096 bytes are the value.

## How the technique works

1. Compile and link a minimal shader program, upload three vertices, clear the canvas to a fixed colour and draw the triangle.
2. Read the pixel buffer with `readPixels()`.
3. Draw and read again.
4. Compare the two buffers byte by byte. Any difference means `noisy` with ruleId `render_mismatch` (noise drawn on every read).
5. If they match, check the WebGL [known-answer probe](https://whiskerjs.com/signals/noise-profile/): the shared drawing buffer is cleared to the colour `(51, 102, 153, 255)` and read back twice with `readPixels()`. If it does not come back exact, the browser adds seeded noise that the double render cannot see, and the component is `noisy` with ruleId `seeded_noise`.
6. Otherwise the bytes are hashed with SHA-256 and the first 128 bits are reported.

The shader’s procedural colour depends on floating-point precision in the GPU’s fragment pipeline, and the triangle’s edges depend on the rasteriser, which is where devices tend to differ.

## Why it helps

Shader arithmetic and rasterisation are implemented by the GPU, its driver and the browser’s graphics layer (for example ANGLE on many Chromium builds). Different combinations can round colours or place edge pixels differently. The same combination normally gives the same bytes. It is part of the `gpu` group of the [structuredId](https://whiskerjs.com/glossary/#structured-id).

## Stability

It can change after a GPU driver update, a browser update that changes the graphics back end, switching hardware acceleration on or off, moving a window to a GPU that the browser treats differently, or a browser mode or extension that adds readback noise. A change of only the `gpu` structuredId part is the typical pattern; see [Comparing two results](https://whiskerjs.com/docs/identifiers/#comparing-two-results).

## Browser support and what blocks it

- `ok` wherever WebGL 1 is available, which includes current desktop and mobile versions of [Chrome](https://whiskerjs.com/browsers/chrome-desktop/), [Edge](https://whiskerjs.com/browsers/edge/), [Safari](https://whiskerjs.com/browsers/safari-macos/), [Firefox](https://whiskerjs.com/browsers/firefox/) and [Samsung Internet](https://whiskerjs.com/browsers/samsung-internet/), unless WebGL is turned off or blocked.
- [Brave](https://whiskerjs.com/browsers/brave/): `excluded` / `brave_farbling`.
- [Firefox with Fingerprinting Protection](https://whiskerjs.com/browsers/firefox/): `excluded` / `firefox_fpp`.
- [Firefox resistFingerprinting, Tor Browser and Mullvad Browser](https://whiskerjs.com/browsers/firefox-rfp-and-tor/): `excluded` / `firefox_rfp`, or `rfp_hardened` for Tor and Mullvad.
- [Safari with Advanced Fingerprinting Protection](https://whiskerjs.com/browsers/safari-macos/): `excluded` / `safari_afp`.
- Elsewhere, noise on every read gives `noisy` / `render_mismatch`, and seeded noise caught by the known-answer probe gives `noisy` / `seeded_noise`.
- Without WebGL (turned off, blocklisted GPU, some containers): `unavailable` / `no_context`. In the headless Firefox test run in a container used for these pages, all four WebGL components were `unavailable` / `no_context`.

## Privacy

The image is a fixed test pattern; it contains nothing about the page or the user. It reveals graphics hardware and driver behaviour. It leaves the library only as a 32-character digest. WebGL needs no permission and WhiskerJS makes no request.

## How WhiskerJS handles noise and failures

| Status | ruleId | When |
| --- | --- | --- |
| `ok` | none | two identical reads; value is a digest |
| `noisy` | `render_mismatch` | the two reads differed |
| `noisy` | `seeded_noise` | the reads agreed, but the WebGL known-answer probe came back wrong |
| `unavailable` | `no_document`, `no_api`, `no_context` | no document, no `getContext`, or no WebGL context |
| `unavailable` | `no_digest` | no Web Crypto to hash the bytes |
| `blocked` | `security_error`, `threw` | a WebGL call threw |
| `excluded` | `brave_farbling`, `firefox_fpp`, `firefox_rfp`, `rfp_hardened`, `safari_afp`, `caller_excluded` | exclusion table or your `exclude` list |
| `timed_out` | `timeout`, `aborted` | 1000 ms inner timeout, overall budget, abort |

## Example value

From a headless Chromium test browser in a container (software rendering through SwiftShader):

```json
"webgl_image": { "status": "ok", "value": "fc51c63a9379589055c4503f7b7a2cb6", "hashed": true }
```

## Questions and answers

### Why is webgl_image unavailable with no_context?

The browser could not create a WebGL 1 context: WebGL is turned off, the GPU is blocklisted, or the environment has no graphics support at all. The other WebGL components report the same reason because they share one context.

### Does WhiskerJS keep WebGL running after collect()?

No. One WebGL context is created per collection and released with the WEBGL_lose_context extension once every collector has settled.

### Why is it excluded in Brave, Firefox and Safari privacy modes?

Brave, Firefox Fingerprinting Protection, Firefox resistFingerprinting (also Tor Browser and Mullvad Browser) and Safari Advanced Fingerprinting Protection randomise or block WebGL readback on purpose. Reading it would only add noise, so the exclusion table skips it and reports brave_farbling, firefox_fpp, firefox_rfp, rfp_hardened or safari_afp.

### What does noisy with seeded_noise mean?

The two drawings agreed, but the WebGL known-answer probe (the drawing buffer cleared to a known colour and read back) came back wrong. The browser adds noise that is the same on every read within a session, so the value is left out and the id stays the same from session to session.

## Related pages

- [canvas](https://whiskerjs.com/signals/canvas/): The 2D canvas scene, also drawn twice.
- [webgl_params](https://whiskerjs.com/signals/webgl-params/): WebGL limits, precisions and extensions.
- [webgl_renderer](https://whiskerjs.com/signals/webgl-renderer/): The GPU name from the same context.
- [Double render](https://whiskerjs.com/glossary/#double-render): Why drawing twice detects noise.
- [noise_profile](https://whiskerjs.com/signals/noise-profile/): The known-answer probes, including the WebGL one.
- [Noise handling](https://whiskerjs.com/docs/how-it-works/#noise-handling-the-double-render-and-the-known-answers): The double render and the known answers in the docs.
