facett-core 0.1.16

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **wgpu glue for the adapter-selection policy** (feature `wgpu`).
//!
//! The *policy* is pure and lives in [`crate::render::adapter`]; this module is
//! the thin, untestable-without-a-GPU edge that
//!
//! 1. converts a `wgpu::AdapterInfo` into the feature-free [`AdapterFacts`],
//! 2. enumerates + selects over real adapters ([`select_adapter`]),
//! 3. replaces the naive `instance.request_adapter(&Default::default())` every
//!    headless probe in this workspace hand-rolled ([`request_best_adapter`]),
//! 4. builds the `egui_wgpu::WgpuConfiguration` an eframe host installs
//!    ([`facett_wgpu_options`]).
//!
//! ## Why (3) matters
//!
//! `request_adapter` with `PowerPreference::default()` is `LowPower`, which on a
//! workstation with an integrated GPU deliberately prefers the *slow* device, and
//! wgpu's own fallback ordering will happily hand back `llvmpipe` when the
//! surface-compatibility filter rejects the real card. Every one of those call
//! sites should go through here.

use super::pass_clock::PASS_CLOCK_FEATURES;
use crate::render::adapter::{
    classify_unavailable, env_override, env_prefers_low_power, record_gpu_unavailable,
    record_selection, AdapterFacts, AdapterKind, AdapterSelection, GpuUnavailable,
};

/// Map wgpu's `DeviceType` onto the feature-free [`AdapterKind`].
#[must_use]
pub fn kind_of(device_type: wgpu::DeviceType) -> AdapterKind {
    match device_type {
        wgpu::DeviceType::DiscreteGpu => AdapterKind::DiscreteGpu,
        wgpu::DeviceType::IntegratedGpu => AdapterKind::IntegratedGpu,
        wgpu::DeviceType::VirtualGpu => AdapterKind::VirtualGpu,
        wgpu::DeviceType::Cpu => AdapterKind::Cpu,
        wgpu::DeviceType::Other => AdapterKind::Other,
    }
}

/// Convert a live `wgpu::AdapterInfo` into the plain facts the policy ranks.
#[must_use]
pub fn facts_of(info: &wgpu::AdapterInfo) -> AdapterFacts {
    AdapterFacts {
        name: info.name.clone(),
        vendor: info.vendor,
        device: info.device,
        kind: kind_of(info.device_type),
        backend: format!("{:?}", info.backend),
        driver: info.driver.clone(),
    }
}

/// Facts for a whole enumeration, in enumeration order.
#[must_use]
pub fn facts_of_all(adapters: &[wgpu::Adapter]) -> Vec<AdapterFacts> {
    adapters.iter().map(|a| facts_of(&a.get_info())).collect()
}

/// Rank a live adapter list and return the decision (honouring
/// `FACETT_GPU_ADAPTER`). Does **not** record it — [`select_adapter`] does.
#[must_use]
pub fn decide_for(adapters: &[wgpu::Adapter]) -> Option<AdapterSelection> {
    let facts = facts_of_all(adapters);
    AdapterSelection::decide(&facts, env_override().as_deref())
}

/// **THE selector.** Pick the adapter facett should render on, log it loudly and
/// record it for `state_json`. Returns the chosen adapter and the decision.
pub fn select_adapter(adapters: &[wgpu::Adapter]) -> Option<(wgpu::Adapter, AdapterSelection)> {
    let sel = decide_for(adapters)?;
    let chosen = adapters[sel.index].clone();
    record_selection(sel.clone());
    Some((chosen, sel))
}

/// Drop-in replacement for `instance.request_adapter(&Default::default())` in the
/// workspace's headless probes/tests.
///
/// Enumerates **every** adapter over `backends` and applies the policy, instead of
/// letting wgpu's internal preference order hand back an integrated/software
/// device. Falls back to a plain `request_adapter` if enumeration comes back empty
/// (some backends only support the request path).
pub fn request_best_adapter(
    instance: &wgpu::Instance,
    backends: wgpu::Backends,
) -> Option<wgpu::Adapter> {
    let adapters = pollster::block_on(instance.enumerate_adapters(backends));
    if !adapters.is_empty() {
        if let Some((a, _)) = select_adapter(&adapters) {
            return Some(a);
        }
    }
    let power_preference = if env_prefers_low_power() {
        wgpu::PowerPreference::LowPower
    } else {
        wgpu::PowerPreference::HighPerformance
    };
    pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
        power_preference,
        force_fallback_adapter: false,
        compatible_surface: None,
    }))
    .ok()
}

