From 4f3bfd61c587d1bc3ce9a8bba92ab7c345ed2c63 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 12 Nov 2026 13:41:07 +0800 Subject: [PATCH 1/5] More GPU probe tests --- .editorconfig | 5 + .mise/config.toml | 24 + .mise/config.windows.toml | 18 +- CHANGELOG.md | 9 +- CHECKS.md | 8 +- Cargo.lock | 126 + Cargo.toml | 28 +- config/cargo-features.toml | 1 + config/conftest/policy/jscpd/jscpd.rego | 2 +- config/deny.toml | 114 +- config/generated-trees.toml | 4 + config/jscpd-baseline.json | 4 +- config/jscpd.json | 2 + config/nextest.toml | 6 + config/osv-scanner.toml | 97 +- config/semgrep/prefer-yaml-toml.yaml | 2 + libs/edge-toolkit/src/ws_server.rs | 13 +- libs/gpu-probe/Cargo.toml | 27 + libs/gpu-probe/signatures.json | 3182 +++++++++++++++++ libs/gpu-probe/signatures.yaml | 2588 ++++++++++++++ libs/gpu-probe/src/lib.rs | 185 +- libs/gpu-probe/src/registry.rs | 90 + libs/gpu-probe/src/sighting.rs | 372 ++ libs/gpu-probe/tests/native.rs | 261 ++ libs/gpu-probe/tests/probe.rs | 79 +- libs/gpu-probe/tests/sighting.rs | 146 + services/recovery-supervisor/src/runtime.rs | 29 +- services/websockify/src/lib.rs | 3 +- services/ws-pyo3-runner/src/lib.rs | 3 +- services/ws-pyo3-runner/src/main.rs | 5 - services/ws-wasi-runner/Cargo.toml | 5 + services/ws-wasi-runner/Dockerfile | 16 +- services/ws-wasi-runner/src/board/wasi.rs | 17 +- .../ws-wasi-runner/src/host/core_module.rs | 4 - services/ws-wasi-runner/tests/nn_probe.rs | 240 ++ services/ws/src/admin.rs | 3 +- services/ws/src/backplane.rs | 6 +- services/ws/src/connection.rs | 3 +- utilities/cli/src/lib.rs | 17 +- .../cli/src/module_package_json/generate.rs | 2 +- utilities/cli/tests/scenario_generation.rs | 3 +- utilities/int-gen/src/checks.rs | 23 +- utilities/int-gen/src/kdl.rs | 3 +- utilities/int-gen/src/wit/messages.rs | 6 +- utilities/repo-check/src/commit_hashes.rs | 2 +- 45 files changed, 7504 insertions(+), 279 deletions(-) create mode 100644 libs/gpu-probe/signatures.json create mode 100644 libs/gpu-probe/signatures.yaml create mode 100644 libs/gpu-probe/src/registry.rs create mode 100644 libs/gpu-probe/src/sighting.rs create mode 100644 libs/gpu-probe/tests/native.rs create mode 100644 libs/gpu-probe/tests/sighting.rs create mode 100644 services/ws-wasi-runner/tests/nn_probe.rs 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.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..53b7c612 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,10 @@ Changelog 1.1.0. ### Changed +- **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/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..7239a01e --- /dev/null +++ b/libs/gpu-probe/signatures.json @@ -0,0 +1,3182 @@ +{ + "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": "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" + } + ], + "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..76fab353 --- /dev/null +++ b/libs/gpu-probe/signatures.yaml @@ -0,0 +1,2588 @@ +# 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: 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" + 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..b7ef23a4 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 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. 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 +309,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 +349,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 +390,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 +453,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 +512,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..bc8f5c54 --- /dev/null +++ b/libs/gpu-probe/src/registry.rs @@ -0,0 +1,90 @@ +//! 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. +fn rows(found: impl Iterator) -> Result, RowError> { + let mut seen = Vec::new(); + for entry in found { + seen.extend(Sighting::read_entry(entry)?); + } + Ok(seen) +} + +/// 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..3486e7ec 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(); @@ -231,3 +239,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..6ce98eb5 100644 --- a/services/ws-wasi-runner/Dockerfile +++ b/services/ws-wasi-runner/Dockerfile @@ -7,9 +7,8 @@ # 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 +# 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 @@ -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..320cf4a6 100644 --- a/services/ws-wasi-runner/src/board/wasi.rs +++ b/services/ws-wasi-runner/src/board/wasi.rs @@ -6,13 +6,10 @@ //! 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" -)] #[cfg(not(feature = "std"))] use alloc::{boxed::Box, format, string::String, vec::Vec}; +use core::convert::Infallible; pub(super) use edge_toolkit::board::PREVIEW2; use wasmtime::StoreContextMut; @@ -46,14 +43,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)] 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"); From d28b20146f08deb744e80642a2e9efb2ed1ad7a0 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 13 Nov 2026 04:00:20 +0800 Subject: [PATCH 2/5] another machine --- libs/gpu-probe/signatures.json | 89 ++++++++++++++++++++++++++++++++++ libs/gpu-probe/signatures.yaml | 75 ++++++++++++++++++++++++++++ libs/gpu-probe/src/lib.rs | 39 ++++++++++----- libs/gpu-probe/tests/probe.rs | 16 ++++++ 4 files changed, 207 insertions(+), 12 deletions(-) diff --git a/libs/gpu-probe/signatures.json b/libs/gpu-probe/signatures.json index 7239a01e..35c69b4d 100644 --- a/libs/gpu-probe/signatures.json +++ b/libs/gpu-probe/signatures.json @@ -2974,6 +2974,82 @@ "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", @@ -3061,6 +3137,19 @@ "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": { diff --git a/libs/gpu-probe/signatures.yaml b/libs/gpu-probe/signatures.yaml index 76fab353..817df647 100644 --- a/libs/gpu-probe/signatures.yaml +++ b/libs/gpu-probe/signatures.yaml @@ -2416,6 +2416,70 @@ signatures: 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: @@ -2489,6 +2553,17 @@ signatures: 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" diff --git a/libs/gpu-probe/src/lib.rs b/libs/gpu-probe/src/lib.rs index b7ef23a4..cca5e4ec 100644 --- a/libs/gpu-probe/src/lib.rs +++ b/libs/gpu-probe/src/lib.rs @@ -64,9 +64,9 @@ 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", @@ -172,13 +172,28 @@ fn main() { "; -/// For each probe with two legal exact answers: its name, then each answer's bits beside what giving it means. -const CHOICES: [(&str, [(u32, &str); 2]); 5] = [ - ("multiply_add", [(0, "rounded_twice"), (0x3380_0000, "fused")]), - ("fma", [(0, "rounded_twice"), (0x3380_0000, "fused")]), - ("swamped_sum", [(0, "in_order"), (0x4080_0000, "reassociated")]), - ("subnormal_product", [(0x0040_0000, "kept"), (0, "flushed")]), - ("dot", [(0, "in_order"), (0x4000_0000, "reassociated")]), +/// For each probe with more than one legal exact answer: its name, then each answer's bits beside what giving it means. +/// +/// A sum the compiler may regroup has one legal answer per grouping that gives a distinct result. `swamped_sum` is +/// `2^24` plus four ones: added one at a time each one is lost, the ones summed first survive as 4, and a loop unrolled +/// to add its last two as a pair keeps only those two. `dot` is `2^24 + 1 + 1 - 2^24`: in order both ones are lost, the +/// two `2^24` cancelling first keeps both, and summed as a pairwise tree, `(2^24 + 1) + (1 - 2^24)`, keeps one. +const CHOICES: [(&str, &[(u32, &str)]); 5] = [ + ("multiply_add", &[(0, "rounded_twice"), (0x3380_0000, "fused")]), + ("fma", &[(0, "rounded_twice"), (0x3380_0000, "fused")]), + ( + "swamped_sum", + &[(0, "in_order"), (0x4080_0000, "reassociated"), (0x4000_0000, "paired")], + ), + ("subnormal_product", &[(0x0040_0000, "kept"), (0, "flushed")]), + ( + "dot", + &[ + (0, "in_order"), + (0x4000_0000, "reassociated"), + (0x3f80_0000, "pairwise"), + ], + ), ]; /// The interface each known GPU was measured through, what it answered in output order, and what that GPU is. @@ -266,8 +281,8 @@ pub fn shaders() -> Vec<(&'static str, String)> { /// /// 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. +/// 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(); diff --git a/libs/gpu-probe/tests/probe.rs b/libs/gpu-probe/tests/probe.rs index 3486e7ec..745cac44 100644 --- a/libs/gpu-probe/tests/probe.rs +++ b/libs/gpu-probe/tests/probe.rs @@ -161,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!( From 290e10c751b5fb1fe04c777af10e4c75345b840c Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 13 Nov 2026 03:59:04 +0800 Subject: [PATCH 3/5] fixes --- CHANGELOG.md | 3 ++ libs/edge-toolkit/src/board.rs | 12 ++--- services/ws-wasi-runner/Dockerfile | 6 +-- services/ws-wasi-runner/src/board/wasi.rs | 57 +++++++++++++++++------ 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53b7c612..4c5968cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -177,6 +177,9 @@ 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 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/services/ws-wasi-runner/Dockerfile b/services/ws-wasi-runner/Dockerfile index 6ce98eb5..a6e554fb 100644 --- a/services/ws-wasi-runner/Dockerfile +++ b/services/ws-wasi-runner/Dockerfile @@ -8,9 +8,9 @@ # # 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 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`. +# 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. # diff --git a/services/ws-wasi-runner/src/board/wasi.rs b/services/ws-wasi-runner/src/board/wasi.rs index 320cf4a6..0c4507af 100644 --- a/services/ws-wasi-runner/src/board/wasi.rs +++ b/services/ws-wasi-runner/src/board/wasi.rs @@ -2,18 +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. +//! 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}; @@ -87,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 @@ -125,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. @@ -213,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}"))? From 4dc5d21fb14bf5f8498f6556fd94211acb431f56 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 13 Nov 2026 04:19:33 +0800 Subject: [PATCH 4/5] another fix --- libs/gpu-probe/src/registry.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/libs/gpu-probe/src/registry.rs b/libs/gpu-probe/src/registry.rs index bc8f5c54..98b51c7a 100644 --- a/libs/gpu-probe/src/registry.rs +++ b/libs/gpu-probe/src/registry.rs @@ -9,9 +9,9 @@ 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 +/// 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, @@ -56,12 +56,15 @@ fn holding<'words>(interface: &'words str, words: &'words [String]) -> impl Iter } /// 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> { - let mut seen = Vec::new(); - for entry in found { - seen.extend(Sighting::read_entry(entry)?); - } - Ok(seen) + 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`. From 2270d35551429ae1670473854da5dc9b85a755c7 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 13 Nov 2026 05:23:45 +0800 Subject: [PATCH 5/5] fix --- .mise/config.mingw.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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).