Files
camoufox/pythonlib/tests/test_webgl.py
T
Jake WriterandClaude Opus 5.5 0d859b3a3d 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>
2026-09-25 21:59:27 -06:00

187 lines
7.9 KiB
Python

"""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 [])