Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facet-wrapped/tests/the_canvas_names_its_adapter.rs

//! **The running canvas must NAME the physical device it opened — not report that one
//! exists.**
//!
//! ## The trap this closes, measured by t14s on 2026-09-06
//!
//! Chrome on Linux needs launch flags before WebGPU gives an adapter at all, and the
//! flags are not equal:
//!
//! * plain Chrome → **no adapter**;
//! * `--use-angle=swiftshader` → **an adapter that is SOFTWARE**, while every check
//!   short of naming it reports success;
//! * `--enable-features=Vulkan --use-angle=vulkan --enable-unsafe-webgpu` → the real
//!   890M, reported as `amd rdna-3`.
//!
//! The middle row is the dangerous one. On it, this crate's existing observables all
//! stay green: `facet_backend()` answers `"webgpu"` (it is a **constant**),
//! `facet_canvas_api()` asks the canvas which context it holds and gets `webgpu`
//! truthfully, and `gpu_lane_is_observable.rs`'s `gpu_lane_selected` / `backend` say
//! the wgpu lane is wired — which it is. **Every one of them is true on a software
//! rasteriser painting seconds per frame.**
//!
//! This crate's own history says so twice already: `Cargo.toml`'s map paragraph records
//! row 3, where the GPU lanes were compiled OUT while both surface probes answered
//! `webgpu`; and `gpu_lane_is_observable.rs:30` writes that those two "describe the
//! SURFACE, not the renderer painting onto it — the identity value in LAW 2's sense".
//! **`gpu_lane_selected` closed the "is the lane wired" question and left the "is it
//! hardware" question open.** This file closes that one.
//!
//! ## No second vocabulary — the answer already existed in facett-core
//!
//! `facett_core::render::adapter` has carried the whole thing since the native lanes
//! needed it: `AdapterFacts`, `AdapterKind`, `is_software_rasteriser` (which knows
//! llvmpipe, lavapipe, SwiftShader and softpipe by name), `AdapterSelection::decide`,
//! and the process-global `record_selection` / `selected_adapter_json` every other
//! facett surface publishes through. The browser canvas joins that ONE writer rather
//! than growing a second answer to "is this software?" — which `render/lane.rs:688`
//! already warns is how two callers get it "subtly differently".
//!
//! ## What this file can and cannot prove
//!
//! It drives the exact function `wasm.rs`'s mount calls, with the exact facts a real
//! adapter produces. It CANNOT call `wgpu::Adapter::get_info()` — that needs a canvas,
//! a browser and a GPU, and oden has none of the three. `facts_of` (the one line
//! between `get_info()` and here) is tested in facett-core. The wasm call site is held
//! by `the_mount_actually_records_what_it_opened` below, at the source.

use facet_wrapped::gpu;
use facett_core::render::adapter::{AdapterFacts, AdapterKind};

/// What `wgpu::AdapterInfo` looks like on the 890M with the flags that work — the row
/// t14s measured as `amd rdna-3`.
fn the_real_890m() -> AdapterFacts {
    AdapterFacts::new("AMD Radeon 890M Graphics (RADV GFX1150)", AdapterKind::IntegratedGpu, "BrowserWebGpu")
        .with_vendor(0x1002)
}

/// What `--use-angle=swiftshader` gives: an adapter, a device, a working pipeline, and
/// a CPU painting every pixel.
fn swiftshader() -> AdapterFacts {
    AdapterFacts::new("SwiftShader Device (Subzero)", AdapterKind::Cpu, "BrowserWebGpu")
}

