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

nordisk/facett

facett-demo/src/gpu_select.rs

//! **Adapter selection** for the native (wgpu) demo binaries — a thin adapter over
//! the ONE shared policy in
//! [`facett_core::render::adapter`](facett_core::render::adapter).
//!
//! ## What the policy is for
//!
//! A workstation enumerates several adapters and most of them are traps:
//! **llvmpipe / lavapipe** (a software rasteriser that is itself a *Vulkan* ICD, so
//! restricting the backends to Vulkan does not exclude it), an **integrated GPU**
//! next to a discrete one, and — on a server board — an **ASPEED / Matrox G200 BMC
//! management display**, which exists to paint a remote IPMI console and cannot
//! render a UI at all. Landing on any of them looks exactly like "the app is slow".
//!
//! ## Where the logic lives (L5 — no twins)
//!
//! The ranking, the BMC/software detection, the `FACETT_GPU_ADAPTER` override and
//! the `state_json` decision record all live in **facett-core**, feature-free and
//! GPU-free, so they are unit-tested without a GPU and every facett host shares
//! them. This module only:
//!
//! 1. converts eframe's `wgpu::AdapterInfo` into the plain
//!    [`AdapterFacts`](facett_core::render::adapter::AdapterFacts), and
//! 2. wraps the result in the eframe [`WgpuConfiguration`] the three native demo
//!    binaries (`facett-demo`, `osm_fly_demo`, `sigtuna_demo`) install.
//!
//! The chosen adapter is logged loudly at startup **and** published in the demo's
//! `state_json` under `"gpu"`, so which device the app opened is robot-testable
//! rather than something a human has to read out of a terminal.

#![cfg(not(target_arch = "wasm32"))]

use eframe::wgpu;
use facett_core::render::adapter::{
    self, AdapterFacts, AdapterKind, AdapterSelection, ENV_ADAPTER,
};

/// eframe's `wgpu::AdapterInfo` → the shared, feature-free facts the policy ranks.
pub fn facts_of(info: &wgpu::AdapterInfo) -> AdapterFacts {
    AdapterFacts {
        name: info.name.clone(),
        vendor: info.vendor,
        device: info.device,
        kind: match info.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,
        },
        backend: format!("{:?}", info.backend),
        driver: info.driver.clone(),
    }
}

/// The index of the adapter to use, or `None` for an empty list.
///
/// Delegates to [`facett_core::render::adapter::choose_adapter_index_with_override`]
/// — discrete GPU first, then integrated, then virtual; software
/// (llvmpipe/lavapipe/SwiftShader) and BMC management displays (ASPEED / Matrox
/// G200) last and only when nothing else exists. `FACETT_GPU_ADAPTER` overrides.
pub fn choose_adapter_index(infos: &[wgpu::AdapterInfo]) -> Option<usize> {
    let facts: Vec<AdapterFacts> = infos.iter().map(facts_of).collect();
    adapter::choose_adapter_index_with_override(&facts, adapter::env_override().as_deref())
}

