feat(python): draw every identity's WebGL from fpgen

WebGL vendor, renderer, context attributes, extensions, parameters and
shader precisions, for WebGL1 and WebGL2, now come from fpgen's recorded
Firefox devices instead of webgl_data.db, which is deleted with the
camoufox/webgl/ package.

camoufox/webgl.py:
- webgl_for_gpu() traces `webgl` given Firefox, the OS and the GPU, then
  `webgl2` given the chosen `webgl` too, and draws each with one seeded
  random.Random. The GPU and the webgl value are pinned by their fpgen
  lookup index: a dict condition is flattened into leaves that overwrite
  each other, so only the renderer applied and Linux "Mesa" and "AMD"
  Radeon HD 3200 devices came back mixed.
- sample_webgl_for_screen() draws the GPU of a generated identity from
  fpgen's per-OS weights, filtering out software rasterisers, GPUs the OS
  cannot report, discrete GPUs behind a netbook screen and the
  resistFingerprinting "Mozilla" mask before the weighted choice, so there
  is no rejection loop. An empty pool raises.
- The draft/host-dependent extension filter moves over unchanged.

A preset's GPU and a caller's webgl_config pair are looked up as given;
a pair fpgen has never seen from Firefox on that OS raises instead of
falling back to another GPU. generate_context_fingerprint no longer
falls back to the host GPU when the draw fails.

