diff --git a/.editorconfig b/.editorconfig index 216aca50..ac973313 100644 --- a/.editorconfig +++ b/.editorconfig @@ -100,6 +100,11 @@ max_line_length = unset [config/upstream-cache/data.toml] max_line_length = 200 +# The signature registry's JSON is written from the YAML beside it, whose labels fit the line length there and +# run a few characters past it once JSON has indented and quoted them. A string value cannot wrap. +[libs/gpu-probe/signatures.json] +max_line_length = unset + # pnpm writes each package's `resolution: {integrity: sha512-...}` on one line, which exceeds the line length. [**/pnpm-lock.yaml] max_line_length = unset diff --git a/.mise/config.mingw.toml b/.mise/config.mingw.toml index 851db8c8..0d49e053 100644 --- a/.mise/config.mingw.toml +++ b/.mise/config.mingw.toml @@ -63,7 +63,13 @@ rusty_v8_src = ":http:denoland/rusty_v8/releases/download/v150.4.0" # The v8 build script decompresses the archive to `rusty_v8.lib` under its deterministic gn_out dir, but # rustc on windows-gnu only accepts the `librusty_v8.a` spelling -- prefetch:mingw stages that copy. # Debug-profile path; a release build needs the same staging under target/x86_64-pc-windows-gnu/release. -rusty_v8_gn_obj = "{{ vars.config_root_fwd }}/target/x86_64-pc-windows-gnu/debug/gn_out/obj" +# gn_out sits three levels above the build script's OUT_DIR, which cargo lays out as `build/v8//out`, so +# it lands inside `build/`. Staged anywhere else, the gnu build stops on +# error: could not find native static library `rusty_v8`, perhaps an -L flag is missing? +# as `override (mingw)` did with cargo 1.100 at commit +# https://github.com/edge-toolkit/core/commit/4dc5d21fb14bf5f8498f6556fd94211acb431f56, at +# https://github.com/edge-toolkit/core/actions/runs/43299211943/job/131465770156. +rusty_v8_gn_obj = "{{ vars.config_root_fwd }}/target/x86_64-pc-windows-gnu/debug/build/gn_out/obj" # winlibs GCC's bin dir, as an absolute path for the same reason config.windows.toml's win_llvm_bin is one. # mise flattens `_` to `-` in the install dir (winlibs-mingw, not winlibs_mingw). The version segment must # match the [tools] pin above (conftest's version_drift enforces). diff --git a/.mise/config.toml b/.mise/config.toml index bc12b2de..dec7db7f 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -877,6 +877,8 @@ CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS = "{{ vars.wasm_rustflags }}" # belt-and-braces (the download code path is gone). ORT_LIB_LOCATION = "{% if os() == 'windows' %}{{ vars.ort_loc_win }}{% else %}{{ vars.ort_loc_unix }}{% endif %}" ORT_SKIP_DOWNLOAD = "1" +# The ONNX Runtime library et-ws-wasi-runner opens when a guest first uses `wasi:nn`, out of that same install. +ORT_DYLIB_PATH = "{{ vars.ort_loc_unix }}/lib/libonnxruntime.{% if os() == 'macos' %}dylib{% else %}so{% endif %}" [tasks] ec = "editorconfig-checker" @@ -944,6 +946,7 @@ depends = [ "gen-help-check", "gen-specs-check", "gitleaks-check", + "gpu-probe-signatures-check", "hadolint-check", "j2lint-check", "jscpd-check", @@ -2681,6 +2684,27 @@ depends = ["gen:dockerignore"] description = "Fail if .dockerignore is stale vs .gitignore (run `mise run gen:dockerignore`)" run = "git diff --exit-code -- .dockerignore" +[tasks."gen:gpu-probe-signatures"] +description = "Regenerate libs/gpu-probe/signatures.json from the signatures.yaml beside it" +# The signature registry is kept as YAML, which takes comments and reads well in a diff, and embedded as JSON. +# `et-gpu-probe` embeds it when the crate is compiled, which a consumer of the published crate does with none of +# this repository's tools, so the JSON is committed beside its source rather than made by a build script. It is +# staged under target/ and renamed into place for the reason the .dockerignore above is: a build running beside +# this never reads half a file. The files are not named `registry`, which .gitignore keeps for the one a hub saves. +run = ''' +coreutils mkdir -p target +staged="target/gpu-probe-signatures.$$" +trap 'coreutils rm -f "$staged"' EXIT +yq -o json libs/gpu-probe/signatures.yaml | coreutils tr -d '\r' >"$staged" +coreutils mv -f "$staged" libs/gpu-probe/signatures.json +''' +shell = "{{ vars.task_shell }}" + +[tasks.gpu-probe-signatures-check] +depends = ["gen:gpu-probe-signatures"] +description = "Fail if libs/gpu-probe/signatures.json is stale (run `mise run gen:gpu-probe-signatures`)" +run = "git diff --exit-code -- libs/gpu-probe/signatures.json" + # Fail when HEAD does not already contain origin/main, because a stale branch makes every other check untrustworthy. # A branch behind main runs its lints against code the rest of the repo has already moved past, so failures land in # files the branch never touched and read as though the branch broke them. No linter can express this -- it is git diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index 210fce9b..0e2d0cd3 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -164,15 +164,6 @@ py3_win_fwd = "{{ vars.a_mise_installs_fwd }}/python/3.13.16/python.exe" # ORT_LIB_LOCATION in config.toml [env] picks this on Windows. Version segment must match the # [tools] pin (conftest's version_drift enforces this). ort_loc_win = '{{ vars.mise_installs }}\github-microsoft-onnxruntime\1.22.0' -# The same install's DLL dir, prepended by the [env] `_.path` below so a binary linking ORT can start. -# ORT_LIB_LOCATION only tells the build where to link from; at load time Windows resolves onnxruntime.dll -# through the classic search, which reaches PATH but not the mise install dir. Until a test linked the -# et-ws-wasi-runner library nothing exercised that -- the runner binary is built but never started in the -# image -- so the servercore container aborted test enumeration outright: -# error: creating test list failed -# for `et-ws-wasi-runner::ws_backend`, command `...\ws_backend-.exe --list --format terse` -# aborted with code 0xc0000135: The specified module could not be found. (os error 126) -ort_bin_win = '{{ vars.ort_loc_win }}\lib' # llvm-mingw's bin dir (the github:mstorsjo/llvm-mingw install above). # The linker/dlltool [env] vars below point at absolute exes in here rather than bare names because mise's # cargo backend doesn't activate llvm-mingw onto the cargo: source-build subprocess's PATH: a bare `clang` @@ -246,12 +237,8 @@ CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER = "{{ vars.win_llvm_bin }}\\clang. CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_RUSTFLAGS = "-Cdlltool={{ vars.win_llvm_bin }}\\llvm-dlltool.exe" LIBCLANG_PATH = "{{ vars.win_libclang }}" PYO3_PYTHON = "{{ vars.py3_win }}" -# Force ort-sys to prefer dynamic linkage on Windows. -# Otherwise it defaults to static ("could not find native static library `onnxruntime`"), but the Microsoft -# ORT release ships only a DLL + import lib, no static archive. Not needed on Linux/macOS where the dynamic -# default already matches the .so / .dylib that ship in the release. The workspace `ort` dep keeps `copy-dylibs` -# so the DLL lands next to linked binaries. -ORT_PREFER_DYNAMIC_LINK = "1" +# The ONNX Runtime library et-ws-wasi-runner opens: the Windows install's, in place of the one config.toml names. +ORT_DYLIB_PATH = '{{ vars.ort_loc_win }}\lib\onnxruntime.dll' # Prepend conda:m2-gnupg's nested binary dir to PATH. # mise's per-tool .mise-bins resolves `/bin/X` and `/Library/bin/X` cleanly, but # msys2-format packages put binaries one level deeper at `/Library/usr/bin/X`. Without this, @@ -267,7 +254,6 @@ _.path = [ "{{ vars.m2_gnupg_bin }}", "{{ vars.m2_make_bin }}", "{{ vars.maven_bin }}", - "{{ vars.ort_bin_win }}", "{{ vars.win_libclang }}", ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 58523c2b..4c5968cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,9 @@ Changelog 1.1.0. event gains a `gpu_numeric_probe` object: how many probes were answered, how many were not correctly rounded, which of two legal answers the device gave where there are two, a fingerprint of every result, and the known GPU that answers the same way. The probe set, its shaders and correctly rounded answers are the new `et-gpu-probe` crate, - which drives no GPU itself. The module's `gpu.renderer` now falls back to WebGL's renderer string where a browser - blanks the WebGPU adapter's. + which drives no GPU itself. Its `host` answers the probes on the CPU, and its `HOST` and `WASI_NN` name the + interfaces a signature is measured through with no browser. The module's `gpu.renderer` now falls back to WebGL's + renderer string where a browser blanks the WebGPU adapter's. - A WASI module can read a lidar. `et-ws-wasi-runner`'s new `lidar` feature, on by default, hosts `et:ws-wasi/serial`, lending a guest the serial ports its `SERIAL_PORTS_` variables name: a device, or `tcp://:` for one a bridge such as ser2net serves. The interface's own `serial-host` world, which the @@ -176,6 +177,13 @@ Changelog 1.1.0. ### Changed +- A board hosts `wasi:clocks/wall-clock`, which every guest built with Rust 1.100 imports for its standard library's + locks. A board keeps no calendar, so the time it gives is how long its clock has run. The interface moves from + `edge_toolkit::board::COVERAGE_IMPORTS` to `HOSTED_IMPORTS`. +- **Breaking:** `et-ws-wasi-runner` built with `nn` no longer links ONNX Runtime: it opens the library when a guest + first uses `wasi:nn`. Set `ORT_DYLIB_PATH` to the library to use, as the runner's image now does, or keep one + beside the runner. Without either it asks the system for one by name, and on Windows that is the older copy in + System32, which the runner refuses by version. - **Breaking:** `et_ws_wasi_runner::run_module` takes the serial ports it lends a component as a fifth argument, a `BTreeMap` from each port's name to its device. Pass an empty map to lend none, as a build without the `lidar` feature does whatever it is given. diff --git a/CHECKS.md b/CHECKS.md index 0ab6468d..817f2056 100644 --- a/CHECKS.md +++ b/CHECKS.md @@ -1,9 +1,9 @@ # Checks -- **Generated:** 2026-11-10 -- **Checks:** 84 +- **Generated:** 2026-11-12 +- **Checks:** 85 - **Tools:** 49 -- **Shell-script checks:** 10 +- **Shell-script checks:** 11 - **Checks with a repair pass:** 22 - **Rules:** 381 custom local rules, or non-default strict linter settings - **Meta rules:** 22 of those rules are rules about rules @@ -1307,6 +1307,8 @@ Rules, from `config/taplo`: - `dockerignore-check` (`default` env; run by `check:rust`) -- Fail if .dockerignore is stale vs .gitignore (run `mise run gen:dockerignore`). - `gen-specs-check` (`default` env; run by `check:rust`) -- Fail if any checked-in et-int-gen artifact is stale. +- `gpu-probe-signatures-check` (`default` env; run by `check:rust`) -- Fail if libs/gpu-probe/signatures.json is stale + (run `mise run gen:gpu-probe-signatures`). - `mise-lock-check` (`default` env; run by `check:rust`) -- Fail if .mise/mise*.lock or .mise/locks/ differ from what a full install resolves. - `readme-mise-version-check` (`default` env; run by `check:rust`) -- Fail if README.md's mise version differs from diff --git a/Cargo.lock b/Cargo.lock index 65740eb5..d91b6bb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5795,6 +5795,17 @@ dependencies = [ [[package]] name = "et-gpu-probe" version = "0.1.0" +dependencies = [ + "document-features", + "et-gpu-probe", + "futures-executor", + "serde_json", + "target-lexicon", + "thiserror 2.0.21", + "webbrowser", + "wgpu", + "wgpu-hal", +] [[package]] name = "et-hub-backplane" @@ -6663,6 +6674,7 @@ dependencies = [ "embassy-sync 0.8.0", "embedded-io-async 0.7.0", "et-core-guest", + "et-gpu-probe", "et-org", "et-otlp", "et-rest-client", @@ -9062,6 +9074,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "matrixmultiply" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "maybe-owned" version = "0.3.4" @@ -9354,6 +9376,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -9588,6 +9631,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -9698,6 +9750,17 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -10048,8 +10111,11 @@ version = "2.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa7e49bd669d32d7bc2a15ec540a527e7764aec722a45467814005725bcd721" dependencies = [ + "libloading 0.8.9", + "ndarray", "ort-sys", "smallvec 2.0.0-alpha.10", + "tracing", ] [[package]] @@ -11291,6 +11357,12 @@ dependencies = [ "objc2-quartz-core", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.12.0" @@ -11462,6 +11534,12 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "reqwest" version = "0.13.5" @@ -15363,6 +15441,22 @@ dependencies = [ "url", ] +[[package]] +name = "webbrowser" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e91ccd4698f2647d6137e8eb17a3f1dcfd63810be9029fb2398f507279b95b" +dependencies = [ + "jni", + "log", + "ndk-context", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "url", + "web-sys", +] + [[package]] name = "webpki-root-certs" version = "1.0.11" @@ -15387,6 +15481,34 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wgpu" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec 1.16.4", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + [[package]] name = "wgpu-core" version = "29.0.1" @@ -15407,6 +15529,7 @@ dependencies = [ "naga", "once_cell", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", "ron", @@ -15484,10 +15607,13 @@ dependencies = [ "once_cell", "ordered-float", "parking_lot", + "portable-atomic", + "portable-atomic-util", "profiling", "range-alloc", "raw-window-handle", "raw-window-metal", + "renderdoc-sys", "smallvec 1.16.4", "thiserror 2.0.21", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 6fdbaa03..64dc22aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,6 +161,7 @@ et-ws-wasi-runner = { path = "services/ws-wasi-runner", version = "0.3.0", defau et-ws-wasm-agent = { path = "services/ws-wasm-agent", version = "0.3.0" } fake = "5" fs-err = "3" +futures-executor = "0.3" futures-util = "0.3" heck = "0.5" hostname = "0.4" @@ -245,7 +246,24 @@ opentelemetry-proto = { version = "0.32", default-features = false, features = [ ] } opentelemetry_sdk = "0.32" # Pinned exactly, because rc.11 and later moved API that wasmtime-wasi-nn calls. -ort = { version = "=2.0.0-rc.10", default-features = false, features = ["copy-dylibs"] } +# `load-dynamic` has et-ws-wasi-runner open ONNX Runtime by path when a guest first uses `wasi:nn`, rather than link +# it: the library is the one `ORT_DYLIB_PATH` names, or without that the platform's ONNX Runtime library beside the +# runner. A linked library is left to the loader's search, which has failed two ways. On Windows the search reaches +# the older copy kept in System32 before anything on PATH, and a binary with no copy of its own beside it stops on +# +# Failed to initialize ORT API +# +# as every test binary did on the Windows lanes of commit +# https://github.com/edge-toolkit/core/commit/8ff27b3d7b9db534872ed54445b869f7c37f6aa1, at +# https://github.com/edge-toolkit/core/actions/runs/43183623049/job/131065342952. On macOS and Linux it relies on +# ort-sys placing the library beside each binary, which it does by counting directories up from its build output, +# and cargo 1.100 moved both that output and the test binaries, so a test binary stops before any test runs on +# +# dyld[23146]: Library not loaded: @rpath/libonnxruntime.1.22.0.dylib +# +# as `nn_probe` did on an Apple M5 Pro at commit +# https://github.com/edge-toolkit/core/commit/dfb88e272297f430106da078fa03d859d0001c74. +ort = { version = "=2.0.0-rc.10", default-features = false, features = ["load-dynamic"] } panic-probe = "1" picoserve = { version = "0.20", default-features = false, features = ["ws"] } port_check = "0.3" @@ -304,6 +322,8 @@ subtle = "2.6" syn = "3" sys_traits = { version = "0.1", features = ["libc", "real"] } temp-env = "0.3" +# Names CPU architectures the way a target triple spells them. +target-lexicon = "0.13" tempfile = "3" textwrap = { version = "0.16", default-features = false } thiserror = { version = "2", default-features = false } @@ -359,8 +379,14 @@ wasmtime-wasi-nn = { version = "49", default-features = false, features = ["onnx # Writes the components the tests read from WebAssembly text. wat = "1" web-sys = "0.3" +# Names the browsers a GPU signature has been seen in; it is never asked to open one. +webbrowser = "1" +# The safe API over wgpu-core, for a test that drives a GPU itself. Each user names the backends it wants. +wgpu = { version = "29", default-features = false } # wgpu-core/wgpu-types come in via wasi-webgpu-wasmtime's re-exports; only the instance handle is ours. wgpu-core = { version = "29", default-features = false } +# Holds the PCI vendor ID of each GPU vendor wgpu knows, which is all et-gpu-probe takes from it. +wgpu-hal = { version = "29", default-features = false } wgpu-types = "29" # Windows-only feature-unification shim: forces `winapi/std` on for deno_io. winapi = { version = "0.3", features = ["std"] } diff --git a/config/cargo-features.toml b/config/cargo-features.toml index fcac1df7..af500cfb 100644 --- a/config/cargo-features.toml +++ b/config/cargo-features.toml @@ -24,6 +24,7 @@ pico-plus2w = "Builds for the Pimoroni Pico Plus 2 W, on its RP2350B." pico2w = "Builds for the Raspberry Pi Pico 2 W, on its RP2350A." precompile = "Compiles guests ahead of time for a board's engine, as a firmware's build script does." qr = "Prints the advertised URL as a terminal QR code at startup." +registry = "Embeds the registry of known GPU probe signatures, to name the hardware a signature was seen on." rustls-0_23 = "Builds actix-web with rustls 0.23, which adds the TLS handshake timeout setting." schema-export = "Derives JSON Schema and AsyncAPI descriptions for the WebSocket protocol types." std = "Builds against the standard library, with everything that needs an operating system." diff --git a/config/conftest/policy/jscpd/jscpd.rego b/config/conftest/policy/jscpd/jscpd.rego index d5fb7f3b..4ec64ebd 100644 --- a/config/conftest/policy/jscpd/jscpd.rego +++ b/config/conftest/policy/jscpd/jscpd.rego @@ -32,7 +32,7 @@ package jscpd # Ceiling on the clone fingerprints recorded in config/jscpd-baseline.json. # Lowered freely, raised only with the operator's sign-off. See the header for what raising it implies. -declared_clones := 320 +declared_clones := 330 # The ceiling is always a multiple of this. ceiling_step := 10 diff --git a/config/deny.toml b/config/deny.toml index a58420f4..bb78e68c 100644 --- a/config/deny.toml +++ b/config/deny.toml @@ -29,12 +29,12 @@ ignore = [ # Proc-macro pulled by v8 (rusty_v8) via deno_core / serde_v8; expands at compile time only, so there is no # runtime exposure. Every published v8 release through 152.2.0 declares it as a non-optional dependency, so no # upgrade clears this -- only a rusty_v8 move to the maintained pastey fork. Awaiting that migration. - { id = "RUSTSEC-2024-0436", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2024-0436", reason = "expires 2027-01-01" }, # rustls-pemfile 2.2.0 (unmaintained; final release -- parsing moved into rustls-pki-types). # PEM cert parser for loading the system CA bundle. Unmaintained, not vulnerable. Pulled directly by both # deno_native_certs and deno_tls, each at its newest release, so no bump drops it. rustls-native-certs # migrated at 0.8.1 and our 0.8.4 copy is clean; the affected 0.7.3 copy is pinned by deno_native_certs. - { id = "RUSTSEC-2025-0134", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2025-0134", reason = "expires 2027-01-01" }, # rustls 0.23.40 -- the four rustls entries below share one blocker, so it is described once, here. # deno_tls pins `rustls = "=0.23.40"` by equality through 0.247.0, its newest release, and because a lockfile # holds one rustls 0.23.x that pin also holds ws-server's own TLS listener and every reqwest client at @@ -47,50 +47,76 @@ ignore = [ # for example a plaintext `EncryptedExtensions` packed into the same record as the `ServerHello`. The transcript # is still authenticated, so an attacker cannot alter or complete a handshake with it. CVSS 5.3, client and # server roles alike, and live code here. - { id = "RUSTSEC-2026-0285", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0285", reason = "expires 2027-01-01" }, # `Debug` on client session values prints the TLS 1.2 master secret or TLS 1.3 resumption PSK, fixed 0.23.46. # Reached only through a custom `ClientSessionStore` that formats those values; nothing here implements one, and # the built-in session cache's `Debug` output is redacted. - { id = "RUSTSEC-2026-0346", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0346", reason = "expires 2027-01-01" }, # Brotli certificate decompression can allocate 1 GiB before authentication, fixed in 0.23.46. # Needs rustls's non-default `brotli` feature, which our resolution does not enable -- `cargo tree -i rustls -e # features` reports aws-lc-rs,default,log,logging,prefer-post-quantum,std,tls12 -- so the decompressor is absent. - { id = "RUSTSEC-2026-0348", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0348", reason = "expires 2027-01-01" }, # An unauthenticated ServerHello can steer later client connections to classical-only key exchange, fixed 0.23.46. # Client role, and live here: `prefer-post-quantum` is on, so the downgrade is possible. The cost is losing # post-quantum confidentiality on the affected connections, not a key compromise or an authentication bypass, # and it needs a network attacker who can answer one connection attempt to the same server name. - { id = "RUSTSEC-2026-0349", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0349", reason = "expires 2027-01-01" }, # smartstring 1.0.1 (unmaintained; repo archived 2026-05-03). # Pulled only by the swc stack (swc_ecma_lexer/parser 26/27) under deno_ast -> deno_runtime. Maintenance # notice, not a vulnerability. Upstream swc migrated to compact_str at swc_ecma_lexer 41.0.0 and # swc_ecma_parser 42.0.0, but deno_ast pins both by equality (=26.0.0 / =27.0.7) through its newest release, # so no bump on our side reaches them. Drop when a deno_runtime/deno_ast bump pulls the migrated swc. - { id = "RUSTSEC-2026-0249", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0249", reason = "expires 2027-01-01" }, # hickory-proto 0.25.2 NSEC3 closest-encloser proof unbounded loop (DoS on DNSSEC validation). # Pulled by deno_net for resolver. The advisory scopes reachability to builds carrying dnssec-ring or # dnssec-aws-lc-rs, and our resolution enables neither -- cargo tree -i hickory-proto -f "{p} [{f}]" reports # default,futures-io,serde,std,tokio -- so DnssecDnsHandle is never compiled in. (Cargo.lock listing ring # under hickory-proto is not evidence otherwise; it records optional deps regardless of activation.) The fix # landed in 0.26.0-beta.1, but deno_net requires ^0.25.2 through its newest release, so it is out of reach. - { id = "RUSTSEC-2026-0118", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0118", reason = "expires 2027-01-01" }, + # hickory-proto 0.25.2 -- four flaws in how DNSSEC validation reads a response, fixed in no hickory-proto release. + # An NSEC3 comparison is inverted, NSEC3 owner names are not checked against the zone, NXDOMAIN is accepted for a + # proven empty non-terminal, and an unrelated signed RRset is accepted as the answer. Each needs a response to be + # validated, and the entry above gives why none is: without a dnssec-* feature the validating handle is not + # compiled in. The fixes are in hickory-net 0.26.2, where validation moved, behind the same deno_net pin. + { id = "RUSTSEC-2026-0504", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0505", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0511", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0512", reason = "expires 2027-01-01" }, # hickory-proto 0.25.2 O(n^2) name compression during message encode. # Encoder side, hit only when we *send* a DNS message. The runner uses the OS resolver via hickory. It # never crafts outbound DNS frames with attacker-controlled name lists. - { id = "RUSTSEC-2026-0119", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0119", reason = "expires 2027-01-01" }, + # hickory-proto 0.25.2 -- three panics parsing a record or zone file from its text form, all fixed in 0.26.2. + # A malformed `alpn` value, a parameter value that is a lone quote, and a token of 4096 characters or more each + # panic the text parser. Records decoded from the DNS wire format do not go through it, and wire format is all the + # web runner's resolver reads: it loads no zone file and parses no record text, and its system configuration is + # read by another parser. The same deno_net pin as the entries above keeps 0.26.2 out of reach. + { id = "RUSTSEC-2026-0487", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0489", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0490", reason = "expires 2027-01-01" }, + # hickory-proto 0.25.2 reserves memory for the record counts a DNS message header claims, fixed in 0.26.2. + # Live code: every response the web runner's resolver decodes goes through it, and one small packet claiming + # 65,535 records reserves about 18 MB before the decode fails. The cost is that allocation per forged response, + # not a wrong answer, and the runner resolves only the ws-server host it is configured with, so the responses + # come from the operator's own DNS unless an attacker can spoof it. The same deno_net pin keeps the fix out of + # reach. + { id = "RUSTSEC-2026-0491", reason = "expires 2027-01-01" }, # hickory-resolver 0.25.2 lookup() / lookup_ip() return Ok for a response DNSSEC validation found bogus. # Pulled by deno_net and deno_fetch for the web runner's resolver. The advisory needs a resolver with DNSSEC # validation enabled, which needs a dnssec-* feature, and our resolution enables none -- cargo tree -i # hickory-resolver -e features reports default,serde,system-config,tokio -- so no response is ever validated and # none can be passed off as valid. The fix is 0.26.2, but deno_net and deno_fetch require ^0.25.2 through their # newest releases (0.252.0 / 0.284.0), so it is out of reach. - { id = "GHSA-5j98-2g5x-46v6", reason = "expires 2026-11-13" }, + { id = "GHSA-5j98-2g5x-46v6", reason = "expires 2027-01-01" }, + # The same advisory under its RustSec id, which is the one cargo-deny knows it by. + { id = "RUSTSEC-2026-0514", reason = "expires 2027-01-01" }, # hickory-resolver 0.25.2 follows CNAME records outside the answer's chain, sending queries it does not need. # Same deno_net / deno_fetch path and the same unreachable 0.26.2 fix as the entry above. Unlike that one, no # feature gates it, so the code is live: a response carrying stray CNAMEs costs extra queries. The cost is query # amplification, not a wrong answer, and the runner resolves only the ws-server host it is configured with, so # the responses come from the operator's own DNS rather than one an attacker chooses. - { id = "GHSA-6f2x-v7q7-m7m5", reason = "expires 2026-11-13" }, + { id = "GHSA-6f2x-v7q7-m7m5", reason = "expires 2027-01-01" }, # rand 0.8.5 unsound ThreadRng reseed when a custom `log` logger calls rand::rng() (UB via aliased &mut). # Transitive via deno_crypto / deno_fs / deno_node (web-runner), used for non-cryptographic randomness # (jitter, salt scratchpads). The advisory's first precondition is the `log` feature, which our resolution @@ -98,7 +124,7 @@ ignore = [ # Fixed in the 0.8 line at 0.8.6, which deleted the log optional dep -- but deno_crypto pins rand "=0.8.5" # by equality through its newest release, so neither a lockfile bump nor a deno bump moves it. cargo-deny # does not match this id and warns advisory-not-detected; the entry stays because osv-scanner does flag it. - { id = "RUSTSEC-2026-0097", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0097", reason = "expires 2027-01-01" }, # rsa 0.9.10 Marvin Attack timing sidechannel on RSA decryption. # No patched version exists -- the advisory lists none, upstream is still migrating to a constant-time # implementation, and 0.9.10 is the newest stable (the 0.10 line is release candidates, equally affected), @@ -106,7 +132,7 @@ ignore = [ # RSA keys or decrypt RSA payloads. Only TLS server cert verification, which uses *signature* checks, no # decryption. Not reachable unless guest JS explicitly imports crypto.subtle and performs RSA-OAEP decrypt # with a private key. Our modules don't. - { id = "RUSTSEC-2023-0071", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2023-0071", reason = "expires 2027-01-01" }, # crypto-bigint 0.5.5 secret-dependent branches in operations meant to be constant-time (timing side channel). # The arithmetic under elliptic-curve 0.13, which deno_crypto and deno_node_crypto use for P-256/384/521 and # secp256k1 in WebCrypto and node:crypto. The fix is only in 0.7.6 and later, and deno_crypto requires @@ -115,7 +141,7 @@ ignore = [ # those operations on the same host. Our modules do neither; the hub's TLS uses rustls and aws-lc, not this. Drop # when deno_crypto moves to elliptic-curve 0.14. cargo-deny ignores by id only, so this also mutes the 0.7 copy # elliptic-curve 0.14 pulls -- Cargo.lock pins the patched 0.7.10 there, so re-check that copy when touching this. - { id = "RUSTSEC-2026-0387", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0387", reason = "expires 2027-01-01" }, # libsqlite3-sys 0.38.2 bundles SQLite 3.53.2, which can return stale page-cache memory, fixed in SQLite 3.54.0. # Needs a database file opened together with a sibling `-wal` file of a different page size that a less trusted # party wrote. rusqlite's `bundled` feature compiles the affected copy in, and the patched libsqlite3-sys 0.39 needs @@ -126,7 +152,7 @@ ignore = [ # never do. That is the whole of the mitigation: the runner grants every module all permissions, so a deployer's own # module that opens a database and `-wal` a less trusted party can write is exposed, and nothing in the runner stops # it. Drop when those crates move to rusqlite 0.41. - { id = "RUSTSEC-2026-0459", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0459", reason = "expires 2027-01-01" }, # h2 0.3.27 unbounded empty DATA frames (low-severity HTTP/2 DoS: undrained streams grow memory). # Accepted for the 0.3 copy only. hyperium patched the 0.4 line (>= 0.4.16; Cargo.lock now pins 0.4.20 for the # hyper/reqwest path) and shipped no 0.3 backport, so 0.3.27 is the last release of a dead line. It arrives @@ -136,14 +162,14 @@ ignore = [ # this when it ships one built on h2 0.4. cargo-deny 0.20 ignores by id only (no version scoping), so this # also mutes the 0.4 line -- Cargo.lock pins 0.4.20 there, and a bump below 0.4.16 would have to come back # through this entry, so re-check both copies when touching it. - { id = "RUSTSEC-2026-0258", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0258", reason = "expires 2027-01-01" }, # unsafe-libyaml 0.2.11 (unmaintained; repository archived 2024-03-25, so no version is patched). # A c2rust translation of libyaml, reached through serde_yaml, itself deprecated. Two things pull serde_yaml: # our own crates, for registry, scenario and generated-deployment YAML, and progenitor-macro, which parses the # OpenAPI document at compile time. Unmaintained, not vulnerable. Moving our crates to a safe-Rust parser # (serde-saphyr is the advisory's suggestion) is in our hands, but progenitor-macro would still carry it, so # drop this only once both have moved. - { id = "RUSTSEC-2024-0449", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2024-0449", reason = "expires 2027-01-01" }, # zstd 0.13.2 / zstd-safe 7.3.0 -- the zstd entries below share one blocker, so it is described once, here. # The fixes are only in zstd 0.14.3 and zstd-safe 8.2.0, and Cargo.lock pins those for every consumer that # accepts them (actix-http, compression-codecs, tonic). Two cannot: deno_node pins `zstd = "=0.13.2"` by equality @@ -155,42 +181,42 @@ ignore = [ # Leftover input from an abandoned stream leaks into the next one on a reused context, fixed in zstd 0.14.3. # Reached only through the `with_context` stream constructors. deno_node's node:zlib ops build their raw # encoders and decoders with `new`, and wasmtime-internal-cache calls only `encode_all` / `decode_all`. - { id = "RUSTSEC-2026-0391", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0391", reason = "expires 2027-01-01" }, # Three memory-safety flaws in `Seekable`, all fixed in 8.2.0 and all in the `seekable` feature, which is off. # `decompress` sets a `Vec` length past its capacity for an offset past the end, an uninitialized or failed # `Seekable` reads a NULL seek table, and `seekable_compressStream` reads past an `InBuffer` whose `pos` is out # of range. - { id = "RUSTSEC-2026-0396", reason = "expires 2026-11-13" }, - { id = "RUSTSEC-2026-0398", reason = "expires 2026-11-13" }, - { id = "RUSTSEC-2026-0405", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0396", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0398", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0405", reason = "expires 2027-01-01" }, # Three memory-safety flaws in `experimental` APIs, all fixed in 8.2.0; that feature is off, so none is compiled in. # `CParameter::StableInBuffer` compresses freed input, `init_using_cdict` / `init_using_ddict` outlive their # dictionary, and `DCtx::try_clone` double-frees one. - { id = "RUSTSEC-2026-0395", reason = "expires 2026-11-13" }, - { id = "RUSTSEC-2026-0399", reason = "expires 2026-11-13" }, - { id = "RUSTSEC-2026-0404", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0395", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0399", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0404", reason = "expires 2027-01-01" }, # Two memory-safety flaws in multithreaded compression, both fixed in 8.2.0 and both needing `zstdmt` (off). # `CCtx::try_clone` overflows the heap after a multithreaded frame, and an unfinished one lets a reset free a # dictionary or job table a worker still uses. deno_node does pass guest JS's `NbWorkers` through, but without # `zstdmt` the bundled library has no worker threads, so no multithreaded frame is ever started. - { id = "RUSTSEC-2026-0402", reason = "expires 2026-11-13" }, - { id = "RUSTSEC-2026-0406", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0402", reason = "expires 2027-01-01" }, + { id = "RUSTSEC-2026-0406", reason = "expires 2027-01-01" }, # `OutBuffer::around_pos` marks a `Vec` initialized up to a position past its length, fixed in 8.2.0. # deno_node wraps plain slices with `OutBuffer::around`, and wasmtime-internal-cache calls only # `encode_all` / `decode_all`, so neither consumer ever builds an `OutBuffer` over a `Vec` with a position. - { id = "RUSTSEC-2026-0397", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0397", reason = "expires 2027-01-01" }, # A one-shot call on a context with an unfinished stream leaves it pointing at a freed dictionary, fixed in 8.2.0. # Needs one context used for both streaming and one-shot calls. deno_node's node:zlib ops only stream, on a # context per JS stream, and wasmtime-internal-cache only makes one-shot calls, each on a context of its own. - { id = "RUSTSEC-2026-0400", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0400", reason = "expires 2027-01-01" }, # `train_from_buffer` reads past its samples buffer when the sample sizes overflow, fixed in 8.2.0. # `zdict_builder` is on, but neither consumer trains a dictionary, so the trainer is compiled in and never called. - { id = "RUSTSEC-2026-0401", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0401", reason = "expires 2027-01-01" }, # A failed streaming decompression of a legacy (v0.4 to v0.7) frame exposes uninitialized memory, fixed in 8.2.0. # Live: `legacy` is on, and deno_node's raw decoder runs `decompress_stream`. It is reached only when guest JS in # the web runner decompresses zstd through node:zlib, which our modules never do. wasmtime-internal-cache # decompresses only its own compilation cache, and ws-wasi-runner configures none, so that path never runs. - { id = "RUSTSEC-2026-0403", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0403", reason = "expires 2027-01-01" }, # block-buffer 0.10.4: a panic caught inside the compress callback leaves the buffer's cursor corrupted. # Fixed only in 0.12.1, and the 0.10 line is pulled by digest 0.10, which the RustCrypto hashes under deno_crypto, # deno_node_crypto and curve25519-dalek require through their newest releases, so no bump reaches it; Cargo.lock @@ -198,18 +224,18 @@ ignore = [ # that copy. Exploiting it needs the callback to panic, the panic to be caught, and the same buffer to be used # again. The callbacks are the hashes' fixed-size block functions, which do not panic, and nothing here catches # a panic mid-hash and keeps the hasher. Drop this when digest 0.10 leaves the tree. - { id = "GHSA-qwgh-2vcv-g2f7", reason = "expires 2026-11-13" }, + { id = "GHSA-qwgh-2vcv-g2f7", reason = "expires 2027-01-01" }, # bare-metal 0.2.5 (deprecated and archived, so no version is patched). # Pulled only by the Pico 2 W firmware, through cortex-m 0.7, which cyw43 0.7 and embassy-rp 0.10 require through # their newest releases. Deprecated != vulnerable: cortex-m uses it for its single-core `Mutex` and # `CriticalSection` types, and the firmware takes its critical sections from embassy-rp's `critical-section` # implementation. Drop this when cortex-m 0.8 reaches them. - { id = "RUSTSEC-2026-0110", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0110", reason = "expires 2027-01-01" }, # proc-macro-error2 2.0.1 (unmaintained, so no version is patched). # Pulled only by the Pico 2 W firmware, through the pio-proc assembler macro embassy-rp 0.10 requires through its # newest release. It runs at compile time inside that macro and reports its errors; none of it is in the firmware. # Drop this when pio-proc leaves it. - { id = "RUSTSEC-2026-0173", reason = "expires 2026-11-13" }, + { id = "RUSTSEC-2026-0173", reason = "expires 2027-01-01" }, # The npm: AsyncAPI generator tools -- the entries below share one setting, so it is described once, here. # @asyncapi/generator 3.5.0 and @asyncapi/markdown-template 2.0.0 are the newest releases, and every fix is a major # version past a range one of them or their dependencies requires, so no reinstall reaches any of them. They run @@ -219,31 +245,31 @@ ignore = [ # # @babel/core 7.12.9 reads any source map named by a sourceMappingURL in the code it compiles, fixed in 7.29.6. # @asyncapi/generator-react-sdk 1.1.3 pins it by equality. It compiles only the template's own JSX. - { id = "GHSA-4x5r-pxfx-6jf8", reason = "expires 2026-11-13" }, + { id = "GHSA-4x5r-pxfx-6jf8", reason = "expires 2027-01-01" }, # simple-git 3.36.0 and its @simple-git/argv-parser 1.1.1 have gaps in their unsafe-operations guard. # It misses `VISUAL`, abbreviated long options, `include.path` and `trailer..cmd`, fixed in simple-git 4, # and the generator requires ^3.36.0. All four need attacker-controlled git arguments or config, and the generator # reaches git only to clone a template named by a remote URL, which `gen:ws-md` never passes. - { id = "GHSA-858h-whjf-mvg5", reason = "expires 2026-11-13" }, - { id = "GHSA-g4wm-2vf7-vfgr", reason = "expires 2026-11-13" }, - { id = "GHSA-v5rq-49vh-5v5c", reason = "expires 2026-11-13" }, - { id = "GHSA-x6jw-m9v5-85vh", reason = "expires 2026-11-13" }, + { id = "GHSA-858h-whjf-mvg5", reason = "expires 2027-01-01" }, + { id = "GHSA-g4wm-2vf7-vfgr", reason = "expires 2027-01-01" }, + { id = "GHSA-v5rq-49vh-5v5c", reason = "expires 2027-01-01" }, + { id = "GHSA-x6jw-m9v5-85vh", reason = "expires 2027-01-01" }, # ajv 5.5.2 and 6.5.2: ReDoS through a `$data` pattern, and prototype pollution from a crafted schema. # Pulled by json-schema-migrate 0.2.0 and webapi-parser 0.5.0 under the generator's parser stack; both need an # attacker-supplied schema or instance, and the only schema validated here is our own spec. - { id = "GHSA-2g4f-4pwh-qvx6", reason = "expires 2026-11-13" }, - { id = "GHSA-v88g-cgmw-v5xw", reason = "expires 2026-11-13" }, + { id = "GHSA-2g4f-4pwh-qvx6", reason = "expires 2027-01-01" }, + { id = "GHSA-v88g-cgmw-v5xw", reason = "expires 2027-01-01" }, # jsonpath-plus 7.2.0 code execution through a crafted JSONPath expression, fixed in 10.3.0. # Pulled by the parser 1.x and 2.x copies @asyncapi/multi-parser bundles for old spec versions. The expressions # come from the parser's own rulesets and our spec, never from an untrusted source. - { id = "GHSA-hw8r-x6gr-5gjp", reason = "expires 2026-11-13" }, - { id = "GHSA-pppg-cpfq-h7wr", reason = "expires 2026-11-13" }, + { id = "GHSA-hw8r-x6gr-5gjp", reason = "expires 2027-01-01" }, + { id = "GHSA-pppg-cpfq-h7wr", reason = "expires 2027-01-01" }, # glob 10.5.0 / 13.0.6 and minimatch 3.1.5 / 9.0.9 / 10.2.6 can be made to do excess work. # glob keeps walking after its AbortSignal fires, and minimatch amplifies a crafted pattern. Both are fixed only in # new majors that @npmcli and @stoplight require below. Each costs time only, and only for a caller that aborts a # walk or matches an attacker's pattern; the patterns here are fixed. - { id = "GHSA-3g3m-2fjr-6pw4", reason = "expires 2026-11-13" }, - { id = "GHSA-g3hv-75wg-m249", reason = "expires 2026-11-13" }, + { id = "GHSA-3g3m-2fjr-6pw4", reason = "expires 2027-01-01" }, + { id = "GHSA-g3hv-75wg-m249", reason = "expires 2027-01-01" }, ] [licenses] diff --git a/config/generated-trees.toml b/config/generated-trees.toml index b0c6babe..9acce42a 100644 --- a/config/generated-trees.toml +++ b/config/generated-trees.toml @@ -22,6 +22,10 @@ # clap-markdown" note), so any two HELP.md files duplicate those lines by construction. That is duplication in a # tool's output, not in code anyone here wrote, and adding a subcommand re-hashes the window around it -- so # without the exclusion the check fails afresh on changes that have nothing to do with duplication. +# +# jscpd also skips libs/gpu-probe/signatures.yaml and the signatures.json written from it, for a like reason: the +# registry is measurements, one entry per signature, and every entry holds the same sixteen probe names. That is the +# shape of the data, and each signature added would otherwise read as a fresh clone of the entries before it. # What each tool resolves an exclusion path against, keyed by the config path (or path prefix) it applies to. # `{tree}` stands in for the tree name. These are three different bases, not three ways of writing one path: diff --git a/config/jscpd-baseline.json b/config/jscpd-baseline.json index ad729bed..b1bd6c83 100644 --- a/config/jscpd-baseline.json +++ b/config/jscpd-baseline.json @@ -24,7 +24,6 @@ "12bdba43f82ce591": 1, "136c57b4180f7238": 1, "151943582f4d3a1c": 1, - "15f330210837288e": 1, "161f873e1c1bf887": 1, "162b7eccbfa52a74": 1, "16acb47f836cc1f8": 1, @@ -211,7 +210,9 @@ "a719456832b47d0c": 1, "a73a923e61e87ae8": 1, "a743e181c2f6424b": 1, + "a82d81489b2761d8": 1, "a896f65e46666bd0": 1, + "a8ed3a387d4d65c6": 1, "a92c94299aa8f156": 1, "aa68ceead4a59e5f": 1, "aad9806c62fff62f": 1, @@ -283,6 +284,7 @@ "dd162080de996936": 1, "ddca47a4caded0fd": 1, "de1a3233f93e1695": 1, + "de556507e92474d3": 1, "de6fdf79d6510892": 1, "e152738c60f43b0e": 1, "e25456ad9cc7f9f9": 1, diff --git a/config/jscpd.json b/config/jscpd.json index 90b5fcbd..4a765486 100644 --- a/config/jscpd.json +++ b/config/jscpd.json @@ -9,6 +9,8 @@ "verification/**", "**/HELP.md", "**/pkg/package.json", + "libs/gpu-probe/signatures.json", + "libs/gpu-probe/signatures.yaml", "modules/**/Cargo.toml", "modules/**/pyproject.toml", "services/ws-wasi-runner/src/bindings.rs" diff --git a/config/nextest.toml b/config/nextest.toml index 897d78c1..17d23fd9 100644 --- a/config/nextest.toml +++ b/config/nextest.toml @@ -27,6 +27,12 @@ web-runner = { max-threads = 1 } filter = 'package(et-ws-web-runner) or binary(scenario_runners)' test-group = 'web-runner' +# Keep the output of the probe tests that run on the machine's own hardware when they pass. +# Each prints the signature the machine gave, which is how a machine's signature comes to be recorded. +[[profile.default.overrides]] +filter = 'binary_id(et-gpu-probe::native) or binary_id(et-ws-wasi-runner::nn_probe)' +success-output = 'final' + # The integration test binaries outside the web-runner group, which already holds only integration tests. # A binary belongs here when it spawns a child process, listens on a real socket, or drives a runner end to end. # Serving an app through actix's in-process `test::init_service` does neither, so those stay unit tests. A whole diff --git a/config/osv-scanner.toml b/config/osv-scanner.toml index 0908f058..085aa55e 100644 --- a/config/osv-scanner.toml +++ b/config/osv-scanner.toml @@ -1,48 +1,57 @@ # AUTO-GENERATED from config/deny.toml by `mise run gen:osv-scanner`. # The list spans every language, so "has unused ignores" warnings are expected: agents must ignore them. IgnoredVulns = [ - { id = "GHSA-2g4f-4pwh-qvx6", ignoreUntil = 2026-11-13 }, - { id = "GHSA-3g3m-2fjr-6pw4", ignoreUntil = 2026-11-13 }, - { id = "GHSA-4x5r-pxfx-6jf8", ignoreUntil = 2026-11-13 }, - { id = "GHSA-5j98-2g5x-46v6", ignoreUntil = 2026-11-13 }, - { id = "GHSA-6f2x-v7q7-m7m5", ignoreUntil = 2026-11-13 }, - { id = "GHSA-858h-whjf-mvg5", ignoreUntil = 2026-11-13 }, - { id = "GHSA-g3hv-75wg-m249", ignoreUntil = 2026-11-13 }, - { id = "GHSA-g4wm-2vf7-vfgr", ignoreUntil = 2026-11-13 }, - { id = "GHSA-hw8r-x6gr-5gjp", ignoreUntil = 2026-11-13 }, - { id = "GHSA-pppg-cpfq-h7wr", ignoreUntil = 2026-11-13 }, - { id = "GHSA-qwgh-2vcv-g2f7", ignoreUntil = 2026-11-13 }, - { id = "GHSA-v5rq-49vh-5v5c", ignoreUntil = 2026-11-13 }, - { id = "GHSA-v88g-cgmw-v5xw", ignoreUntil = 2026-11-13 }, - { id = "GHSA-x6jw-m9v5-85vh", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2023-0071", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2024-0436", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2024-0449", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2025-0134", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0097", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0110", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0118", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0119", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0173", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0249", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0258", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0285", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0346", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0348", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0349", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0387", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0391", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0395", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0396", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0397", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0398", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0399", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0400", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0401", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0402", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0403", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0404", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0405", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0406", ignoreUntil = 2026-11-13 }, - { id = "RUSTSEC-2026-0459", ignoreUntil = 2026-11-13 }, + { id = "GHSA-2g4f-4pwh-qvx6", ignoreUntil = 2027-01-01 }, + { id = "GHSA-3g3m-2fjr-6pw4", ignoreUntil = 2027-01-01 }, + { id = "GHSA-4x5r-pxfx-6jf8", ignoreUntil = 2027-01-01 }, + { id = "GHSA-5j98-2g5x-46v6", ignoreUntil = 2027-01-01 }, + { id = "GHSA-6f2x-v7q7-m7m5", ignoreUntil = 2027-01-01 }, + { id = "GHSA-858h-whjf-mvg5", ignoreUntil = 2027-01-01 }, + { id = "GHSA-g3hv-75wg-m249", ignoreUntil = 2027-01-01 }, + { id = "GHSA-g4wm-2vf7-vfgr", ignoreUntil = 2027-01-01 }, + { id = "GHSA-hw8r-x6gr-5gjp", ignoreUntil = 2027-01-01 }, + { id = "GHSA-pppg-cpfq-h7wr", ignoreUntil = 2027-01-01 }, + { id = "GHSA-qwgh-2vcv-g2f7", ignoreUntil = 2027-01-01 }, + { id = "GHSA-v5rq-49vh-5v5c", ignoreUntil = 2027-01-01 }, + { id = "GHSA-v88g-cgmw-v5xw", ignoreUntil = 2027-01-01 }, + { id = "GHSA-x6jw-m9v5-85vh", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2023-0071", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2024-0436", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2024-0449", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2025-0134", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0097", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0110", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0118", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0119", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0173", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0249", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0258", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0285", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0346", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0348", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0349", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0387", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0391", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0395", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0396", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0397", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0398", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0399", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0400", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0401", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0402", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0403", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0404", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0405", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0406", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0459", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0487", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0489", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0490", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0491", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0504", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0505", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0511", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0512", ignoreUntil = 2027-01-01 }, + { id = "RUSTSEC-2026-0514", ignoreUntil = 2027-01-01 }, ] diff --git a/config/semgrep/prefer-yaml-toml.yaml b/config/semgrep/prefer-yaml-toml.yaml index 3eb5da1b..7488b96a 100644 --- a/config/semgrep/prefer-yaml-toml.yaml +++ b/config/semgrep/prefer-yaml-toml.yaml @@ -19,6 +19,8 @@ rules: - "/**/oxfmtrc.jsonc" # oxfmt: only JSON / JSONC supported - "/**/tsconfig.json" # TypeScript/tsgolint: mandates the JSON `tsconfig.json` name (oxlint --type-aware) - "/**/math1-input.json" # math1 wire payload: served to modules verbatim, parsed by their JSON parsers + # Written from the signatures.yaml beside it: the form the crate embeds, since its embedding macro reads JSON. + - "/libs/gpu-probe/signatures.json" # Both jscpd files have to be plain JSON, and .jsonc would misrepresent them. # jscpd parses its config with plain serde_json and reads no YAML/TOML, so a `//` comment aborts it with # `key must be a string at line 2 column 3` -- a .jsonc extension would advertise comments the parser diff --git a/libs/edge-toolkit/src/board.rs b/libs/edge-toolkit/src/board.rs index 3b3cebc9..9d6caa64 100644 --- a/libs/edge-toolkit/src/board.rs +++ b/libs/edge-toolkit/src/board.rs @@ -10,7 +10,7 @@ pub const PREVIEW2: &str = "0.2.12"; /// /// The `runner` world's own imports, and the WASI Preview 2 a ws-module guest built for `wasm32-wasip2` imports, the /// latter at [`PREVIEW2`]. -pub const HOSTED_IMPORTS: [&str; 18] = [ +pub const HOSTED_IMPORTS: [&str; 19] = [ "et:ws-messages/messages@0.2.0", "et:ws-wasi/ws@0.2.0", "wasi:cli/environment@0.2.12", @@ -24,6 +24,7 @@ pub const HOSTED_IMPORTS: [&str; 18] = [ "wasi:cli/terminal-stdin@0.2.12", "wasi:cli/terminal-stdout@0.2.12", "wasi:clocks/monotonic-clock@0.2.12", + "wasi:clocks/wall-clock@0.2.12", "wasi:io/error@0.2.12", "wasi:io/poll@0.2.12", "wasi:io/streams@0.2.12", @@ -34,13 +35,8 @@ pub const HOSTED_IMPORTS: [&str; 18] = [ /// What a guest instrumented for coverage imports beyond [`HOSTED_IMPORTS`], to write its profile into `/cov`. /// /// A board's runner links these only when built for coverage, which the repo's coverage workflow does to run the -/// instrumented guests; a board's firmware never is. `wall-clock` is imported for the timestamp type alone, which a -/// linker needs no definition of. -pub const COVERAGE_IMPORTS: [&str; 3] = [ - "wasi:clocks/wall-clock@0.2.12", - "wasi:filesystem/preopens@0.2.12", - "wasi:filesystem/types@0.2.12", -]; +/// instrumented guests; a board's firmware never is. +pub const COVERAGE_IMPORTS: [&str; 2] = ["wasi:filesystem/preopens@0.2.12", "wasi:filesystem/types@0.2.12"]; /// Every interface this build's board runner links: [`HOSTED_IMPORTS`]. #[cfg(not(feature = "coverage"))] diff --git a/libs/edge-toolkit/src/ws_server.rs b/libs/edge-toolkit/src/ws_server.rs index a2280db3..4fd3a97c 100644 --- a/libs/edge-toolkit/src/ws_server.rs +++ b/libs/edge-toolkit/src/ws_server.rs @@ -490,14 +490,11 @@ impl AgentRegistry { #[must_use] pub fn pending_messages_for(&self, agent_id: &str) -> Vec { let agents = lock_recovering(&self.agents); - agents - .get(agent_id) - .map(|record| { - let mut pending = record.pending_direct_messages.values().cloned().collect::>(); - pending.sort_by(|left, right| left.message_id.cmp(&right.message_id)); - pending - }) - .unwrap_or_default() + agents.get(agent_id).map_or_default(|record| { + let mut pending = record.pending_direct_messages.values().cloned().collect::>(); + pending.sort_by(|left, right| left.message_id.cmp(&right.message_id)); + pending + }) } /// Returns `(message_id, sender_session, sender_agent_id)` on success. diff --git a/libs/gpu-probe/Cargo.toml b/libs/gpu-probe/Cargo.toml index 5a7515ea..8480ed45 100644 --- a/libs/gpu-probe/Cargo.toml +++ b/libs/gpu-probe/Cargo.toml @@ -7,8 +7,35 @@ edition.workspace = true license.workspace = true repository.workspace = true +[package.metadata.docs.rs] +features = ["docs"] + [lib] doctest = false +[features] +## Embeds the registry of every signature the probes have been seen to give. +## `Signature::identify` then names the hardware a signature was seen on, and `Signature::seen` lists each place. +registry = ["dep:serde_json", "dep:target-lexicon", "dep:thiserror", "dep:webbrowser", "dep:wgpu-hal"] +## Internal: renders this feature list into the crate documentation, as docs.rs builds it. +docs = ["dep:document-features"] + +[dependencies] +document-features = { workspace = true, optional = true } +serde_json = { workspace = true, features = ["std"], optional = true } +# The three below each name a kind of thing a registry row records: a CPU architecture, a browser, a GPU vendor. +target-lexicon = { workspace = true, optional = true } +thiserror = { workspace = true, optional = true } +webbrowser = { workspace = true, optional = true } +wgpu-hal = { workspace = true, optional = true } + +# What the native test puts the probes to this machine's GPU with, on the backends `et-ws-wasi-runner` hosts. +# The crate's dependency on itself turns the registry on for every test. +[dev-dependencies] +et-gpu-probe = { workspace = true, features = ["registry"] } +futures-executor.workspace = true +serde_json = { workspace = true, features = ["std"] } +wgpu = { workspace = true, features = ["dx12", "metal", "std", "vulkan", "wgsl"] } + [lints] workspace = true diff --git a/libs/gpu-probe/signatures.json b/libs/gpu-probe/signatures.json new file mode 100644 index 00000000..35c69b4d --- /dev/null +++ b/libs/gpu-probe/signatures.json @@ -0,0 +1,3271 @@ +{ + "probe_set": 1, + "signatures": [ + { + "interface": "webgl2-split", + "label": "AMD Radeon Graphics (0x15E7) through Direct3D 11 on Windows, HP EliteBook -- WebGL 2 on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "amd", + "gpu_version": "Radeon Graphics (0x15E7)", + "backend": "d3d11", + "device": "HP EliteBook", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaaa", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb897", + "tan": "3dcd7c45", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087707", + "tanh": "3dcc1ebe", + "dot": "3f800000" + } + }, + { + "interface": "webgl2-split", + "label": "Qualcomm Adreno on Android -- WebGL 2 on Chrome: Oppo (Adreno 610), Samsung Galaxy S22 Ultra (Adreno 730)", + "seen": [ + { + "browser": "chrome", + "os": "android", + "cpu_arch": "aarch64", + "gpu": "qualcomm", + "gpu_version": "Adreno 610", + "backend": "opengl-es", + "device": "Oppo phone", + "window": "normal" + }, + { + "browser": "chrome", + "os": "android", + "cpu_arch": "aarch64", + "gpu": "qualcomm", + "gpu_version": "Adreno 730", + "backend": "opengl-es", + "device": "Samsung Galaxy S22 Ultra", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "????????", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc758b", + "cos": "3f7eb898", + "tan": "3dcd7c58", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b9e", + "tanh": "3dcc1eba", + "dot": "00000000" + } + }, + { + "interface": "webgl2-split", + "label": "Intel UHD Graphics 630 through Metal on macOS -- WebGL 2 on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "macos", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "UHD Graphics 630", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc75ec", + "cos": "3f7eb8a9", + "tan": "3dcd7cac", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebc", + "dot": "40000000" + } + }, + { + "interface": "webgl2-combined", + "label": "ARM Mali-G76, OpenGL ES, Galaxy S10 -- WebGL 2 on Chrome 157, Samsung Internet 31, Brave 1.99, Firefox 159", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "opengl-es", + "device": "Samsung Galaxy S10", + "window": "normal" + }, + { + "browser": "samsung-internet", + "browser_version": "31", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "opengl-es", + "device": "Samsung Galaxy S10", + "window": "normal" + }, + { + "browser": "brave", + "browser_version": "1.99", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "opengl-es", + "device": "Samsung Galaxy S10", + "window": "normal" + }, + { + "browser": "firefox", + "browser_version": "159", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "opengl-es", + "device": "Samsung Galaxy S10", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb898", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b99", + "tanh": "3dcc1ebe", + "dot": "00000000" + } + }, + { + "interface": "webgl2-split", + "label": "Intel Iris Xe (Alder Lake GT2) through Mesa on Linux, Dell Precision 5570 -- WebGL 2 on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "linux", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Iris Xe (Alder Lake GT2), Mesa", + "backend": "opengl", + "device": "Dell Precision 5570", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087ae6", + "tanh": "3dcc1eb6", + "dot": "3f800000" + } + }, + { + "interface": "webgpu-split", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Chromium and Brave", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + }, + { + "browser": "brave", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "webgl2-split", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGL 2 on Chromium, Safari, Brave and Firefox", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + }, + { + "browser": "safari", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + }, + { + "browser": "brave", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + }, + { + "browser": "firefox", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebc", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Safari", + "seen": [ + { + "browser": "safari", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "40000000" + } + }, + { + "interface": "webgpu-split", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU as wgpu implements it: Firefox, Deno and wgpu 29 alone", + "seen": [ + { + "browser": "firefox", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + }, + { + "browser": "deno", + "browser_version": "deno_runtime 0.268.0, deno_webgpu 0.227.0", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "runtime": "et-ws-web-runner 0.3.0", + "window": "none", + "date": "2026-11-12" + }, + { + "browser": "none", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "runtime": "wgpu 29", + "window": "none", + "date": "2026-11-12" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "webgl2-split", + "label": "Mesa / llvmpipe, or similar on Linux (Firefox 159.0)", + "seen": [ + { + "browser": "firefox", + "browser_version": "159.0", + "os": "linux", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "reported": "Mesa / llvmpipe, or similar", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "????????", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb898", + "tan": "3dcd7c44", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087ae6", + "tanh": "3dcc1ebe", + "dot": "40000000" + } + }, + { + "interface": "webgl2-split", + "label": "NVIDIA GB10/PCIe through OpenGL on Linux -- WebGL 2 on Chromium and Firefox", + "seen": [ + { + "browser": "chromium", + "os": "linux", + "cpu_arch": "aarch64", + "cpu_version": "NVIDIA GB10", + "gpu": "nvidia", + "gpu_version": "GB10", + "backend": "opengl", + "window": "normal" + }, + { + "browser": "firefox", + "os": "linux", + "cpu_arch": "aarch64", + "cpu_version": "NVIDIA GB10", + "gpu": "nvidia", + "gpu_version": "GB10", + "backend": "opengl", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7566", + "cos": "3f7eb897", + "tan": "3dcd7c34", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebc", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "NVIDIA GB10 through Vulkan on Linux -- WebGPU on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "linux", + "cpu_arch": "aarch64", + "cpu_version": "NVIDIA GB10", + "gpu": "nvidia", + "gpu_version": "GB10", + "backend": "vulkan", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7566", + "cos": "3f7eb897", + "tan": "3dcd7c34", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "webgl2-split", + "label": "NVIDIA GB10 through Vulkan on Linux -- WebGL 2 on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "linux", + "cpu_arch": "aarch64", + "cpu_version": "NVIDIA GB10", + "gpu": "nvidia", + "gpu_version": "GB10", + "backend": "vulkan", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7566", + "cos": "3f7eb897", + "tan": "3dcd7c34", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "40000000" + } + }, + { + "interface": "webgpu-split", + "label": "Intel Xe-LPG on Windows -- WebGPU on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Xe-LPG", + "backend": "unknown", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087708", + "tanh": "3dcc1eb6", + "dot": "00000000" + } + }, + { + "interface": "webgl2-split", + "label": "Intel Xe-LPG through Direct3D 11 on Windows -- WebGL 2 on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Xe-LPG", + "backend": "d3d11", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087708", + "tanh": "3dcc1eb6", + "dot": "00000000" + } + }, + { + "interface": "webgpu-combined", + "label": "ARM Mali-G76, Vulkan, Galaxy S10 -- WebGPU on Chrome 157, Samsung Internet 31, Brave 1.99", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "vulkan", + "device": "Samsung Galaxy S10", + "window": "normal" + }, + { + "browser": "samsung-internet", + "browser_version": "31", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "vulkan", + "device": "Samsung Galaxy S10", + "window": "normal" + }, + { + "browser": "brave", + "browser_version": "1.99", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "vulkan", + "device": "Samsung Galaxy S10", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb898", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b99", + "tanh": "3dcc1ebe", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "Qualcomm Adreno 730 (Samsung Galaxy S22 Ultra) through Vulkan on Android -- WebGPU on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "android", + "cpu_arch": "aarch64", + "gpu": "qualcomm", + "gpu_version": "Adreno 730", + "backend": "vulkan", + "device": "Samsung Galaxy S22 Ultra", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc758b", + "cos": "3f7eb898", + "tan": "3dcd7c58", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b9e", + "tanh": "3dcc1eba", + "dot": "00000000" + } + }, + { + "interface": "webgl2-split", + "label": "Imagination PowerVR Rogue GE8300 through OpenGL ES on Android, Laser tablet -- WebGL 2 on Chrome 135, 157", + "seen": [ + { + "browser": "chrome", + "browser_version": "135", + "os": "android", + "os_version": "10", + "cpu_arch": "unknown", + "gpu": "imgtec", + "gpu_version": "PowerVR Rogue GE8300", + "backend": "opengl-es", + "device": "Laser tablet", + "window": "normal" + }, + { + "browser": "chrome", + "browser_version": "157", + "os": "android", + "os_version": "10", + "cpu_arch": "unknown", + "gpu": "imgtec", + "gpu_version": "PowerVR Rogue GE8300", + "backend": "opengl-es", + "device": "Laser tablet", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c42", + "exp": "3f8d763e", + "log": "3f8c9f53", + "pow": "3f8edd1f", + "atan2": "3d087b98", + "tanh": "3dcc1ec2", + "dot": "3f800000" + } + }, + { + "interface": "webgpu-split", + "label": "Google SwiftShader (software renderer) on Linux -- WebGPU on Chrome, Dell Precision 5570", + "seen": [ + { + "browser": "chrome", + "os": "linux", + "cpu_arch": "x86_64", + "gpu": "google", + "gpu_version": "SwiftShader (software renderer)", + "backend": "vulkan", + "device": "Dell Precision 5570", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc6e85", + "cos": "3f7eadd1", + "tan": "3dcd7dfb", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b99", + "tanh": "3dcc1eba", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "AMD Radeon RDNA 2 (HP EliteBook) through Direct3D 12 on Windows -- WebGPU on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "amd", + "gpu_version": "Radeon RDNA 2", + "backend": "d3d12", + "device": "HP EliteBook", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "40800000", + "divide": "3eaaaaaa", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb897", + "tan": "3dcd7c45", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b98", + "tanh": "3dcc1ebe", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "AMD Radeon Pro (RDNA 1) through Metal on macOS, Intel MacBook Pro -- WebGPU on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "x86_64", + "gpu": "amd", + "gpu_version": "Radeon Pro (RDNA 1)", + "backend": "metal", + "device": "Intel MacBook Pro", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaaa", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb897", + "tan": "3dcd7c45", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b98", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "webgpu-combined", + "label": "Qualcomm Adreno 830 (Samsung Galaxy S25 Ultra) through Vulkan on Android -- WebGPU on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "android", + "cpu_arch": "aarch64", + "gpu": "qualcomm", + "gpu_version": "Adreno 830", + "backend": "vulkan", + "device": "Samsung Galaxy S25 Ultra", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7578", + "cos": "3f7eb898", + "tan": "3dcd7c45", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b9e", + "tanh": "3dcc1eba", + "dot": "00000000" + } + }, + { + "interface": "webgl2-combined", + "label": "Qualcomm Adreno 830 (Galaxy S25 Ultra), OpenGL ES, Android -- WebGL 2 on Chrome (mixed shader grouping)", + "seen": [ + { + "browser": "chrome", + "os": "android", + "cpu_arch": "aarch64", + "gpu": "qualcomm", + "gpu_version": "Adreno 830", + "backend": "opengl-es", + "device": "Samsung Galaxy S25 Ultra", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "????????", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7578", + "cos": "3f7eb898", + "tan": "3dcd7c45", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b9e", + "tanh": "3dcc1eba", + "dot": "00000000" + } + }, + { + "interface": "canvas", + "label": "Apple M5 Pro, macOS -- canvas 2D on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "unknown", + "window": "normal" + } + ], + "results": { + "lane0": "4e31fa9b", + "lane1": "76b62595", + "lane2": "5debc5c0", + "lane3": "52ef3664" + } + }, + { + "interface": "webgl2-combined", + "label": "Intel / HD Graphics 400, or similar on Linux (Firefox 149.0)", + "seen": [ + { + "browser": "firefox", + "browser_version": "149.0", + "os": "linux", + "cpu_arch": "x86_64", + "gpu": "intel", + "backend": "unknown", + "reported": "Intel / HD Graphics 400, or similar", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc75ec", + "cos": "3f7eb8a9", + "tan": "3dcd7cac", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087ae6", + "tanh": "3dcc1eb6", + "dot": "3f800000" + } + }, + { + "interface": "canvas", + "label": "Firefox 149.0 on Linux -- canvas 2D", + "seen": [ + { + "browser": "firefox", + "browser_version": "149.0", + "os": "linux", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "window": "normal" + } + ], + "results": { + "lane0": "b11cf4d5", + "lane1": "9f52fa33", + "lane2": "1d528dc3", + "lane3": "205c1f18" + } + }, + { + "interface": "canvas", + "label": "Firefox 160.0 on macOS -- canvas 2D", + "seen": [ + { + "browser": "firefox", + "browser_version": "160.0", + "os": "macos", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "window": "normal" + } + ], + "results": { + "lane0": "e713762c", + "lane1": "7af1305a", + "lane2": "a45f2769", + "lane3": "4b50c285" + } + }, + { + "interface": "canvas", + "label": "Safari 27.0 on macOS -- canvas 2D", + "seen": [ + { + "browser": "safari", + "browser_version": "27.0", + "os": "macos", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "window": "normal" + } + ], + "results": { + "lane0": "a6b65bfe", + "lane1": "5086a288", + "lane2": "656c1bac", + "lane3": "3da9f222" + } + }, + { + "interface": "canvas", + "label": "Chrome 157.0.8103.50 and Microsoft Edge on Windows -- canvas 2D", + "seen": [ + { + "browser": "chrome", + "browser_version": "157.0.8103.50", + "os": "windows", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "window": "normal" + }, + { + "browser": "edge", + "os": "windows", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "window": "normal" + } + ], + "results": { + "lane0": "6f919589", + "lane1": "21590c7f", + "lane2": "99fa99fb", + "lane3": "459a0e48" + } + }, + { + "interface": "canvas", + "label": "Brave 157.0.0 on macOS 26.7.1 -- canvas 2D (farbled; session-keyed noise, not reproducible across sessions)", + "seen": [ + { + "browser": "brave", + "browser_version": "157.0.0", + "os": "macos", + "os_version": "26.7.1", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "defence": "Shields farbling: noise keyed to the session", + "window": "normal" + } + ], + "results": { + "lane0": "c8310eb1", + "lane1": "6d12025f", + "lane2": "25aa14b4", + "lane3": "453e64a3" + } + }, + { + "interface": "webgpu", + "label": "Tor Browser (reports itself as Firefox 153.0.0) on macOS, Apple M5 Pro", + "seen": [ + { + "browser": "tor-browser", + "browser_version": "reports itself as Firefox 153.0.0", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "unknown", + "window": "normal" + } + ], + "blocked": true, + "reason": "WebGPU is unavailable (navigator.gpu is absent; the browser does not expose it)" + }, + { + "interface": "webgl2", + "label": "Tor Browser (reports itself as Firefox 153.0.0) on macOS, Apple M5 Pro", + "seen": [ + { + "browser": "tor-browser", + "browser_version": "reports itself as Firefox 153.0.0", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "unknown", + "window": "normal" + } + ], + "blocked": true, + "reason": "WebGL 2 is unavailable (getContext returned null; only WebGL 1 is available, not WebGL 2)" + }, + { + "interface": "canvas", + "label": "Tor Browser (reports itself as Firefox 153.0.0) on macOS, Apple M5 Pro", + "seen": [ + { + "browser": "tor-browser", + "browser_version": "reports itself as Firefox 153.0.0", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "unknown", + "defence": "canvas reads carry per-read noise", + "window": "normal" + } + ], + "blocked": true, + "reason": "randomised: two reads of the same scene differed (per-read noise)" + }, + { + "interface": "canvas", + "label": "Samsung Galaxy S10, Android 12 -- canvas 2D on Chrome 157 and Samsung Internet 31", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "unknown", + "device": "Samsung Galaxy S10", + "window": "normal" + }, + { + "browser": "samsung-internet", + "browser_version": "31", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "unknown", + "device": "Samsung Galaxy S10", + "window": "normal" + } + ], + "results": { + "lane0": "c1161ae6", + "lane1": "4f61a1f8", + "lane2": "65ec1468", + "lane3": "c8bf17ef" + } + }, + { + "interface": "canvas", + "label": "Samsung Galaxy S10, Android 12 -- canvas 2D on Firefox 159", + "seen": [ + { + "browser": "firefox", + "browser_version": "159", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "unknown", + "device": "Samsung Galaxy S10", + "window": "normal" + } + ], + "results": { + "lane0": "16b5cc90", + "lane1": "4a208b5e", + "lane2": "536d9443", + "lane3": "5c29fe34" + } + }, + { + "interface": "webgpu", + "label": "Firefox 159 on Android, Samsung Galaxy S10", + "seen": [ + { + "browser": "firefox", + "browser_version": "159", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "unknown", + "device": "Samsung Galaxy S10", + "window": "normal" + } + ], + "blocked": true, + "reason": "WebGPU is unavailable (navigator.gpu is absent; the browser does not expose it)" + }, + { + "interface": "canvas", + "label": "Samsung Galaxy S10, Android 12 -- canvas 2D on Brave 1.99 (farbled; differs from Chrome's on this phone)", + "seen": [ + { + "browser": "brave", + "browser_version": "1.99", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "unknown", + "device": "Samsung Galaxy S10", + "defence": "Shields farbling", + "window": "normal" + } + ], + "results": { + "lane0": "1419645e", + "lane1": "7aa1bb28", + "lane2": "ea31c65e", + "lane3": "9637a16c" + } + }, + { + "interface": "webgl2-combined", + "label": "Intel Iris Xe (Alder Lake GT2) through Mesa on Linux, Dell Precision 5570 -- WebGL 2 on Chrome 157", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "linux", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Iris Xe (Alder Lake GT2), Mesa", + "backend": "opengl", + "device": "Dell Precision 5570", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087ae6", + "tanh": "3dcc1eb6", + "dot": "3f800000" + } + }, + { + "interface": "canvas", + "label": "Dell Precision 5570, Linux -- canvas 2D on Chrome 157", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "linux", + "cpu_arch": "x86_64", + "gpu": "unknown", + "backend": "unknown", + "device": "Dell Precision 5570", + "window": "normal" + } + ], + "results": { + "lane0": "e55c4313", + "lane1": "fdb387dd", + "lane2": "e659dabe", + "lane3": "d711a464" + } + }, + { + "interface": "webgl2-split", + "label": "ARM Mali-G76 through OpenGL ES on Android, Samsung Galaxy S10 -- WebGL 2 on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "opengl-es", + "device": "Samsung Galaxy S10", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb898", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b99", + "tanh": "3dcc1ebe", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "ARM Mali-G76 through Vulkan on Android, Samsung Galaxy S10 -- WebGPU on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "android", + "os_version": "12", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "vulkan", + "device": "Samsung Galaxy S10", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb898", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b99", + "tanh": "3dcc1ebe", + "dot": "00000000" + } + }, + { + "interface": "webgl2-combined", + "label": "AMD Radeon Graphics (0x15E7) through Direct3D 11 on Windows, HP EliteBook -- WebGL 2 on Chrome 157", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "amd", + "gpu_version": "Radeon Graphics (0x15E7)", + "backend": "d3d11", + "device": "HP EliteBook", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaaa", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb897", + "tan": "3dcd7c45", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087707", + "tanh": "3dcc1ebe", + "dot": "3f800000" + } + }, + { + "interface": "webgpu-combined", + "label": "AMD Radeon RDNA 2 (HP EliteBook) through Direct3D 12 on Windows -- WebGPU on Chrome 157", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "amd", + "gpu_version": "Radeon RDNA 2", + "backend": "d3d12", + "device": "HP EliteBook", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "40800000", + "divide": "3eaaaaaa", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb897", + "tan": "3dcd7c45", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b98", + "tanh": "3dcc1ebe", + "dot": "00000000" + } + }, + { + "interface": "webgpu-combined", + "label": "Google SwiftShader (software renderer) on Linux -- WebGPU on Chrome, Dell Precision 5570", + "seen": [ + { + "browser": "chrome", + "os": "linux", + "cpu_arch": "x86_64", + "gpu": "google", + "gpu_version": "SwiftShader (software renderer)", + "backend": "vulkan", + "device": "Dell Precision 5570", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc6e85", + "cos": "3f7eadd1", + "tan": "3dcd7dfb", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b99", + "tanh": "3dcc1eba", + "dot": "00000000" + } + }, + { + "interface": "webgpu-combined", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "webgl2-combined", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGL 2 on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebc", + "dot": "00000000" + } + }, + { + "interface": "webgpu-combined", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU as wgpu implements it: Firefox 160 and wgpu 29 alone", + "seen": [ + { + "browser": "firefox", + "browser_version": "160", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + }, + { + "browser": "none", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "runtime": "wgpu 29", + "window": "none", + "date": "2026-11-12" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "webgpu", + "label": "Chrome 157 on Android 10, Laser tablet", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "android", + "os_version": "10", + "cpu_arch": "unknown", + "gpu": "imgtec", + "gpu_version": "PowerVR Rogue GE8300", + "backend": "unknown", + "device": "Laser tablet", + "window": "normal" + } + ], + "blocked": true, + "reason": "no WebGPU adapter was returned" + }, + { + "interface": "webgl2-combined", + "label": "Imagination PowerVR Rogue GE8300 through OpenGL ES on Android, Laser tablet -- WebGL 2 on Chrome 157", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "android", + "os_version": "10", + "cpu_arch": "unknown", + "gpu": "imgtec", + "gpu_version": "PowerVR Rogue GE8300", + "backend": "opengl-es", + "device": "Laser tablet", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c42", + "exp": "3f8d763e", + "log": "3f8c9f53", + "pow": "3f8edd1f", + "atan2": "3d087b98", + "tanh": "3dcc1ec2", + "dot": "3f800000" + } + }, + { + "interface": "canvas", + "label": "Laser tablet, Android 10 -- canvas 2D on Chrome 157", + "seen": [ + { + "browser": "chrome", + "browser_version": "157", + "os": "android", + "os_version": "10", + "cpu_arch": "unknown", + "gpu": "imgtec", + "gpu_version": "PowerVR Rogue GE8300", + "backend": "unknown", + "device": "Laser tablet", + "window": "normal" + } + ], + "results": { + "lane0": "0d2dacc4", + "lane1": "01059e2a", + "lane2": "26b6c476", + "lane3": "68dfadcf" + } + }, + { + "interface": "webgpu-combined", + "label": "AMD Radeon Pro (RDNA 1) through Metal on macOS, Intel MacBook Pro -- WebGPU on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "x86_64", + "gpu": "amd", + "gpu_version": "Radeon Pro (RDNA 1)", + "backend": "metal", + "device": "Intel MacBook Pro", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaaa", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb897", + "tan": "3dcd7c45", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b98", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "webgl2-combined", + "label": "Intel UHD Graphics 630 through Metal on macOS -- WebGL 2 on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "macos", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "UHD Graphics 630", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc75ec", + "cos": "3f7eb8a9", + "tan": "3dcd7cac", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebc", + "dot": "40000000" + } + }, + { + "interface": "canvas", + "label": "Intel MacBook Pro, macOS -- canvas 2D on Chrome 154", + "seen": [ + { + "browser": "chrome", + "browser_version": "154", + "os": "macos", + "cpu_arch": "x86_64", + "gpu": "unknown", + "backend": "unknown", + "device": "Intel MacBook Pro", + "window": "normal" + } + ], + "results": { + "lane0": "61a3f194", + "lane1": "f467a8a2", + "lane2": "bf84affe", + "lane3": "84e85fe3" + } + }, + { + "interface": "webgpu-split", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Chromium and Brave", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + }, + { + "browser": "brave", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "webgpu-combined", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "webgl2-split", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGL 2 on Chromium, Safari, Brave and Firefox", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + }, + { + "browser": "safari", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + }, + { + "browser": "brave", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + }, + { + "browser": "firefox", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebc", + "dot": "00000000" + } + }, + { + "interface": "webgl2-combined", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGL 2 on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebc", + "dot": "00000000" + } + }, + { + "interface": "canvas", + "label": "Apple M5 Pro, macOS -- canvas 2D on Chromium", + "seen": [ + { + "browser": "chromium", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "unknown", + "window": "private" + } + ], + "results": { + "lane0": "4e31fa9b", + "lane1": "76b62595", + "lane2": "5debc5c0", + "lane3": "52ef3664" + } + }, + { + "interface": "canvas", + "label": "Firefox 160 on macOS, privacy.fingerprintingProtection on -- canvas 2D (differs from the unprotected hash)", + "seen": [ + { + "browser": "firefox", + "browser_version": "160", + "os": "macos", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "defence": "privacy.fingerprintingProtection on", + "window": "normal" + } + ], + "results": { + "lane0": "a60e6953", + "lane1": "e2f45f75", + "lane2": "956014b5", + "lane3": "1b2515f8" + } + }, + { + "interface": "webgpu-split", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Firefox", + "seen": [ + { + "browser": "firefox", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "canvas", + "label": "Firefox 160 on macOS, privacy.fingerprintingProtection on -- canvas 2D (differs from the unprotected hash)", + "seen": [ + { + "browser": "firefox", + "browser_version": "160", + "os": "macos", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "defence": "privacy.fingerprintingProtection on", + "window": "private" + } + ], + "results": { + "lane0": "a60e6953", + "lane1": "e2f45f75", + "lane2": "956014b5", + "lane3": "1b2515f8" + } + }, + { + "interface": "webgpu-combined", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Firefox 160", + "seen": [ + { + "browser": "firefox", + "browser_version": "160", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "00000000" + } + }, + { + "interface": "canvas-browserleaks", + "label": "Firefox 160 on macOS, fingerprintingProtection on -- BrowserLeaks test (one session's value; see next row)", + "seen": [ + { + "browser": "firefox", + "browser_version": "160", + "os": "macos", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "defence": "privacy.fingerprintingProtection on", + "window": "normal" + } + ], + "results": { + "md5": "67456dde9d4089a8cea2e53d4ecf4b8b" + } + }, + { + "interface": "canvas-browserleaks", + "label": "Firefox 160 on macOS, privacy.fingerprintingProtection on", + "seen": [ + { + "browser": "firefox", + "browser_version": "160", + "os": "macos", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "defence": "privacy.fingerprintingProtection on", + "window": "normal" + } + ], + "blocked": true, + "reason": "randomised: hash differs between visits in one browser (noise keyed to the session)" + }, + { + "interface": "canvas-browserleaks", + "label": "Brave 157.0.0 on macOS 26.7.1 -- canvas 2D, BrowserLeaks test (noise added to reads)", + "seen": [ + { + "browser": "brave", + "browser_version": "157.0.0", + "os": "macos", + "os_version": "26.7.1", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "defence": "Shields farbling: noise added to reads", + "window": "normal" + } + ], + "results": { + "md5": "ead7c33e84beeb434b1d384c6e45268b" + } + }, + { + "interface": "canvas-browserleaks", + "label": "Firefox 160.0.0 on macOS -- canvas 2D, BrowserLeaks test", + "seen": [ + { + "browser": "firefox", + "browser_version": "160.0.0", + "os": "macos", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "window": "private" + } + ], + "results": { + "md5": "4a05912a83d4fdbab55e5ddfaef2d3b0" + } + }, + { + "interface": "webgpu", + "label": "LibreWolf (reports itself as Firefox 160.0.0) on macOS, Apple M5 Pro", + "seen": [ + { + "browser": "librewolf", + "browser_version": "reports itself as Firefox 160.0.0", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "unknown", + "window": "normal" + } + ], + "blocked": true, + "reason": "WebGPU is unavailable (navigator.gpu is absent; the browser does not expose it)" + }, + { + "interface": "webgl2", + "label": "LibreWolf (reports itself as Firefox 160.0.0) on macOS, Apple M5 Pro", + "seen": [ + { + "browser": "librewolf", + "browser_version": "reports itself as Firefox 160.0.0", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "unknown", + "window": "normal" + } + ], + "blocked": true, + "reason": "WebGL 2 is unavailable (getContext returned null; no WebGL context at all, so WebGL is turned off)" + }, + { + "interface": "canvas", + "label": "LibreWolf (reports itself as Firefox 160.0.0) on macOS, Apple M5 Pro", + "seen": [ + { + "browser": "librewolf", + "browser_version": "reports itself as Firefox 160.0.0", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "unknown", + "window": "normal" + } + ], + "blocked": true, + "reason": "randomised: two reads of the same scene differed (per-read noise)" + }, + { + "interface": "canvas-browserleaks", + "label": "LibreWolf (reports itself as Firefox 160.0.0) on macOS, Apple M5 Pro", + "seen": [ + { + "browser": "librewolf", + "browser_version": "reports itself as Firefox 160.0.0", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "unknown", + "window": "normal" + } + ], + "blocked": true, + "reason": "randomised: two reads of the same scene differed (per-read noise)" + }, + { + "interface": "webgl1-split", + "label": "Apple M5 Pro through Metal on macOS 26.7.1 (Chrome 157.0.0 and Safari 27.0)", + "seen": [ + { + "browser": "chrome", + "browser_version": "157.0.0", + "os": "macos", + "os_version": "26.7.1", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + }, + { + "browser": "safari", + "browser_version": "27.0", + "os": "macos", + "os_version": "26.7.1", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "????????", + "dot": "00000000" + } + }, + { + "interface": "canvas-browserleaks", + "label": "Chrome 157.0.0 on macOS 26.7.1 -- canvas 2D, BrowserLeaks test", + "seen": [ + { + "browser": "chrome", + "browser_version": "157.0.0", + "os": "macos", + "os_version": "26.7.1", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "window": "normal" + } + ], + "results": { + "md5": "1dc44a8e2846f0a156399977bf5ca67d" + } + }, + { + "interface": "webgl1-combined", + "label": "Apple M5 Pro through Metal on macOS 26.7.1 (Chrome 157.0.0 and Safari 27.0)", + "seen": [ + { + "browser": "chrome", + "browser_version": "157.0.0", + "os": "macos", + "os_version": "26.7.1", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + }, + { + "browser": "safari", + "browser_version": "27.0", + "os": "macos", + "os_version": "26.7.1", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "????????", + "dot": "00000000" + } + }, + { + "interface": "webgl1", + "label": "Firefox 160.0.0 on macOS", + "seen": [ + { + "browser": "firefox", + "browser_version": "160.0.0", + "os": "macos", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "window": "normal" + } + ], + "blocked": true, + "reason": "WebGL 1 is unavailable (getContext returned null, so WebGL is turned off)" + }, + { + "interface": "webgl1-split", + "label": "Apple M5 Pro through Metal on macOS 26.7.1 (Chrome 157.0.0)", + "seen": [ + { + "browser": "chrome", + "browser_version": "157.0.0", + "os": "macos", + "os_version": "26.7.1", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "????????", + "dot": "00000000" + } + }, + { + "interface": "webgl1-combined", + "label": "Apple M5 Pro through Metal on macOS 26.7.1 (Chrome 157.0.0)", + "seen": [ + { + "browser": "chrome", + "browser_version": "157.0.0", + "os": "macos", + "os_version": "26.7.1", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "private" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "????????", + "dot": "00000000" + } + }, + { + "interface": "webgpu-combined", + "label": "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Safari", + "seen": [ + { + "browser": "safari", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "apple", + "gpu_version": "Apple M5 Pro GPU", + "backend": "metal", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb899", + "tan": "3dcd7c43", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1ebd", + "dot": "40000000" + } + }, + { + "interface": "canvas-browserleaks", + "label": "Safari 27.0.0 on macOS -- canvas 2D, BrowserLeaks test", + "seen": [ + { + "browser": "safari", + "browser_version": "27.0.0", + "os": "macos", + "cpu_arch": "unknown", + "gpu": "unknown", + "backend": "unknown", + "window": "normal" + } + ], + "results": { + "md5": "f7316747f9adf8387510e46790f09d66" + } + }, + { + "interface": "webgpu-combined", + "label": "Intel Xe-LPG on Windows, HP EliteBook (HSN-I6IC) -- WebGPU on Edge 153", + "seen": [ + { + "browser": "edge", + "browser_version": "153", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Xe-LPG", + "backend": "unknown", + "device": "HP EliteBook (HSN-I6IC)", + "window": "normal" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087708", + "tanh": "3dcc1eb6", + "dot": "00000000" + } + }, + { + "interface": "webgl2-combined", + "label": "Intel Xe-LPG through Direct3D 11 on Windows -- WebGL 2 on Chrome", + "seen": [ + { + "browser": "chrome", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Xe-LPG", + "backend": "d3d11", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087708", + "tanh": "3dcc1eb6", + "dot": "00000000" + } + }, + { + "interface": "webgl1-split", + "label": "Intel Arc Graphics through Direct3D 11 on Windows, HP EliteBook (HSN-I6IC) -- WebGL 1 on Edge 153", + "seen": [ + { + "browser": "edge", + "browser_version": "153", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Arc Graphics", + "backend": "d3d11", + "device": "HP EliteBook (HSN-I6IC)", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087708", + "tanh": "????????", + "dot": "00000000" + } + }, + { + "interface": "webgl1-combined", + "label": "Intel Arc Graphics through Direct3D 11 on Windows, HP EliteBook (HSN-I6IC) -- WebGL 1 on Edge 153", + "seen": [ + { + "browser": "edge", + "browser_version": "153", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Arc Graphics", + "backend": "d3d11", + "device": "HP EliteBook (HSN-I6IC)", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "????????", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087708", + "tanh": "????????", + "dot": "00000000" + } + }, + { + "interface": "canvas-browserleaks", + "label": "HP EliteBook (HSN-I6IC), Windows -- canvas 2D, BrowserLeaks test on Edge 153", + "seen": [ + { + "browser": "edge", + "browser_version": "153", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "backend": "unknown", + "device": "HP EliteBook (HSN-I6IC)", + "window": "normal" + } + ], + "results": { + "md5": "8c781e6c79d33a68ef6ae90ae071c989" + } + }, + { + "interface": "webgpu-split", + "label": "Intel Xe-LPG on Windows, HP EliteBook (HSN-I6IC) -- WebGPU on Firefox 155 (adapter info blanked)", + "seen": [ + { + "browser": "firefox", + "browser_version": "155", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Xe-LPG", + "backend": "unknown", + "device": "HP EliteBook (HSN-I6IC)", + "reported": "adapter info blanked", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087708", + "tanh": "3dcc1eb6", + "dot": "00000000" + } + }, + { + "interface": "canvas", + "label": "Firefox 155.0.0 on Windows, HP EliteBook (HSN-I6IC)", + "seen": [ + { + "browser": "firefox", + "browser_version": "155.0.0", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "backend": "unknown", + "device": "HP EliteBook (HSN-I6IC)", + "window": "normal" + } + ], + "blocked": true, + "reason": "randomised: hash differs between visits in one browser (noise keyed to the session)" + }, + { + "interface": "canvas-browserleaks", + "label": "Firefox 155.0.0 on Windows, HP EliteBook (HSN-I6IC)", + "seen": [ + { + "browser": "firefox", + "browser_version": "155.0.0", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "backend": "unknown", + "device": "HP EliteBook (HSN-I6IC)", + "window": "normal" + } + ], + "blocked": true, + "reason": "randomised: hash differs between visits in one browser (noise keyed to the session)" + }, + { + "interface": "webgpu-combined", + "label": "Intel Xe-LPG on Windows, HP EliteBook (HSN-I6IC) -- WebGPU on Firefox 155 (adapter info blanked)", + "seen": [ + { + "browser": "firefox", + "browser_version": "155", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "intel", + "gpu_version": "Xe-LPG", + "backend": "unknown", + "device": "HP EliteBook (HSN-I6IC)", + "reported": "adapter info blanked", + "window": "normal" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087708", + "tanh": "3dcc1eb6", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "Apple Paravirtual device through Metal on macOS arm64, GitHub macos-latest runner -- wgpu 29, no browser", + "seen": [ + { + "browser": "none", + "os": "macos", + "cpu_arch": "aarch64", + "gpu": "apple", + "gpu_version": "Apple Paravirtual device", + "backend": "metal", + "device": "GitHub macos-latest runner", + "runtime": "wgpu 29", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836477" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7576", + "cos": "3f7eb896", + "tan": "3dcd7c46", + "exp": "3f8d763d", + "log": "3f8c9f53", + "pow": "3f8edd1f", + "atan2": "3d087b99", + "tanh": "3dcc1eb5", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "llvmpipe (LLVM 20.1.2, Mesa 25.2.8) through Vulkan on Linux arm64, GitHub runner -- wgpu 29, no browser", + "seen": [ + { + "browser": "none", + "os": "linux", + "os_version": "Ubuntu 24.04", + "cpu_arch": "aarch64", + "gpu": "mesa", + "gpu_version": "llvmpipe (LLVM 20.1.2, 128 bits), Mesa 25.2.8-0ubuntu0.24.04.4", + "backend": "vulkan", + "device": "GitHub ubuntu-24.04-arm runner", + "runtime": "wgpu 29", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836545" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb898", + "tan": "3dcd7c44", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087ae6", + "tanh": "3dcc1ebe", + "dot": "40000000" + } + }, + { + "interface": "webgpu-split", + "label": "Microsoft Basic Render Driver through Direct3D 12 on Windows x64, GitHub runner -- wgpu 29 built for msvc", + "seen": [ + { + "browser": "none", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "microsoft", + "gpu_version": "Basic Render Driver 10.0.26100.33648", + "backend": "d3d12", + "device": "GitHub windows-latest runner", + "runtime": "wgpu 29, built for x86_64-pc-windows-msvc", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836624" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756b", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087708", + "tanh": "3dcc1ebc", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "Microsoft Basic Render Driver through Direct3D 12 on Windows x64, GitHub runner -- wgpu 29 built for GNU", + "seen": [ + { + "browser": "none", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "microsoft", + "gpu_version": "Basic Render Driver 10.0.26100.33648", + "backend": "d3d12", + "device": "GitHub windows-latest runner", + "runtime": "wgpu 29, built for x86_64-pc-windows-gnullvm", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836502" + }, + { + "browser": "none", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "microsoft", + "gpu_version": "Basic Render Driver 10.0.26100.33648", + "backend": "d3d12", + "device": "GitHub windows-latest runner", + "runtime": "wgpu 29, built for x86_64-pc-windows-gnu", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836594" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756b", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087708", + "tanh": "3dcc1ebe", + "dot": "00000000" + } + }, + { + "interface": "webgpu-split", + "label": "llvmpipe (LLVM 21.1.8, Mesa 26.0.8) through Vulkan on Linux x86_64, GitHub runner -- wgpu 29, no browser", + "seen": [ + { + "browser": "none", + "os": "linux", + "cpu_arch": "x86_64", + "gpu": "mesa", + "gpu_version": "llvmpipe (LLVM 21.1.8, 256 bits), Mesa 26.0.8-1ubuntu0.3", + "backend": "vulkan", + "device": "GitHub ubuntu-latest runner", + "runtime": "wgpu 29", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303201/job/131012174690" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb898", + "tan": "3dcd7c44", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087ae6", + "tanh": "3dcc1ebe", + "dot": "40000000" + } + }, + { + "interface": "webgpu-split", + "label": "Intel Iris Xe (Alder Lake GT2) through Mesa's Vulkan driver on Linux, Dell Precision 5570 -- wgpu 29", + "seen": [ + { + "browser": "none", + "os": "linux", + "os_version": "Pop!_OS 24.04", + "cpu_arch": "x86_64", + "cpu_version": "12th Gen Intel Core i7-12800H", + "gpu": "intel", + "gpu_version": "Iris Xe (Alder Lake GT2), Mesa 26.1.6-1pop0~1787580452~24.04~a5619ea", + "backend": "vulkan", + "device": "Dell Precision 5570", + "runtime": "wgpu 29", + "window": "none", + "date": "2026-11-13" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087ae6", + "tanh": "3dcc1eb6", + "dot": "3f800000" + } + }, + { + "interface": "webgpu-combined", + "label": "Intel Iris Xe (Alder Lake GT2) through Mesa's Vulkan driver on Linux, Dell Precision 5570 -- wgpu 29", + "seen": [ + { + "browser": "none", + "os": "linux", + "os_version": "Pop!_OS 24.04", + "cpu_arch": "x86_64", + "cpu_version": "12th Gen Intel Core i7-12800H", + "gpu": "intel", + "gpu_version": "Iris Xe (Alder Lake GT2), Mesa 26.1.6-1pop0~1787580452~24.04~a5619ea", + "backend": "vulkan", + "device": "Dell Precision 5570", + "runtime": "wgpu 29", + "window": "none", + "date": "2026-11-13" + } + ], + "results": { + "multiply_add": "33800000", + "fma": "33800000", + "swamped_sum": "40000000", + "divide": "3eaaaaab", + "subnormal_product": "00000000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc756a", + "cos": "3f7eb898", + "tan": "3dcd7c38", + "exp": "3f8d763d", + "log": "3f8c9f54", + "pow": "3f8edd1f", + "atan2": "3d087ae6", + "tanh": "3dcc1eb6", + "dot": "3f800000" + } + }, + { + "interface": "host", + "label": "Rust f32 on the CPU, every probe correctly rounded: macOS arm64, Linux arm64 and x86_64, Windows x64", + "seen": [ + { + "browser": "none", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "none", + "backend": "none", + "runtime": "Rust f32 and Apple's math library", + "window": "none", + "date": "2026-11-12" + }, + { + "browser": "none", + "os": "macos", + "cpu_arch": "aarch64", + "gpu": "none", + "backend": "none", + "device": "GitHub macos-latest runner", + "runtime": "Rust f32 and Apple's math library", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836477" + }, + { + "browser": "none", + "os": "linux", + "os_version": "Ubuntu 24.04", + "cpu_arch": "aarch64", + "gpu": "none", + "backend": "none", + "device": "GitHub ubuntu-24.04-arm runner", + "runtime": "Rust f32 and glibc", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836545" + }, + { + "browser": "none", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "none", + "backend": "none", + "device": "GitHub windows-latest runner", + "runtime": "Rust f32 and the MSVC runtime", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836624" + }, + { + "browser": "none", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "none", + "backend": "none", + "device": "GitHub windows-latest runner", + "runtime": "Rust f32, built for x86_64-pc-windows-gnullvm", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836502" + }, + { + "browser": "none", + "os": "windows", + "cpu_arch": "x86_64", + "gpu": "none", + "backend": "none", + "device": "GitHub windows-latest runner", + "runtime": "Rust f32, built for x86_64-pc-windows-gnu", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836594" + }, + { + "browser": "none", + "os": "linux", + "cpu_arch": "x86_64", + "gpu": "none", + "backend": "none", + "device": "GitHub ubuntu-latest runner", + "runtime": "Rust f32 and glibc", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303201/job/131012174690" + }, + { + "browser": "none", + "os": "linux", + "os_version": "Pop!_OS 24.04", + "cpu_arch": "x86_64", + "cpu_version": "12th Gen Intel Core i7-12800H", + "gpu": "none", + "backend": "none", + "device": "Dell Precision 5570", + "runtime": "Rust f32 and glibc", + "window": "none", + "date": "2026-11-13" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "33800000", + "swamped_sum": "00000000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb898", + "tan": "3dcd7c44", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b99", + "tanh": "3dcc1ebc", + "dot": "00000000" + } + }, + { + "interface": "wasi-nn", + "label": "ONNX Runtime 1.22 on the CPU behind wasi:nn: Linux x86_64, where its multiply-add is not fused", + "seen": [ + { + "browser": "none", + "os": "linux", + "cpu_arch": "x86_64", + "gpu": "none", + "backend": "none", + "device": "GitHub ubuntu-latest runner", + "runtime": "ONNX Runtime 1.22", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303201/job/131012174690" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "00000000", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb898", + "tan": "3dcd7c44", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b99", + "tanh": "3dcc1ebb", + "dot": "00000000" + } + }, + { + "interface": "wasi-nn", + "label": "ONNX Runtime 1.22 on the CPU behind wasi:nn: macOS arm64 and Linux arm64", + "seen": [ + { + "browser": "none", + "os": "macos", + "cpu_arch": "aarch64", + "cpu_version": "Apple M5 Pro", + "gpu": "none", + "backend": "none", + "runtime": "ONNX Runtime 1.22", + "window": "none", + "date": "2026-11-12" + }, + { + "browser": "none", + "os": "macos", + "cpu_arch": "aarch64", + "gpu": "none", + "backend": "none", + "device": "GitHub macos-latest runner", + "runtime": "ONNX Runtime 1.22", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303201/job/131012174374" + }, + { + "browser": "none", + "os": "linux", + "os_version": "Ubuntu 24.04", + "cpu_arch": "aarch64", + "gpu": "none", + "backend": "none", + "device": "GitHub ubuntu-24.04-arm runner", + "runtime": "ONNX Runtime 1.22", + "window": "none", + "date": "2026-11-12", + "source": "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836545" + } + ], + "results": { + "multiply_add": "00000000", + "fma": "33800000", + "swamped_sum": "40800000", + "divide": "3eaaaaab", + "subnormal_product": "00400000", + "sqrt": "3fddb3d7", + "inverse_sqrt": "3f13cd3a", + "sin": "3dcc7577", + "cos": "3f7eb898", + "tan": "3dcd7c44", + "exp": "3f8d763e", + "log": "3f8c9f54", + "pow": "3f8edd20", + "atan2": "3d087b99", + "tanh": "3dcc1ebb", + "dot": "00000000" + } + } + ] +} diff --git a/libs/gpu-probe/signatures.yaml b/libs/gpu-probe/signatures.yaml new file mode 100644 index 00000000..817df647 --- /dev/null +++ b/libs/gpu-probe/signatures.yaml @@ -0,0 +1,2663 @@ +# Every signature the probe set has been seen to give, one entry per distinct signature. +# An entry is one GPU, driver, backend, interface and browser combination, or a CPU and math library where no GPU +# is involved. A numeric signature is filed under its interface and the mode it was taken in: webgpu-split, +# webgl2-split or webgl1-split when a browser compiled each expression in a shader of its own, and the -combined +# forms when it compiled them all in one shader, because the two can differ on one device. WebGPU is filed the same +# way whoever implemented it, a row with no browser being wgpu called directly. host and wasi-nn are not WebGPU: the +# CPU answering as f32 arithmetic and its math library, and the probes as ONNX operators behind wasi:nn. canvas is a +# classic 2-D canvas fingerprint kept for comparison: a page's own scene, read with getImageData from a canvas the +# page made. canvas-browserleaks is the BrowserLeaks canvas test, read with toDataURL from a canvas made in a +# sandboxed frame; its md5 result is the signature that site shows. A new mechanism brings its own interface name. +# A signature can be shared by several devices, so each entry lists every place it has been seen, one row each, under +# "seen"; when a new sighting matches an existing entry, add a row rather than a duplicate entry. The label is only a +# summary of the rows. A row names, from fixed sets of values: +# browser chrome, firefox, safari, opera (and whatever else the webbrowser crate names), brave, chromium, deno, +# edge, librewolf, samsung-internet, tor-browser, or none where no browser was involved +# os android, linux, macos, windows +# cpu_arch an architecture as a target triple spells it (aarch64, x86_64), or unknown +# gpu the GPU's vendor as wgpu names it (amd, apple, arm, broadcom, imgtec, intel, mesa, nvidia, qualcomm), +# google or microsoft for their software renderers, none where no GPU was involved, or unknown +# backend metal, d3d11, d3d12, vulkan, opengl, opengl-es, none, or unknown +# window normal, private (an incognito window), or none where no browser was involved +# and may add, as free text: browser_version, os_version, cpu_version (the chip), gpu_version (the model and driver), +# device (the machine), runtime (what ran the probes where no browser did), defence (a privacy setting in force), +# reported (what the interface said the GPU was, where that is not what it is), date and source (where the sighting +# is on record). +# Each result is keyed by a name its mechanism gives it (a probe, a hash lane) so fields can be added. +# An interface a browser blocks or does not offer is recorded as an entry with "blocked: true" and a "reason" +# instead of results, so the registry also shows which browsers withhold which interface, and why. +# probe_set numbers the probes and inputs the numeric signatures answer. It goes up when either changes, since a +# signature taken before the change cannot be compared with one taken after. +probe_set: 1 +signatures: + - interface: webgl2-split + label: "AMD Radeon Graphics (0x15E7) through Direct3D 11 on Windows, HP EliteBook -- WebGL 2 on Chrome" + seen: + - browser: chrome + os: windows + cpu_arch: x86_64 + gpu: amd + gpu_version: "Radeon Graphics (0x15E7)" + backend: d3d11 + device: "HP EliteBook" + window: normal + results: + multiply_add: "00000000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaaa" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb897" + tan: "3dcd7c45" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087707" + tanh: "3dcc1ebe" + dot: "3f800000" + - interface: webgl2-split + label: "Qualcomm Adreno on Android -- WebGL 2 on Chrome: Oppo (Adreno 610), Samsung Galaxy S22 Ultra (Adreno 730)" + seen: + - browser: chrome + os: android + cpu_arch: aarch64 + gpu: qualcomm + gpu_version: "Adreno 610" + backend: opengl-es + device: "Oppo phone" + window: normal + - browser: chrome + os: android + cpu_arch: aarch64 + gpu: qualcomm + gpu_version: "Adreno 730" + backend: opengl-es + device: "Samsung Galaxy S22 Ultra" + window: normal + results: + multiply_add: "00000000" + fma: "????????" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc758b" + cos: "3f7eb898" + tan: "3dcd7c58" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b9e" + tanh: "3dcc1eba" + dot: "00000000" + - interface: webgl2-split + label: "Intel UHD Graphics 630 through Metal on macOS -- WebGL 2 on Chrome" + seen: + - browser: chrome + os: macos + cpu_arch: x86_64 + gpu: intel + gpu_version: "UHD Graphics 630" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc75ec" + cos: "3f7eb8a9" + tan: "3dcd7cac" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebc" + dot: "40000000" + - interface: webgl2-combined + label: "ARM Mali-G76, OpenGL ES, Galaxy S10 -- WebGL 2 on Chrome 157, Samsung Internet 31, Brave 1.99, Firefox 159" + seen: + - browser: chrome + browser_version: "157" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: opengl-es + device: "Samsung Galaxy S10" + window: normal + - browser: samsung-internet + browser_version: "31" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: opengl-es + device: "Samsung Galaxy S10" + window: normal + - browser: brave + browser_version: "1.99" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: opengl-es + device: "Samsung Galaxy S10" + window: normal + - browser: firefox + browser_version: "159" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: opengl-es + device: "Samsung Galaxy S10" + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb898" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b99" + tanh: "3dcc1ebe" + dot: "00000000" + - interface: webgl2-split + label: "Intel Iris Xe (Alder Lake GT2) through Mesa on Linux, Dell Precision 5570 -- WebGL 2 on Chrome" + seen: + - browser: chrome + os: linux + cpu_arch: x86_64 + gpu: intel + gpu_version: "Iris Xe (Alder Lake GT2), Mesa" + backend: opengl + device: "Dell Precision 5570" + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087ae6" + tanh: "3dcc1eb6" + dot: "3f800000" + - interface: webgpu-split + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Chromium and Brave" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + - browser: brave + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: webgl2-split + label: "Apple M5 Pro GPU through Metal on macOS -- WebGL 2 on Chromium, Safari, Brave and Firefox" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + - browser: safari + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + - browser: brave + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + - browser: firefox + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebc" + dot: "00000000" + - interface: webgpu-split + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Safari" + seen: + - browser: safari + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "40000000" + - interface: webgpu-split + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU as wgpu implements it: Firefox, Deno and wgpu 29 alone" + seen: + - browser: firefox + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + - browser: deno + browser_version: "deno_runtime 0.268.0, deno_webgpu 0.227.0" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + runtime: "et-ws-web-runner 0.3.0" + window: none + date: "2026-11-12" + - browser: none + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + runtime: "wgpu 29" + window: none + date: "2026-11-12" + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: webgl2-split + label: "Mesa / llvmpipe, or similar on Linux (Firefox 159.0)" + seen: + - browser: firefox + browser_version: "159.0" + os: linux + cpu_arch: unknown + gpu: unknown + backend: unknown + reported: "Mesa / llvmpipe, or similar" + window: normal + results: + multiply_add: "00000000" + fma: "????????" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb898" + tan: "3dcd7c44" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087ae6" + tanh: "3dcc1ebe" + dot: "40000000" + - interface: webgl2-split + label: "NVIDIA GB10/PCIe through OpenGL on Linux -- WebGL 2 on Chromium and Firefox" + seen: + - browser: chromium + os: linux + cpu_arch: aarch64 + cpu_version: "NVIDIA GB10" + gpu: nvidia + gpu_version: "GB10" + backend: opengl + window: normal + - browser: firefox + os: linux + cpu_arch: aarch64 + cpu_version: "NVIDIA GB10" + gpu: nvidia + gpu_version: "GB10" + backend: opengl + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7566" + cos: "3f7eb897" + tan: "3dcd7c34" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebc" + dot: "00000000" + - interface: webgpu-split + label: "NVIDIA GB10 through Vulkan on Linux -- WebGPU on Chromium" + seen: + - browser: chromium + os: linux + cpu_arch: aarch64 + cpu_version: "NVIDIA GB10" + gpu: nvidia + gpu_version: "GB10" + backend: vulkan + window: normal + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7566" + cos: "3f7eb897" + tan: "3dcd7c34" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: webgl2-split + label: "NVIDIA GB10 through Vulkan on Linux -- WebGL 2 on Chromium" + seen: + - browser: chromium + os: linux + cpu_arch: aarch64 + cpu_version: "NVIDIA GB10" + gpu: nvidia + gpu_version: "GB10" + backend: vulkan + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7566" + cos: "3f7eb897" + tan: "3dcd7c34" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "40000000" + - interface: webgpu-split + label: "Intel Xe-LPG on Windows -- WebGPU on Chrome" + seen: + - browser: chrome + os: windows + cpu_arch: x86_64 + gpu: intel + gpu_version: "Xe-LPG" + backend: unknown + window: normal + results: + multiply_add: "00000000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087708" + tanh: "3dcc1eb6" + dot: "00000000" + - interface: webgl2-split + label: "Intel Xe-LPG through Direct3D 11 on Windows -- WebGL 2 on Chrome" + seen: + - browser: chrome + os: windows + cpu_arch: x86_64 + gpu: intel + gpu_version: "Xe-LPG" + backend: d3d11 + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087708" + tanh: "3dcc1eb6" + dot: "00000000" + - interface: webgpu-combined + label: "ARM Mali-G76, Vulkan, Galaxy S10 -- WebGPU on Chrome 157, Samsung Internet 31, Brave 1.99" + seen: + - browser: chrome + browser_version: "157" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: vulkan + device: "Samsung Galaxy S10" + window: normal + - browser: samsung-internet + browser_version: "31" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: vulkan + device: "Samsung Galaxy S10" + window: normal + - browser: brave + browser_version: "1.99" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: vulkan + device: "Samsung Galaxy S10" + window: normal + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb898" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b99" + tanh: "3dcc1ebe" + dot: "00000000" + - interface: webgpu-split + label: "Qualcomm Adreno 730 (Samsung Galaxy S22 Ultra) through Vulkan on Android -- WebGPU on Chrome" + seen: + - browser: chrome + os: android + cpu_arch: aarch64 + gpu: qualcomm + gpu_version: "Adreno 730" + backend: vulkan + device: "Samsung Galaxy S22 Ultra" + window: normal + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc758b" + cos: "3f7eb898" + tan: "3dcd7c58" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b9e" + tanh: "3dcc1eba" + dot: "00000000" + - interface: webgl2-split + label: "Imagination PowerVR Rogue GE8300 through OpenGL ES on Android, Laser tablet -- WebGL 2 on Chrome 135, 157" + seen: + - browser: chrome + browser_version: "135" + os: android + os_version: "10" + cpu_arch: unknown + gpu: imgtec + gpu_version: "PowerVR Rogue GE8300" + backend: opengl-es + device: "Laser tablet" + window: normal + - browser: chrome + browser_version: "157" + os: android + os_version: "10" + cpu_arch: unknown + gpu: imgtec + gpu_version: "PowerVR Rogue GE8300" + backend: opengl-es + device: "Laser tablet" + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c42" + exp: "3f8d763e" + log: "3f8c9f53" + pow: "3f8edd1f" + atan2: "3d087b98" + tanh: "3dcc1ec2" + dot: "3f800000" + - interface: webgpu-split + label: "Google SwiftShader (software renderer) on Linux -- WebGPU on Chrome, Dell Precision 5570" + seen: + - browser: chrome + os: linux + cpu_arch: x86_64 + gpu: google + gpu_version: "SwiftShader (software renderer)" + backend: vulkan + device: "Dell Precision 5570" + window: normal + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc6e85" + cos: "3f7eadd1" + tan: "3dcd7dfb" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b99" + tanh: "3dcc1eba" + dot: "00000000" + - interface: webgpu-split + label: "AMD Radeon RDNA 2 (HP EliteBook) through Direct3D 12 on Windows -- WebGPU on Chrome" + seen: + - browser: chrome + os: windows + cpu_arch: x86_64 + gpu: amd + gpu_version: "Radeon RDNA 2" + backend: d3d12 + device: "HP EliteBook" + window: normal + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "40800000" + divide: "3eaaaaaa" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb897" + tan: "3dcd7c45" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b98" + tanh: "3dcc1ebe" + dot: "00000000" + - interface: webgpu-split + label: "AMD Radeon Pro (RDNA 1) through Metal on macOS, Intel MacBook Pro -- WebGPU on Chromium" + seen: + - browser: chromium + os: macos + cpu_arch: x86_64 + gpu: amd + gpu_version: "Radeon Pro (RDNA 1)" + backend: metal + device: "Intel MacBook Pro" + window: normal + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaaa" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb897" + tan: "3dcd7c45" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b98" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: webgpu-combined + label: "Qualcomm Adreno 830 (Samsung Galaxy S25 Ultra) through Vulkan on Android -- WebGPU on Chrome" + seen: + - browser: chrome + os: android + cpu_arch: aarch64 + gpu: qualcomm + gpu_version: "Adreno 830" + backend: vulkan + device: "Samsung Galaxy S25 Ultra" + window: normal + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7578" + cos: "3f7eb898" + tan: "3dcd7c45" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b9e" + tanh: "3dcc1eba" + dot: "00000000" + - interface: webgl2-combined + label: "Qualcomm Adreno 830 (Galaxy S25 Ultra), OpenGL ES, Android -- WebGL 2 on Chrome (mixed shader grouping)" + seen: + - browser: chrome + os: android + cpu_arch: aarch64 + gpu: qualcomm + gpu_version: "Adreno 830" + backend: opengl-es + device: "Samsung Galaxy S25 Ultra" + window: normal + results: + multiply_add: "00000000" + fma: "????????" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7578" + cos: "3f7eb898" + tan: "3dcd7c45" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b9e" + tanh: "3dcc1eba" + dot: "00000000" + - interface: canvas + label: "Apple M5 Pro, macOS -- canvas 2D on Chromium" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: unknown + window: normal + results: + lane0: "4e31fa9b" + lane1: "76b62595" + lane2: "5debc5c0" + lane3: "52ef3664" + - interface: webgl2-combined + label: "Intel / HD Graphics 400, or similar on Linux (Firefox 149.0)" + seen: + - browser: firefox + browser_version: "149.0" + os: linux + cpu_arch: x86_64 + gpu: intel + backend: unknown + reported: "Intel / HD Graphics 400, or similar" + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc75ec" + cos: "3f7eb8a9" + tan: "3dcd7cac" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087ae6" + tanh: "3dcc1eb6" + dot: "3f800000" + - interface: canvas + label: "Firefox 149.0 on Linux -- canvas 2D" + seen: + - browser: firefox + browser_version: "149.0" + os: linux + cpu_arch: unknown + gpu: unknown + backend: unknown + window: normal + results: + lane0: "b11cf4d5" + lane1: "9f52fa33" + lane2: "1d528dc3" + lane3: "205c1f18" + - interface: canvas + label: "Firefox 160.0 on macOS -- canvas 2D" + seen: + - browser: firefox + browser_version: "160.0" + os: macos + cpu_arch: unknown + gpu: unknown + backend: unknown + window: normal + results: + lane0: "e713762c" + lane1: "7af1305a" + lane2: "a45f2769" + lane3: "4b50c285" + - interface: canvas + label: "Safari 27.0 on macOS -- canvas 2D" + seen: + - browser: safari + browser_version: "27.0" + os: macos + cpu_arch: unknown + gpu: unknown + backend: unknown + window: normal + results: + lane0: "a6b65bfe" + lane1: "5086a288" + lane2: "656c1bac" + lane3: "3da9f222" + - interface: canvas + label: "Chrome 157.0.8103.50 and Microsoft Edge on Windows -- canvas 2D" + seen: + - browser: chrome + browser_version: "157.0.8103.50" + os: windows + cpu_arch: unknown + gpu: unknown + backend: unknown + window: normal + - browser: edge + os: windows + cpu_arch: unknown + gpu: unknown + backend: unknown + window: normal + results: + lane0: "6f919589" + lane1: "21590c7f" + lane2: "99fa99fb" + lane3: "459a0e48" + - interface: canvas + label: "Brave 157.0.0 on macOS 26.7.1 -- canvas 2D (farbled; session-keyed noise, not reproducible across sessions)" + seen: + - browser: brave + browser_version: "157.0.0" + os: macos + os_version: "26.7.1" + cpu_arch: unknown + gpu: unknown + backend: unknown + defence: "Shields farbling: noise keyed to the session" + window: normal + results: + lane0: "c8310eb1" + lane1: "6d12025f" + lane2: "25aa14b4" + lane3: "453e64a3" + - interface: webgpu + label: "Tor Browser (reports itself as Firefox 153.0.0) on macOS, Apple M5 Pro" + seen: + - browser: tor-browser + browser_version: "reports itself as Firefox 153.0.0" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: unknown + window: normal + blocked: true + reason: "WebGPU is unavailable (navigator.gpu is absent; the browser does not expose it)" + - interface: webgl2 + label: "Tor Browser (reports itself as Firefox 153.0.0) on macOS, Apple M5 Pro" + seen: + - browser: tor-browser + browser_version: "reports itself as Firefox 153.0.0" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: unknown + window: normal + blocked: true + reason: "WebGL 2 is unavailable (getContext returned null; only WebGL 1 is available, not WebGL 2)" + - interface: canvas + label: "Tor Browser (reports itself as Firefox 153.0.0) on macOS, Apple M5 Pro" + seen: + - browser: tor-browser + browser_version: "reports itself as Firefox 153.0.0" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: unknown + defence: "canvas reads carry per-read noise" + window: normal + blocked: true + reason: "randomised: two reads of the same scene differed (per-read noise)" + - interface: canvas + label: "Samsung Galaxy S10, Android 12 -- canvas 2D on Chrome 157 and Samsung Internet 31" + seen: + - browser: chrome + browser_version: "157" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: unknown + device: "Samsung Galaxy S10" + window: normal + - browser: samsung-internet + browser_version: "31" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: unknown + device: "Samsung Galaxy S10" + window: normal + results: + lane0: "c1161ae6" + lane1: "4f61a1f8" + lane2: "65ec1468" + lane3: "c8bf17ef" + - interface: canvas + label: "Samsung Galaxy S10, Android 12 -- canvas 2D on Firefox 159" + seen: + - browser: firefox + browser_version: "159" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: unknown + device: "Samsung Galaxy S10" + window: normal + results: + lane0: "16b5cc90" + lane1: "4a208b5e" + lane2: "536d9443" + lane3: "5c29fe34" + - interface: webgpu + label: "Firefox 159 on Android, Samsung Galaxy S10" + seen: + - browser: firefox + browser_version: "159" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: unknown + device: "Samsung Galaxy S10" + window: normal + blocked: true + reason: "WebGPU is unavailable (navigator.gpu is absent; the browser does not expose it)" + - interface: canvas + label: "Samsung Galaxy S10, Android 12 -- canvas 2D on Brave 1.99 (farbled; differs from Chrome's on this phone)" + seen: + - browser: brave + browser_version: "1.99" + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: unknown + device: "Samsung Galaxy S10" + defence: "Shields farbling" + window: normal + results: + lane0: "1419645e" + lane1: "7aa1bb28" + lane2: "ea31c65e" + lane3: "9637a16c" + - interface: webgl2-combined + label: "Intel Iris Xe (Alder Lake GT2) through Mesa on Linux, Dell Precision 5570 -- WebGL 2 on Chrome 157" + seen: + - browser: chrome + browser_version: "157" + os: linux + cpu_arch: x86_64 + gpu: intel + gpu_version: "Iris Xe (Alder Lake GT2), Mesa" + backend: opengl + device: "Dell Precision 5570" + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087ae6" + tanh: "3dcc1eb6" + dot: "3f800000" + - interface: canvas + label: "Dell Precision 5570, Linux -- canvas 2D on Chrome 157" + seen: + - browser: chrome + browser_version: "157" + os: linux + cpu_arch: x86_64 + gpu: unknown + backend: unknown + device: "Dell Precision 5570" + window: normal + results: + lane0: "e55c4313" + lane1: "fdb387dd" + lane2: "e659dabe" + lane3: "d711a464" + - interface: webgl2-split + label: "ARM Mali-G76 through OpenGL ES on Android, Samsung Galaxy S10 -- WebGL 2 on Chrome" + seen: + - browser: chrome + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: opengl-es + device: "Samsung Galaxy S10" + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb898" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b99" + tanh: "3dcc1ebe" + dot: "00000000" + - interface: webgpu-split + label: "ARM Mali-G76 through Vulkan on Android, Samsung Galaxy S10 -- WebGPU on Chromium" + seen: + - browser: chromium + os: android + os_version: "12" + cpu_arch: aarch64 + gpu: arm + gpu_version: "Mali-G76" + backend: vulkan + device: "Samsung Galaxy S10" + window: normal + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb898" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b99" + tanh: "3dcc1ebe" + dot: "00000000" + - interface: webgl2-combined + label: "AMD Radeon Graphics (0x15E7) through Direct3D 11 on Windows, HP EliteBook -- WebGL 2 on Chrome 157" + seen: + - browser: chrome + browser_version: "157" + os: windows + cpu_arch: x86_64 + gpu: amd + gpu_version: "Radeon Graphics (0x15E7)" + backend: d3d11 + device: "HP EliteBook" + window: normal + results: + multiply_add: "00000000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaaa" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb897" + tan: "3dcd7c45" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087707" + tanh: "3dcc1ebe" + dot: "3f800000" + - interface: webgpu-combined + label: "AMD Radeon RDNA 2 (HP EliteBook) through Direct3D 12 on Windows -- WebGPU on Chrome 157" + seen: + - browser: chrome + browser_version: "157" + os: windows + cpu_arch: x86_64 + gpu: amd + gpu_version: "Radeon RDNA 2" + backend: d3d12 + device: "HP EliteBook" + window: normal + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "40800000" + divide: "3eaaaaaa" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb897" + tan: "3dcd7c45" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b98" + tanh: "3dcc1ebe" + dot: "00000000" + - interface: webgpu-combined + label: "Google SwiftShader (software renderer) on Linux -- WebGPU on Chrome, Dell Precision 5570" + seen: + - browser: chrome + os: linux + cpu_arch: x86_64 + gpu: google + gpu_version: "SwiftShader (software renderer)" + backend: vulkan + device: "Dell Precision 5570" + window: normal + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc6e85" + cos: "3f7eadd1" + tan: "3dcd7dfb" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b99" + tanh: "3dcc1eba" + dot: "00000000" + - interface: webgpu-combined + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Chromium" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: webgl2-combined + label: "Apple M5 Pro GPU through Metal on macOS -- WebGL 2 on Chromium" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebc" + dot: "00000000" + - interface: webgpu-combined + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU as wgpu implements it: Firefox 160 and wgpu 29 alone" + seen: + - browser: firefox + browser_version: "160" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + - browser: none + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + runtime: "wgpu 29" + window: none + date: "2026-11-12" + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: webgpu + label: "Chrome 157 on Android 10, Laser tablet" + seen: + - browser: chrome + browser_version: "157" + os: android + os_version: "10" + cpu_arch: unknown + gpu: imgtec + gpu_version: "PowerVR Rogue GE8300" + backend: unknown + device: "Laser tablet" + window: normal + blocked: true + reason: "no WebGPU adapter was returned" + - interface: webgl2-combined + label: "Imagination PowerVR Rogue GE8300 through OpenGL ES on Android, Laser tablet -- WebGL 2 on Chrome 157" + seen: + - browser: chrome + browser_version: "157" + os: android + os_version: "10" + cpu_arch: unknown + gpu: imgtec + gpu_version: "PowerVR Rogue GE8300" + backend: opengl-es + device: "Laser tablet" + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c42" + exp: "3f8d763e" + log: "3f8c9f53" + pow: "3f8edd1f" + atan2: "3d087b98" + tanh: "3dcc1ec2" + dot: "3f800000" + - interface: canvas + label: "Laser tablet, Android 10 -- canvas 2D on Chrome 157" + seen: + - browser: chrome + browser_version: "157" + os: android + os_version: "10" + cpu_arch: unknown + gpu: imgtec + gpu_version: "PowerVR Rogue GE8300" + backend: unknown + device: "Laser tablet" + window: normal + results: + lane0: "0d2dacc4" + lane1: "01059e2a" + lane2: "26b6c476" + lane3: "68dfadcf" + - interface: webgpu-combined + label: "AMD Radeon Pro (RDNA 1) through Metal on macOS, Intel MacBook Pro -- WebGPU on Chromium" + seen: + - browser: chromium + os: macos + cpu_arch: x86_64 + gpu: amd + gpu_version: "Radeon Pro (RDNA 1)" + backend: metal + device: "Intel MacBook Pro" + window: normal + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaaa" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb897" + tan: "3dcd7c45" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b98" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: webgl2-combined + label: "Intel UHD Graphics 630 through Metal on macOS -- WebGL 2 on Chrome" + seen: + - browser: chrome + os: macos + cpu_arch: x86_64 + gpu: intel + gpu_version: "UHD Graphics 630" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc75ec" + cos: "3f7eb8a9" + tan: "3dcd7cac" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebc" + dot: "40000000" + - interface: canvas + label: "Intel MacBook Pro, macOS -- canvas 2D on Chrome 154" + seen: + - browser: chrome + browser_version: "154" + os: macos + cpu_arch: x86_64 + gpu: unknown + backend: unknown + device: "Intel MacBook Pro" + window: normal + results: + lane0: "61a3f194" + lane1: "f467a8a2" + lane2: "bf84affe" + lane3: "84e85fe3" + - interface: webgpu-split + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Chromium and Brave" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + - browser: brave + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: webgpu-combined + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Chromium" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: webgl2-split + label: "Apple M5 Pro GPU through Metal on macOS -- WebGL 2 on Chromium, Safari, Brave and Firefox" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + - browser: safari + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + - browser: brave + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + - browser: firefox + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebc" + dot: "00000000" + - interface: webgl2-combined + label: "Apple M5 Pro GPU through Metal on macOS -- WebGL 2 on Chromium" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebc" + dot: "00000000" + - interface: canvas + label: "Apple M5 Pro, macOS -- canvas 2D on Chromium" + seen: + - browser: chromium + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: unknown + window: private + results: + lane0: "4e31fa9b" + lane1: "76b62595" + lane2: "5debc5c0" + lane3: "52ef3664" + - interface: canvas + label: "Firefox 160 on macOS, privacy.fingerprintingProtection on -- canvas 2D (differs from the unprotected hash)" + seen: + - browser: firefox + browser_version: "160" + os: macos + cpu_arch: unknown + gpu: unknown + backend: unknown + defence: "privacy.fingerprintingProtection on" + window: normal + results: + lane0: "a60e6953" + lane1: "e2f45f75" + lane2: "956014b5" + lane3: "1b2515f8" + - interface: webgpu-split + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Firefox" + seen: + - browser: firefox + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: canvas + label: "Firefox 160 on macOS, privacy.fingerprintingProtection on -- canvas 2D (differs from the unprotected hash)" + seen: + - browser: firefox + browser_version: "160" + os: macos + cpu_arch: unknown + gpu: unknown + backend: unknown + defence: "privacy.fingerprintingProtection on" + window: private + results: + lane0: "a60e6953" + lane1: "e2f45f75" + lane2: "956014b5" + lane3: "1b2515f8" + - interface: webgpu-combined + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Firefox 160" + seen: + - browser: firefox + browser_version: "160" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "00000000" + - interface: canvas-browserleaks + label: "Firefox 160 on macOS, fingerprintingProtection on -- BrowserLeaks test (one session's value; see next row)" + seen: + - browser: firefox + browser_version: "160" + os: macos + cpu_arch: unknown + gpu: unknown + backend: unknown + defence: "privacy.fingerprintingProtection on" + window: normal + results: + md5: "67456dde9d4089a8cea2e53d4ecf4b8b" + - interface: canvas-browserleaks + label: "Firefox 160 on macOS, privacy.fingerprintingProtection on" + seen: + - browser: firefox + browser_version: "160" + os: macos + cpu_arch: unknown + gpu: unknown + backend: unknown + defence: "privacy.fingerprintingProtection on" + window: normal + blocked: true + reason: "randomised: hash differs between visits in one browser (noise keyed to the session)" + - interface: canvas-browserleaks + label: "Brave 157.0.0 on macOS 26.7.1 -- canvas 2D, BrowserLeaks test (noise added to reads)" + seen: + - browser: brave + browser_version: "157.0.0" + os: macos + os_version: "26.7.1" + cpu_arch: unknown + gpu: unknown + backend: unknown + defence: "Shields farbling: noise added to reads" + window: normal + results: + md5: "ead7c33e84beeb434b1d384c6e45268b" + - interface: canvas-browserleaks + label: "Firefox 160.0.0 on macOS -- canvas 2D, BrowserLeaks test" + seen: + - browser: firefox + browser_version: "160.0.0" + os: macos + cpu_arch: unknown + gpu: unknown + backend: unknown + window: private + results: + md5: "4a05912a83d4fdbab55e5ddfaef2d3b0" + - interface: webgpu + label: "LibreWolf (reports itself as Firefox 160.0.0) on macOS, Apple M5 Pro" + seen: + - browser: librewolf + browser_version: "reports itself as Firefox 160.0.0" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: unknown + window: normal + blocked: true + reason: "WebGPU is unavailable (navigator.gpu is absent; the browser does not expose it)" + - interface: webgl2 + label: "LibreWolf (reports itself as Firefox 160.0.0) on macOS, Apple M5 Pro" + seen: + - browser: librewolf + browser_version: "reports itself as Firefox 160.0.0" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: unknown + window: normal + blocked: true + reason: "WebGL 2 is unavailable (getContext returned null; no WebGL context at all, so WebGL is turned off)" + - interface: canvas + label: "LibreWolf (reports itself as Firefox 160.0.0) on macOS, Apple M5 Pro" + seen: + - browser: librewolf + browser_version: "reports itself as Firefox 160.0.0" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: unknown + window: normal + blocked: true + reason: "randomised: two reads of the same scene differed (per-read noise)" + - interface: canvas-browserleaks + label: "LibreWolf (reports itself as Firefox 160.0.0) on macOS, Apple M5 Pro" + seen: + - browser: librewolf + browser_version: "reports itself as Firefox 160.0.0" + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: unknown + window: normal + blocked: true + reason: "randomised: two reads of the same scene differed (per-read noise)" + - interface: webgl1-split + label: "Apple M5 Pro through Metal on macOS 26.7.1 (Chrome 157.0.0 and Safari 27.0)" + seen: + - browser: chrome + browser_version: "157.0.0" + os: macos + os_version: "26.7.1" + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + - browser: safari + browser_version: "27.0" + os: macos + os_version: "26.7.1" + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "????????" + dot: "00000000" + - interface: canvas-browserleaks + label: "Chrome 157.0.0 on macOS 26.7.1 -- canvas 2D, BrowserLeaks test" + seen: + - browser: chrome + browser_version: "157.0.0" + os: macos + os_version: "26.7.1" + cpu_arch: unknown + gpu: unknown + backend: unknown + window: normal + results: + md5: "1dc44a8e2846f0a156399977bf5ca67d" + - interface: webgl1-combined + label: "Apple M5 Pro through Metal on macOS 26.7.1 (Chrome 157.0.0 and Safari 27.0)" + seen: + - browser: chrome + browser_version: "157.0.0" + os: macos + os_version: "26.7.1" + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + - browser: safari + browser_version: "27.0" + os: macos + os_version: "26.7.1" + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "????????" + dot: "00000000" + - interface: webgl1 + label: "Firefox 160.0.0 on macOS" + seen: + - browser: firefox + browser_version: "160.0.0" + os: macos + cpu_arch: unknown + gpu: unknown + backend: unknown + window: normal + blocked: true + reason: "WebGL 1 is unavailable (getContext returned null, so WebGL is turned off)" + - interface: webgl1-split + label: "Apple M5 Pro through Metal on macOS 26.7.1 (Chrome 157.0.0)" + seen: + - browser: chrome + browser_version: "157.0.0" + os: macos + os_version: "26.7.1" + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "????????" + dot: "00000000" + - interface: webgl1-combined + label: "Apple M5 Pro through Metal on macOS 26.7.1 (Chrome 157.0.0)" + seen: + - browser: chrome + browser_version: "157.0.0" + os: macos + os_version: "26.7.1" + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: private + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "????????" + dot: "00000000" + - interface: webgpu-combined + label: "Apple M5 Pro GPU through Metal on macOS -- WebGPU on Safari" + seen: + - browser: safari + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: apple + gpu_version: "Apple M5 Pro GPU" + backend: metal + window: normal + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb899" + tan: "3dcd7c43" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1ebd" + dot: "40000000" + - interface: canvas-browserleaks + label: "Safari 27.0.0 on macOS -- canvas 2D, BrowserLeaks test" + seen: + - browser: safari + browser_version: "27.0.0" + os: macos + cpu_arch: unknown + gpu: unknown + backend: unknown + window: normal + results: + md5: "f7316747f9adf8387510e46790f09d66" + - interface: webgpu-combined + label: "Intel Xe-LPG on Windows, HP EliteBook (HSN-I6IC) -- WebGPU on Edge 153" + seen: + - browser: edge + browser_version: "153" + os: windows + cpu_arch: x86_64 + gpu: intel + gpu_version: "Xe-LPG" + backend: unknown + device: "HP EliteBook (HSN-I6IC)" + window: normal + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087708" + tanh: "3dcc1eb6" + dot: "00000000" + - interface: webgl2-combined + label: "Intel Xe-LPG through Direct3D 11 on Windows -- WebGL 2 on Chrome" + seen: + - browser: chrome + os: windows + cpu_arch: x86_64 + gpu: intel + gpu_version: "Xe-LPG" + backend: d3d11 + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087708" + tanh: "3dcc1eb6" + dot: "00000000" + - interface: webgl1-split + label: "Intel Arc Graphics through Direct3D 11 on Windows, HP EliteBook (HSN-I6IC) -- WebGL 1 on Edge 153" + seen: + - browser: edge + browser_version: "153" + os: windows + cpu_arch: x86_64 + gpu: intel + gpu_version: "Arc Graphics" + backend: d3d11 + device: "HP EliteBook (HSN-I6IC)" + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087708" + tanh: "????????" + dot: "00000000" + - interface: webgl1-combined + label: "Intel Arc Graphics through Direct3D 11 on Windows, HP EliteBook (HSN-I6IC) -- WebGL 1 on Edge 153" + seen: + - browser: edge + browser_version: "153" + os: windows + cpu_arch: x86_64 + gpu: intel + gpu_version: "Arc Graphics" + backend: d3d11 + device: "HP EliteBook (HSN-I6IC)" + window: normal + results: + multiply_add: "33800000" + fma: "????????" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087708" + tanh: "????????" + dot: "00000000" + - interface: canvas-browserleaks + label: "HP EliteBook (HSN-I6IC), Windows -- canvas 2D, BrowserLeaks test on Edge 153" + seen: + - browser: edge + browser_version: "153" + os: windows + cpu_arch: x86_64 + gpu: intel + backend: unknown + device: "HP EliteBook (HSN-I6IC)" + window: normal + results: + md5: "8c781e6c79d33a68ef6ae90ae071c989" + - interface: webgpu-split + label: "Intel Xe-LPG on Windows, HP EliteBook (HSN-I6IC) -- WebGPU on Firefox 155 (adapter info blanked)" + seen: + - browser: firefox + browser_version: "155" + os: windows + cpu_arch: x86_64 + gpu: intel + gpu_version: "Xe-LPG" + backend: unknown + device: "HP EliteBook (HSN-I6IC)" + reported: "adapter info blanked" + window: normal + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087708" + tanh: "3dcc1eb6" + dot: "00000000" + - interface: canvas + label: "Firefox 155.0.0 on Windows, HP EliteBook (HSN-I6IC)" + seen: + - browser: firefox + browser_version: "155.0.0" + os: windows + cpu_arch: x86_64 + gpu: intel + backend: unknown + device: "HP EliteBook (HSN-I6IC)" + window: normal + blocked: true + reason: "randomised: hash differs between visits in one browser (noise keyed to the session)" + - interface: canvas-browserleaks + label: "Firefox 155.0.0 on Windows, HP EliteBook (HSN-I6IC)" + seen: + - browser: firefox + browser_version: "155.0.0" + os: windows + cpu_arch: x86_64 + gpu: intel + backend: unknown + device: "HP EliteBook (HSN-I6IC)" + window: normal + blocked: true + reason: "randomised: hash differs between visits in one browser (noise keyed to the session)" + - interface: webgpu-combined + label: "Intel Xe-LPG on Windows, HP EliteBook (HSN-I6IC) -- WebGPU on Firefox 155 (adapter info blanked)" + seen: + - browser: firefox + browser_version: "155" + os: windows + cpu_arch: x86_64 + gpu: intel + gpu_version: "Xe-LPG" + backend: unknown + device: "HP EliteBook (HSN-I6IC)" + reported: "adapter info blanked" + window: normal + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087708" + tanh: "3dcc1eb6" + dot: "00000000" + - interface: webgpu-split + label: "Apple Paravirtual device through Metal on macOS arm64, GitHub macos-latest runner -- wgpu 29, no browser" + seen: + - browser: none + os: macos + cpu_arch: aarch64 + gpu: apple + gpu_version: "Apple Paravirtual device" + backend: metal + device: "GitHub macos-latest runner" + runtime: "wgpu 29" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836477" + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7576" + cos: "3f7eb896" + tan: "3dcd7c46" + exp: "3f8d763d" + log: "3f8c9f53" + pow: "3f8edd1f" + atan2: "3d087b99" + tanh: "3dcc1eb5" + dot: "00000000" + - interface: webgpu-split + label: "llvmpipe (LLVM 20.1.2, Mesa 25.2.8) through Vulkan on Linux arm64, GitHub runner -- wgpu 29, no browser" + seen: + - browser: none + os: linux + os_version: "Ubuntu 24.04" + cpu_arch: aarch64 + gpu: mesa + gpu_version: "llvmpipe (LLVM 20.1.2, 128 bits), Mesa 25.2.8-0ubuntu0.24.04.4" + backend: vulkan + device: "GitHub ubuntu-24.04-arm runner" + runtime: "wgpu 29" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836545" + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb898" + tan: "3dcd7c44" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087ae6" + tanh: "3dcc1ebe" + dot: "40000000" + - interface: webgpu-split + label: "Microsoft Basic Render Driver through Direct3D 12 on Windows x64, GitHub runner -- wgpu 29 built for msvc" + seen: + - browser: none + os: windows + cpu_arch: x86_64 + gpu: microsoft + gpu_version: "Basic Render Driver 10.0.26100.33648" + backend: d3d12 + device: "GitHub windows-latest runner" + runtime: "wgpu 29, built for x86_64-pc-windows-msvc" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836624" + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756b" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087708" + tanh: "3dcc1ebc" + dot: "00000000" + - interface: webgpu-split + label: "Microsoft Basic Render Driver through Direct3D 12 on Windows x64, GitHub runner -- wgpu 29 built for GNU" + seen: + - browser: none + os: windows + cpu_arch: x86_64 + gpu: microsoft + gpu_version: "Basic Render Driver 10.0.26100.33648" + backend: d3d12 + device: "GitHub windows-latest runner" + runtime: "wgpu 29, built for x86_64-pc-windows-gnullvm" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836502" + - browser: none + os: windows + cpu_arch: x86_64 + gpu: microsoft + gpu_version: "Basic Render Driver 10.0.26100.33648" + backend: d3d12 + device: "GitHub windows-latest runner" + runtime: "wgpu 29, built for x86_64-pc-windows-gnu" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836594" + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756b" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087708" + tanh: "3dcc1ebe" + dot: "00000000" + - interface: webgpu-split + label: "llvmpipe (LLVM 21.1.8, Mesa 26.0.8) through Vulkan on Linux x86_64, GitHub runner -- wgpu 29, no browser" + seen: + - browser: none + os: linux + cpu_arch: x86_64 + gpu: mesa + gpu_version: "llvmpipe (LLVM 21.1.8, 256 bits), Mesa 26.0.8-1ubuntu0.3" + backend: vulkan + device: "GitHub ubuntu-latest runner" + runtime: "wgpu 29" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303201/job/131012174690" + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb898" + tan: "3dcd7c44" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087ae6" + tanh: "3dcc1ebe" + dot: "40000000" + - interface: webgpu-split + label: "Intel Iris Xe (Alder Lake GT2) through Mesa's Vulkan driver on Linux, Dell Precision 5570 -- wgpu 29" + seen: + - browser: none + os: linux + os_version: "Pop!_OS 24.04" + cpu_arch: x86_64 + cpu_version: "12th Gen Intel Core i7-12800H" + gpu: intel + gpu_version: "Iris Xe (Alder Lake GT2), Mesa 26.1.6-1pop0~1787580452~24.04~a5619ea" + backend: vulkan + device: "Dell Precision 5570" + runtime: "wgpu 29" + window: none + date: "2026-11-13" + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087ae6" + tanh: "3dcc1eb6" + dot: "3f800000" + - interface: webgpu-combined + label: "Intel Iris Xe (Alder Lake GT2) through Mesa's Vulkan driver on Linux, Dell Precision 5570 -- wgpu 29" + seen: + - browser: none + os: linux + os_version: "Pop!_OS 24.04" + cpu_arch: x86_64 + cpu_version: "12th Gen Intel Core i7-12800H" + gpu: intel + gpu_version: "Iris Xe (Alder Lake GT2), Mesa 26.1.6-1pop0~1787580452~24.04~a5619ea" + backend: vulkan + device: "Dell Precision 5570" + runtime: "wgpu 29" + window: none + date: "2026-11-13" + results: + multiply_add: "33800000" + fma: "33800000" + swamped_sum: "40000000" + divide: "3eaaaaab" + subnormal_product: "00000000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc756a" + cos: "3f7eb898" + tan: "3dcd7c38" + exp: "3f8d763d" + log: "3f8c9f54" + pow: "3f8edd1f" + atan2: "3d087ae6" + tanh: "3dcc1eb6" + dot: "3f800000" + - interface: host + label: "Rust f32 on the CPU, every probe correctly rounded: macOS arm64, Linux arm64 and x86_64, Windows x64" + seen: + - browser: none + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: none + backend: none + runtime: "Rust f32 and Apple's math library" + window: none + date: "2026-11-12" + - browser: none + os: macos + cpu_arch: aarch64 + gpu: none + backend: none + device: "GitHub macos-latest runner" + runtime: "Rust f32 and Apple's math library" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836477" + - browser: none + os: linux + os_version: "Ubuntu 24.04" + cpu_arch: aarch64 + gpu: none + backend: none + device: "GitHub ubuntu-24.04-arm runner" + runtime: "Rust f32 and glibc" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836545" + - browser: none + os: windows + cpu_arch: x86_64 + gpu: none + backend: none + device: "GitHub windows-latest runner" + runtime: "Rust f32 and the MSVC runtime" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836624" + - browser: none + os: windows + cpu_arch: x86_64 + gpu: none + backend: none + device: "GitHub windows-latest runner" + runtime: "Rust f32, built for x86_64-pc-windows-gnullvm" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836502" + - browser: none + os: windows + cpu_arch: x86_64 + gpu: none + backend: none + device: "GitHub windows-latest runner" + runtime: "Rust f32, built for x86_64-pc-windows-gnu" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836594" + - browser: none + os: linux + cpu_arch: x86_64 + gpu: none + backend: none + device: "GitHub ubuntu-latest runner" + runtime: "Rust f32 and glibc" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303201/job/131012174690" + - browser: none + os: linux + os_version: "Pop!_OS 24.04" + cpu_arch: x86_64 + cpu_version: "12th Gen Intel Core i7-12800H" + gpu: none + backend: none + device: "Dell Precision 5570" + runtime: "Rust f32 and glibc" + window: none + date: "2026-11-13" + results: + multiply_add: "00000000" + fma: "33800000" + swamped_sum: "00000000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb898" + tan: "3dcd7c44" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b99" + tanh: "3dcc1ebc" + dot: "00000000" + - interface: wasi-nn + label: "ONNX Runtime 1.22 on the CPU behind wasi:nn: Linux x86_64, where its multiply-add is not fused" + seen: + - browser: none + os: linux + cpu_arch: x86_64 + gpu: none + backend: none + device: "GitHub ubuntu-latest runner" + runtime: "ONNX Runtime 1.22" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303201/job/131012174690" + results: + multiply_add: "00000000" + fma: "00000000" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb898" + tan: "3dcd7c44" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b99" + tanh: "3dcc1ebb" + dot: "00000000" + - interface: wasi-nn + label: "ONNX Runtime 1.22 on the CPU behind wasi:nn: macOS arm64 and Linux arm64" + seen: + - browser: none + os: macos + cpu_arch: aarch64 + cpu_version: "Apple M5 Pro" + gpu: none + backend: none + runtime: "ONNX Runtime 1.22" + window: none + date: "2026-11-12" + - browser: none + os: macos + cpu_arch: aarch64 + gpu: none + backend: none + device: "GitHub macos-latest runner" + runtime: "ONNX Runtime 1.22" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303201/job/131012174374" + - browser: none + os: linux + os_version: "Ubuntu 24.04" + cpu_arch: aarch64 + gpu: none + backend: none + device: "GitHub ubuntu-24.04-arm runner" + runtime: "ONNX Runtime 1.22" + window: none + date: "2026-11-12" + source: "https://github.com/edge-toolkit/core/actions/runs/43167303165/job/131012836545" + results: + multiply_add: "00000000" + fma: "33800000" + swamped_sum: "40800000" + divide: "3eaaaaab" + subnormal_product: "00400000" + sqrt: "3fddb3d7" + inverse_sqrt: "3f13cd3a" + sin: "3dcc7577" + cos: "3f7eb898" + tan: "3dcd7c44" + exp: "3f8d763e" + log: "3f8c9f54" + pow: "3f8edd20" + atan2: "3d087b99" + tanh: "3dcc1ebb" + dot: "00000000" diff --git a/libs/gpu-probe/src/lib.rs b/libs/gpu-probe/src/lib.rs index 0b1f650f..cca5e4ec 100644 --- a/libs/gpu-probe/src/lib.rs +++ b/libs/gpu-probe/src/lib.rs @@ -1,15 +1,27 @@ //! Numeric probes that tell one GPU's floating-point behaviour from another's. //! -//! A shading language leaves an implementation room on most floating-point operations: whether a multiply and an -//! add are fused, the order a sum is taken in, whether a subnormal survives, and how closely each mathematical -//! function is rounded. Two GPUs can therefore give different bits for the same expression and both be correct. -//! This crate holds a fixed set of such expressions, the WGSL that evaluates each, and the correctly rounded -//! answer, and reads what a device returned as a [`Signature`]: which choices it made, how far each result sits -//! from the correctly rounded one, and which known GPU, if any, answers the same way. +//! A shading language leaves an implementation room on most floating-point operations: whether a multiply and an add +//! are fused, the order a sum is taken in, whether a subnormal survives, and how closely each mathematical function is +//! rounded. Two GPUs can therefore give different bits for the same expression and both be correct. This crate holds +//! a fixed set of such expressions, the WGSL that evaluates each, and the correctly rounded answer, and reads what a +//! device returned as a [`Signature`]: which choices it made, how far each result sits from the correctly rounded one, +//! and which known GPU, if any, answers the same way. //! -//! It drives no GPU itself, so the same probes serve a browser's WebGPU, a native host's, and a caller with none. -//! Each probe is a shader of its own: a device that refuses one expression still answers the rest, and a -//! signature with gaps in it identifies a device better than no signature. +//! It drives no GPU itself, so the same probes serve a browser's WebGPU, a native host's, and a caller with none. Each +//! probe is a shader of its own: a device that refuses one expression still answers the rest, and a signature with gaps +//! in it identifies a device better than no signature. +#![cfg_attr(feature = "docs", doc = "## Feature flags")] +#![cfg_attr(feature = "docs", doc = document_features::document_features!())] + +#[cfg(feature = "registry")] +mod registry; +#[cfg(feature = "registry")] +mod sighting; + +#[cfg(feature = "registry")] +pub use self::registry::every_sighting; +#[cfg(feature = "registry")] +pub use self::sighting::{Backend, Browser, GpuVendor, OperatingSystem, RowError, Sighting, Window}; /// How many expressions the probe set evaluates. pub const PROBE_COUNT: usize = 16; @@ -17,8 +29,8 @@ pub const PROBE_COUNT: usize = 16; /// The probes' inputs, handed to each shader in a storage buffer so that no expression is folded at compile time. /// /// In order: `1 + 2^-12`, whose square falls exactly halfway between two f32 values; the negation of that square -/// rounded to f32; `2^24`, above which adding 1 is lost; 1; an angle; 3; the smallest normal f32; and one half. -/// The first two are given as bit patterns, being exact values with more decimal digits than an f32 literal takes. +/// rounded to f32; `2^24`, above which adding 1 is lost; 1; an angle; 3; the smallest normal f32; and one half. The +/// first two are given as bit patterns, being exact values with more decimal digits than an f32 literal takes. pub const INPUTS: [f32; 8] = [ f32::from_bits(0x3f80_0800), f32::from_bits(0xbf80_1000), @@ -31,16 +43,30 @@ pub const INPUTS: [f32; 8] = [ ]; /// The interface a signature was measured through, where the probes ran as WGSL compute shaders on WebGPU. +/// +/// That is WebGPU whoever implements it: a browser, Deno, or wgpu called with no browser at all. One implementation +/// answers the same way in each; a native wgpu device gives the signature Firefox gives on the same GPU. pub const WEBGPU: &str = "webgpu"; +/// [`WEBGPU`] where every probe ran in the one shader [`combined_shader`] gives, not in a shader of its own. +pub const WEBGPU_COMBINED: &str = "webgpu-combined"; + /// The interface a signature was measured through, where the probes ran as GLSL fragment shaders on WebGL 2. pub const WEBGL2: &str = "webgl2"; +/// The interface a signature was measured through, where the CPU evaluated the probes as `f32` arithmetic. +/// +/// The mathematical functions are then the answers of the platform's own math library. +pub const HOST: &str = "host"; + +/// The interface a signature was measured through, where the probes ran as ONNX operators behind `wasi:nn`. +pub const WASI_NN: &str = "wasi-nn"; + /// Each probe's name, the WGSL expression it evaluates, and the same expression in GLSL ES 3.00, in output order. /// -/// The first five and the last have an exact answer under strict evaluation and a second, equally legal one, -/// which [`CHOICES`] names. The rest carry an accuracy bound instead of an exact answer. GLSL ES 3.00 has no -/// `fma`, so that probe is one a WebGL 2 device is expected to refuse. +/// The first five and the last have an exact answer under strict evaluation and at least one other, equally legal one, +/// which [`CHOICES`] names. The rest carry an accuracy bound instead of an exact answer. GLSL ES 3.00 has no `fma`, so +/// that probe is one a WebGL 2 device is expected to refuse. const PROBES: [(&str, &str, &str); PROBE_COUNT] = [ ( "multiply_add", @@ -72,6 +98,9 @@ const PROBES: [(&str, &str, &str); PROBE_COUNT] = [ ), ]; +/// Where in output order the probes of mathematical functions start, which run up to the last probe. +const FIRST_FUNCTION: usize = 6; + /// The vertex shader every fragment probe is drawn with: one point covering the single pixel that is read back. pub const VERTEX_SHADER: &str = "#version 300 es void main() { @@ -117,10 +146,13 @@ const FRAGMENT_TAIL: &str = "); } "; -/// The part of every compute probe shader that comes before its expression. -const SHADER_HEAD: &str = " +/// The part of every compute probe shader that comes before the number of words it writes. +const SHADER_OPEN: &str = " @group(0) @binding(0) var probeIn : array; -@group(0) @binding(1) var probeOut : array; +@group(0) @binding(1) var probeOut : array Vec<(&'static str, String)> { PROBES .iter() - .map(|probe| (probe.0, [SHADER_HEAD, probe.1, SHADER_TAIL].concat())) + .map(|probe| (probe.0, compute_shader(&[probe.1]))) .collect() } +/// The one WGSL compute shader that evaluates every probe, writing each result's bits in output order. +/// +/// It is bound as each of [`shaders`] is, except that its buffer at binding 1 holds [`PROBE_COUNT`] words. A compiler +/// may treat an expression differently when it sits beside the others, so a device can answer this shader and the +/// separate ones differently, and a signature taken with it is filed under [`WEBGPU_COMBINED`]. A device that refuses +/// one expression refuses the whole of it. +#[must_use] +pub fn combined_shader() -> String { + let expressions: Vec<&str> = PROBES.iter().map(|probe| probe.1).collect(); + compute_shader(&expressions) +} + +/// A WGSL compute shader that writes the bits of each of `expressions` to a word of its own, in order. +fn compute_shader(expressions: &[&str]) -> String { + let writes: Vec = expressions + .iter() + .enumerate() + .map(|(word, expression)| format!(" probeOut[{word}] = bitcast({expression});\n")) + .collect(); + format!("{SHADER_OPEN}{}{SHADER_HEAD}{}}}\n", expressions.len(), writes.concat()) +} + /// Each probe's name beside the GLSL ES 3.00 fragment shader that evaluates it, in output order. /// -/// Every shader reads [`INPUTS`] from the 8-element `float` uniform array `probeIn` and writes its result's four -/// bytes, lowest first, to the red, green, blue and alpha of its one output, so it is drawn with -/// [`VERTEX_SHADER`] into an 8-bit RGBA target and the pixel read back. +/// Every shader reads [`INPUTS`] from the 8-element `float` uniform array `probeIn` and writes its result's four bytes, +/// lowest first, to the red, green, blue and alpha of its one output, so it is drawn with [`VERTEX_SHADER`] into an 8- +/// bit RGBA target and the pixel read back. #[must_use] pub fn fragment_shaders() -> Vec<(&'static str, String)> { PROBES @@ -260,9 +324,9 @@ const fn nearest(value: f64) -> f32 { /// The bits of each probe's correctly rounded answer, in output order. /// -/// The exact probes are evaluated as written, one f32 operation at a time. The mathematical functions are -/// evaluated in f64 and rounded once to f32, which is the correctly rounded f32 answer unless the true value -/// lies within an f64 rounding error of halfway between two f32 values -- and none of these inputs' does. +/// The exact probes are evaluated as written, one f32 operation at a time. The mathematical functions are evaluated +/// in f64 and rounded once to f32, which is the correctly rounded f32 answer unless the true value lies within an f64 +/// rounding error of halfway between two f32 values -- and none of these inputs' does. #[must_use] #[expect( clippy::float_arithmetic, @@ -300,10 +364,37 @@ pub fn reference() -> [u32; PROBE_COUNT] { ] } +/// The bits of each probe as the CPU this runs on answers it, in output order. +/// +/// The arithmetic is `f32` operations taken one at a time, so the exact probes give their strict answers, as they do +/// in [`reference()`]. The mathematical functions are the `f32` ones of the platform's math library, which are not +/// correctly rounded and differ from one library to the next. The inputs pass through `black_box` so that the functions +/// are evaluated when this is called, on this machine, and not folded by the compiler on the one that built it. +#[must_use] +pub fn host() -> [u32; PROBE_COUNT] { + let [_, _, _, _, angle, three, _, _] = core::hint::black_box(INPUTS); + let functions = [ + three.sqrt().recip(), + angle.sin(), + angle.cos(), + angle.tan(), + angle.exp(), + three.ln(), + three.powf(angle), + angle.atan2(three), + angle.tanh(), + ]; + let mut bits = reference(); + for (answer, function) in bits.iter_mut().skip(FIRST_FUNCTION).zip(functions) { + *answer = function.to_bits(); + } + bits +} + /// What a device did with one probe. /// -/// A refusal is an outcome to record, not a failure of the probe set: which probes a device will not run is -/// part of what tells it from another. +/// A refusal is an outcome to record, not a failure of the probe set: which probes a device will not run is part of +/// what tells it from another. #[non_exhaustive] pub enum Answer { /// The bits of the result the device computed. @@ -314,7 +405,7 @@ pub enum Answer { /// What one device returned for the probe set through one interface. pub struct Signature { - /// The interface the probes ran through: [`WEBGPU`] or [`WEBGL2`]. + /// The interface the probes ran through: [`WEBGPU`], [`WEBGPU_COMBINED`], [`WEBGL2`], [`HOST`] or [`WASI_NN`]. interface: &'static str, /// What the device did with each probe, in output order. observed: Vec, @@ -377,6 +468,37 @@ impl Signature { .map_or(UNRECOGNISED, |known| known.2) } + /// The label of every registry entry holding the answers this device gave, through the same interface. + /// + /// A label names every device, browser and backend the registry has seen the signature on. No label means the + /// registry has not seen it, and more than one that it is filed more than once, as a private window's is beside a + /// normal one's. + /// + /// # Stability + /// + /// Not a stable part of this crate's API. The registry is data kept for checking and looking into what a device + /// answered, and its entries, how they are structured and what a label says all change without that being a + /// breaking change. Nothing a user ships should depend on the text returned. + #[cfg(feature = "registry")] + #[must_use] + pub fn identify(&self) -> Vec<&'static str> { + registry::labels(self.interface, &self.words()) + } + + /// Every place the registry has seen the answers this device gave, through the same interface. + /// + /// Each is one row of an entry [`identify`](Self::identify) would name: the browser, operating system, CPU + /// architecture, GPU vendor, graphics API and kind of window the signature was seen with together. Only what a row + /// names from fixed sets of values is given; its free text stays in the registry. + /// + /// # Stability + /// + /// The rows the registry holds change as signatures are seen in new places, without that being a breaking change. + #[cfg(feature = "registry")] + pub fn seen(&self) -> Result, RowError> { + registry::seen(self.interface, &self.words()) + } + /// Which legal answer the device gave to each probe that has two, as `name=choice` joined by spaces. /// /// A probe the device did not answer reads `failed`, and an answer that is neither of the two reads `other`. @@ -405,8 +527,8 @@ impl Signature { /// One line per probe, saying what the device did with it. /// - /// An answered probe gives its name, the device's bits, the correctly rounded bits, and the distance between - /// them in units in the last place. A refused one gives its name and the reason. + /// An answered probe gives its name, the device's bits, the correctly rounded bits, and the distance between them + /// in units in the last place. A refused one gives its name and the reason. #[must_use] pub fn describe(&self) -> Vec { PROBES diff --git a/libs/gpu-probe/src/registry.rs b/libs/gpu-probe/src/registry.rs new file mode 100644 index 00000000..98b51c7a --- /dev/null +++ b/libs/gpu-probe/src/registry.rs @@ -0,0 +1,93 @@ +//! The registry of every signature the probes have been seen to give, and what it says a signature was seen on. + +use std::sync::LazyLock; + +use serde_json::Value; + +use crate::PROBES; +use crate::sighting::{RowError, Sighting}; + +/// The registry: the JSON written from `signatures.yaml`, embedded as text when the crate is compiled. +/// +/// The text is read into a value the first time anything is asked of the registry, rather than written out as code +/// for the compiler to build, because building a value this size in one function takes more stack than a thread can +/// be counted on to have. The file is in this crate's own directory, which is what the variable names, and a published +/// copy of the crate has no repository root to find it from instead. +#[expect( + clippy::unwrap_used, + reason = "the text is this crate's own file, which the task that writes it only ever writes as JSON" +)] +static REGISTRY: LazyLock = LazyLock::new(|| { + // nosemgrep: no-semgrep-mentions, no-ast-grep-mentions -- Required compile-time path annotation. + // ast-grep-ignore: no-cargo-manifest-dir + let text = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/signatures.json")); + serde_json::from_str(text).unwrap() +}); + +/// Every entry of the registry. +fn entries() -> impl Iterator { + REGISTRY + .get("signatures") + .and_then(Value::as_array) + .into_iter() + .flatten() +} + +/// Every registry entry holding the answers `words`, measured through `interface`. +/// +/// An entry was measured through `interface` when it is filed under that name, or under it with `-split` added, which +/// is how a browser's signature is filed when each probe ran in a shader of its own, as this crate's do. An entry holds +/// the answers when it has a result under each probe's name equal to the word given for that probe, so an entry with no +/// results, kept for an interface a browser withheld, holds none. +fn holding<'words>(interface: &'words str, words: &'words [String]) -> impl Iterator + 'words { + let split = format!("{interface}-split"); + entries() + .filter(move |entry| { + let filed = entry.get("interface").and_then(Value::as_str); + filed == Some(interface) || filed == Some(split.as_str()) + }) + .filter(move |entry| { + let results = entry.get("results"); + words.len() == PROBES.len() + && PROBES.iter().zip(words).all(|(probe, word)| { + results.and_then(|held| held.get(probe.0)).and_then(Value::as_str) == Some(word.as_str()) + }) + }) +} + +/// Every row the entries `found` list as places they have been seen, in the order the entries come. +/// +/// The first entry [`Sighting::read_entry`] refuses is what this fails with. Every entry handed here comes from the +/// registry compiled into this crate, which `every_row_the_registry_holds_can_be_read` proves readable, so the refusal +/// is passed through by `collect` rather than by a branch of this function's own that nothing could take. +fn rows(found: impl Iterator) -> Result, RowError> { + found + .map(Sighting::read_entry) + .collect::, _>>() + .map(|entries| entries.into_iter().flatten().collect()) +} + +/// The label of every registry entry holding the answers `words`, measured through `interface`. +#[expect( + clippy::single_call_fn, + reason = "one of the registry's questions, kept beside the data it is asked of" +)] +pub fn labels(interface: &str, words: &[String]) -> Vec<&'static str> { + holding(interface, words) + .filter_map(|entry| entry.get("label").and_then(Value::as_str)) + .collect() +} + +/// Every place the answers `words`, measured through `interface`, have been seen. +#[expect( + clippy::single_call_fn, + reason = "one of the registry's questions, kept beside the data it is asked of" +)] +pub fn seen(interface: &str, words: &[String]) -> Result, RowError> { + rows(holding(interface, words)) +} + +/// Every place the registry has seen any signature. +pub fn every_sighting() -> Result, RowError> { + rows(entries()) +} diff --git a/libs/gpu-probe/src/sighting.rs b/libs/gpu-probe/src/sighting.rs new file mode 100644 index 00000000..05ec8e71 --- /dev/null +++ b/libs/gpu-probe/src/sighting.rs @@ -0,0 +1,372 @@ +//! Where a signature has been seen: a row of a registry entry, read into typed values. + +use core::str::FromStr; + +use serde_json::Value; +use target_lexicon::Architecture; +use wgpu_hal::auxil::db; + +/// The browsers a row can name that the `webbrowser` crate does not, each beside the name a row gives it. +const BROWSERS: [(&str, Browser); 8] = [ + ("brave", Browser::Brave), + ("chromium", Browser::Chromium), + ("deno", Browser::Deno), + ("edge", Browser::Edge), + ("librewolf", Browser::LibreWolf), + ("samsung-internet", Browser::SamsungInternet), + ("tor-browser", Browser::Tor), + ("none", Browser::Absent), +]; + +/// The operating systems a row can name, each beside the name a row gives it. +const SYSTEMS: [(&str, OperatingSystem); 4] = [ + ("android", OperatingSystem::Android), + ("linux", OperatingSystem::Linux), + ("macos", OperatingSystem::MacOs), + ("windows", OperatingSystem::Windows), +]; + +/// The GPU vendors a row can name, each beside the name a row gives it. +const GPU_VENDORS: [(&str, GpuVendor); 13] = [ + ("amd", GpuVendor::Amd), + ("apple", GpuVendor::Apple), + ("arm", GpuVendor::Arm), + ("broadcom", GpuVendor::Broadcom), + ("google", GpuVendor::Google), + ("imgtec", GpuVendor::ImgTec), + ("intel", GpuVendor::Intel), + ("mesa", GpuVendor::Mesa), + ("microsoft", GpuVendor::Microsoft), + ("nvidia", GpuVendor::Nvidia), + ("qualcomm", GpuVendor::Qualcomm), + ("unknown", GpuVendor::Unknown), + ("none", GpuVendor::Absent), +]; + +/// Each GPU vendor beside the PCI vendor ID an adapter of its reports. +/// +/// The IDs wgpu knows are wgpu's own. Google's and Microsoft's are those of their software renderers, `SwiftShader` and +/// the Basic Render Driver, which wgpu has no name for. +const GPU_VENDOR_IDS: [(u32, GpuVendor); 11] = [ + (db::amd::VENDOR, GpuVendor::Amd), + (db::apple::VENDOR, GpuVendor::Apple), + (db::arm::VENDOR, GpuVendor::Arm), + (db::broadcom::VENDOR, GpuVendor::Broadcom), + (0x1AE0, GpuVendor::Google), + (db::imgtec::VENDOR, GpuVendor::ImgTec), + (db::intel::VENDOR, GpuVendor::Intel), + (db::mesa::VENDOR, GpuVendor::Mesa), + (0x1414, GpuVendor::Microsoft), + (db::nvidia::VENDOR, GpuVendor::Nvidia), + (db::qualcomm::VENDOR, GpuVendor::Qualcomm), +]; + +/// The graphics backends a row can name, each beside the name a row gives it. +const BACKENDS: [(&str, Backend); 8] = [ + ("metal", Backend::Metal), + ("d3d11", Backend::D3d11), + ("d3d12", Backend::D3d12), + ("vulkan", Backend::Vulkan), + ("opengl", Backend::OpenGl), + ("opengl-es", Backend::OpenGlEs), + ("unknown", Backend::Unknown), + ("none", Backend::Absent), +]; + +/// The windows a row can name, each beside the name a row gives it. +const WINDOWS: [(&str, Window); 3] = [ + ("normal", Window::Normal), + ("private", Window::Private), + ("none", Window::Absent), +]; + +/// Every field a row may carry: the six it must, then its free text. +const FIELDS: [&str; 16] = [ + "browser", + "os", + "cpu_arch", + "gpu", + "backend", + "window", + "browser_version", + "os_version", + "cpu_version", + "gpu_version", + "device", + "runtime", + "defence", + "reported", + "date", + "source", +]; + +/// What is wrong with a registry row that cannot be read. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum RowError { + /// The row, or the entry it should be in, lacks something every one carries. + #[error("the registry has no `{0}` where one is required")] + Missing(&'static str), + /// The row gives a field a value the registry does not know, or carries a field it does not know. + #[error("the registry gives `{field}` as `{value}`, which is not one it knows")] + Unknown { + /// The field the value was given for. + field: &'static str, + /// The value the row gave. + value: String, + }, +} + +/// The browser a signature was seen in. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Browser { + /// A browser the `webbrowser` crate names, such as Chrome, Firefox or Safari. + Listed(webbrowser::Browser), + /// Brave. + Brave, + /// Chromium itself, as distinct from the browsers built on it. + Chromium, + /// Deno, which runs a page's modules with no window, and gives them WebGPU. + Deno, + /// Microsoft Edge. + Edge, + /// `LibreWolf`. + LibreWolf, + /// Samsung Internet. + SamsungInternet, + /// Tor Browser. + Tor, + /// No browser: the probes ran natively. + Absent, +} + +impl FromStr for Browser { + type Err = RowError; + + fn from_str(name: &str) -> Result { + name.parse() + .map(Self::Listed) + .or_else(|_unlisted: webbrowser::ParseBrowserError| named(&BROWSERS, "browser", name)) + } +} + +/// The operating system a signature was seen on. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum OperatingSystem { + /// Android. + Android, + /// Linux. + Linux, + /// macOS. + MacOs, + /// Windows. + Windows, +} + +impl FromStr for OperatingSystem { + type Err = RowError; + + fn from_str(name: &str) -> Result { + named(&SYSTEMS, "os", name) + } +} + +/// Who makes the GPU a signature was seen on, as wgpu names GPU vendors, with those it has no name for. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum GpuVendor { + /// AMD. + Amd, + /// Apple. + Apple, + /// Arm, whose GPUs are the Mali line. + Arm, + /// Broadcom. + Broadcom, + /// Google, whose `SwiftShader` renders on the CPU. + Google, + /// Imagination Technologies, whose GPUs are the `PowerVR` line. + ImgTec, + /// Intel. + Intel, + /// Mesa, whose llvmpipe renders on the CPU. + Mesa, + /// Microsoft, whose Basic Render Driver renders on the CPU. + Microsoft, + /// NVIDIA. + Nvidia, + /// Qualcomm, whose GPUs are the Adreno line. + Qualcomm, + /// A GPU was involved, and whose it was is not on record. + Unknown, + /// No GPU was involved. + Absent, +} + +impl GpuVendor { + /// The vendor whose adapters report `id` as their PCI vendor ID, or [`Self::Unknown`]. + /// + /// wgpu gives an adapter's vendor as this ID, though not on every backend: Metal reports none. + #[must_use] + pub fn from_id(id: u32) -> Self { + GPU_VENDOR_IDS + .iter() + .find(|vendor| vendor.0 == id) + .map_or(Self::Unknown, |vendor| vendor.1) + } +} + +impl FromStr for GpuVendor { + type Err = RowError; + + fn from_str(name: &str) -> Result { + named(&GPU_VENDORS, "gpu", name) + } +} + +/// The graphics API a signature reached the GPU through. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Backend { + /// Metal. + Metal, + /// Direct3D 11. + D3d11, + /// Direct3D 12. + D3d12, + /// Vulkan. + Vulkan, + /// OpenGL. + OpenGl, + /// OpenGL ES. + OpenGlEs, + /// A graphics API was involved, and which is not on record. + Unknown, + /// No graphics API was involved. + Absent, +} + +impl FromStr for Backend { + type Err = RowError; + + fn from_str(name: &str) -> Result { + named(&BACKENDS, "backend", name) + } +} + +/// The kind of browser window a signature was seen in. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Window { + /// An ordinary window. + Normal, + /// A private, or incognito, window. + Private, + /// No browser was involved, so no window was. + Absent, +} + +impl FromStr for Window { + type Err = RowError; + + fn from_str(name: &str) -> Result { + named(&WINDOWS, "window", name) + } +} + +/// One place a signature has been seen: what a row of a registry entry names from fixed sets of values. +/// +/// A row's free text -- its versions, the device, where the sighting is on record -- stays in the registry. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct Sighting { + /// The graphics API the signature reached the GPU through. + pub backend: Backend, + /// The browser the signature was seen in. + pub browser: Browser, + /// The architecture of the CPU, as `target-lexicon` names architectures. + pub cpu_arch: Architecture, + /// Who makes the GPU. + pub gpu: GpuVendor, + /// The operating system. + pub system: OperatingSystem, + /// The kind of browser window. + pub window: Window, +} + +impl Sighting { + /// Read `row`, an object as a registry entry lists them under `seen`. + /// + /// A row has to give each of `browser`, `os`, `cpu_arch`, `gpu`, `backend` and `window` a value the registry knows, + /// and may carry no field the registry does not know. + pub fn read(row: &Value) -> Result { + let stray = row + .as_object() + .into_iter() + .flatten() + .map(|field| field.0) + .find(|field| !FIELDS.contains(&field.as_str())); + if let Some(field) = stray { + return Err(RowError::Unknown { + field: "field", + value: field.clone(), + }); + } + let cpu_arch = required(row, "cpu_arch")?; + Ok(Self { + backend: required(row, "backend")?.parse()?, + browser: required(row, "browser")?.parse()?, + cpu_arch: cpu_arch.parse().ok().ok_or_else(|| RowError::Unknown { + field: "cpu_arch", + value: cpu_arch.to_owned(), + })?, + gpu: required(row, "gpu")?.parse()?, + system: required(row, "os")?.parse()?, + window: required(row, "window")?.parse()?, + }) + } + + /// Read every row `entry`, an entry of the registry, lists under `seen`, in the order it lists them. + /// + /// An entry has to carry a `seen` list, and each row in it has to read as [`Self::read`] requires. + pub fn read_entry(entry: &Value) -> Result, RowError> { + entry + .get("seen") + .and_then(Value::as_array) + .ok_or(RowError::Missing("seen"))? + .iter() + .map(Self::read) + .collect() + } + + /// Whether this sighting was on the operating system and CPU architecture this code is running on. + #[must_use] + pub fn is_here(&self) -> bool { + std::env::consts::OS + .parse() + .is_ok_and(|system: OperatingSystem| system == self.system) + && self.cpu_arch.to_string() == std::env::consts::ARCH + } +} + +/// The value `name` stands for in `table`, which lists each name `field` can take beside its value. +fn named(table: &[(&str, Kind)], field: &'static str, name: &str) -> Result +where + Kind: Copy, +{ + table + .iter() + .find(|known| known.0 == name) + .map(|known| known.1) + .ok_or_else(|| RowError::Unknown { + field, + value: name.to_owned(), + }) +} + +/// The text `row` gives `field`, which every row has to give. +fn required<'row>(row: &'row Value, field: &'static str) -> Result<&'row str, RowError> { + row.get(field).and_then(Value::as_str).ok_or(RowError::Missing(field)) +} diff --git a/libs/gpu-probe/tests/native.rs b/libs/gpu-probe/tests/native.rs new file mode 100644 index 00000000..8c04edfb --- /dev/null +++ b/libs/gpu-probe/tests/native.rs @@ -0,0 +1,261 @@ +//! Puts the probe set to this machine with no browser involved, to its GPU through wgpu and to its CPU. +//! +//! Each signature is checked against the one recorded for that hardware. A machine nobody has recorded still has to +//! answer every probe legally, and the same way twice; what it answered is printed, which is how its signature comes to +//! be recorded. +#![cfg(test)] + +use std::sync::LazyLock; + +use et_gpu_probe::{ + Answer, HOST, INPUTS, PROBE_COUNT, Signature, WEBGPU, WEBGPU_COMBINED, combined_shader, host, shaders, +}; +use futures_executor::block_on; + +/// How many bytes the storage buffer every probe shader reads [`INPUTS`] from holds. +const INPUT_BYTES: u64 = 32; + +/// How many bytes the one `u32` every probe shader writes takes. +const OUTPUT_BYTES: u64 = 4; + +/// How many bytes the answers to the whole probe set take. +const SIGNATURE_BYTES: u64 = 64; + +/// How many words the answers to the whole probe set take, one a probe. +const PROBE_WORDS: u64 = 16; + +/// This machine's GPU, opened once and with the probe set compiled for it. +static GPU: LazyLock = LazyLock::new(|| { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())).unwrap(); + let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).unwrap(); + let info = adapter.get_info(); + Gpu { + hardware: format!( + "{} | {} {} | {}", + info.name, info.driver, info.driver_info, info.backend + ), + combined: compile(&device, &combined_shader()), + probes: shaders().iter().map(|shader| compile(&device, &shader.1)).collect(), + vendor: info.vendor, + device, + queue, + } +}); + +/// A probe as a device holds it: the pipeline that evaluates it, or the device's reason for refusing to build one. +type Probe = Result; + +/// A GPU the probes are put to. +struct Gpu { + /// The whole probe set as one shader. + combined: Probe, + /// The device the probes run on. + device: wgpu::Device, + /// What the adapter behind the device reports itself as: its name, driver and backend. + hardware: String, + /// The probe set, in output order. + probes: Vec, + /// Where the device's work is submitted. + queue: wgpu::Queue, + /// The PCI vendor ID the adapter reports, which is none on some backends. + vendor: u32, +} + +/// Print `signature` beside the `hardware` it was measured on through `interface`. +/// +/// Hardware the registry does not hold yet has no label to print, and its fingerprint is what gets it one. +fn report(interface: &str, hardware: &str, signature: &Signature) { + println!("{interface} hardware: {hardware}"); + println!("{interface} fingerprint: {}", signature.fingerprint()); + println!("{interface} registry: {}", signature.identify().join(" | ")); + println!("{interface} traits: {}", signature.traits()); + for line in signature.describe() { + println!("{interface} {line}"); + } +} + +/// A buffer of `size` bytes on `device`, for the uses `usage` names. +fn buffer(device: &wgpu::Device, size: u64, usage: wgpu::BufferUsages) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size, + usage, + mapped_at_creation: false, + }) +} + +/// The pipeline on `device` that runs the shader `wgsl` holds. +/// +/// A shader the device will not compile, or a pipeline it will not build, is a refusal, which the error scope catches. +fn compile(device: &wgpu::Device, wgsl: &str) -> Probe { + let scope = device.push_error_scope(wgpu::ErrorFilter::Validation); + let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(wgsl.into()), + }); + let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: None, + layout: None, + module: &module, + entry_point: Some("main"), + compilation_options: wgpu::PipelineCompilationOptions::default(), + cache: None, + }); + block_on(scope.pop()).map_or(Ok(pipeline), Err) +} + +/// Add to `encoder` a run of `pipeline` on `device`, reading `input` and leaving what it writes in `output`. +#[expect( + clippy::single_call_fn, + reason = "one stage's run, kept apart from the loop over the stages" +)] +fn run( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + pipeline: &wgpu::ComputePipeline, + input: &wgpu::Buffer, + output: &wgpu::Buffer, +) { + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &pipeline.get_bind_group_layout(0), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: input.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: output.as_entire_binding(), + }, + ], + }); + let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default()); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, &bind_group, &[]); + pass.dispatch_workgroups(1, 1, 1); +} + +/// What this machine's GPU answers to `stages`, all of them run in one submission. +/// +/// A stage is a shader as the device holds it, beside how many probes it evaluates: each of the probe set's shaders +/// evaluates one, and the combined shader all of them. A stage the device refused answers each of its probes with +/// the refusal. +#[expect( + clippy::host_endian_bytes, + reason = "a GPU buffer holds its values in the machine's own byte order" +)] +fn gpu_answers(stages: &[(&Probe, u64)]) -> Vec { + let Gpu { device, queue, .. } = &*GPU; + let input = buffer( + device, + INPUT_BYTES, + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + ); + let input_bytes: Vec = INPUTS.iter().flat_map(|value| value.to_ne_bytes()).collect(); + queue.write_buffer(&input, 0, &input_bytes); + let readback = buffer( + device, + SIGNATURE_BYTES, + wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + ); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + let mut offset = 0_u64; + for (probe, probes) in stages { + let bytes = probes.saturating_mul(OUTPUT_BYTES); + if let Ok(pipeline) = probe { + let output = buffer( + device, + bytes, + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, + ); + run(device, &mut encoder, pipeline, &input, &output); + encoder.copy_buffer_to_buffer(&output, 0, &readback, offset, bytes); + } + offset = offset.saturating_add(bytes); + } + let _submission: wgpu::SubmissionIndex = queue.submit([encoder.finish()]); + readback.map_async(wgpu::MapMode::Read, .., |mapped| mapped.unwrap()); + let _status: wgpu::PollStatus = device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); + let held = readback.get_mapped_range(..); + let mut words = held.as_chunks::<4>().0.iter(); + let mut answers = Vec::new(); + for (probe, probes) in stages { + for _probe in 0..*probes { + let word = words.next().unwrap(); + answers.push(match probe { + Ok(_) => Answer::Bits(u32::from_ne_bytes(*word)), + Err(refusal) => Answer::Refused(refusal.to_string()), + }); + } + } + answers +} + +/// What this machine's GPU answers to the probe set, each probe in a shader of its own. +fn gpu_signature() -> Signature { + let stages: Vec<(&Probe, u64)> = GPU.probes.iter().map(|probe| (probe, 1)).collect(); + Signature::new(WEBGPU, gpu_answers(&stages)) +} + +/// What this machine's GPU answers to the probe set, every probe in the one combined shader. +fn combined_signature() -> Signature { + Signature::new(WEBGPU_COMBINED, gpu_answers(&[(&GPU.combined, PROBE_WORDS)])) +} + +/// What this machine's CPU answers to the probe set. +fn cpu_signature() -> Signature { + Signature::new(HOST, host().into_iter().map(Answer::Bits).collect()) +} + +#[test] +fn this_gpu_answers_every_probe_legally_and_the_same_way_twice() { + let signature = gpu_signature(); + report("wgpu", &GPU.hardware, &signature); + println!("wgpu vendor id: {:#06x}", GPU.vendor); + + assert_eq!(signature.completed(), PROBE_COUNT); + assert!(!signature.traits().contains("=other")); + assert_eq!(signature.fingerprint(), gpu_signature().fingerprint()); +} + +#[test] +fn this_gpu_answers_the_combined_shader_legally_and_the_same_way_twice() { + let signature = combined_signature(); + report("wgpu combined", &GPU.hardware, &signature); + + assert_eq!(signature.completed(), PROBE_COUNT); + assert!(!signature.traits().contains("=other")); + assert_eq!(signature.fingerprint(), combined_signature().fingerprint()); +} + +// The platforms whose GPU and CPU each have a signature on record: an Apple GPU through Metal, llvmpipe through +// Vulkan and the Basic Render Driver through Direct3D 12. A machine of one of these platforms with hardware not yet +// on record fails here, and the fingerprint the tests beside this one print is what gets it recorded. +#[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64"), + all(target_os = "windows", target_arch = "x86_64"), +))] +#[test] +fn the_registry_has_seen_this_gpu_and_this_cpu_on_this_platform() { + // An adapter that reports no vendor ID, as Metal's does, is held to its platform alone. + let vendor = et_gpu_probe::GpuVendor::from_id(GPU.vendor); + let gpu_seen = gpu_signature().seen().unwrap(); + assert!(gpu_seen.iter().any(|sighting| { + sighting.is_here() && (vendor == et_gpu_probe::GpuVendor::Unknown || sighting.gpu == vendor) + })); + let cpu_seen = cpu_signature().seen().unwrap(); + assert!(cpu_seen.iter().any(et_gpu_probe::Sighting::is_here)); +} + +#[test] +fn this_cpu_answers_every_probe_the_same_way_twice() { + let signature = cpu_signature(); + let hardware = format!("{} {}", std::env::consts::OS, std::env::consts::ARCH); + report(HOST, &hardware, &signature); + + assert_eq!(signature.fingerprint(), cpu_signature().fingerprint()); +} diff --git a/libs/gpu-probe/tests/probe.rs b/libs/gpu-probe/tests/probe.rs index 2a1206bc..745cac44 100644 --- a/libs/gpu-probe/tests/probe.rs +++ b/libs/gpu-probe/tests/probe.rs @@ -1,10 +1,10 @@ -//! Covers the probe set itself -- its shaders and correctly rounded answers -- and what a signature reads out of -//! a device's results: its counts, fingerprint, choices and per-probe lines. +//! Covers the probe set itself -- its shaders and correctly rounded answers -- and what a signature reads out of a +//! device's results: its counts, fingerprint, choices and per-probe lines. #![cfg(test)] use et_gpu_probe::{ - Answer, INPUTS, PROBE_COUNT, Signature, UNRECOGNISED, VERTEX_SHADER, WEBGL2, WEBGPU, fragment_shaders, reference, - shaders, + Answer, HOST, PROBE_COUNT, Signature, UNRECOGNISED, VERTEX_SHADER, WEBGL2, WEBGPU, combined_shader, + fragment_shaders, host, reference, shaders, }; /// The fingerprint the probes gave through WebGPU on an Apple M5 Pro. @@ -69,6 +69,19 @@ fn every_probe_has_a_shader_of_its_own_writing_one_word() { assert_eq!(names.len(), PROBE_COUNT); } +#[test] +fn the_combined_shader_writes_every_probe_to_a_word_of_its_own() { + let combined = combined_shader(); + assert!(combined.contains("probeOut : array;")); + for (word, (_, alone)) in shaders().iter().enumerate() { + assert!(alone.contains("probeOut : array;")); + // The one write a probe's own shader makes is the write the combined shader makes to that probe's word. + let write = alone.lines().find(|line| line.contains("probeOut[0] = ")).unwrap(); + assert!(combined.contains(&write.replace("probeOut[0]", &format!("probeOut[{word}]")))); + } + assert_eq!(combined.matches("] = bitcast(").count(), PROBE_COUNT); +} + #[test] fn a_probe_shader_holds_its_own_expression() { let shaders = shaders(); @@ -105,23 +118,18 @@ fn the_exact_probes_have_their_strict_answers() { #[test] fn the_function_probes_are_within_one_ulp_of_the_host_f32_functions() { - let [_, _, _, _, angle, three, _, _] = INPUTS; - let host = [ - three.sqrt().recip(), - angle.sin(), - angle.cos(), - angle.tan(), - angle.exp(), - three.ln(), - three.powf(angle), - angle.atan2(three), - angle.tanh(), - ]; - for (correct, near) in reference().into_iter().skip(6).zip(host) { - assert!(correct.abs_diff(near.to_bits()) <= 1); + for (correct, near) in reference().into_iter().zip(host()) { + assert!(correct.abs_diff(near) <= 1); } } +#[test] +fn the_host_gives_the_exact_probes_their_strict_answers() { + let signature = Signature::new(HOST, host().into_iter().map(Answer::Bits).collect()); + assert_eq!(signature.completed(), PROBE_COUNT); + assert_eq!(signature.traits(), correct().traits()); +} + #[test] fn a_device_giving_every_correct_answer_deviates_nowhere() { let signature = correct(); @@ -153,6 +161,22 @@ fn a_fused_multiply_add_is_a_deviation_named_as_a_choice() { ); } +#[test] +fn a_sum_regrouped_part_way_is_a_choice_of_its_own() { + // Intel Iris Xe (Alder Lake GT2) through Mesa's Vulkan driver, every probe in the one combined shader. + let signature = measured( + WEBGPU, + concat!( + "33800000-33800000-40000000-3eaaaaab-00000000-3fddb3d7-3f13cd3a-3dcc756a-", + "3f7eb898-3dcd7c38-3f8d763d-3f8c9f54-3f8edd1f-3d087ae6-3dcc1eb6-3f800000" + ), + ); + assert_eq!( + signature.traits(), + "multiply_add=fused fma=fused swamped_sum=paired subnormal_product=flushed dot=pairwise" + ); +} + #[test] fn an_answer_that_is_neither_choice_reads_other() { assert!( @@ -231,3 +255,38 @@ fn the_answers_measured_on_an_apple_m5_pro_are_recognised() { "multiply_add=fused fma=fused swamped_sum=reassociated subnormal_product=flushed dot=in_order" ); } + +#[test] +fn the_registry_finds_a_browsers_signature_where_each_probe_had_a_shader_of_its_own() { + let labels = measured(WEBGPU, M5_PRO_WEBGPU).identify(); + assert_ne!(labels, Vec::<&str>::new()); + for label in labels { + assert!(label.contains("Apple M5 Pro")); + } +} + +#[test] +fn the_registry_finds_a_signature_filed_under_the_interface_itself() { + let correct = Signature::new(HOST, reference().into_iter().map(Answer::Bits).collect()); + assert_eq!(correct.identify().len(), 1); +} + +#[test] +fn the_registry_does_not_find_a_signature_through_another_interface() { + assert_eq!(measured(WEBGL2, M5_PRO_WEBGPU).identify(), Vec::<&str>::new()); +} + +#[test] +fn the_registry_has_no_label_for_a_signature_it_has_not_seen() { + assert_eq!(correct().identify(), Vec::<&str>::new()); +} + +#[test] +fn the_registry_matches_nothing_to_a_signature_missing_answers() { + assert_eq!( + Signature::new(HOST, vec![Answer::Bits(0)]).identify(), + Vec::<&str>::new() + ); + // No answers at all is what an entry kept for a withheld interface holds, and still names nothing. + assert_eq!(Signature::new(WEBGPU, Vec::new()).identify(), Vec::<&str>::new()); +} diff --git a/libs/gpu-probe/tests/sighting.rs b/libs/gpu-probe/tests/sighting.rs new file mode 100644 index 00000000..4f1fcd22 --- /dev/null +++ b/libs/gpu-probe/tests/sighting.rs @@ -0,0 +1,146 @@ +//! Covers the rows of the registry: the values a row's fields take, what makes a row unreadable, and that every row the +//! registry holds can be read. +#![cfg(test)] + +use et_gpu_probe::{ + Answer, Backend, Browser, GpuVendor, HOST, OperatingSystem, RowError, Sighting, Signature, WEBGPU, Window, + every_sighting, reference, +}; +use serde_json::{Value, json}; + +/// A row as the registry holds one, seen in Brave on an Android phone. +fn row() -> Value { + json!({ + "browser": "brave", + "browser_version": "1.99", + "os": "android", + "cpu_arch": "aarch64", + "gpu": "arm", + "gpu_version": "Mali-G76", + "backend": "vulkan", + "window": "private", + }) +} + +/// `row` with `field` given as `value`. +fn row_with(field: &str, value: &str) -> Value { + let mut changed = row(); + let _replaced: Option = changed + .as_object_mut() + .unwrap() + .insert(field.to_owned(), Value::String(value.to_owned())); + changed +} + +/// What reading `row` fails with, as the error words it. +fn refusal(row: &Value) -> String { + Sighting::read(row).unwrap_err().to_string() +} + +#[test] +fn a_row_reads_as_what_it_names() { + let sighting = Sighting::read(&row()).unwrap(); + assert_eq!(sighting.browser, Browser::Brave); + assert_eq!(sighting.system, OperatingSystem::Android); + assert_eq!(sighting.cpu_arch.to_string(), "aarch64"); + assert_eq!(sighting.gpu, GpuVendor::Arm); + assert_eq!(sighting.backend, Backend::Vulkan); + assert_eq!(sighting.window, Window::Private); +} + +#[test] +fn a_browser_the_webbrowser_crate_names_is_one_it_lists() { + assert!(matches!("chrome".parse::(), Ok(Browser::Listed(_)))); + assert_eq!("tor-browser".parse::().unwrap(), Browser::Tor); + assert_eq!("deno".parse::().unwrap(), Browser::Deno); + assert_eq!("none".parse::().unwrap(), Browser::Absent); +} + +#[test] +fn a_gpu_vendor_is_known_by_the_id_its_adapters_report() { + assert_eq!(GpuVendor::from_id(0x10DE), GpuVendor::Nvidia); + assert_eq!(GpuVendor::from_id(0x1414), GpuVendor::Microsoft); + // Metal reports no vendor ID at all. + assert_eq!(GpuVendor::from_id(0), GpuVendor::Unknown); +} + +#[test] +fn a_value_the_registry_does_not_know_is_refused_by_name() { + for field in ["browser", "os", "cpu_arch", "gpu", "backend", "window"] { + assert_eq!( + refusal(&row_with(field, "nonesuch")), + format!("the registry gives `{field}` as `nonesuch`, which is not one it knows") + ); + } +} + +#[test] +fn a_field_the_registry_does_not_know_is_refused() { + assert_eq!( + refusal(&row_with("colour", "blue")), + "the registry gives `field` as `colour`, which is not one it knows" + ); +} + +#[test] +fn a_row_lacking_a_field_every_row_carries_is_refused() { + let mut lacking = row(); + let _removed: Option = lacking.as_object_mut().unwrap().remove("window"); + assert!(matches!(Sighting::read(&lacking), Err(RowError::Missing("window")))); + assert_eq!(refusal(&lacking), "the registry has no `window` where one is required"); +} + +#[test] +fn an_entry_reads_as_each_row_it_lists() { + let entry = json!({ "seen": [row(), row_with("browser", "deno")] }); + let browsers: Vec = Sighting::read_entry(&entry) + .unwrap() + .iter() + .map(|sighting| sighting.browser) + .collect(); + assert_eq!(browsers, [Browser::Brave, Browser::Deno]); +} + +#[test] +fn an_entry_listing_no_rows_is_refused() { + let entry = json!({ "label": "seen nowhere" }); + assert!(matches!(Sighting::read_entry(&entry), Err(RowError::Missing("seen")))); +} + +#[test] +fn an_entry_listing_a_row_that_cannot_be_read_is_refused() { + let entry = json!({ "seen": [row(), row_with("os", "nonesuch")] }); + assert_eq!( + Sighting::read_entry(&entry).unwrap_err().to_string(), + "the registry gives `os` as `nonesuch`, which is not one it knows" + ); +} + +#[test] +fn every_row_the_registry_holds_can_be_read() { + let sightings = every_sighting().unwrap(); + assert!(sightings.len() > 100); + assert!(sightings.iter().any(|sighting| sighting.gpu == GpuVendor::Apple)); + // Rows on a platform like this one, on its operating system with another architecture, and on neither. + assert!(sightings.iter().any(Sighting::is_here)); + assert!(!sightings.iter().all(Sighting::is_here)); +} + +#[test] +fn a_signature_lists_each_place_it_was_seen() { + let correct = Signature::new(HOST, reference().into_iter().map(Answer::Bits).collect()); + let seen = correct.seen().unwrap(); + assert!(seen.len() > 1); + for sighting in seen { + assert_eq!(sighting.browser, Browser::Absent); + assert_eq!(sighting.gpu, GpuVendor::Absent); + assert_eq!(sighting.backend, Backend::Absent); + assert_eq!(sighting.window, Window::Absent); + } +} + +#[test] +fn a_signature_the_registry_has_not_seen_was_seen_nowhere() { + let unseen = Signature::new(WEBGPU, reference().into_iter().map(Answer::Bits).collect()); + assert_eq!(unseen.seen().unwrap().len(), 0); +} diff --git a/services/recovery-supervisor/src/runtime.rs b/services/recovery-supervisor/src/runtime.rs index b0df92ad..eeb7262b 100644 --- a/services/recovery-supervisor/src/runtime.rs +++ b/services/recovery-supervisor/src/runtime.rs @@ -133,10 +133,6 @@ where } /// Run hub transport and supervision concurrently so backend operations never block WebSocket heartbeats. -#[expect( - clippy::integer_division_remainder_used, - reason = "tokio::select! expands to remainder operations" -)] pub async fn run(config: Config, mut backend: B) -> Result<(), RecoveryError> where B: Backend, @@ -167,11 +163,7 @@ where } /// Keep policy state and its ownership lease while re-establishing transport; persistence errors remain fatal. -#[expect( - clippy::integer_division_remainder_used, - clippy::single_call_fn, - reason = "connection ownership is separate from shutdown; tokio::select! expands to remainder operations" -)] +#[expect(clippy::single_call_fn, reason = "connection ownership is separate from shutdown")] async fn supervise( config: &Config, supervisor: &mut Supervisor, @@ -204,9 +196,8 @@ where /// Bound registration attempts and maintain established observer health while recording unavailable evidence. #[expect( - clippy::integer_division_remainder_used, clippy::single_call_fn, - reason = "registration is separate from connected observation; tokio::select! expands to remainder operations" + reason = "registration is separate from connected observation" )] async fn reconnect( config: &Config, @@ -261,11 +252,7 @@ async fn connect(config: &Config) -> Result { } /// Consume evidence and run bounded observation cycles while the hub handles transport independently. -#[expect( - clippy::integer_division_remainder_used, - clippy::single_call_fn, - reason = "named control loop separates shutdown ownership; tokio::select! expands to remainder operations" -)] +#[expect(clippy::single_call_fn, reason = "named control loop separates shutdown ownership")] async fn control( config: &Config, supervisor: &mut Supervisor, @@ -339,10 +326,6 @@ fn accept(supervisor: &mut Supervisor, identity: &str, event: &CloudEvent, start } /// Receive direct `CloudEvents` through Core's existing acknowledged agent-message transport. -#[expect( - clippy::integer_division_remainder_used, - reason = "tokio::select! expands to remainder operations" -)] pub async fn hub( mut socket: RegisteredSocket, sender: mpsc::Sender<(String, CloudEvent)>, @@ -374,11 +357,7 @@ pub async fn hub( } /// Retain pending evidence until queue admission, sending keepalive pings while data reads are backpressured. -#[expect( - clippy::integer_division_remainder_used, - clippy::single_call_fn, - reason = "separate bounded delivery from frame handling; tokio::select! expands to remainder operations" -)] +#[expect(clippy::single_call_fn, reason = "separate bounded delivery from frame handling")] async fn enqueue( socket: &mut RegisteredSocket, sender: &mpsc::Sender<(String, CloudEvent)>, diff --git a/services/websockify/src/lib.rs b/services/websockify/src/lib.rs index 8794e098..6e5e9239 100644 --- a/services/websockify/src/lib.rs +++ b/services/websockify/src/lib.rs @@ -282,10 +282,9 @@ async fn accept_client( #[expect( clippy::cognitive_complexity, clippy::future_not_send, - clippy::integer_division_remainder_used, clippy::single_call_fn, clippy::too_many_lines, - reason = "Rc-backed !Send actix-ws types; select! uses % internally; one call site; one select! loop is the bridge" + reason = "Rc-backed !Send actix-ws types; one call site; one select! loop is the bridge" )] async fn bridge(mut session: Session, mut stream: AggregatedMessageStream, tcp: TcpStream, head: Vec) { let (mut tcp_read, mut tcp_write) = tcp.into_split(); diff --git a/services/ws-pyo3-runner/src/lib.rs b/services/ws-pyo3-runner/src/lib.rs index c75a843a..ecf1df2b 100644 --- a/services/ws-pyo3-runner/src/lib.rs +++ b/services/ws-pyo3-runner/src/lib.rs @@ -2,9 +2,8 @@ #![cfg_attr(feature = "docs", doc = "## Feature flags")] #![cfg_attr(feature = "docs", doc = document_features::document_features!())] #![expect( - clippy::integer_division_remainder_used, clippy::single_call_fn, - reason = "register/drive/storage_worker/python_worker single-use; select! uses %; RunnerError: tungstenite::Error" + reason = "register/drive/storage_worker/python_worker single-use; RunnerError: tungstenite::Error" )] pub mod agent; diff --git a/services/ws-pyo3-runner/src/main.rs b/services/ws-pyo3-runner/src/main.rs index ce306d07..eeb22258 100644 --- a/services/ws-pyo3-runner/src/main.rs +++ b/services/ws-pyo3-runner/src/main.rs @@ -5,11 +5,6 @@ //! list (`RUNNER_MODULE`, `RUNNER_TIMEOUT`, `WS_SERVER_URL`, `PYO3_PYTHONPATH`, //! `PYO3_AGENT_ID`). -#![expect( - clippy::integer_division_remainder_used, - reason = "tokio::select! expands to % internally" -)] - use et_ws_pyo3_runner::agent::{AgentConfig, initialize, resolved_python_path, run as run_agent}; use et_ws_pyo3_runner::config::Config; use et_ws_pyo3_runner::hub_module::fetch_if_absent; diff --git a/services/ws-wasi-runner/Cargo.toml b/services/ws-wasi-runner/Cargo.toml index c9c88d1a..7e518ce1 100644 --- a/services/ws-wasi-runner/Cargo.toml +++ b/services/ws-wasi-runner/Cargo.toml @@ -45,6 +45,10 @@ required-features = ["core-modules", "std"] name = "modules" required-features = ["std"] +[[test]] +name = "nn_probe" +required-features = ["nn", "std"] + [[test]] name = "o2_ingest_search" required-features = ["std"] @@ -186,6 +190,7 @@ libc = { workspace = true, optional = true } [dev-dependencies] command-error.workspace = true critical-section = { workspace = true, features = ["std"] } +et-gpu-probe = { workspace = true, features = ["registry"] } et-org.workspace = true et-test-helpers.workspace = true et-test-otlp.workspace = true diff --git a/services/ws-wasi-runner/Dockerfile b/services/ws-wasi-runner/Dockerfile index 1cc0656a..a6e554fb 100644 --- a/services/ws-wasi-runner/Dockerfile +++ b/services/ws-wasi-runner/Dockerfile @@ -7,11 +7,10 @@ # makes by setting RUNNER_MODULE. # # Unlike the other two runners this one cannot be a single static binary, because two of the WASI interfaces it -# offers guests are backed by native libraries. `wasi:nn` runs through ONNX Runtime, which ort links dynamically -# (the workspace enables its `copy-dylibs` feature rather than `download-binaries`), so libonnxruntime has to be -# present at run time. `wasi:webgpu` runs through wgpu, which needs a Vulkan loader plus a driver -- mesa's -# lavapipe is the software one, so a container with no GPU still resolves an adapter instead of failing -# `request-adapter`. +# offers guests are backed by native libraries. `wasi:nn` runs through ONNX Runtime, which the runner opens when a +# guest first uses the interface rather than links, so libonnxruntime has to be present at run time. `wasi:webgpu` +# runs through wgpu, which needs a Vulkan loader plus a driver -- mesa's lavapipe is the software one, so a container +# with no GPU still resolves an adapter instead of failing `request-adapter`. # The CUDA execution provider is deliberately not built in: it is opt-in behind the crate's `cuda` feature because # the prebuilt only exists for some triples, and enabling it here would bind the image to those. # @@ -102,16 +101,9 @@ EOF WORKDIR /app -# ONNX Runtime goes on the loader's search path rather than beside the binary. -# ort's `copy-dylibs` also drops a copy next to the executable in the target directory, which resolves through an -# $ORIGIN rpath -- but that only holds while the binary keeps its build-time neighbours, and this stage moves it to -# /usr/local/bin alone. A SONAME lookup through ldconfig does not care where either file ended up. +# ONNX Runtime is opened by the path named here, so the loader's search plays no part in finding it. COPY --from=builder /opt/onnxruntime/lib /usr/local/lib/onnxruntime -RUN bash <<'EOF' -set -euo pipefail -echo /usr/local/lib/onnxruntime > /etc/ld.so.conf.d/onnxruntime.conf -ldconfig -EOF +ENV ORT_DYLIB_PATH=/usr/local/lib/onnxruntime/libonnxruntime.so COPY --from=builder /workspace/target/release/et-ws-wasi-runner /usr/local/bin/et-ws-wasi-runner diff --git a/services/ws-wasi-runner/src/board/wasi.rs b/services/ws-wasi-runner/src/board/wasi.rs index d8fe3343..0c4507af 100644 --- a/services/ws-wasi-runner/src/board/wasi.rs +++ b/services/ws-wasi-runner/src/board/wasi.rs @@ -2,21 +2,21 @@ //! by hand, since `wasmtime-wasi` needs `std`. //! //! Every ws-module guest imports the same few functions: its standard streams and environment, `exit`, the terminal -//! queries, and the monotonic clock and polling it sleeps with. A component imports only the functions it calls, so -//! these are all a guest needs; one importing any other fails to instantiate, naming it. Its standard streams go to the -//! board's console, its environment is empty, it has no terminal, and a pollable is ready at once or at a deadline on -//! the board's clock. -#![expect( - clippy::integer_division_remainder_used, - reason = "wasmtime's ComponentType derive, on StreamError, aligns with `%` in the impls it generates" -)] +//! queries, the monotonic clock and polling it sleeps with, and the time of day its locks' timed waits read. A +//! component imports only the functions it calls, so these are all a guest needs; one importing any other fails +//! to instantiate, naming it. Its standard streams go to the board's console, its environment is empty, it has no +//! terminal, a pollable is ready at once or at a deadline on the board's clock, and the time of day is how long that +//! clock has run. #[cfg(not(feature = "std"))] use alloc::{boxed::Box, format, string::String, vec::Vec}; +use core::convert::Infallible; +use core::time::Duration; pub(super) use edge_toolkit::board::PREVIEW2; use wasmtime::StoreContextMut; -use wasmtime::component::{ComponentType, Lift, Linker, Lower, Resource, ResourceType}; +use wasmtime::component::types::ComponentFunc; +use wasmtime::component::{ComponentType, Lift, Linker, Lower, Resource, ResourceType, Val}; use super::host::BoardHost; use super::platform::{Console, Platform, Transport}; @@ -46,14 +46,14 @@ pub(super) enum OutputStream { /// A `wasi:io/streams` input stream, the guest's stdin, which never has anything to read. pub(super) struct InputStream; -/// A `wasi:io/error`, which a board never raises. -pub(super) enum IoError {} +/// A `wasi:io/error`, which a board never raises, so there is no value of one. +pub(super) struct IoError(Infallible); -/// A `wasi:cli/terminal-input`, which a board never has. -pub(super) enum TerminalInput {} +/// A `wasi:cli/terminal-input`, which a board never has, so there is no value of one. +pub(super) struct TerminalInput(Infallible); -/// A `wasi:cli/terminal-output`, which a board never has. -pub(super) enum TerminalOutput {} +/// A `wasi:cli/terminal-output`, which a board never has, so there is no value of one. +pub(super) struct TerminalOutput(Infallible); /// `wasi:io/streams`' `stream-error`, which a board's console never fails with. #[derive(ComponentType, Lift, Lower)] @@ -90,7 +90,7 @@ where Ok(()) } -/// `wasi:io/poll`, and the monotonic clock the guest's pollables come from. +/// `wasi:io/poll`, the monotonic clock the guest's pollables come from, and the time of day. #[expect(clippy::single_call_fn, reason = "the polling part of add_to_linker")] fn add_poll(linker: &mut Linker>) -> wasmtime::Result<()> where @@ -128,13 +128,33 @@ where }, )?; + let mut monotonic = linker.instance(&format!("wasi:clocks/monotonic-clock@{PREVIEW2}"))?; + monotonic.func_wrap("now", |store: Ctx<'_, P, T>, (): ()| { + Ok((store.data().platform().now(),)) + })?; + monotonic.func_wrap("subscribe-duration", |mut store: Ctx<'_, P, T>, (duration,): (u64,)| { + let host = store.data_mut(); + let deadline = host.platform().now().saturating_add(duration); + Ok((host.table().push(Pollable::Due(deadline))?,)) + })?; + + // A guest's standard library imports the time of day for the timed waits of its locks, whether or not it ever waits + // on one. A board keeps no calendar, so the time it gives is how long its clock has run, as though that clock had + // started at the Unix epoch. The `datetime` it answers with is built as a value rather than derived as a type, so + // that the guest's own type describes it. linker - .instance(&format!("wasi:clocks/monotonic-clock@{PREVIEW2}"))? - .func_wrap("subscribe-duration", |mut store: Ctx<'_, P, T>, (duration,): (u64,)| { - let host = store.data_mut(); - let deadline = host.platform().now().saturating_add(duration); - Ok((host.table().push(Pollable::Due(deadline))?,)) - }) + .instance(&format!("wasi:clocks/wall-clock@{PREVIEW2}"))? + .func_new( + "now", + |store: Ctx<'_, P, T>, _: ComponentFunc, _: &[Val], results: &mut [Val]| { + let running = Duration::from_nanos(store.data().platform().now()); + results.fill(Val::Record(Vec::from([ + (String::from("seconds"), Val::U64(running.as_secs())), + (String::from("nanoseconds"), Val::U32(running.subsec_nanos())), + ]))); + Ok(()) + }, + ) } /// `wasi:io/streams` and its errors: the guest's standard streams, written to the board's console. @@ -216,13 +236,21 @@ where .func_wrap("get-environment", |_: Ctx<'_, P, T>, (): ()| { Ok((Vec::<(String, String)>::new(),)) })?; - linker.instance(&format!("wasi:cli/exit@{PREVIEW2}"))?.func_wrap( + let mut exit = linker.instance(&format!("wasi:cli/exit@{PREVIEW2}"))?; + exit.func_wrap( "exit", |_: Ctx<'_, P, T>, (status,): (Result<(), ()>,)| -> wasmtime::Result<()> { let outcome = if status.is_ok() { "success" } else { "failure" }; Err(wasmtime::Error::msg(format!("the guest exited with {outcome}"))) }, )?; + // A guest's standard library imports this beside `exit` for `std::process::exit` with a status of its own. + exit.func_wrap( + "exit-with-code", + |_: Ctx<'_, P, T>, (code,): (u8,)| -> wasmtime::Result<()> { + Err(wasmtime::Error::msg(format!("the guest exited with code {code}"))) + }, + )?; linker .instance(&format!("wasi:cli/terminal-input@{PREVIEW2}"))? diff --git a/services/ws-wasi-runner/src/host/core_module.rs b/services/ws-wasi-runner/src/host/core_module.rs index b343b982..01a6bbde 100644 --- a/services/ws-wasi-runner/src/host/core_module.rs +++ b/services/ws-wasi-runner/src/host/core_module.rs @@ -67,10 +67,6 @@ impl CoreHost for HostState { /// # Errors /// /// When the thread cannot be started, the module fails as [`core_module::run`] describes, or the thread dies first. -#[expect( - clippy::integer_division_remainder_used, - reason = "tokio::select! expands to remainder operations" -)] pub async fn run_core_module(wasm: Vec, host: H) -> Result<(), RunnerError> where H: CoreHost + Send + 'static, diff --git a/services/ws-wasi-runner/tests/nn_probe.rs b/services/ws-wasi-runner/tests/nn_probe.rs new file mode 100644 index 00000000..6dd13d2d --- /dev/null +++ b/services/ws-wasi-runner/tests/nn_probe.rs @@ -0,0 +1,240 @@ +//! Puts the GPU probe set to the `wasi:nn` backend the runner hosts, which evaluates it on the CPU. +//! +//! The probes are spelled as an ONNX graph, one operator per operation, and ONNX Runtime's own kernels answer them: no +//! GPU and no browser is involved, so the signature is that of the runtime's arithmetic on this machine. It is checked +//! against the one recorded for the machine, and printed, which is how a machine's signature comes to be recorded. +#![cfg(test)] + +use et_gpu_probe::{Answer, INPUTS, PROBE_COUNT, Signature, WASI_NN, shaders}; +use wasmtime_wasi_nn::backend::NamedTensor; +use wasmtime_wasi_nn::wit::{ExecutionTarget, TensorType}; +use wasmtime_wasi_nn::{ExecutionContext, Tensor}; + +/// The name each of [`INPUTS`] has in the graph, in order, which is the name the probe shaders give it. +const INPUT_NAMES: [&str; 8] = [ + "ulpBase", + "negatedSquare", + "big", + "one", + "angle", + "three", + "tiny", + "oneHalf", +]; + +/// Each operator of the probe graph that takes no attribute: its name, its inputs, and the tensor it produces. +/// +/// Every tensor is a 1x1 matrix, so that the operators with a matrix kernel behind them take part. `Gemm` is the one +/// operator that multiplies and adds in a single kernel, which is what `fma` asks for, and `MatMul` of a row by a +/// column is the dot product. ONNX has no two-argument arctangent, so `atan2` is the arctangent of the quotient, which +/// is the same value while the divisor is positive, as it is here. +const OPERATORS: [(&str, &[&str], &str); 19] = [ + ("Mul", &["ulpBase", "ulpBase"], "square"), + ("Add", &["square", "negatedSquare"], "multiply_add"), + ("Gemm", &["ulpBase", "ulpBase", "negatedSquare"], "fma"), + ("ReduceSum", &["terms"], "sum"), + ("Sub", &["sum", "big"], "swamped_sum"), + ("Div", &["one", "three"], "divide"), + ("Mul", &["tiny", "oneHalf"], "subnormal_product"), + ("Sqrt", &["three"], "sqrt"), + ("Reciprocal", &["sqrt"], "inverse_sqrt"), + ("Sin", &["angle"], "sin"), + ("Cos", &["angle"], "cos"), + ("Tan", &["angle"], "tan"), + ("Exp", &["angle"], "exp"), + ("Log", &["three"], "log"), + ("Pow", &["three", "angle"], "pow"), + ("Div", &["angle", "three"], "slope"), + ("Atan", &["slope"], "atan2"), + ("Tanh", &["angle"], "tanh"), + ("MatMul", &["row", "column"], "dot"), +]; + +/// A protobuf message being written: the bytes of the fields given to it so far. +#[derive(Default)] +struct Message(Vec); + +impl Message { + /// Append `value` as a base-128 varint, low seven bits first. + fn push_varint(&mut self, mut value: u64) { + while value >= 0x80 { + self.0.push(u8::try_from(value & 0x7f).unwrap() | 0x80); + value >>= 7_u32; + } + self.0.push(u8::try_from(value).unwrap()); + } + + /// This message with the integer `value` as its field numbered `field`. + fn int(mut self, field: u64, value: u64) -> Self { + self.push_varint(field << 3_u32); + self.push_varint(value); + self + } + + /// This message with `payload` as its length-delimited field numbered `field`. + fn bytes(mut self, field: u64, payload: &[u8]) -> Self { + self.push_varint((field << 3_u32) | 2); + self.push_varint(u64::try_from(payload.len()).unwrap()); + self.0.extend_from_slice(payload); + self + } + + /// This message with `text` as its string field numbered `field`. + fn text(self, field: u64, text: &str) -> Self { + self.bytes(field, text.as_bytes()) + } + + /// This message with `inner` as its nested message field numbered `field`. + fn message(self, field: u64, inner: &Self) -> Self { + self.bytes(field, &inner.0) + } +} + +/// An ONNX `NodeProto` applying `operator` to `inputs` and naming what it produces `outputs`. +fn node(operator: &str, inputs: &[&str], outputs: &[&str]) -> Message { + let named = inputs + .iter() + .fold(Message::default(), |message, input| message.text(1, input)); + outputs + .iter() + .fold(named, |message, output| message.text(2, output)) + .text(4, operator) +} + +/// `node` with the integer attribute `axis`, which `Split` and `Concat` work along. +fn along(node: Message, axis: u64) -> Message { + node.message(5, &Message::default().text(1, "axis").int(3, axis).int(20, 2)) +} + +/// An ONNX `ValueInfoProto` for the `f32` tensor `name` of one row and `width` columns. +fn row_of(name: &str, width: u64) -> Message { + let dimension = |size| Message::default().int(1, size); + let shape = Message::default() + .message(1, &dimension(1)) + .message(1, &dimension(width)); + let tensor = Message::default().int(1, 1).message(2, &shape); + Message::default() + .text(1, name) + .message(2, &Message::default().message(1, &tensor)) +} + +/// The probe set as an ONNX model: [`INPUTS`] in as one row, every probe's answer out as another. +#[expect( + clippy::single_call_fn, + reason = "the graph's construction, kept apart from the run that answers it" +)] +fn probe_model() -> Vec { + let probes = shaders(); + let answers: Vec<&str> = probes.iter().map(|probe| probe.0).collect(); + let graph = [ + along(node("Split", &["probeIn"], &INPUT_NAMES), 1), + node("Neg", &["big"], &["negatedBig"]), + along(node("Concat", &["big", "one", "one", "one", "one"], &["terms"]), 1), + along(node("Concat", &["big", "one", "one", "negatedBig"], &["row"]), 1), + along(node("Concat", &["one", "one", "one", "one"], &["column"]), 0), + ] + .into_iter() + .chain( + OPERATORS + .iter() + .map(|operator| node(operator.0, operator.1, &[operator.2])), + ) + .chain([along(node("Concat", &answers, &["probeOut"]), 1)]) + .fold(Message::default(), |graph, operation| graph.message(1, &operation)) + .text(2, "probes") + .message(11, &row_of("probeIn", 8)) + .message(12, &row_of("probeOut", 16)); + // IR version 8 with operator set 17, the last in which `Split` divides its input evenly with no attribute. + Message::default() + .int(1, 8) + .message(8, &Message::default().int(2, 17)) + .message(7, &graph) + .0 +} + +/// What the graph behind `context` answers to the probe set. +#[expect( + clippy::little_endian_bytes, + reason = "the ONNX backend reads and writes a tensor's values little-endian" +)] +fn nn_signature(context: &mut ExecutionContext) -> Signature { + let input = Tensor::new( + vec![1, 8], + TensorType::Fp32, + INPUTS.iter().flat_map(|value| value.to_le_bytes()).collect(), + ); + let outputs = context + .compute_with_io(vec![NamedTensor { + name: "probeIn".to_owned(), + tensor: input, + }]) + .unwrap(); + let output = outputs.first().unwrap(); + Signature::new( + WASI_NN, + output + .tensor + .data + .as_chunks::<4>() + .0 + .iter() + .map(|word| Answer::Bits(u32::from_le_bytes(*word))) + .collect(), + ) +} + +#[test] +fn onnx_runtime_answers_every_probe_and_the_same_way_twice() { + let model = probe_model(); + let mut backends = wasmtime_wasi_nn::backend::list(); + let graph = backends + .first_mut() + .unwrap() + .load(&[&model], ExecutionTarget::Cpu) + .unwrap(); + let mut context = graph.init_execution_context().unwrap(); + + let signature = nn_signature(&mut context); + let hardware = format!("{} {}", std::env::consts::OS, std::env::consts::ARCH); + println!("{WASI_NN} hardware: {hardware}"); + println!("{WASI_NN} fingerprint: {}", signature.fingerprint()); + println!("{WASI_NN} registry: {}", signature.identify().join(" | ")); + println!("{WASI_NN} traits: {}", signature.traits()); + println!("{WASI_NN} {}", signature.describe().join("\n")); + + assert_eq!(signature.completed(), PROBE_COUNT); + assert!(!signature.traits().contains("=other")); + assert_eq!(signature.fingerprint(), nn_signature(&mut context).fingerprint()); + // The platforms whose answers through ONNX Runtime are on record in the registry. + #[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64"), + ))] + assert!(signature.seen().unwrap().iter().any(et_gpu_probe::Sighting::is_here)); + + // On macOS a process that has run an ONNX Runtime 1.22 session aborts as it exits, after the test has passed: + // + // libc++abi: terminating due to uncaught exception of type + // std::__1::system_error: mutex lock failed: Invalid argument + // (test aborted with signal 6: SIGABRT) + // + // Seen on every run of this test on an Apple M5 Pro, at the commit below. The runner's `main` leaves by `_exit` + // for the same abort, which skips the exit handlers it happens in, and so does this. + // + // https://github.com/edge-toolkit/core/commit/aba5d88c170c70fae30511f2ef6b44ad9a27fa1d + #[cfg(target_os = "macos")] + { + use std::io::Write as _; + + std::io::stdout().flush().unwrap(); + #[expect( + unsafe_code, + reason = "libc::_exit is the only way to leave without running the exit handlers that abort" + )] + // SAFETY: `_exit` has no preconditions, and everything the test printed is flushed above. + unsafe { + libc::_exit(0); + } + } +} diff --git a/services/ws/src/admin.rs b/services/ws/src/admin.rs index 8671fc97..22914865 100644 --- a/services/ws/src/admin.rs +++ b/services/ws/src/admin.rs @@ -167,8 +167,7 @@ impl FromRequest for AdminRequest { fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let token = req .app_data::>() - .map(|config| config.admin_token.clone()) - .unwrap_or_default(); + .map_or_default(|config| config.admin_token.clone()); ready(authorize(req, &token).map(|()| Self { #[cfg(feature = "nats")] backplane: backplane_of(req), diff --git a/services/ws/src/backplane.rs b/services/ws/src/backplane.rs index fc2e21ec..4a5971f3 100644 --- a/services/ws/src/backplane.rs +++ b/services/ws/src/backplane.rs @@ -70,11 +70,7 @@ fn warn_if_broker_payload_too_small(max_payload: usize, max_frame_size: usize) { } /// Hand each frame `inbox` yields to the local agents it is for, until `stopped` fires or the inbox ends. -#[expect( - clippy::integer_division_remainder_used, - clippy::single_call_fn, - reason = "the delivery task start_backplane spawns; tokio::select! uses % internally" -)] +#[expect(clippy::single_call_fn, reason = "the delivery task start_backplane spawns")] async fn deliver_until_stopped( mut inbox: Pin>>, mut stopped: oneshot::Receiver<()>, diff --git a/services/ws/src/connection.rs b/services/ws/src/connection.rs index 8a610f12..6ecef6fe 100644 --- a/services/ws/src/connection.rs +++ b/services/ws/src/connection.rs @@ -496,8 +496,7 @@ impl Connection { #[expect( clippy::cognitive_complexity, clippy::future_not_send, - clippy::integer_division_remainder_used, - reason = "actix-ws AggregatedMessageStream is Rc-backed and !Send; tokio::select! macro uses % internally" + reason = "actix-ws AggregatedMessageStream is Rc-backed and !Send" )] pub(crate) async fn run( mut self, diff --git a/utilities/cli/src/lib.rs b/utilities/cli/src/lib.rs index 4c66cccf..1d600415 100644 --- a/utilities/cli/src/lib.rs +++ b/utilities/cli/src/lib.rs @@ -968,16 +968,13 @@ fn module_entry(module_path: &Path, ws_server_dir: &Path, docker_path: &str) -> // the same module. A source manifest declares a dependency the way it declares its own crate -- unscoped -- and // publishing scopes both; reading one side raw would leave a dependency naming something the registry has no // key for. - dependencies: package - .as_ref() - .map(|package| { - package - .dependencies - .keys() - .map(|name| module_package_json::scoped_npm_dependency_name(name)) - .collect() - }) - .unwrap_or_default(), + dependencies: package.as_ref().map_or_default(|package| { + package + .dependencies + .keys() + .map(|name| module_package_json::scoped_npm_dependency_name(name)) + .collect() + }), source: ModuleSource::Repo(repo_path), package_name: served_name, } diff --git a/utilities/cli/src/module_package_json/generate.rs b/utilities/cli/src/module_package_json/generate.rs index f45558dd..259073a4 100644 --- a/utilities/cli/src/module_package_json/generate.rs +++ b/utilities/cli/src/module_package_json/generate.rs @@ -174,7 +174,7 @@ fn package_json_from_pyproject(module_dir: &Path) -> Result { let pyproject_path = module_dir.join("pyproject.toml"); let pyproject: Pyproject = read_toml(&pyproject_path)?; let project = &pyproject.project; - let ws_module = pyproject.tool.map(|tool| tool.ws_module).unwrap_or_default(); + let ws_module = pyproject.tool.map_or_default(|tool| tool.ws_module); let pkg_dir = module_dir.join("pkg"); let kind = detect_python_kind(module_dir); diff --git a/utilities/cli/tests/scenario_generation.rs b/utilities/cli/tests/scenario_generation.rs index 500985ba..bf8aaf1d 100644 --- a/utilities/cli/tests/scenario_generation.rs +++ b/utilities/cli/tests/scenario_generation.rs @@ -363,8 +363,7 @@ fn dekit_deployment(input: &str) -> (tempfile::TempDir, std::path::PathBuf, serd fn task_deps(task: &serde_yaml::Value) -> Vec<&str> { task.get("deps") .and_then(serde_yaml::Value::as_sequence) - .map(|deps| deps.iter().map(|dep| dep.as_str().unwrap()).collect()) - .unwrap_or_default() + .map_or_default(|deps| deps.iter().map(|dep| dep.as_str().unwrap()).collect()) } #[test] diff --git a/utilities/int-gen/src/checks.rs b/utilities/int-gen/src/checks.rs index dc78c235..898a7f29 100644 --- a/utilities/int-gen/src/checks.rs +++ b/utilities/int-gen/src/checks.rs @@ -465,7 +465,7 @@ fn load_tasks(mise_dir: &Path) -> Result, Error> { /// Build one task from its TOML value: a full table, or the `name = "command"` shorthand. fn task(name: &str, env: &str, value: &toml::Value) -> Task { - let text = |key: &str| value.get(key).map(strings).unwrap_or_default().join("\n"); + let text = |key: &str| value.get(key).map_or_default(strings).join("\n"); let mut body = value.as_str().map_or_else(|| text("run"), str::to_owned); let windows = text("run_windows"); if !windows.is_empty() { @@ -488,7 +488,7 @@ fn task(name: &str, env: &str, value: &toml::Value) -> Task { env: env.to_owned(), description: text("description"), body, - depends: value.get("depends").map(strings).unwrap_or_default(), + depends: value.get("depends").map_or_default(strings), } } @@ -497,16 +497,13 @@ fn strings(value: &toml::Value) -> Vec { if let Some(single) = value.as_str() { return vec![single.to_owned()]; } - value - .as_array() - .map(|items| { - items - .iter() - .filter_map(|item| item.as_str().or_else(|| item.get("task").and_then(toml::Value::as_str))) - .map(str::to_owned) - .collect() - }) - .unwrap_or_default() + value.as_array().map_or_default(|items| { + items + .iter() + .filter_map(|item| item.as_str().or_else(|| item.get("task").and_then(toml::Value::as_str))) + .map(str::to_owned) + .collect() + }) } /// Map each check to the repair passes the `fix` and `fmt` aggregates run for it. @@ -698,7 +695,7 @@ fn command_tool(words: &[String], assignment: &Regex, command: &Regex) -> Result continue; } if assignment.is_match(word) { - let value = word.split_once('=').map(|(_, value)| value).unwrap_or_default(); + let value = word.split_once('=').map_or_default(|(_, value)| value); let inner = value.strip_prefix("$(").and_then(|inner| inner.strip_suffix(')')); if let Some(Found::Tool(tool)) = inner.map(substitution_tool).transpose()? { return Ok(Found::Tool(tool)); diff --git a/utilities/int-gen/src/kdl.rs b/utilities/int-gen/src/kdl.rs index 04830337..57347a91 100644 --- a/utilities/int-gen/src/kdl.rs +++ b/utilities/int-gen/src/kdl.rs @@ -219,8 +219,7 @@ fn class_node(name: &str, schema: &serde_json::Value, discriminator: Option<&str let required: std::collections::HashSet<&str> = schema .get("required") .and_then(|val| val.as_array()) - .map(|arr| arr.iter().filter_map(|val| val.as_str()).collect()) - .unwrap_or_default(); + .map_or_default(|arr| arr.iter().filter_map(|val| val.as_str()).collect()); let mut node = KdlNode::new("class"); node.push(quoted_string_entry(name)); diff --git a/utilities/int-gen/src/wit/messages.rs b/utilities/int-gen/src/wit/messages.rs index 14c8bf8b..5076dff5 100644 --- a/utilities/int-gen/src/wit/messages.rs +++ b/utilities/int-gen/src/wit/messages.rs @@ -128,13 +128,12 @@ fn merge_defs(client_root: &serde_json::Value, server_root: &serde_json::Value) fn collect_enum_names(root: &serde_json::Value) -> EnumSet { root.get("$defs") .and_then(|val| val.as_object()) - .map(|defs| { + .map_or_default(|defs| { defs.iter() .filter(|(_, def)| def.get("enum").is_some()) .map(|(name, _)| name.clone()) .collect() }) - .unwrap_or_default() } #[expect( @@ -269,8 +268,7 @@ fn build_record( let required: HashSet<&str> = schema .get("required") .and_then(|val| val.as_array()) - .map(|arr| arr.iter().filter_map(|val| val.as_str()).collect()) - .unwrap_or_default(); + .map_or_default(|arr| arr.iter().filter_map(|val| val.as_str()).collect()); let mut fields: Vec = Vec::new(); if let Some(props) = props { let mut keys: Vec<&String> = props diff --git a/utilities/repo-check/src/commit_hashes.rs b/utilities/repo-check/src/commit_hashes.rs index 25135f7b..6dfa2a76 100644 --- a/utilities/repo-check/src/commit_hashes.rs +++ b/utilities/repo-check/src/commit_hashes.rs @@ -144,7 +144,7 @@ pub(crate) fn run(files: &[String]) -> Result { } let slug = origin_slug().unwrap_or_else(|| "/".to_owned()); for hash in &missing { - for site in hashes.get(*hash).map(Vec::as_slice).unwrap_or_default() { + for site in hashes.get(*hash).map_or_default(Vec::as_slice) { println!("{}:{}: no commit {hash} in this repository", site.path, site.line); } println!(" write it as https://github.com/{slug}/commit/{hash} if the commit is on a merged branch");