/// The **backends facett asks for** — WebGPU-class only (GFX_V2 Decision 0).
///
/// Native: `PRIMARY` (Vulkan, DX12, Metal). Wasm: `BROWSER_WEBGPU`. **`Backends::GL`
/// is deliberately absent.** WebGL2 has no compute shaders, no atomics, no storage
/// buffers and no indirect draws, so every item on the GFX_V2 roadmap — GPU culling
/// into an indirect buffer, GPU label collision, OIT, compute tessellation — is
/// *impossible* under it. While GL was in the mask we were either maintaining two
/// renderers or holding the good one back, and paying a standing tax for it (the
/// zero-area-sliver workaround exists only because slivers degrade to LINES on GL).
///
/// A host with no WebGPU-class driver now gets **no adapter**, loudly — the caller
/// surfaces that as a typed error. It does NOT silently soft-render: a wrong picture
/// that looks like a picture is the false-green class this suite keeps getting burned
/// by. The **CPU/egui-painter path is a different thing and stays** — that is how
/// headless tests and no-GPU hosts render, and it is not a WebGL fallback.
#[must_use]
pub fn preferred_backends() -> wgpu::Backends {
    #[cfg(target_arch = "wasm32")]
    {
        wgpu::Backends::BROWSER_WEBGPU
    }
    #[cfg(not(target_arch = "wasm32"))]
    {
        wgpu::Backends::PRIMARY
    }
}

/// Build the `egui_wgpu::WgpuConfiguration` an eframe host installs so the whole
/// app renders on the device the policy chose.
///
/// - `HighPerformance` power preference (or `LowPower` when `WGPU_POWER_PREF=low`),
/// - a `native_adapter_selector` driven by the pure policy — this is what rejects
///   the ASPEED/Matrox BMC console and llvmpipe,
/// - the decision logged loudly and recorded for `state_json`.
#[must_use]
pub fn facett_wgpu_options() -> egui_wgpu::WgpuConfiguration {
    use egui_wgpu::{WgpuConfiguration, WgpuSetup, WgpuSetupCreateNew};

    // `without_display_handle` — eframe's winit integration fills the display
    // handle in for us (needed only for Wayland+GLES); everything else here is
    // ours to set.
    let mut setup = WgpuSetupCreateNew::without_display_handle();
    setup.instance_descriptor.backends = preferred_backends();
    setup.power_preference = if env_prefers_low_power() {
        wgpu::PowerPreference::LowPower
    } else {
        wgpu::PowerPreference::HighPerformance
    };
    // ── Request the pass-clock's timestamp features, WHERE THE ADAPTER HAS THEM. ──
    // egui-wgpu's default `device_descriptor` requests `Features::empty()`, and a
    // feature not requested at device creation is absent from `device.features()`
    // forever — so without this closure the per-lane GPU pass clock
    // (`pass_clock::PassClock`) would honestly report "unavailable" on every box,
    // including the Vulkan/4090 targets it exists for. The intersection with
    // `adapter.features()` keeps bring-up from FAILING on an adapter that lacks them
    // (llvmpipe on some Mesa builds): there the clock reports `None`, which is the
    // designed answer, not an error. The limits mirror egui-wgpu's own default
    // (`max_texture_dimension_2d: 8192` for 4k+ displays, downlevel on GL).
    setup.device_descriptor = std::sync::Arc::new(|adapter: &wgpu::Adapter| {
        let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
            wgpu::Limits::downlevel_webgl2_defaults()
        } else {
            wgpu::Limits::default()
        };
        wgpu::DeviceDescriptor {
            label: Some("facett wgpu device"),
            required_features: adapter.features() & PASS_CLOCK_FEATURES,
            required_limits: wgpu::Limits { max_texture_dimension_2d: 8192, ..base_limits },
            ..Default::default()
        }
    });
    setup.native_adapter_selector = Some(std::sync::Arc::new(
        |adapters: &[wgpu::Adapter], _surface: Option<&wgpu::Surface<'_>>| match select_adapter(
            adapters,
        ) {
            Some((a, _)) => Ok(a),
            // GFX_V2 Decision 0: fail LOUDLY and TYPED. eframe only accepts a
            // String here, so the typed value is recorded first (for `state_json`
            // and the stderr banner) and its Display — which carries the
            // `facet-gpu-<n>` code and the remedy — becomes the message eframe
            // surfaces. Never a silent soft-render fallback.
            None => Err(gpu_unavailable_for(adapters).to_string()),
        },
    ));

    WgpuConfiguration { wgpu_setup: WgpuSetup::CreateNew(setup), ..Default::default() }
}