/// **One test, deliberately.** `record_selection` writes a PROCESS-GLOBAL, so three
/// `#[test]`s asserting on it would race each other under cargo's default thread pool
/// and pass or fail by scheduling. The states are asserted in order instead.
#[test]
fn the_canvas_names_the_device_it_opened_and_a_software_one_is_named_software() {
    // ── before any mount: null, NOT a guess ─────────────────────────────────
    //
    // The whole failure mode being closed is a value that is right by accident, so the
    // unmounted state must be unmistakably "nothing has been opened" rather than a
    // default that happens to read like hardware.
    let before: serde_json::Value = serde_json::from_str(&gpu::adapter_json()).unwrap();
    assert!(
        before.is_null(),
        "no canvas has mounted, so there is no adapter to name: {before}"
    );

    // ── the software row: the one the trap produces ─────────────────────────
    gpu::record_adapter(swiftshader());
    let sw: serde_json::Value = serde_json::from_str(&gpu::adapter_json()).unwrap();
    assert_eq!(sw["software"], true, "SwiftShader must be named as software: {sw}");
    assert_eq!(sw["kind"], "cpu", "…and classed as a CPU rasteriser: {sw}");
    assert!(
        sw["name"].as_str().unwrap_or_default().contains("SwiftShader"),
        "…BY NAME, because 'a device exists' is what the trap already reports: {sw}"
    );

    // ── the hardware row: the same call, the opposite verdict ───────────────
    //
    // This is the half that makes the assertions above mean something. A predicate that
    // answers "software" to everything would pass every line before this one.
    gpu::record_adapter(the_real_890m());
    let hw: serde_json::Value = serde_json::from_str(&gpu::adapter_json()).unwrap();
    assert_eq!(hw["software"], false, "the 890M is not software: {hw}");
    assert_eq!(hw["kind"], "integrated", "…it is an on-die GPU: {hw}");
    assert_eq!(hw["vendor"], "0x1002", "…AMD's PCI vendor id: {hw}");
    assert!(
        hw["name"].as_str().unwrap_or_default().contains("890M"),
        "…and the page can print the device a person can recognise: {hw}"
    );

    // ── and the two are DISTINGUISHABLE, which is the whole row ─────────────
    assert_ne!(sw["software"], hw["software"]);
    assert_ne!(sw["kind"], hw["kind"]);
    assert_ne!(sw["name"], hw["name"]);
}

/// **The identity value, pinned rather than deleted.** `facet_backend()` answers the
/// same constant for both rows above, and so does the canvas-context probe. That is not
/// a bug to fix — the surface really is WebGPU in both cases — but it is why neither can
/// answer the question this row exists for, and a reader who reaches for one of them
/// should meet this test first.
///
/// If `facet_backend()` is ever made to vary, delete this and point the robots at it.
#[test]
fn the_backend_string_is_the_same_for_hardware_and_software_which_is_why_it_is_not_evidence() {
    // `facet_backend()` is `#[wasm_bindgen]` and wasm32-only, so the constant it returns
    // is asserted where a host test can see it: in the source, next to its own doc.
    let src = include_str!("../src/wasm.rs");
    assert!(
        src.contains(r#"    "webgpu".to_string()"#),
        "`facet_backend` no longer returns a constant — if it now names the adapter, \
         retire this test and assert on it directly"
    );
    // The two rows differ in every field that matters and in none that `facet_backend`
    // reports, which is the sentence this test exists to make checkable.
    let sw = swiftshader();
    let hw = the_real_890m();
    assert_eq!(sw.backend, hw.backend, "the SURFACE is WebGPU in both cases");
    assert_ne!(sw.kind, hw.kind, "…and the RENDERER is not");
}

/// **The mount records what it opened.** The one line this suite cannot execute — it
/// needs a browser, a canvas and a GPU — is held at the source instead of assumed.
///
/// Comments are stripped first: this crate's own doc paragraphs discuss `record_adapter`
/// at length, and a substring scan over the raw file would pass a build where the call
/// had been deleted and only the prose remained.
#[test]
fn the_mount_actually_records_what_it_opened() {
    let src = include_str!("../src/wasm.rs");
    let code: String = src
        .lines()
        .filter(|l| !l.trim_start().starts_with("//"))
        .collect::<Vec<_>>()
        .join("\n");

    // NON-VACUITY: the stripper must leave the real module behind. One that returned
    // "" would fail the assertions below loudly; one that returned the whole file would
    // make them pass on prose alone, and nothing would say so.
    assert!(
        code.contains("pub fn facet_mount(") && code.len() > 2_000,
        "the comment stripper ate wasm.rs: {} bytes left",
        code.len()
    );
    assert!(
        !code.contains("the identity value in LAW 2's sense"),
        "the stripper left doc prose behind, so the assertion below proves nothing"
    );

    assert!(
        code.contains("crate::gpu::record_adapter("),
        "`start()` no longer records the adapter it opened, so `facet_adapter()` will \
         answer null on a live canvas and the software trap comes straight back"
    );
    assert!(
        code.contains("wgpu_render_state"),
        "…and it must read the adapter off eframe's real RenderState, not from anywhere else"
    );
}