# Conformance suites as oracles: canvas, Web Audio and WebGL
Canvas 2D, WebGL and Web Audio each have a conformance suite with its own pass/fail
harness, so a generated engine can be checked against them without comparing pixels
across engines. What matters is how much of each suite the three shipping engines agree
on: a test all three pass has a verdict that trusts no single engine; two passing is a
majority; one or none says nothing reliable. Measured for WPT `html/canvas`, WPT
`webaudio` and the Khronos WebGL suite in Chromium, Firefox and WebKit on a GPU-less Linux
host; the `webaudio` numbers come from a build lane, whose PulseAudio null sink gives
realtime audio somewhere to play.
## Usable oracle coverage
| suite | tests run | all three pass | usable (≥2 pass) | usable, excluding tentative |
|---|---:|---:|---:|---:|
| WPT `html/canvas` | 3,889 | 77.5% | 82.5% | 92.7% of 3,435 |
| WPT `webaudio` | 352 | 52.6% | 79.8% | 88.4% of 302 |
| Khronos WebGL, with dEQP | 3,836 | 96.5% | 99.7% | (no tentative tests) |
- **WebGL is the strongest.** Its tests check pixels against values the test computes,
so software rasterizers agree; nearly every disagreement is a known engine gap.
- **Canvas 2D is strong for what has shipped.** Paths, transforms, state, shadows, line
styles, images, fills and strokes are 97–100% usable; the unusable part is unshipped
(`layers`, `filters`, `draw-element-image`). Text sits at 76%.
- **Web Audio is an oracle for offline rendering, and a fair one for realtime**:
`OfflineAudioContext` tests are 92.7% usable, realtime ones 77.1% with an audio sink
present.
None of the three fits "no practical oracle" in `docs/platform-economics.md`.
### By browser-compat-data entry
`oracle/suites/entry-coverage.mjs` takes each area's entries from browser-compat-data
8.0.4 by `spec_url` and counts an entry covered when a usable test names it in its own
source (members, constructors, events; interface members only in tests that also name
the interface; context members only in tests of the right kind). Harness and utility
scripts are not scanned.
| area | entries | mentioned by a usable test | mentioned only by tests without a two-engine verdict | not mentioned |
|---|---:|---:|---:|---:|
| Canvas 2D | 270 | 216 (80.0%) | 3 | 51 |
| Canvas 2D without `PaintRenderingContext2D` | 231 | 216 (93.5%) | 3 | 12 |
| WebGL | 495 | 484 (97.8%) | 0 | 11 |
| Web Audio | 257 | 215 (83.7%) | 16 | 26 |
Uncovered: in Canvas 2D, the 39 CSS Paint API context entries (tested elsewhere),
`isContextLost`, `getContextAttributes`, `transferControlToOffscreen`, OffscreenCanvas
context-loss events and seven `Path2D` methods only exercised on the context; in WebGL,
`WebGLContextEvent`, `drawingBufferColorSpace`/`Format`, `webglcontextcreationerror` and
three never-named interface objects; in Web Audio, `AudioPlaybackStats`, `AudioParamMap`,
the output-device entries and `ScriptProcessorNode.bufferSize`.
Read entry figures as a range: a mention is not an assertion (upper bound), unscanned
helpers and worklet modules hide real coverage (lower bound), and one passing test covers
an entry however shallowly. The not-mentioned counts are the firmest figure.
## How it was run
```mermaid
flowchart LR
fetch["fetch.mjs
pinned sparse checkouts"] --> serve["server.mjs
wptserve subset"]
serve --> run["run.mjs
one browser per engine,
contexts reused"]
run --> results["JSON line per
test per engine"]
results --> sum["summarize.mjs,
compare-wptfyi.mjs,
entry-coverage.mjs"]
```
- `oracle/suites/run.mjs` is separate from the oracle service, so the service's stdout
protocol is untouched. Each worker navigates one browser context from test to test
(recycled after a crash, a timeout or 100 tests); each result records
`browser.version()`. Raw results stay out of git.
- Suites are pinned by commit in `oracle/suites/suites.json`.
- `oracle/suites/server.mjs` implements the `wptserve` subset these directories need
(`.any.js`/`.window.js`/`.worker.js` wrappers, `.sub.`, `.headers`, the `header()`,
`status()` and `sub` pipes, an HTTPS twin). Python handlers answer 501 and their tests
are tagged `uses-py-handler`.
- WPT results come from a replacement `testharnessreport.js`; WebGL results from the
suite's own `webglTestHarness` hook. Reftests are compared by screenshot, honouring
`fuzzy`.
- `testdriver.js` tests run: `oracle/suites/server.mjs` serves a `testdriver-vendor.js` (WPT
ships it empty) that forwards every `test_driver_internal` call to a binding
`run.mjs` installs on each page, and `oracle/suites/testdriver.mjs` carries `click`,
`send_keys`, `action_sequence` and `set_permission` out through `page.mouse`,
`page.keyboard` and the context's permissions. A call it does not implement rejects by
name rather than hanging. `-manual` tests are still skipped by design.
- Timeouts follow WPT (10 s, 60 s long) and the WebGL runner (20 s idle, 300 s cap).
| engine | version | mode | WebGL renderer |
|---|---|---|---|
| Chromium | 153.0.8010.12 | headless, `--enable-unsafe-swiftshader` | ANGLE on SwiftShader (Vulkan) |
| Firefox | 155.0 | headful on a private Xvfb display | Mesa llvmpipe |
| WebKit | 26.6 | headless, with a GStreamer workaround preloaded (below) | reports "Apple GPU" (the real renderer string is masked) |
Firefox runs headful because headless Firefox on Linux has no GL. Chromium also gets
`--force-color-profile=srgb` and `--disable-lcd-text`, or every canvas grid reftest fails
on text antialiasing differences.
**WebM with a header-only Vorbis track.** A WebGL test video has a Vorbis track with
headers but no packets; GStreamer treats that as fatal and WebKit rejects the whole
element. No GStreamer release avoids it, so suite runs preload
`oracle/suites/gst-empty-vorbis.c` into WebKit (built by `engines.mjs`
`buildVorbisShim()`), which drops that one error. Without a C compiler, 174 video tests
time out. Canvas and Web Audio verdicts are unaffected. `oracle/suites/webm-probe.mjs`
checks video decoding alone.
### Checking the runner against wpt.fyi
`oracle/suites/compare-wptfyi.mjs` compares test verdicts with wpt.fyi at the same WPT
commit (wpt.fyi runs newer builds with experimental features on):
| engine | canvas agreement with wpt.fyi | webaudio agreement with wpt.fyi |
|---|---:|---:|
| Chromium | 3310/3875 (85.4%) | 306/352 (86.9%) |
| Firefox | 3852/3875 (99.4%) | 343/352 (97.4%) |
| WebKit | 3835/3875 (99.0%) | 314/352 (89.2%) |
Chromium's canvas gap is feature flags: with `run.mjs --experimental`, 346 of its 539
wpt.fyi-only passes pass; 176 more need another Blink flag; the rest are version
differences. Web Audio disagreements are mostly the tentative
`media-playback-while-not-visible-permission-policy` tests — 36 of Chromium's 46 and 10 of
WebKit's 38 — with realtime tests making up most of the rest: all 9 of Firefox's, 9 of
Chromium's and 15 of WebKit's. WebKit's other 13 run the opposite way, offline tests that
pass here and fail on wpt.fyi's Safari 27.0 against the 26.6 run here.
## WPT html/canvas
WPT commit `26f417a8c09fbfcc0b4fcf55760f31090ef2b772`. 3,905 tests, 3,889 run in all three
engines, 16 not run (all `-manual`).
| engine | pass | fail | timeout | error |
|---|---:|---:|---:|---:|
| Chromium | 3266 | 538 | 75 | 10 |
| Firefox | 3162 | 643 | 75 | 9 |
| WebKit | 3086 | 717 | 76 | 10 |
| tests | all three pass | two pass | one passes | none pass |
|---|---:|---:|---:|---:|
| all 3,889 | 3013 (77.5%) | 196 (5.0%) | 83 (2.1%) | 597 (15.4%) |
| 3,435 excluding tentative | 2990 (87.0%) | 194 (5.6%) | 78 (2.3%) | 173 (5.0%) |
Two-pass pairs: Chromium–Firefox 129, Chromium–WebKit 61, Firefox–WebKit 6; the lone
passer is Chromium in 63 of 83. Testharness tests are 90.2% usable, reftests 33.2%.
The seven tests the `testdriver.js` bridge recovered are all in `draw-element-image` and
`the-offscreen-canvas`, and their verdicts are about the feature under test rather than
about input: only `content-drawable-no-hit-test.tentative.html` passes, and it passes in
all three engines. The other six fail or time out in all three, every one of them on
`canvas.requestPaint` or `ctx.drawElementImage` being absent — the unshipped
`draw-element-image` proposal — except
`offscreencanvas.transferrable.sw.https.window.html`, which fails on the service-worker
transfer itself (an error in Chromium, a failure in Firefox and WebKit). The bridge
itself carried every call: pointer moves, double clicks, wheel scrolls and `bless`.
| area (element and offscreen together) | tests | usable (≥2 pass) |
|---|---:|---:|
| `path-objects`, `shadows`, `line-styles`, `transformations`, `the-canvas-state`, `canvas-host`, `drawing-rectangles-to-the-canvas`, `drawing-images-to-the-canvas`, `reset` | 1,435 | 100% |
| `canvas-context`, `color-type`, `conformance-requirements` and other small areas | 84 | 100% |
| `fill-and-stroke-styles` | 738 | 97.4% |
| `compositing`, `pixel-manipulation` | 520 | 91.7% |
| `text` | 376 | 76.3% |
| `manual` | 401 | 41.1% |
| `layers` | 242 | 13.6% |
| `filters` | 81 | 7.4% |
| `wide-gamut-canvas` | 12 | 25.0% |
`offscreen/` (2,329 tests) is 86.5% usable, close to `element/`. Subtests, over the 5,888
the full run covered: all three pass 71.2%, two 13.6%, one 3.1%, none 12.2%; the seven
recovered tests add seven more, three passing in all three engines and four in none.
Wall-clock, four in flight per engine, all three at once: Chromium 337 s, Firefox 524 s,
WebKit 337 s.
## WPT webaudio
Same commit. All 352 tests run, none skipped: the 4 that need `testdriver.js` are reached
by the bridge, and the run was taken on a build lane, where a PulseAudio null sink
(`docs/infrastructure.md` → "Build lane") gives realtime audio somewhere to play. All three
engines report `running` for a realtime `AudioContext` there; on a host without a sink
Firefox's stayed `suspended`.
| engine | pass | fail | timeout | error |
|---|---:|---:|---:|---:|
| Chromium | 291 | 54 | 3 | 4 |
| Firefox | 225 | 111 | 14 | 2 |
| WebKit | 257 | 61 | 27 | 7 |
| tests | all three pass | two pass | one passes | none pass |
|---|---:|---:|---:|---:|
| all 352 | 185 (52.6%) | 96 (27.3%) | 26 (7.4%) | 45 (12.8%) |
| 302 excluding tentative | 181 (59.9%) | 86 (28.5%) | 26 (8.6%) | 9 (3.0%) |
Firefox is still the outlier (66 of 96 two-pass tests are Chromium and WebKit), clustered
in `the-audioparam-interface` (26) and `the-audioworklet-interface` (11). The sink helped it
most: the same suite on a host without one gave Firefox 194 passes and 43 timeouts, against
225 and 14 here.
| tests | count | all three pass | usable (≥2 pass) |
|---|---:|---:|---:|
| uses `OfflineAudioContext` | 193 | 119 | 92.7% |
| creates a realtime `AudioContext` | 96 | 49 | 77.1% |
(Tagged by a source scan in `oracle/suites/wpt.mjs`; a test can carry both.) The offline
figure is unchanged by the sink, as expected; the realtime one rose from 67.7% and is now a
measurement rather than a floor.
Subtest names embed computed values, so test-level verdicts are the stable key. Wall-clock:
Chromium 111 s, Firefox 115 s, WebKit 145 s.
## Khronos WebGL conformance suite
KhronosGroup/WebGL commit `714857a28445e8f5d8d6ae1c78498578009534d8`, `sdk/tests`, read
through the suite's own `00_test_list.txt` rules, enumerated as 1.0.4 (`webglVersion=1`)
and 2.0.1 (`webglVersion=2`). One engine at a time, four in flight; timeouts rerun alone
(all six Chromium ones passed, one of Firefox's two, none of WebKit's fourteen).
| engine | pass | fail | timeout |
|---|---:|---:|---:|
| Chromium | 3002 | 10 | 0 |
| Firefox | 2948 | 63 | 1 |
| WebKit | 2963 | 35 | 14 |
| tests | all three pass | two pass | one passes | none pass |
|---|---:|---:|---:|---:|
| all 3,012 | 2904 (96.4%) | 96 (3.2%) | 9 (0.3%) | 3 (0.1%) |
| 940 under WebGL 1 | 916 | 20 | 3 | 1 |
| 2,072 under WebGL 2 | 1988 | 76 | 6 | 2 |
99.6% of the suite has a verdict at least two engines agree on. WebKit's timeouts are
OffscreenCanvas in workers and `khr-parallel-shader-compile`; Firefox's failures are
spread thin (GLSL validation, wide points, `ovr_multiview2`, 16 GB wasm memory).
Wall-clock on a loaded host: Chromium 1,061 s, Firefox 645 s, WebKit 341 s.
### dEQP ports
824 tests (793 WebGL 2), included with `run.mjs --deqp`:
| engine | pass | fail |
|---|---:|---:|
| Chromium | 805 | 19 |
| Firefox | 816 | 8 |
| WebKit | 822 | 2 |
All three pass 796 (96.6%), two 27, one 1, none 0: 99.9% usable. Across both parts,
3,836 tests: 3,700 pass everywhere (96.5%), 3,823 (99.7%) have a two-engine verdict.
## What could not run, and why
- **`-manual` canvas tests** (16): skipped by design, and not recoverable by driving input.
Each one draws something and asks a person to judge it. Fourteen are generated by WPT's
`gentest.py` and come in `element/` and `offscreen/` pairs: the two `shadows` blur tests
(`2d.shadow.blur.high`, `2d.shadow.blur.low`) and five `text` tests
(`2d.text.draw.fill.basic`, `.fill.maxWidth.large`, `.fill.rtl`, `.kern.consistent`,
`.stroke.basic`), all of which put a canvas beside a reference PNG for the eye to
compare, the `offscreen/` half rendering off-DOM with no comparison image at all. The
other two are `2d.canvas.host.scaled-manual.html`, the same eye-comparison for a
CSS-scaled canvas, and
`canvas_focus_drawFocusIfNeeded_AAPI_001-manual.html`, which asks for an accessibility
API inspector to watch a bounding rectangle move as focus moves. None of the sixteen
waits on a click or a key, so the `testdriver.js` bridge does not reach them; they would
need reference images and a pixel comparison, not input.
- **What the `testdriver.js` bridge does not carry.** It implements `click`, `send_keys`,
`action_sequence` (pointer, key, wheel and null sources) and `set_permission`, and
`bless` through `click`; every other call rejects with its own name in the message, so a
test that needs one fails rather than hanging. Coordinates and input go to the page's
own mouse and keyboard, so a call aimed at a child frame lands in the wrong place:
cross-frame and cross-origin driving (`test_driver.set_test_context`) is left out, along
with window management, virtual authenticators and device posture. Within
`action_sequence`, a touch or pen pointer has no Playwright device, and a sequence's
sources run one after another within a tick rather than together. `set_permission`
reaches only the permissions `context.grantPermissions` names, and which of those an
engine takes differs by engine; the rejection says which engine refused. No canvas test
calls `set_permission`, so that path is exercised only by
`oracle/suites/testdriver.test.mjs`.
- **Realtime audio, residual.** The build lanes provide a PulseAudio null sink
(`docs/infrastructure.md` → "Build lane") and the webaudio numbers above were measured
there, which closed most of the gap: realtime tests passing on wpt.fyi but failing here
fell from 20 to 8 for Firefox and from 16 to 13 for WebKit, with 8 for Chromium. The 29
that remain are not spread evenly — 13 are in `the-audioworklet-interface` (module
resolution and `MessagePort`, mostly timeouts), 5 are the
`the-mediaelementaudiosourcenode-interface` CORS tests, which error rather than fail, and
4 are `audiocontext-suspend-resume` and `suspend-with-navigation`. The other 7 are
singletons across analyser, destination, convolver, context-construction and
`MediaStreamAudioSourceNode` routing. Whether a sink with a real output clock would move
any of them is untested; the cluster shape suggests worklet startup and media loading
rather than the sink.
- **WebKit OffscreenCanvas-in-worker and `khr-parallel-shader-compile`**: 14 tests hang.
- **Python handlers**: 2 webaudio tests reach one.
- **No external check of WebGL verdicts**: nothing publishes Khronos results per build.
- **Renderer identity**: WebKit masks it.
- **Versions**: Playwright's Chromium 153, Firefox 155, WebKit 26.6; a verdict holds only
for the recorded build.
## Reproducing
```
cd oracle && npm install
node oracle/suites/fetch.mjs wpt
node oracle/suites/fetch.mjs webgl
node oracle/suites/run.mjs canvas --out=oracle/suites/results/canvas.jsonl
node oracle/suites/summarize.mjs --depth=2 oracle/suites/results/canvas.jsonl
node oracle/suites/compare-wptfyi.mjs oracle/suites/results/canvas.jsonl
node oracle/suites/run.mjs webaudio --out=oracle/suites/results/webaudio.jsonl
node oracle/suites/summarize.mjs --depth=2 oracle/suites/results/webaudio.jsonl
node oracle/suites/compare-wptfyi.mjs oracle/suites/results/webaudio.jsonl
for e in chromium firefox webkit; do
node oracle/suites/run.mjs webgl --engines=$e --out=oracle/suites/results/webgl-$e.jsonl
done
node oracle/suites/summarize.mjs --depth=2 oracle/suites/results/webgl-*.jsonl
```
Entry coverage, with `data.json` from `npm pack @mdn/browser-compat-data@8.0.4`:
```
node oracle/suites/entry-coverage.mjs --suite=canvas --bcd=DATA_JSON --list oracle/suites/results/canvas.jsonl
node oracle/suites/entry-coverage.mjs --suite=webaudio --bcd=DATA_JSON --list oracle/suites/results/webaudio.jsonl
node oracle/suites/entry-coverage.mjs --suite=webgl --bcd=DATA_JSON --list \
oracle/suites/results/webgl-{chromium,firefox,webkit}.jsonl \
oracle/suites/results/webgl-rerun-*.jsonl oracle/suites/results/webgl-deqp-*.jsonl
```
The `webaudio` run belongs on a build lane, whose null sink is what lets a realtime
`AudioContext` leave `suspended`: `bench/remote-lane.sh --pull
oracle/suites/results/webaudio.jsonl . node oracle/suites/run.mjs webaudio
--out=oracle/suites/results/webaudio.jsonl` runs it there and brings the results back.
`run.mjs --engines=firefox` runs one engine; `summarize.mjs` merges files, later records
replacing earlier ones, so a subset rerun (`--ids=FILE`) layers over a full run;
`--resume=FILE` continues an interrupted run.