/// Classify a failed bring-up over the CURRENT adapter list, record it, and hand
/// back the typed error.
///
/// The distinction is the whole value: "nothing enumerated" means no driver or no
/// device, while "all rejected" means hardware exists and the policy refused it
/// (llvmpipe, an ASPEED/Matrox BMC console). Collapsing both into one message —
/// as the previous bare `"no wgpu adapters enumerated"` string did, even when
/// adapters plainly HAD been enumerated — sends the operator down the wrong path.
/// ⚠ In practice this only ever returns [`GpuUnavailable::NothingEnumerated`]: its one
/// caller is the selector below, which is reached only when `select_adapter` returned
/// `None`, and that happens **only for an empty list** (see
/// [`GpuUnavailable::AllRejected`]'s note). The branch is kept because the distinction is
/// what a stricter policy would need; `the_policy_never_refuses_a_non_empty_adapter_list`
/// in [`crate::render::adapter`] states that invariant so it cannot change silently.
#[must_use]
pub fn gpu_unavailable_for(adapters: &[wgpu::Adapter]) -> GpuUnavailable {
    // ONE classification, and it is the PURE one — so the reachability of each branch is
    // testable without a GPU. This edge only converts wgpu types into facts.
    let err = classify_unavailable(format!("{:?}", preferred_backends()), facts_of_all(adapters));
    record_gpu_unavailable(err.clone());
    err
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The wgpu→facts mapping must be exhaustive and lossless for the fields the
    /// policy ranks on. RED if a wgpu upgrade adds a device type we silently drop
    /// into the wrong tier.
    #[test]
    fn device_type_mapping_is_exact() {
        assert_eq!(kind_of(wgpu::DeviceType::DiscreteGpu), AdapterKind::DiscreteGpu);
        assert_eq!(kind_of(wgpu::DeviceType::IntegratedGpu), AdapterKind::IntegratedGpu);
        assert_eq!(kind_of(wgpu::DeviceType::VirtualGpu), AdapterKind::VirtualGpu);
        assert_eq!(kind_of(wgpu::DeviceType::Cpu), AdapterKind::Cpu);
        assert_eq!(kind_of(wgpu::DeviceType::Other), AdapterKind::Other);
    }

    fn info(name: &str, dt: wgpu::DeviceType, vendor: u32) -> wgpu::AdapterInfo {
        wgpu::AdapterInfo {
            name: name.to_owned(),
            vendor,
            device: 0,
            device_type: dt,
            device_pci_bus_id: String::new(),
            driver: String::new(),
            driver_info: String::new(),
            backend: wgpu::Backend::Vulkan,
            subgroup_min_size: 0,
            subgroup_max_size: 0,
            transient_saves_memory: false,
        }
    }

    /// **RED-when-broken:** the real oden enumeration expressed in *wgpu* types
    /// (the shape eframe hands the selector) must still resolve to the 4090. This
    /// is the same assertion as the pure test but through the conversion, so a
    /// broken `facts_of` is caught too. No GPU needed — `AdapterInfo` is a plain
    /// struct.
    #[test]
    fn oden_enumeration_resolves_to_the_discrete_gpu() {
        use crate::render::adapter::{choose_adapter_index, VENDOR_ASPEED, VENDOR_NVIDIA};
        let infos = [
            info("ASPEED Graphics Family", wgpu::DeviceType::Other, VENDOR_ASPEED),
            info("llvmpipe (LLVM 21.1.8, 256 bits)", wgpu::DeviceType::Cpu, 0x1_0005),
            info("NVIDIA GeForce RTX 4090", wgpu::DeviceType::DiscreteGpu, VENDOR_NVIDIA),
        ];
        let facts: Vec<_> = infos.iter().map(facts_of).collect();
        assert_eq!(facts[2].backend, "Vulkan");
        let pick = choose_adapter_index(&facts).expect("must pick");
        assert_eq!(facts[pick].name, "NVIDIA GeForce RTX 4090");
        let sel = AdapterSelection::decide(&facts, None).expect("decides");
        assert!(!sel.software);
        assert_eq!(sel.index, 2);
    }

    /// The eframe configuration must be a freshly-created setup with the policy
    /// selector installed and high-performance requested. If someone drops the
    /// selector, the app silently falls back to whatever wgpu picks (software on
    /// this box) — this goes red first, with no GPU.
    #[test]
    fn wgpu_options_install_the_policy() {
        use egui_wgpu::WgpuSetup;
        let cfg = facett_wgpu_options();
        match cfg.wgpu_setup {
            WgpuSetup::CreateNew(setup) => {
                assert!(
                    setup.native_adapter_selector.is_some(),
                    "the adapter-selection policy must be installed"
                );
                assert_eq!(setup.power_preference, wgpu::PowerPreference::HighPerformance);
                assert!(
                    setup.instance_descriptor.backends.contains(wgpu::Backends::VULKAN),
                    "Vulkan must be among the requested backends"
                );
            }
            _ => panic!("expected CreateNew"),
        }
    }
}