/// Build the eframe [`WgpuConfiguration`](eframe::egui_wgpu::WgpuConfiguration) the
/// native demo binaries install: the primary backends plus GL as a genuine last
/// resort, `HighPerformance` power preference (or `LowPower` under
/// `WGPU_POWER_PREF=low`), and a `native_adapter_selector` driven by the shared
/// policy which logs + records the decision.
pub fn discrete_gpu_wgpu_options() -> eframe::egui_wgpu::WgpuConfiguration {
    use eframe::egui_wgpu::{WgpuConfiguration, WgpuSetup, WgpuSetupCreateNew};

    let mut setup = WgpuSetupCreateNew::without_display_handle();
    // WebGPU-class backends ONLY (GFX_V2 Decision 0) — the ONE mask, shared with the
    // library host so the demo and the app can never disagree about what a GPU is.
    // GL is gone: WebGL2 has no compute/atomics/storage-buffers/indirect-draws, so it
    // cannot run the renderer we are building. Note this does NOT by itself exclude
    // software — lavapipe is a Vulkan ICD, so it still shows up under PRIMARY; the
    // policy below is what ranks llvmpipe/lavapipe and the ASPEED/Matrox BMC consoles
    // beneath every hardware adapter.
    setup.instance_descriptor.backends = facett_core::render::gpu::preferred_backends();
    setup.power_preference = if adapter::env_prefers_low_power() {
        wgpu::PowerPreference::LowPower
    } else {
        wgpu::PowerPreference::HighPerformance
    };
    setup.native_adapter_selector = Some(std::sync::Arc::new(
        |adapters: &[wgpu::Adapter], _surface: Option<&wgpu::Surface<'_>>| {
            let facts: Vec<AdapterFacts> = adapters.iter().map(|a| facts_of(&a.get_info())).collect();
            match AdapterSelection::decide(&facts, adapter::env_override().as_deref()) {
                Some(sel) => {
                    let i = sel.index;
                    // Logs the banner to stderr (the demo installs no logger) and
                    // publishes the decision for `state_json`.
                    adapter::record_selection(sel);
                    Ok(adapters[i].clone())
                }
                None => Err(format!(
                    "facett-demo: no wgpu adapters enumerated (set {ENV_ADAPTER} to force one)"
                )),
            }
        },
    ));

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

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

    /// Minimal `wgpu::AdapterInfo` builder — no GPU required, we only exercise the
    /// policy through this crate's conversion.
    fn info(name: &str, device_type: wgpu::DeviceType, vendor: u32) -> wgpu::AdapterInfo {
        wgpu::AdapterInfo {
            name: name.to_owned(),
            vendor,
            device: 0,
            device_type,
            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: Some(false),
            limit_bucket: None,
        }
    }

    /// **RED-when-broken — the oden enumeration.** A discrete RTX 4090, an
    /// integrated GPU, the ASPEED **BMC management display** and llvmpipe. The
    /// selector must return the 4090. If this regresses, every GPU optimisation in
    /// the repo becomes invisible because the frames are drawn on a CPU rasteriser
    /// (or not at all, on a BMC framebuffer).
    #[test]
    fn rejects_bmc_and_software_prefers_discrete() {
        let adapters = [
            info("ASPEED Graphics Family", wgpu::DeviceType::Other, 0x1a03),
            info("llvmpipe (LLVM 21.1.8, 256 bits)", wgpu::DeviceType::Cpu, 0x1_0005),
            info("AMD Radeon Graphics (RADV RAPHAEL)", wgpu::DeviceType::IntegratedGpu, 0x1002),
            info("NVIDIA GeForce RTX 4090", wgpu::DeviceType::DiscreteGpu, 0x10de),
        ];
        let pick = choose_adapter_index(&adapters).expect("must pick an adapter");
        assert_eq!(pick, 3, "must select the discrete NVIDIA RTX 4090");
        assert_eq!(adapters[pick].device_type, wgpu::DeviceType::DiscreteGpu);
    }

    /// With only an integrated GPU and software present, prefer the iGPU.
    #[test]
    fn prefers_integrated_over_cpu() {
        let adapters = [
            info("llvmpipe (LLVM 21)", wgpu::DeviceType::Cpu, 0),
            info("AMD Radeon Graphics", wgpu::DeviceType::IntegratedGpu, 0x1002),
        ];
        let pick = choose_adapter_index(&adapters).expect("must pick an adapter");
        assert_eq!(adapters[pick].device_type, wgpu::DeviceType::IntegratedGpu);
    }

    /// Among two discrete GPUs the NVIDIA/RTX one wins the tie-break.
    #[test]
    fn prefers_nvidia_among_discrete() {
        let adapters = [
            info("AMD Radeon RX 7900 XTX", wgpu::DeviceType::DiscreteGpu, 0x1002),
            info("NVIDIA GeForce RTX 5080", wgpu::DeviceType::DiscreteGpu, 0x10de),
        ];
        assert_eq!(choose_adapter_index(&adapters), Some(1));
    }

    /// Last resort: software alone is still selected (a slow window beats none).
    #[test]
    fn falls_back_to_cpu_when_alone() {
        let adapters = [info("llvmpipe", wgpu::DeviceType::Cpu, 0)];
        assert_eq!(choose_adapter_index(&adapters), Some(0));
    }

    /// Empty enumeration → no choice.
    #[test]
    fn none_when_empty() {
        assert_eq!(choose_adapter_index(&[]), None);
    }

    /// **RED-when-broken (the shared policy both demo binaries consume):** the
    /// eframe config `main.rs`, `osm_fly_demo` and `sigtuna_demo` install must be a
    /// freshly-created setup with the policy selector installed and high performance
    /// requested. Dropping the selector silently hands the choice back to wgpu's
    /// internal ordering — this goes red first, with no GPU.
    #[test]
    fn discrete_gpu_options_install_the_shared_policy() {
        use eframe::egui_wgpu::WgpuSetup;
        let cfg = discrete_gpu_wgpu_options();
        match cfg.wgpu_setup {
            WgpuSetup::CreateNew(setup) => {
                assert!(
                    setup.instance_descriptor.backends.contains(wgpu::Backends::VULKAN),
                    "Vulkan must be requested"
                );
                assert_eq!(setup.power_preference, wgpu::PowerPreference::HighPerformance);
                assert!(
                    setup.native_adapter_selector.is_some(),
                    "the adapter-selection policy must be installed (rejects BMC + software)"
                );
            }
            _ => panic!("expected a freshly-created wgpu setup (CreateNew), not an existing device"),
        }
    }

    /// The demo's `state_json` key exists whether or not a GPU backend came up, so a
    /// robot can tell "no GPU lane" (`null`) from "software GPU" (`software: true`).
    #[test]
    fn selected_adapter_json_key_is_always_present() {
        let v = adapter::selected_adapter_json();
        assert!(v.is_null() || v.get("kind").is_some());
    }
}