For 10 of the 15 (GPU, OS) pairs the two sources share, one of fpgen's
records converts to exactly the database row on every value the browser
reads. The other five rows (Linux R9 200 and Radeon HD 3200, macOS
Intel HD, and two software rasterisers) are devices fpgen does not carry;
those GPUs now present fpgen's recorded devices instead. The Linux
GTX 980 row is kept as a test fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jake Writer
2026-09-25 21:59:27 -06:00
co-authored by Claude Opus 5.5
parent 40edebb925
commit 0d859b3a3d
14 changed files with 986 additions and 524 deletions
@@ -0,0 +1,567 @@
{
"webGl:renderer": "NVIDIA GeForce GTX 980, or similar",
"webGl:vendor": "NVIDIA Corporation",
"webGl:contextAttributes": {
"alpha": true,
"antialias": true,
"depth": true,
"failIfMajorPerformanceCaveat": false,
"powerPreference": "default",
"premultipliedAlpha": true,
"preserveDrawingBuffer": false,
"stencil": false
},
"webGl:supportedExtensions": [
"ANGLE_instanced_arrays",
"EXT_blend_minmax",
"EXT_color_buffer_half_float",
"EXT_depth_clamp",
"EXT_float_blend",
"EXT_frag_depth",
"EXT_shader_texture_lod",
"EXT_sRGB",
"EXT_texture_compression_bptc",
"EXT_texture_compression_rgtc",
"EXT_texture_filter_anisotropic",
"OES_element_index_uint",
"OES_fbo_render_mipmap",
"OES_standard_derivatives",
"OES_texture_float",
"OES_texture_float_linear",
"OES_texture_half_float",
"OES_texture_half_float_linear",
"OES_vertex_array_object",
"WEBGL_color_buffer_float",
"WEBGL_compressed_texture_etc",
"WEBGL_compressed_texture_s3tc",
"WEBGL_compressed_texture_s3tc_srgb",
"WEBGL_debug_renderer_info",
"WEBGL_debug_shaders",
"WEBGL_depth_texture",
"WEBGL_draw_buffers",
"WEBGL_lose_context"
],
"webGl:parameters": {
"2849": 1,
"2884": false,
"2885": 1029,
"2886": 2305,
"2928": [
0,
1
],
"2929": false,
"2930": true,
"2931": 1,
"2932": 513,
"2960": false,
"2961": 0,
"2962": 519,
"2963": 4294967295,
"2964": 7680,
"2965": 7680,
"2966": 7680,
"2967": 0,
"2968": 4294967295,
"2978": [
0,
0,
300,
150
],
"3024": true,
"3042": false,
"3074": null,
"3088": [
0,
0,
300,
150
],
"3089": false,
"3106": [
0,
0,
0,
0
],
"3107": [
true,
true,
true,
true
],
"3314": null,
"3315": null,
"3316": null,
"3317": 4,
"3330": null,
"3331": null,
"3332": null,
"3333": 4,
"3379": 32768,
"3386": [
32768,
32768
],
"3408": 8,
"3410": 8,
"3411": 8,
"3412": 8,
"3413": 8,
"3414": 24,
"3415": 0,
"7936": "Mozilla",
"7937": "NVIDIA GeForce GTX 980, or similar",
"7938": "WebGL 1.0",
"10752": 0,
"32773": [
0,
0,
0,
0
],
"32777": 32774,
"32823": false,
"32824": 0,
"32873": null,
"32877": null,
"32878": null,
"32883": null,
"32926": false,
"32928": false,
"32936": 1,
"32937": 4,
"32938": 1,
"32939": false,
"32968": 0,
"32969": 1,
"32970": 0,
"32971": 1,
"33000": null,
"33001": null,
"33170": 4352,
"33901": [
1,
2047
],
"33902": [
1,
10
],
"34016": 33984,
"34024": 32768,
"34045": null,
"34047": null,
"34068": null,
"34076": 32768,
"34467": null,
"34816": 519,
"34817": 7680,
"34818": 7680,
"34819": 7680,
"34852": null,
"34853": null,
"34854": null,
"34855": null,
"34856": null,
"34857": null,
"34858": null,
"34859": null,
"34860": null,
"34877": 32774,
"34921": 16,
"34930": 32,
"34964": null,
"34965": null,
"35071": null,
"35076": null,
"35077": null,
"35371": null,
"35373": null,
"35374": null,
"35375": null,
"35376": null,
"35377": null,
"35379": null,
"35380": null,
"35657": null,
"35658": null,
"35659": null,
"35660": 32,
"35661": 192,
"35723": null,
"35724": "WebGL GLSL ES 1.0",
"35725": null,
"35738": 5121,
"35739": 6408,
"35968": null,
"35977": null,
"35978": null,
"35979": null,
"36003": 0,
"36004": 4294967295,
"36005": 4294967295,
"36006": null,
"36007": null,
"36063": null,
"36183": null,
"36203": null,
"36345": null,
"36347": 1024,
"36348": 32,
"36349": 1024,
"36387": null,
"36388": null,
"36392": null,
"36795": null,
"37137": null,
"37154": null,
"37157": null,
"37440": false,
"37441": false,
"37443": 37444,
"37444": null,
"37445": "NVIDIA Corporation",
"37446": "NVIDIA GeForce GTX 980, or similar",
"37447": null,
"38449": null
},
"webGl:shaderPrecisionFormats": {
"35633,36336": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35633,36337": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35633,36338": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35633,36339": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35633,36340": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35633,36341": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35632,36336": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35632,36337": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35632,36338": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35632,36339": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35632,36340": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35632,36341": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
}
},
"webGl2:contextAttributes": {
"alpha": true,
"antialias": true,
"depth": true,
"failIfMajorPerformanceCaveat": false,
"powerPreference": "default",
"premultipliedAlpha": true,
"preserveDrawingBuffer": false,
"stencil": false
},
"webGl2:supportedExtensions": [
"EXT_color_buffer_float",
"EXT_depth_clamp",
"EXT_float_blend",
"EXT_texture_compression_bptc",
"EXT_texture_compression_rgtc",
"EXT_texture_filter_anisotropic",
"OES_draw_buffers_indexed",
"OES_texture_float_linear",
"WEBGL_compressed_texture_etc",
"WEBGL_compressed_texture_s3tc",
"WEBGL_compressed_texture_s3tc_srgb",
"WEBGL_debug_renderer_info",
"WEBGL_debug_shaders",
"WEBGL_lose_context"
],
"webGl2:parameters": {
"2849": 1,
"2884": false,
"2885": 1029,
"2886": 2305,
"2928": [
0,
1
],
"2929": false,
"2930": true,
"2931": 1,
"2932": 513,
"2960": false,
"2961": 0,
"2962": 519,
"2963": 4294967295,
"2964": 7680,
"2965": 7680,
"2966": 7680,
"2967": 0,
"2968": 4294967295,
"2978": [
0,
0,
300,
150
],
"3024": true,
"3042": false,
"3074": 1029,
"3088": [
0,
0,
300,
150
],
"3089": false,
"3106": [
0,
0,
0,
0
],
"3107": [
true,
true,
true,
true
],
"3314": 0,
"3315": 0,
"3316": 0,
"3317": 4,
"3330": 0,
"3331": 0,
"3332": 0,
"3333": 4,
"3379": 32768,
"3386": [
32768,
32768
],
"3408": 8,
"3410": 8,
"3411": 8,
"3412": 8,
"3413": 8,
"3414": 24,
"3415": 0,
"7936": "Mozilla",
"7937": "NVIDIA GeForce GTX 980, or similar",
"7938": "WebGL 2.0",
"10752": 0,
"32773": [
0,
0,
0,
0
],
"32777": 32774,
"32823": false,
"32824": 0,
"32873": null,
"32877": 0,
"32878": 0,
"32883": 16384,
"32926": false,
"32928": false,
"32936": 1,
"32937": 4,
"32938": 1,
"32939": false,
"32968": 0,
"32969": 1,
"32970": 0,
"32971": 1,
"33000": 1048576,
"33001": 1048576,
"33170": 4352,
"33901": [
1,
2047
],
"33902": [
1,
1
],
"34016": 33984,
"34024": 32768,
"34045": 15,
"34047": null,
"34068": null,
"34076": 32768,
"34467": null,
"34816": 519,
"34817": 7680,
"34818": 7680,
"34819": 7680,
"34852": 8,
"34853": 1029,
"34854": 0,
"34855": 0,
"34856": 0,
"34857": 0,
"34858": 0,
"34859": 0,
"34860": 0,
"34877": 32774,
"34921": 16,
"34930": 32,
"34964": null,
"34965": null,
"35071": 2048,
"35076": -8,
"35077": 7,
"35371": 14,
"35373": 14,
"35374": 84,
"35375": 84,
"35376": 65536,
"35377": 233472,
"35379": 233472,
"35380": 256,
"35657": 4096,
"35658": 4096,
"35659": 124,
"35660": 32,
"35661": 192,
"35723": 4352,
"35724": "WebGL GLSL ES 3.00",
"35725": null,
"35738": 5121,
"35739": 6408,
"35968": 4,
"35977": false,
"35978": 128,
"35979": 4,
"36003": 0,
"36004": 4294967295,
"36005": 4294967295,
"36006": null,
"36007": null,
"36063": 8,
"36183": 32,
"36203": 4294967295,
"36345": null,
"36347": 1024,
"36348": 32,
"36349": 1024,
"36387": false,
"36388": false,
"36392": null,
"36795": null,
"37137": 1.8446744073709552e+19,
"37154": 128,
"37157": 128,
"37440": false,
"37441": false,
"37443": 37444,
"37444": null,
"37445": "NVIDIA Corporation",
"37446": "NVIDIA GeForce GTX 980, or similar",
"37447": 1000000000,
"38449": null
},
"webGl2:shaderPrecisionFormats": {
"35633,36336": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35633,36337": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35633,36338": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35633,36339": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35633,36340": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35633,36341": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35632,36336": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35632,36337": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35632,36338": {
"rangeMin": 127,
"rangeMax": 127,
"precision": 23
},
"35632,36339": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35632,36340": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
},
"35632,36341": {
"rangeMin": 24,
"rangeMax": 24,
"precision": 0
}
},
"webGl2Enabled": true
}
-14
View File
@@ -5,8 +5,6 @@ a page can see. Each site warns with a report block the user can paste into a
GitHub issue, so the failure reaches us instead of shipping silently.
"""
import sqlite3
import pytest
from test_identity_salt import launch
@@ -56,7 +54,6 @@ def test_preset_voice_draw(monkeypatch):
[
("_generate_random_font_subset", OSError("fonts.json missing"), "fonts"),
("_generate_random_voice_subset", ValueError("bad manifest"), "voices"),
("sample_webgl_for_screen", sqlite3.OperationalError("no such table"), "webGl:renderer"),
],
)
def test_context_draws(monkeypatch, target, error, key):
@@ -83,14 +80,3 @@ def test_launch_font_draw(monkeypatch):
with pytest.warns(FallbackWarning, match=REPORT):
config = launch()
assert config["fonts"]
def test_preset_gpu_not_in_webgl_data():
# Windows reports every GPU through ANGLE, so only an ANGLE string reaches the lookup.
GPU = "ANGLE (Acme, Acme GPU 9000 Direct3D11 vs_5_0 ps_5_0)"
preset = fp.load_presets("152")["presets"]["windows"][0]
preset = {**preset, "webgl": {"unmaskedVendor": "Google Inc. (Acme)", "unmaskedRenderer": GPU}}
with pytest.warns(FallbackWarning, match=REPORT) as record:
config = launch(os="windows", fingerprint_preset=preset)
assert GPU in _report(record)
assert config["webGl:renderer"] != GPU
+3 -13
View File
@@ -83,23 +83,13 @@ class TestPinnedIdentityIsStable:
@pytest.mark.parametrize("os_name", ["windows", "macos", "linux"])
def test_every_bundled_preset_launches(self, os_name):
# The test above draws ONE preset at random, so a preset that cannot
# launch shows up as a 1-in-11 flake rather than a failure -- which is
# how it reached CI. 39 of the 435 bundled presets name a GPU that is
# not among the 31 in webgl_data.db, and sample_webgl raises for those.
# Every preset has to produce launch options; see the fallback in
# utils.launch_options.
from camoufox.webgl import sample_webgl
# launch would show up as a flake rather than a failure. Every preset
# must launch with its own GPU and that GPU's recorded parameters.
presets = fp.load_presets("150")["presets"][os_name]
key = {"windows": "win", "macos": "mac", "linux": "lin"}[os_name]
for i, preset in enumerate(presets):
config = launch(os=os_name, fingerprint_preset=preset)
# Whatever GPU survives, the renderer the page reads and the
# parameters behind it must come from the SAME recorded device --
# merge_into does not overwrite, so a fallback that forgets to drop
# the preset's pair leaves one device's name on another's data.
assert config["webGl:renderer"] == preset["webgl"]["unmaskedRenderer"], (os_name, i)
assert config.get("webGl:parameters"), (os_name, i)
sample_webgl(key, config["webGl:vendor"], config["webGl:renderer"])
def test_caller_seed_is_kept(self):
assert launch(config={"audio:seed": 9})["audio:seed"] == 9
+2 -54
View File
@@ -9,11 +9,10 @@ rules -- `scripts/clean-fingerprint-data.py --write` is what makes these pass.
Dropped on 2026-09-17: 38 of 435 presets (26 with a GPU their OS cannot report,
7 pairing Apple Silicon with a core count Apple never shipped, 4 with a colour
depth their GPU contradicts, 3 with a phone viewport, 1 claiming 40 touch
points), and 2 impossible macOS weights in webgl_data.db.
points).
"""
import json
import sqlite3
import sys
from os.path import dirname, join
from pathlib import Path
@@ -24,7 +23,6 @@ sys.path.insert(0, join(dirname(__file__), ".."))
from camoufox import coherence # noqa: E402
from camoufox.fingerprints import from_preset # noqa: E402
from camoufox.webgl.sample import DB_PATH # noqa: E402
DATA = Path(__file__).parent.parent / "camoufox"
PRESET_FILES = ("fingerprint-presets.json", "fingerprint-presets-v150.json")
@@ -46,62 +44,12 @@ def test_every_bundled_preset_is_coherent_as_stored(filename):
)
def test_no_gpu_is_offered_to_an_os_that_cannot_report_it():
connection = sqlite3.connect(DB_PATH)
try:
rows = connection.execute(
"SELECT vendor, renderer, win, mac, lin FROM webgl_fingerprints"
).fetchall()
finally:
connection.close()
assert rows, "webgl_data.db is empty"
for vendor, renderer, *weights in rows:
for os_key, weight in zip(("win", "mac", "lin"), weights):
if weight and weight > 0:
assert coherence.gpu_fits_os(renderer, os_key), (
f"{renderer!r} is offered to {os_key} at {weight}"
)
def test_each_os_still_has_gpus_to_draw_from():
"""The filter must not empty a pool -- a single GPU per OS is its own tell."""
connection = sqlite3.connect(DB_PATH)
try:
for os_key in ("win", "mac", "lin"):
count = connection.execute(
f"SELECT COUNT(*) FROM webgl_fingerprints WHERE {os_key} > 0" # nosec
).fetchone()[0]
assert count >= 2, f"{os_key} has {count} GPU(s) left"
finally:
connection.close()
def test_every_gpu_row_can_be_drawn_on_some_os():
"""A row no OS can draw is dead weight a reader would take as real data."""
connection = sqlite3.connect(DB_PATH)
try:
rows = connection.execute(
"SELECT vendor, renderer, win, mac, lin FROM webgl_fingerprints"
).fetchall()
finally:
connection.close()
undrawable = [
(vendor, renderer)
for vendor, renderer, *weights in rows
if not any(
weight > 0 and coherence.gpu_fits_os(renderer, os_key)
for os_key, weight in zip(("win", "mac", "lin"), weights)
)
]
assert undrawable == []
@pytest.mark.parametrize("filename", PRESET_FILES)
def test_every_preset_gpu_has_webgl_data(filename):
"""A preset records only its GPU's name; the WebGL parameters behind it come
from fpgen. A GPU fpgen has never seen Firefox report on that OS has none, so
launching it would pair the name with another device's parameters."""
from camoufox.fingerprints import firefox_gpus
from camoufox.webgl import firefox_gpus
presets = json.loads((DATA / filename).read_text())["presets"]
for os_name, entries in presets.items():
+186
View File
@@ -0,0 +1,186 @@
"""WebGL identities drawn from fpgen's recorded Firefox devices (camoufox.webgl)."""
import json
from pathlib import Path
import pytest
from test_identity_salt import host, launch
from camoufox import coherence, utils
from camoufox import fingerprints as fp
from camoufox import webgl
from camoufox.fingerprints import gpu_screen_is_plausible, is_software_renderer
from camoufox.webgl import sample_webgl_for_screen, webgl_for_gpu
SEEDS = range(300)
OSES = ("win", "mac", "lin")
_GTX_980_LINUX = ("NVIDIA Corporation", "NVIDIA GeForce GTX 980, or similar")
_BASIC_RENDER_DRIVER = (
"Google Inc. (Microsoft)",
"ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar",
)
# Limits WebGL1 and WebGL2 read from the same device: MAX_TEXTURE_SIZE,
# MAX_VIEWPORT_DIMS, MAX_RENDERBUFFER_SIZE, MAX_CUBE_MAP_TEXTURE_SIZE,
# MAX_VERTEX_ATTRIBS, MAX_TEXTURE_IMAGE_UNITS, MAX_VERTEX_TEXTURE_IMAGE_UNITS,
# MAX_COMBINED_TEXTURE_IMAGE_UNITS and the three uniform/varying vector limits.
_SHARED_LIMITS = ("3379", "3386", "34024", "34076", "34921", "34930", "35660", "35661", "36347", "36348", "36349")
def _as_json(value):
# JSON has one number type: 2**64 stored as an int and as a float is the
# same value to the browser's parser.
return json.loads(json.dumps(value), parse_int=float)
def test_converter_reproduces_the_recorded_device():
"""The fixture is what the retired webgl_data.db gave launch_options for
this GPU. fpgen records the same device, so everything the browser reads
must come out identical: parameters are compared where the old row had a
value, less the UNMASKED_* strings, which the browser takes from
webGl:vendor/renderer rather than the table."""
old = json.loads((Path(__file__).parent / "data" / "webgl-gtx980-linux.json").read_text())
new = webgl_for_gpu("lin", *_GTX_980_LINUX, seed=0)
assert new.keys() == old.keys()
for key in old:
if key.endswith(":parameters"):
recorded = {
pname: value
for pname, value in old[key].items()
if value is not None and pname not in ("37445", "37446")
}
assert _as_json({pname: new[key].get(pname) for pname in recorded}) == _as_json(recorded), key
else:
assert new[key] == old[key], key
def test_same_seed_same_device():
for target_os in OSES:
for seed in (0, 1, 12345):
assert sample_webgl_for_screen(target_os, 1920, 1080, seed) == sample_webgl_for_screen(
target_os, 1920, 1080, seed
)
gpu = ("AMD", "Radeon R9 200 Series, or similar")
assert webgl_for_gpu("lin", *gpu, seed=7) == webgl_for_gpu("lin", *gpu, seed=7)
def test_seed_chooses_among_a_gpus_recorded_devices():
# fpgen records several Linux R9 200 devices; one seed must not pin them all.
drawn = {json.dumps(webgl_for_gpu("lin", "AMD", "Radeon R9 200 Series, or similar", seed=s)) for s in range(40)}
assert len(drawn) > 1
@pytest.mark.parametrize("target_os", OSES)
def test_synthetic_draw_is_a_hardware_gpu_the_os_reports(target_os):
renderers = {sample_webgl_for_screen(target_os, 1920, 1080, s)["webGl:renderer"] for s in SEEDS}
for renderer in renderers:
assert not is_software_renderer(renderer), renderer
assert renderer != "Mozilla"
assert coherence.gpu_fits_os(renderer, target_os), renderer
# A single GPU per OS is its own tell.
assert len(renderers) >= 2
@pytest.mark.parametrize("target_os", OSES)
def test_netbook_screen_never_draws_a_discrete_gpu(target_os):
for seed in SEEDS:
renderer = sample_webgl_for_screen(target_os, 1024, 600, seed)["webGl:renderer"]
assert gpu_screen_is_plausible(renderer, 1024, 600), renderer
def test_no_coherent_gpu_raises(monkeypatch):
# A pool with nothing that fits is a data defect; substituting a GPU the
# filters rejected would present exactly what they exist to prevent.
only_software = tuple(r for r in webgl._trace("gpu", "lin") if is_software_renderer(r.value["renderer"]))
assert only_software
monkeypatch.setattr(webgl, "_trace", lambda *a, **kw: only_software)
with pytest.raises(ValueError, match="No recorded lin GPU"):
sample_webgl_for_screen("lin", 1920, 1080, seed=0)
@pytest.mark.parametrize("target_os", OSES)
def test_webgl2_comes_from_the_same_device_as_webgl1(target_os):
# webgl2 is pinned to the drawn webgl; drawn on the GPU alone, a Linux
# Intel identity paired MAX_TEXTURE_SIZE 8192 with 16384.
for seed in SEEDS:
config = sample_webgl_for_screen(target_os, 1920, 1080, seed)
for pname in _SHARED_LIMITS:
assert config["webGl:parameters"][pname] == config["webGl2:parameters"][pname], (seed, pname)
def test_gpu_is_pinned_by_vendor_and_renderer():
# "Mesa" and "AMD" both report this renderer on Linux. A dict condition on
# fpgen matches the renderer alone and mixed their devices.
for seed in range(40):
assert webgl_for_gpu("lin", "Mesa", "Radeon HD 3200 Graphics, or similar", seed)["webGl:vendor"] == "Mesa"
def test_device_without_webgl2():
config = webgl_for_gpu("win", *_BASIC_RENDER_DRIVER, seed=0)
assert config["webGl2Enabled"] is False
assert not any(key.startswith("webGl2:") for key in config)
with host():
options = utils.launch_options(
os="windows", webgl_config=_BASIC_RENDER_DRIVER, headless=True, i_know_what_im_doing=True
)
assert options["firefox_user_prefs"]["webgl.enable-webgl2"] is False
def test_unknown_webgl_config_raises():
with pytest.raises(ValueError, match="No recorded WebGL data"):
launch(os="windows", webgl_config=("Apple", "Apple M1, or similar"))
def test_preset_keeps_its_own_gpu():
preset = fp.load_presets("152")["presets"]["linux"][0]
gpu = (preset["webgl"]["unmaskedVendor"], preset["webgl"]["unmaskedRenderer"])
config = launch(os="linux", fingerprint_preset=preset)
assert (config["webGl:vendor"], config["webGl:renderer"]) == gpu
pinned = (webgl._pin("gpu", {"vendor": gpu[0], "renderer": gpu[1]}),)
recorded = [webgl.to_config(r.value, [], "lin")["webGl:parameters"] for r in webgl._trace("webgl", "lin", pinned)]
assert config["webGl:parameters"] in recorded
def test_preset_gpu_fpgen_has_never_seen_raises():
preset = fp.load_presets("152")["presets"]["windows"][0]
gpu = "ANGLE (Acme, Acme GPU 9000 Direct3D11 vs_5_0 ps_5_0)"
preset = {**preset, "webgl": {"unmaskedVendor": "Google Inc. (Acme)", "unmaskedRenderer": gpu}}
with pytest.raises(ValueError, match="Acme GPU 9000"):
launch(os="windows", fingerprint_preset=preset)
# -- extensions ---------------------------------------------------------------
def _extensions(target_os, key):
return [
set(sample_webgl_for_screen(target_os, 1920, 1080, s).get(key) or ()) for s in range(100)
]
def test_windows_keeps_ovr_multiview2_on_webgl2():
assert any("OVR_multiview2" in exts for exts in _extensions("win", "webGl2:supportedExtensions"))
def test_linux_filters_ovr_multiview2():
# fpgen records it on ~20% of Linux WebGL2 devices.
assert not any("OVR_multiview2" in exts for exts in _extensions("lin", "webGl2:supportedExtensions"))
def test_draft_extensions_filtered_on_every_os():
recorded = {
"vendor": "v",
"renderer": "r",
"contextAttributes": {},
"params": {},
"shaderPrecisionFormats": [],
"supportedExtensions": ["ANGLE_instanced_arrays", "WEBGL_multi_draw", "WEBGL_compressed_texture_etc1"],
}
webgl2 = {**recorded, "supportedExtensions": ["EXT_texture_norm16", "WEBGL_clip_cull_distance", "OVR_multiview2"]}
for target_os in OSES:
config = webgl.to_config(recorded, webgl2, target_os)
assert config["webGl:supportedExtensions"] == ["ANGLE_instanced_arrays"]
assert config["webGl2:supportedExtensions"] == (["OVR_multiview2"] if target_os == "win" else [])
@@ -1,60 +0,0 @@
"""Which sampled WebGL extensions reach the page, per OS."""
import sqlite3
import orjson
from camoufox.webgl import sample
from camoufox.webgl.sample import DB_PATH, _load_webgl_data
def _row_with(ext, key="webGl2:supportedExtensions", os="win"):
con = sqlite3.connect(DB_PATH)
try:
for (data,) in con.execute(f"SELECT data FROM webgl_fingerprints WHERE {os} > 0"): # nosec
if ext in (orjson.loads(data).get(key) or []):
return data
finally:
con.close()
raise AssertionError(f"no {os} row carries {ext}")
def test_windows_keeps_ovr_multiview2_on_webgl2():
data = _load_webgl_data(_row_with("OVR_multiview2"), "win")
assert "OVR_multiview2" in data["webGl2:supportedExtensions"]
def test_linux_filters_ovr_multiview2():
data = _load_webgl_data(_row_with("OVR_multiview2", os="lin"), "lin")
assert "OVR_multiview2" not in data["webGl2:supportedExtensions"]
def test_ovr_multiview2_never_on_webgl1_in_corpus():
con = sqlite3.connect(DB_PATH)
try:
for (data,) in con.execute("SELECT data FROM webgl_fingerprints"):
assert "OVR_multiview2" not in (orjson.loads(data).get("webGl:supportedExtensions") or [])
finally:
con.close()
def test_draft_extensions_filtered_on_every_os():
blob = orjson.dumps(
{
"webGl:supportedExtensions": ["ANGLE_instanced_arrays", "WEBGL_multi_draw"],
"webGl2:supportedExtensions": ["EXT_texture_norm16", "WEBGL_clip_cull_distance", "OVR_multiview2"],
}
)
for os in ("win", "mac", "lin"):
data = _load_webgl_data(blob, os)
assert data["webGl:supportedExtensions"] == ["ANGLE_instanced_arrays"]
assert "EXT_texture_norm16" not in data["webGl2:supportedExtensions"]
assert "WEBGL_clip_cull_distance" not in data["webGl2:supportedExtensions"]
def test_sampled_windows_identities_can_carry_it():
hits = sum(
"OVR_multiview2" in (sample.sample_webgl("win", seed=s).get("webGl2:supportedExtensions") or [])
for s in range(200)
)
assert hits > 0
@@ -4,8 +4,8 @@ Tests for the WebGL <-> screen coherence helpers in camoufox.fingerprints.
Run with:
cd pythonlib && python -m pytest tests/test_webgl_screen_consistency.py -v
The regression these guard (daijro/camoufox#729): BrowserForge picks the
screen, webgl_data.db picks the GPU, and nothing ties them together -- so the
The regression these guard (daijro/camoufox#729): the generator picks the
screen, camoufox.webgl picks the GPU, and nothing ties them together -- so the
synthetic path can emit pairs no real machine ships (a discrete GPU behind a
1024x600 netbook panel).
@@ -24,15 +24,14 @@ import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from camoufox import fingerprints # noqa: E402
from camoufox.fingerprints import ( # noqa: E402
MODERN_SCREEN_FLOOR,
_renderer_bucket,
gpu_screen_is_plausible,
is_software_renderer,
raise_screen_to_modern_floor,
sample_webgl_for_screen,
)
from camoufox.webgl import sample_webgl_for_screen # noqa: E402
# The three spellings Gecko emits for one discrete-NVIDIA bucket.
_NV_ANGLE = "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar"
@@ -153,64 +152,10 @@ def test_hardware_is_not_mistaken_for_software(monkeypatch):
assert not is_software_renderer(renderer)
def test_software_first_draw_is_resampled_to_hardware(monkeypatch):
"""A presented llvmpipe / WARP / SwiftShader is the first thing every
consumer-hardware check flags (measured 2026-09-14 with sundial), so a
software first draw is retried until a hardware renderer that fits the
screen comes up, and only kept when the pool offers nothing else."""
draws = iter([{"webGl:renderer": _LLVMPIPE}, {"webGl:renderer": _INTEL}])
monkeypatch.setattr(fingerprints, "sample_webgl", lambda *a, **kw: next(draws))
assert sample_webgl_for_screen("lin", 1024, 600)["webGl:renderer"] == _INTEL
only_software = iter([{"webGl:renderer": _LLVMPIPE}] * 40)
monkeypatch.setattr(fingerprints, "sample_webgl", lambda *a, **kw: next(only_software))
assert sample_webgl_for_screen("lin", 1920, 1080)["webGl:renderer"] == _LLVMPIPE
def test_software_draws_are_skipped_when_resampling(monkeypatch):
# A hardware first draw settles the class as hardware, so a software
# candidate mid-loop is skipped instead of accepted -- otherwise the
# rejection loop still leaks probability mass onto the rasterizers.
draws = iter(
[
{"webGl:renderer": _NV_ANGLE}, # implausible at 1024x600
{"webGl:renderer": _LLVMPIPE}, # plausible, but wrong class
{"webGl:renderer": _INTEL}, # the coherent hardware answer
]
)
monkeypatch.setattr(fingerprints, "sample_webgl", lambda *a, **kw: next(draws))
assert sample_webgl_for_screen("lin", 1024, 600)["webGl:renderer"] == _INTEL
def test_falls_back_to_the_first_draw_when_nothing_is_coherent(monkeypatch):
monkeypatch.setattr(
fingerprints, "sample_webgl", lambda *a, **kw: {"webGl:renderer": _NV_ANGLE}
)
fp = sample_webgl_for_screen("win", 800, 600, attempts=4)
assert fp["webGl:renderer"] == _NV_ANGLE
def test_a_plausible_first_draw_costs_one_query(monkeypatch):
# sample_webgl opens a fresh sqlite connection per call, and the common
# case (any screen at or above the floor) must not pay for 32 of them.
calls = []
def _counted(*args, **kwargs):
calls.append(args)
return {"webGl:renderer": _NV_ANGLE}
monkeypatch.setattr(fingerprints, "sample_webgl", _counted)
sample_webgl_for_screen("win", 1920, 1080)
assert len(calls) == 1
@pytest.mark.parametrize("target_os", ["win", "mac", "lin"])
def test_sampled_gpu_is_coherent_with_the_screen(target_os):
# Against the real webgl_data.db pool.
for _ in range(25):
fp = sample_webgl_for_screen(target_os, 1280, 800)
for seed in range(25):
fp = sample_webgl_for_screen(target_os, 1280, 800, seed=seed)
assert gpu_screen_is_plausible(fp.get("webGl:renderer"), 1280, 800)
@@ -257,8 +202,8 @@ def test_screen_floor_is_a_no_op_without_screen_values():
@pytest.mark.parametrize("target_os", ["windows", "macos", "linux"])
def test_context_fingerprints_get_the_same_treatment(target_os):
"""generate_context_fingerprint() is the per-context API #729 names, and
build-tester drives the browser through it. It sampled the GPU with a bare
sample_webgl() and never applied the floor, so the coherence fix reached
build-tester drives the browser through it. It drew the GPU without the
screen and never applied the floor, so the coherence fix reached
launch_options() only."""
from camoufox.fingerprints import generate_context_fingerprint