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());
}
}