facet-wrapped/src/gpu.rs
//! **Which physical device this bundle actually rendered on** — published through
//! facett-core's one writer, and readable from a browser.
//!
//! ## Why a bundle needs this at all
//!
//! Chrome on Linux does not hand out a WebGPU adapter without launch flags, and the
//! flags are not interchangeable (measured by t14s on the Radeon 890M, 2026-09-06):
//! plain Chrome gives **no adapter**; `--use-angle=swiftshader` gives one that is
//! **software**; `--enable-features=Vulkan --use-angle=vulkan --enable-unsafe-webgpu`
//! gives the real card.
//!
//! The middle case is the one that costs a day. On it, everything this crate could
//! previously report stays green: [`crate::wasm::facet_backend`] answers `"webgpu"`
//! because it is a constant, the loader's `facet_canvas_api` probe asks the canvas
//! which context it holds and is told `webgpu` truthfully, and
//! `tests/gpu_lane_is_observable.rs` proves the wgpu lane is *wired* — which it is.
//! None of them can tell a Radeon from a CPU rasteriser painting seconds per frame.
//!
//! So the canvas names its device. `"a device exists"` is exactly what the trap already
//! reports.
//!
//! ## No second vocabulary (LAW 5)
//!
//! Nothing here decides anything. `facett_core::render::adapter` has owned this 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` that every other facett surface already publishes through.
//! This module is the two lines that join a browser canvas to it, so the answer a robot
//! reads out of the wasm is the answer a native `korp-ui` prints, in the same words.
//!
//! `render/lane.rs:688` says why that matters: two callers asking *"software?"* of an
//! `AdapterInfo` get it "subtly differently". There is one caller.
//!
//! ## This module is NOT wasm32-only, on purpose
//!
//! The wgpu handle only exists in a browser, but the reporting does not have to. Keeping
//! this half host-compilable is what lets `tests/the_canvas_names_its_adapter.rs` drive
//! the real function with the real facts of a real adapter on a box with no GPU — which
//! is the only way this guard could be seen red at all.
use facett_core::render::adapter::{
AdapterFacts, AdapterSelection, record_selection, selected_adapter_json,
};
/// Record the adapter a mounted canvas opened.
///
/// One candidate, no override: a browser enumerates nothing and offers no choice — it
/// hands over the device the flags and the driver settled on. So the *selection* here is
/// a formality and the *facts* are the point; going through `AdapterSelection` anyway is
/// what makes `software`, `kind` and the banner come out in facett's own vocabulary
/// instead of a second one written for the browser.
pub fn record_adapter(facts: AdapterFacts) {
if let Some(sel) = AdapterSelection::decide(&[facts], None) {
record_selection(sel);
}
}
/// The recorded adapter as JSON — `null` before any canvas has mounted.
///
/// `null` and not an empty object: the failure being closed is a value that reads
/// plausible by accident, so "nothing has opened a device" must be unmistakable.
#[must_use]
pub fn adapter_json() -> String {
selected_adapter_json().to_string()
}
/// Whether the recorded adapter is a CPU software rasteriser. `false` when nothing has
/// mounted — a host asks [`adapter_json`] whether anything is there at all.
///
/// This is a convenience for the loader, not a second predicate: the value is the one
/// `AdapterSelection` already computed with `is_software_rasteriser`.
#[must_use]
pub fn is_software() -> bool {
facett_core::render::adapter::selected_adapter().is_some_and(|s| s.software)
}
/// Build [`AdapterFacts`] from the browser's OWN `GPUAdapterInfo`, for the case wgpu
/// cannot answer: **a masked `AdapterInfo`**.
///
/// # The defect this exists for, measured on the 890M on 2026-09-06
///
/// [`record_adapter`] is fed `rs.adapter.get_info()`, which is the right source on
/// every native backend. On `BrowserWebGpu` it is **masked**: measured in a real
/// Chrome window on real hardware, with
/// `--enable-features=Vulkan --use-angle=vulkan --enable-unsafe-webgpu`,
/// `facet_adapter()` returned
///
/// ```json
/// { "backend":"BrowserWebGpu", "kind":"other", "name":"", "vendor":"0x0000",
/// "driver":"", "software":false }
/// ```
///
/// So the loader printed `GPU: (other, BrowserWebGpu)` — **an empty name on a
/// genuine Radeon**, while the doc on `facet_adapter` promises the device "a person
/// recognises". **And the browser HAS the identity and hands it over**: in the same
/// window, in the same arm, `(await navigator.gpu.requestAdapter()).info` gave
/// `vendor: "amd"`, `architecture: "rdna-3"`.
///
/// **Why that is a correctness problem and not a cosmetic one.** `software` is the
/// discriminator the whole SwiftShader trap rests on, and
/// [`facett_core::render::adapter::is_software_rasteriser`] decides it by looking for
/// `swiftshader` / `llvmpipe` / `lavapipe` **in the name and driver**. Fed a masked
/// record, those strings can never appear — so `software:false` was not a finding
/// about the device, it was an artefact of an empty string. A field that cannot name
/// a Radeon is not evidence that it would name SwiftShader. Chrome reports that
/// adapter as vendor `google`, architecture `swiftshader`, so composing the name from
/// the browser's fields is what makes the existing detector able to see it at all.
///
/// The name is composed rather than invented: `description` when the browser gives
/// one (it is the most human of the four), else `vendor` and `architecture` joined.
/// Empty in, empty out — this never manufactures an identity it was not given, so a
/// browser that masks everything still reports honestly rather than plausibly.
#[must_use]
pub fn facts_from_browser_info(
vendor: &str,
architecture: &str,
device: &str,
description: &str,
backend: &str,
) -> AdapterFacts {
// The composition lives in facett-core (one copy: the start-time software refusal
// in `adapter_retry` and the probes' `browser_bench::software_adapter` use it too).
facett_core::render::adapter::facts_from_browser_info(vendor, architecture, device, description, backend)
}
/// Whether the recorded adapter still has no name — i.e. whether
/// [`facts_from_browser_info`] has something to add.
///
/// Asked BEFORE overwriting a record, so a backend that *does* report a real name
/// keeps it: the browser's `vendor`/`architecture` pair is coarser than a driver
/// string like `AMD Radeon 890M Graphics (RADV GFX1150)`, and replacing a good name
/// with a coarse one would be a regression dressed as a fix.
#[must_use]
pub fn recorded_name_is_empty() -> bool {
facett_core::render::adapter::selected_adapter().is_none_or(|s| s.chosen.name.trim().is_empty())
}