From ab48df68c8a7f9921d9667ea55a7c21f5f6162a7 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 18:17:07 +0000 Subject: [PATCH] refactor(designcraft): move the tests module of gpu.rs to gpu/tests.rs --- apps/designcraft/src/gpu.rs | 581 +----------------------------- apps/designcraft/src/gpu/tests.rs | 571 +++++++++++++++++++++++++++++ 2 files changed, 572 insertions(+), 580 deletions(-) create mode 100644 apps/designcraft/src/gpu/tests.rs diff --git a/apps/designcraft/src/gpu.rs b/apps/designcraft/src/gpu.rs index 01b3ca6d4..907117b0c 100644 --- a/apps/designcraft/src/gpu.rs +++ b/apps/designcraft/src/gpu.rs @@ -758,583 +758,4 @@ fn restart(skip: &[String], startup: &Startup) -> std::io::Result<()> { } #[cfg(test)] -mod tests { - use super::*; - - fn gpu(name: &str, backend: Backend, device_type: DeviceType) -> Candidate { - Candidate { key: format!("{backend:?}:{name}"), name: name.into(), backend, device_type, pci: (0, 0), presents: true } - } - - fn names(c: &[Candidate], order: &[usize]) -> Vec { - order.iter().map(|&i| c[i].name.clone()).collect() - } - - const NVIDIA_3090: (u32, u32) = (0x10de, 0x2204); - const AMD_IGPU: (u32, u32) = (0x1002, 0x164e); - const INTEL_IGPU: (u32, u32) = (0x8086, 0xa780); - const WARP: (u32, u32) = (0x1414, 0x008c); - - fn pci_gpu(name: &str, backend: Backend, device_type: DeviceType, pci: (u32, u32)) -> Candidate { - Candidate { key: key(backend, pci.0, pci.1, name), name: name.into(), backend, device_type, pci, presents: true } - } - - fn monitor(pci: (u32, u32)) -> DisplayGpu { - DisplayGpu { pci, primary: false } - } - - fn primary(pci: (u32, u32)) -> DisplayGpu { - DisplayGpu { pci, primary: true } - } - - /// A Windows desktop as wgpu lists it with every backend: a Ryzen's integrated GPU without a - /// monitor and an RTX 3090 driving both monitors, each under Vulkan and DX12, then WARP and - /// OpenGL (which reports no device id). - fn ryzen_desktop() -> Vec { - vec![ - pci_gpu("NVIDIA GeForce RTX 3090", Backend::Vulkan, DeviceType::DiscreteGpu, NVIDIA_3090), - pci_gpu("AMD Radeon(TM) Graphics", Backend::Vulkan, DeviceType::IntegratedGpu, AMD_IGPU), - pci_gpu("NVIDIA GeForce RTX 3090", Backend::Dx12, DeviceType::DiscreteGpu, NVIDIA_3090), - pci_gpu("AMD Radeon(TM) Graphics", Backend::Dx12, DeviceType::IntegratedGpu, AMD_IGPU), - pci_gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu, WARP), - pci_gpu("NVIDIA GeForce RTX 3090/PCIe/SSE2", Backend::Gl, DeviceType::Other, (NVIDIA_3090.0, 0)), - ] - } - - /// Power saving picked the integrated GPU, whose driver reset when Windows had to present its - /// frames on the monitors of the NVIDIA card. The GPU that drives the displays comes first, - /// through DX12; the integrated GPU stays the fallback a restart reaches. - #[test] - fn a_desktop_draws_on_the_gpu_that_drives_its_monitors() { - let c = ryzen_desktop(); - for displays in [vec![primary(NVIDIA_3090)], vec![monitor(NVIDIA_3090)]] { - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, &[]), [2, 0, 3, 1, 5, 4], "{displays:?}"); - assert_eq!(adapter_order(&c, PowerPreference::None, &displays, None, &[]), [2, 0, 3, 1, 5, 4], "{displays:?}"); - } - // Without the display information the order is power saving's. - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], None, &[]), [3, 1, 2, 0, 5, 4]); - // After the NVIDIA adapters failed, the integrated GPU is next. - let skip = vec![c[2].key.clone(), c[0].key.clone()]; - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[primary(NVIDIA_3090)], None, &skip), [3, 1, 5, 4]); - } - - /// A hybrid laptop: the panel (primary) on the integrated GPU, an external monitor on the - /// discrete one. Power saving and the display agree, in either enumeration order. - #[test] - fn a_hybrid_laptop_draws_on_the_integrated_gpu_that_drives_its_panel() { - let c = ryzen_desktop(); - for displays in [vec![primary(AMD_IGPU), monitor(NVIDIA_3090)], vec![monitor(NVIDIA_3090), primary(AMD_IGPU)], vec![primary(AMD_IGPU)]] { - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, &[]), [3, 1, 2, 0, 5, 4], "{displays:?}"); - } - // Linux names no primary unless there is a built-in panel: a monitor on each GPU keeps the - // system's order among them (Mesa puts the compositor's GPU first), and a GPU without one - // comes after both. - let linux = vec![ - pci_gpu("AMD Radeon Graphics (RADV RAPHAEL_MENDOCINO)", Backend::Vulkan, DeviceType::IntegratedGpu, AMD_IGPU), - pci_gpu("NVIDIA GeForce RTX 3090", Backend::Vulkan, DeviceType::DiscreteGpu, NVIDIA_3090), - pci_gpu("Intel(R) Arc(TM) A380", Backend::Vulkan, DeviceType::DiscreteGpu, INTEL_IGPU), - pci_gpu("llvmpipe (LLVM 20.1.8, 256 bits)", Backend::Vulkan, DeviceType::Cpu, (0x10005, 0)), - ]; - let both = [monitor(AMD_IGPU), monitor(NVIDIA_3090)]; - assert_eq!(adapter_order(&linux, PowerPreference::None, &both, None, &[]), [0, 1, 2, 3]); - assert_eq!(adapter_order(&linux, PowerPreference::None, &[monitor(INTEL_IGPU)], None, &[]), [2, 0, 1, 3]); - assert_eq!(adapter_order(&linux, PowerPreference::None, &[monitor(NVIDIA_3090), primary(AMD_IGPU)], None, &[]), [0, 1, 2, 3]); - } - - /// A Windows desktop with a monitor on each GPU: the window opens on the primary display, so - /// its GPU draws, whichever kind it is. - #[test] - fn the_primary_display_decides_between_gpus_that_both_drive_a_monitor() { - let c = ryzen_desktop(); - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[monitor(AMD_IGPU), primary(NVIDIA_3090)], None, &[]), [2, 0, 3, 1, 5, 4]); - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[primary(AMD_IGPU), monitor(NVIDIA_3090)], None, &[]), [3, 1, 2, 0, 5, 4]); - // The displays only matter among adapters that can show the window; a GPU driving no - // display still loses to one that does. - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[monitor(AMD_IGPU), monitor(INTEL_IGPU)], None, &[]), [3, 1, 2, 0, 5, 4]); - } - - /// Display GPUs none of the adapters match (another driver's ids, OpenGL's missing device id, - /// Metal's zeros) change nothing: power saving's order. - #[test] - fn displays_no_adapter_matches_leave_the_power_preference_in_charge() { - let c = ryzen_desktop(); - let low = adapter_order(&c, PowerPreference::LowPower, &[], None, &[]); - for displays in [vec![monitor(INTEL_IGPU)], vec![primary((0x8086, 0x1234)), monitor((0x8086, 0x5678))], vec![primary((0, 0))]] { - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, &[]), low, "{displays:?}"); - } - assert_eq!(names(&c, &adapter_order(&c, PowerPreference::HighPerformance, &[monitor(INTEL_IGPU)], None, &[]))[0], "NVIDIA GeForce RTX 3090"); - // The Wayland desktop's adapters report no ids in these tests: the primary display never matches. - let c = wayland_desktop(); - assert_eq!( - adapter_order(&c, PowerPreference::None, &[primary(NVIDIA_3090)], None, &[]), - adapter_order(&c, PowerPreference::None, &[], None, &[]) - ); - assert!(adapter_order(&[], PowerPreference::LowPower, &[primary(NVIDIA_3090)], None, &[]).is_empty()); - } - - /// `WGPU_ADAPTER_NAME` and "can't present" still come before the displays. - #[test] - fn a_named_adapter_comes_before_the_one_driving_the_display() { - let mut c = ryzen_desktop(); - let displays = [primary(NVIDIA_3090)]; - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, Some("radeon"), &[]), [3, 1, 2, 0, 5, 4]); - c[2].presents = false; - c[0].presents = false; - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, &[]), [3, 1, 5, 4]); - } - - /// `WGPU_POWER_PREF`, the user's own choice, is kept as it is; only without it do the displays - /// count. - #[test] - fn only_automatic_considers_which_gpu_drives_the_display() { - assert!(automatic(None)); - assert!(!automatic(Some(PowerPreference::None))); - assert!(!automatic(Some(PowerPreference::LowPower))); - assert!(preferred_displays(Some(PowerPreference::HighPerformance)).is_empty()); - // Automatic reports whatever this machine has (nothing on macOS, in CI, or under Remote Desktop). - assert_eq!(preferred_displays(None), display_gpus()); - } - - #[test] - fn pci_ids_come_from_windows_device_ids() { - assert_eq!(pci_ids("PCI\\VEN_10DE&DEV_2204&SUBSYS_40421458&REV_A1"), Some(NVIDIA_3090)); - assert_eq!(pci_ids("PCI\\VEN_1002&DEV_164E&SUBSYS_88771043&REV_C1"), Some(AMD_IGPU)); - assert_eq!(pci_ids("pci\\ven_1002&dev_164e"), Some(AMD_IGPU)); - // Remote Desktop and other non-PCI display devices, and malformed ids. - assert_eq!(pci_ids("ROOT\\BasicDisplay\\0000"), None); - assert_eq!(pci_ids("PCI\\VEN_10DE&SUBSYS_40421458"), None); - assert_eq!(pci_ids("PCI\\VEN_10DE&DEV_ZZZZ"), None); - assert_eq!(pci_ids("VEN_&DEV_"), None); - assert_eq!(pci_ids("PCI\\VEN_10DÉ&DEV_2204"), None); - assert_eq!(pci_ids(""), None); - } - - /// Whatever this machine has: every GPU listed is a PCI device, at most one drives the primary - /// display, and none is listed twice. (A headless CI runner may list none.) - #[cfg(windows)] - #[test] - fn windows_lists_each_gpu_with_a_monitor_once() { - let gpus = windows_display_gpus(); - assert!(gpus.iter().filter(|g| g.primary).count() <= 1, "{gpus:?}"); - for (i, g) in gpus.iter().enumerate() { - assert_ne!(g.pci.0, 0, "{gpus:?}"); - assert!(!gpus[..i].iter().any(|h| h.pci == g.pci), "{gpus:?}"); - } - } - - #[test] - fn internal_panels_are_edp_lvds_and_dsi_connectors() { - for name in ["eDP-1", "LVDS-1", "DSI-1"] { - assert!(is_internal_panel(name), "{name}"); - } - for name in ["DP-3", "HDMI-A-1", "DVI-D-1", "VGA-1", "Writeback-1", ""] { - assert!(!is_internal_panel(name), "{name}"); - } - } - - /// A sysfs like a hybrid laptop's with an external monitor: the panel and a disconnected port - /// on the integrated GPU (card0), the monitor on the discrete one (card1), and a card without - /// PCI ids. Each GPU is listed once, the one with the panel as primary. - #[test] - fn linux_display_gpus_come_from_the_connected_connectors() -> std::io::Result<()> { - let dir = std::env::temp_dir().join(format!("designcraft-drm-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - let card = |name: &str, (vendor, device): (u32, u32)| -> std::io::Result<()> { - std::fs::create_dir_all(dir.join(name).join("device"))?; - std::fs::write(dir.join(name).join("device/vendor"), format!("{vendor:#06x}\n"))?; - std::fs::write(dir.join(name).join("device/device"), format!("{device:#06x}\n")) - }; - let connector = |name: &str, status: &str| -> std::io::Result<()> { - std::fs::create_dir_all(dir.join(name))?; - std::fs::write(dir.join(name).join("status"), format!("{status}\n")) - }; - card("card0", AMD_IGPU)?; - card("card1", NVIDIA_3090)?; - std::fs::create_dir_all(dir.join("card2"))?; - connector("card0-eDP-1", "connected")?; - connector("card0-HDMI-A-1", "disconnected")?; - connector("card1-DP-1", "connected")?; - connector("card1-DP-2", "connected")?; - connector("card2-Virtual-1", "connected")?; - std::fs::write(dir.join("renderD128"), "")?; - let mut gpus = linux_display_gpus(&dir); - gpus.sort_by_key(|g| g.pci); - assert_eq!(gpus, [primary(AMD_IGPU), monitor(NVIDIA_3090)]); - assert!(linux_display_gpus(&dir.join("no such dir")).is_empty()); - std::fs::remove_dir_all(&dir) - } - - #[test] - fn display_gpus_are_logged_as_pci_ids() { - assert_eq!(primary(NVIDIA_3090).to_string(), "10de:2204 (primary)"); - assert_eq!(monitor(AMD_IGPU).to_string(), "1002:164e"); - let mut gpus = vec![]; - add_display_gpu(&mut gpus, NVIDIA_3090, false); - add_display_gpu(&mut gpus, NVIDIA_3090, true); - add_display_gpu(&mut gpus, AMD_IGPU, false); - assert_eq!(gpus, [primary(NVIDIA_3090), monitor(AMD_IGPU)]); - } - - /// A dual-GPU Linux desktop as Vulkan lists it with Mesa's device-select layer: the GPU KDE's - /// compositor runs on (NVIDIA) first, then the Ryzen's integrated GPU, Mesa's software - /// renderer, and OpenGL. - fn wayland_desktop() -> Vec { - vec![ - gpu("NVIDIA GeForce RTX 5070", Backend::Vulkan, DeviceType::DiscreteGpu), - gpu("AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", Backend::Vulkan, DeviceType::IntegratedGpu), - gpu("llvmpipe (LLVM 20.1.8, 256 bits)", Backend::Vulkan, DeviceType::Cpu), - gpu("NVIDIA GeForce RTX 5070/PCIe/SSE2", Backend::Gl, DeviceType::Other), - ] - } - - #[test] - fn automatic_keeps_the_systems_order_on_linux_and_saves_power_elsewhere() { - let c = wayland_desktop(); - let order = adapter_order(&c, power_preference(None), &[], None, &[]); - let first = &c[order[0]].name; - if cfg!(any(windows, target_os = "macos")) { - assert_eq!(first, "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)"); - } else { - // The integrated GPU couldn't show frames to a compositor on the NVIDIA GPU. - assert_eq!(first, "NVIDIA GeForce RTX 5070"); - } - } - - #[test] - fn each_preference_orders_the_adapters_then_falls_back_to_opengl_and_software() { - let c = wayland_desktop(); - let order = |power| names(&c, &adapter_order(&c, power, &[], None, &[])); - assert_eq!( - order(PowerPreference::None), - [ - "NVIDIA GeForce RTX 5070", - "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", - "NVIDIA GeForce RTX 5070/PCIe/SSE2", - "llvmpipe (LLVM 20.1.8, 256 bits)" - ] - ); - assert_eq!( - order(PowerPreference::LowPower), - [ - "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", - "NVIDIA GeForce RTX 5070", - "NVIDIA GeForce RTX 5070/PCIe/SSE2", - "llvmpipe (LLVM 20.1.8, 256 bits)" - ] - ); - assert_eq!( - order(PowerPreference::HighPerformance), - [ - "NVIDIA GeForce RTX 5070", - "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", - "NVIDIA GeForce RTX 5070/PCIe/SSE2", - "llvmpipe (LLVM 20.1.8, 256 bits)" - ] - ); - } - - /// A restart leaves out the adapter the window failed on, and once every adapter failed there - /// is nothing to try. A dual-GPU Mac reports both GPUs under Metal with no PCI ids: their keys - /// still differ, so when the integrated GPU fails, the restart renders on the discrete one. - #[test] - fn gpus_without_pci_ids_are_told_apart_by_name() { - let mac = |name: &str, device_type| Candidate { - key: key(Backend::Metal, 0, 0, name), - name: name.into(), - backend: Backend::Metal, - device_type, - pci: (0, 0), - presents: true, - }; - let c = vec![mac("NVIDIA GeForce GT 750M", DeviceType::DiscreteGpu), mac("Intel Iris Pro Graphics", DeviceType::IntegratedGpu)]; - assert_eq!((c[0].key.as_str(), c[1].key.as_str()), ("Metal:NVIDIA GeForce GT 750M", "Metal:Intel Iris Pro Graphics")); - let first = adapter_order(&c, PowerPreference::LowPower, &[], None, &[]); - assert_eq!(names(&c, &first)[0], "Intel Iris Pro Graphics"); - let skip = parse_skip(&c[first[0]].key); - assert_eq!(names(&c, &adapter_order(&c, PowerPreference::LowPower, &[], None, &skip)), ["NVIDIA GeForce GT 750M"]); - // A comma in a name can't split the list. - assert_eq!(parse_skip(&key(Backend::Gl, 0, 0, "Mesa, llvmpipe")), ["Gl:Mesa llvmpipe"]); - } - - fn sample(frames: u64, busy: bool, leaves: u64, hidden: bool) -> Sample { - Sample { frames, busy, leaves, hidden } - } - - /// Runs `ticks` looks with the same sample; the verdicts other than `Wait`. - fn run(w: &mut Watch, s: Sample, ticks: u32) -> Vec<(u32, Verdict)> { - (1..=ticks).filter_map(|t| Some((t, w.step(s, WATCH_TICK))).filter(|(_, v)| *v != Verdict::Wait)).collect() - } - - const TICKS_PER_S: u32 = 4; - - /// The UI thread stuck in the graphics stack (not in the app's code, no progress) with the - /// window not known to be hidden: late after 20 s, hung after 60 s. - #[test] - fn a_ui_thread_stuck_in_the_graphics_stack_hangs_its_adapter() { - let mut w = Watch::default(); - let v = run(&mut w, sample(1, false, 1, false), 61 * TICKS_PER_S); - assert_eq!(v.first(), Some(&(20 * TICKS_PER_S, Verdict::Late))); - // `leaves` moved from 0 to 1 on the first look, so the stuck time starts one look later. - assert_eq!(v.get(1), Some(&(60 * TICKS_PER_S + 1, Verdict::Hung))); - // The second frame began: the first was presented. - assert_eq!(Watch::default().step(sample(2, false, 9, false), WATCH_TICK), Verdict::Shown); - } - - /// A busy UI thread (a large document composing on a slow machine) is only ever late. - #[test] - fn a_busy_ui_thread_never_hangs_its_adapter() { - let mut w = Watch::default(); - let v = run(&mut w, sample(0, true, 0, false), 600 * TICKS_PER_S); - assert_eq!(v, [(20 * TICKS_PER_S, Verdict::Late)]); - // Nor does one that keeps coming back from the graphics stack. - let mut w = Watch::default(); - for t in 0..(600 * TICKS_PER_S) { - assert_ne!(w.step(sample(1, false, u64::from(t), false), WATCH_TICK), Verdict::Hung); - } - } - - /// A window known to be hidden (locked screen, another workspace, minimized) may have its frames - /// held back by the compositor: that time counts for nothing and starts the stuck time over. - #[test] - fn time_while_the_window_is_hidden_does_not_count() { - let mut w = Watch::default(); - assert!(run(&mut w, sample(1, false, 0, true), 600 * TICKS_PER_S).is_empty()); - assert!(run(&mut w, sample(1, false, 0, false), 50 * TICKS_PER_S).iter().all(|(_, v)| *v == Verdict::Late)); - assert!(run(&mut w, sample(1, false, 0, true), 10 * TICKS_PER_S).is_empty()); - assert!(run(&mut w, sample(1, false, 0, false), 59 * TICKS_PER_S).is_empty()); - assert_eq!(run(&mut w, sample(1, false, 0, false), TICKS_PER_S), [(TICKS_PER_S, Verdict::Hung)]); - } - - #[test] - fn a_hung_adapter_is_left_out_once() { - let skip = vec!["Dx12:10de:28a0".to_string()]; - assert_eq!(restart_without(true, 1, Some("Dx12:10de:28a0"), &skip), None); - assert_eq!(restart_without(true, 1, Some("Dx12:8086:a7a8"), &skip), Some("Dx12:8086:a7a8")); - } - - /// Windows, a hardware DX12 adapter failing: the software renderer (WARP) is passed over while - /// another backend is left, the restarts go through OpenGL and Vulkan, and only then back to - /// every backend with software allowed. The chain ends. - #[test] - fn software_renderers_come_after_every_backends_hardware() { - use backend::Backend as B; - let c = ryzen_desktop(); - let order = adapter_order(&c, PowerPreference::LowPower, &[], None, &[]); - assert_eq!(without_software(order.clone(), &c, false), (order.clone(), false)); - let (hardware, dropped) = without_software(order, &c, true); - assert!(dropped && !hardware.contains(&4), "{hardware:?}"); - - let dx12 = "Dx12:10de:2204".to_string(); - // The adapter that failed first. - let (skip, _) = next_start(&[], Some(&dx12), Some((B::Dx12, Some(B::Gl))), true).unwrap(); - assert_eq!(skip, [dx12.as_str()]); - // Only WARP left on DX12 (passed over, nothing picked): OpenGL next. - let (skip, what) = next_start(&skip, None, Some((B::Dx12, Some(B::Gl))), true).unwrap(); - assert_eq!(skip, [dx12.as_str(), "Dx12", SOFTWARE_LEFT]); - assert!(what.contains("OpenGL"), "{what}"); - // OpenGL has no adapter, Vulkan next; then Vulkan has none and no backend is left. - let (skip, _) = next_start(&skip, None, Some((B::Gl, Some(B::Vulkan))), false).unwrap(); - assert_eq!(skip, [dx12.as_str(), "Dx12", SOFTWARE_LEFT, "Gl"]); - let (skip, what) = next_start(&skip, None, Some((B::Vulkan, None)), false).unwrap(); - assert_eq!(skip, [dx12.as_str(), SOFTWARE], "{what}"); - assert!(backend::skipped_backends(&skip).is_empty()); - // With software allowed and nothing left: the end. - assert_eq!(next_start(&skip, None, Some((B::Dx12, None)), false), None); - // Nothing was passed over (a Mac whose GPUs all failed): no software round. - assert_eq!(next_start(&[], None, Some((B::Metal, None)), false), None); - // Without the backend fallback (`WGPU_BACKEND`), only adapters. - assert_eq!(next_start(&[], None, None, false), None); - } - - /// The skip list travels as an argument, which processes the app starts don't inherit. - #[test] - fn the_skip_list_is_an_argument() { - assert_eq!(skip_arg("--gpu-skip=Dx12:10de:2204,Dx12"), Some(vec!["Dx12:10de:2204".to_string(), "Dx12".to_string()])); - assert_eq!(skip_arg("--gpu-skip="), Some(vec![])); - assert_eq!(skip_arg("--gpu-skip"), None); - assert_eq!(skip_arg("layout.designcraft"), None); - } - - #[test] - fn a_restart_tries_the_next_adapter() { - let c = wayland_desktop(); - let mut skip = vec![]; - let mut tried = vec![]; - while let Some(&i) = adapter_order(&c, PowerPreference::LowPower, &[], None, &skip).first() { - tried.push(c[i].name.clone()); - skip.push(c[i].key.clone()); - } - assert_eq!( - tried, - [ - "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", - "NVIDIA GeForce RTX 5070", - "NVIDIA GeForce RTX 5070/PCIe/SSE2", - "llvmpipe (LLVM 20.1.8, 256 bits)" - ] - ); - } - - #[test] - fn adapters_that_cannot_present_to_the_window_are_never_tried() { - let mut c = wayland_desktop(); - c[0].presents = false; - c[3].presents = false; - for power in [PowerPreference::None, PowerPreference::LowPower, PowerPreference::HighPerformance] { - let order = adapter_order(&c, power, &[], None, &[]); - assert!(!order.contains(&0) && !order.contains(&3), "{power:?}: {order:?}"); - assert_eq!(order.len(), 2); - } - for p in &mut c { - p.presents = false; - } - assert!(adapter_order(&c, PowerPreference::None, &[], None, &[]).is_empty()); - assert!(adapter_order(&[], PowerPreference::HighPerformance, &[], None, &[]).is_empty()); - } - - #[test] - fn wgpu_adapter_name_picks_an_adapter_first_and_the_rest_stay_as_fallbacks() { - let c = wayland_desktop(); - let order = adapter_order(&c, PowerPreference::HighPerformance, &[], Some("radv"), &[]); - assert_eq!(c[order[0]].name, "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)"); - assert_eq!(order.len(), 4); - // Any case; every match comes before the rest, natively first. - let order = adapter_order(&c, PowerPreference::LowPower, &[], Some("GeForce"), &[]); - assert_eq!(names(&c, &order[..2]), ["NVIDIA GeForce RTX 5070", "NVIDIA GeForce RTX 5070/PCIe/SSE2"]); - // An empty name or one nothing matches changes nothing. - for named in ["", "no such gpu"] { - assert_eq!( - adapter_order(&c, PowerPreference::LowPower, &[], Some(named), &[]), - adapter_order(&c, PowerPreference::LowPower, &[], None, &[]) - ); - } - // A named adapter that failed is left out like any other. - let skip = vec![c[1].key.clone()]; - assert_eq!(adapter_order(&c, PowerPreference::None, &[], Some("radv"), &skip)[0], 0); - } - - /// A Windows machine lists every GPU twice when Vulkan and DX12 are both in the instance: - /// power saving still takes the integrated GPU first, each GPU through DX12 first. - #[test] - fn hybrid_laptop_on_windows_renders_on_the_integrated_gpu_with_power_saving() { - let c = vec![ - gpu("Intel(R) Arc(TM) A370M", Backend::Vulkan, DeviceType::DiscreteGpu), - gpu("Intel(R) Iris(R) Xe Graphics", Backend::Vulkan, DeviceType::IntegratedGpu), - gpu("Intel(R) Arc(TM) A370M", Backend::Dx12, DeviceType::DiscreteGpu), - gpu("Intel(R) Iris(R) Xe Graphics", Backend::Dx12, DeviceType::IntegratedGpu), - gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu), - ]; - let order = adapter_order(&c, PowerPreference::LowPower, &[], None, &[]); - assert_eq!(order, [3, 1, 2, 0, 4]); - assert_eq!(adapter_order(&c, PowerPreference::HighPerformance, &[], None, &[]), [2, 0, 3, 1, 4]); - } - - /// One Intel GPU whose Vulkan driver made the window flicker black: DX12 comes first whatever - /// the preference; Vulkan stays a fallback, and `WGPU_ADAPTER_NAME` or a restart leaving DX12 - /// out still reach it. - #[test] - fn a_windows_gpu_renders_through_dx12_before_vulkan() { - let c = vec![ - gpu("Intel(R) Graphics", Backend::Vulkan, DeviceType::IntegratedGpu), - gpu("Intel(R) Graphics", Backend::Dx12, DeviceType::IntegratedGpu), - gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu), - gpu("Intel(R) Graphics", Backend::Gl, DeviceType::Other), - ]; - for power in [PowerPreference::LowPower, PowerPreference::HighPerformance, PowerPreference::None] { - assert_eq!(adapter_order(&c, power, &[], None, &[]), [1, 0, 3, 2], "{power:?}"); - } - assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], None, &[c[1].key.clone()]), [0, 3, 2], "DX12 failed: Vulkan next"); - } - - #[test] - fn power_preference_follows_the_environment() { - assert_eq!(power_preference(None), AUTOMATIC); - for p in [PowerPreference::LowPower, PowerPreference::HighPerformance, PowerPreference::None] { - assert_eq!(power_preference(Some(p)), p); - } - assert_eq!(AUTOMATIC, if cfg!(any(windows, target_os = "macos")) { PowerPreference::LowPower } else { PowerPreference::None }); - } - - /// The Intel Mac of #334 lost its Metal device compiling the compute shader of wgpu's indirect - /// call check: off unless `WGPU_VALIDATION_INDIRECT_CALL` turns it on. - #[test] - fn the_indirect_call_check_is_off() { - if std::env::var_os("WGPU_VALIDATION_INDIRECT_CALL").is_none() { - assert!(!instance_flags().contains(wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL)); - } - // The other flags of the build stay as wgpu sets them. - let others = wgpu::InstanceFlags::from_build_config() - wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL; - if std::env::vars_os().all(|(k, _)| !k.to_string_lossy().starts_with("WGPU_")) { - assert_eq!(instance_flags(), others); - } - } - - #[test] - fn skipped_adapters_parse_from_a_comma_separated_list() { - assert_eq!(parse_skip("Vulkan:1002:164e, Gl:10de:2f04,,"), ["Vulkan:1002:164e", "Gl:10de:2f04"]); - assert!(parse_skip("").is_empty()); - assert_eq!(key(Backend::Vulkan, 0x1002, 0x164e, "AMD Radeon"), "Vulkan:1002:164e"); - assert_eq!(key(Backend::Gl, 0x10de, 0x2f04, "NVIDIA"), "Gl:10de:2f04"); - // A whole backend left out is its bare name; it never matches an adapter's key. - let skip = parse_skip("Dx12,Dx12:10de:2204"); - assert_eq!(backend::skipped_backends(&skip), [backend::Backend::Dx12]); - let c = vec![pci_gpu("NVIDIA GeForce RTX 3090", Backend::Dx12, DeviceType::DiscreteGpu, NVIDIA_3090)]; - assert!(adapter_order(&c, PowerPreference::None, &[], None, &skip).is_empty()); - assert_eq!(adapter_order(&c, PowerPreference::None, &[], None, &skip[..1]), [0]); - } - - #[test] - fn panics_in_wgpu_egui_wgpu_and_naga_are_graphics_failures() { - for file in [ - "/home/u/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/egui-wgpu-0.36.2/src/winit.rs", - r"C:\Users\u\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\wgpu-core-30.0.1\src\device\mod.rs", - "/rustc/deps/wgpu-hal-30.0.1/src/vulkan/adapter.rs", - "naga-30.0.0/src/back/spv/writer.rs", - ] { - assert!(in_graphics_stack(file), "{file}"); - } - for file in [ - "apps/designcraft/src/main.rs", - "/home/wgpu/designcraft/crates/ui-egui/src/lib.rs", - "egui-0.36.2/src/context.rs", - "winit-0.30.12/src/x.rs", - ] { - assert!(!in_graphics_stack(file), "{file}"); - } - } - - #[test] - fn only_a_graphics_failure_while_starting_up_restarts_without_the_adapter() { - let amd = Some("Vulkan:1002:164e"); - assert_eq!(restart_without(true, 0, amd, &[]), amd); - assert_eq!(restart_without(true, STARTUP_FRAMES - 1, amd, &[]), amd); - // After start-up the adapter has shown frames; a lost window is something else. - assert_eq!(restart_without(true, STARTUP_FRAMES, amd, &[]), None); - // A bug outside the graphics, or no adapter picked: nothing another adapter would change. - assert_eq!(restart_without(false, 0, amd, &[]), None); - assert_eq!(restart_without(true, 0, None, &[]), None); - // Never the same adapter twice, so restarts end. - assert_eq!(restart_without(true, 0, amd, &["Vulkan:1002:164e".into()]), None); - } - - /// Without a backend fallback (`WGPU_BACKEND` set), a start whose graphics failed with no - /// adapter to leave out ends with the error and the hint; a bug ends with its message. - #[test] - fn a_normal_exit_or_another_failure_does_not_restart() { - let startup = Startup::new(None, Vec::new()); - assert_eq!(finish(Ok(Ok(())), &startup), Ok(())); - // No adapter was picked (none can show the window): the error is returned, nothing restarts. - let none = - eframe::Error::Wgpu(eframe::egui_wgpu::WgpuError::CustomNativeAdapterSelectionError("no graphics adapter can show the window".into())); - assert_eq!( - finish(Ok(Err(none)), &startup), - Err(format!("WGPU error: Adapter selection failed: no graphics adapter can show the window ({HELP})")) - ); - let _ = startup.adapter.set("Vulkan:1002:164e".into()); - GRAPHICS_PANIC.store(false, Ordering::Relaxed); - assert_eq!(finish(Err("a bug".into()), &startup), Err("a bug".into())); - // Nothing to record without a fallback. - startup.presented(); - startup.started_with(wgpu::Backend::Gl); - assert_eq!(startup.status_line(), None); - } -} +mod tests; diff --git a/apps/designcraft/src/gpu/tests.rs b/apps/designcraft/src/gpu/tests.rs new file mode 100644 index 000000000..c7990aab0 --- /dev/null +++ b/apps/designcraft/src/gpu/tests.rs @@ -0,0 +1,571 @@ +use super::*; + +fn gpu(name: &str, backend: Backend, device_type: DeviceType) -> Candidate { + Candidate { key: format!("{backend:?}:{name}"), name: name.into(), backend, device_type, pci: (0, 0), presents: true } +} + +fn names(c: &[Candidate], order: &[usize]) -> Vec { + order.iter().map(|&i| c[i].name.clone()).collect() +} + +const NVIDIA_3090: (u32, u32) = (0x10de, 0x2204); +const AMD_IGPU: (u32, u32) = (0x1002, 0x164e); +const INTEL_IGPU: (u32, u32) = (0x8086, 0xa780); +const WARP: (u32, u32) = (0x1414, 0x008c); + +fn pci_gpu(name: &str, backend: Backend, device_type: DeviceType, pci: (u32, u32)) -> Candidate { + Candidate { key: key(backend, pci.0, pci.1, name), name: name.into(), backend, device_type, pci, presents: true } +} + +fn monitor(pci: (u32, u32)) -> DisplayGpu { + DisplayGpu { pci, primary: false } +} + +fn primary(pci: (u32, u32)) -> DisplayGpu { + DisplayGpu { pci, primary: true } +} + +/// A Windows desktop as wgpu lists it with every backend: a Ryzen's integrated GPU without a +/// monitor and an RTX 3090 driving both monitors, each under Vulkan and DX12, then WARP and +/// OpenGL (which reports no device id). +fn ryzen_desktop() -> Vec { + vec![ + pci_gpu("NVIDIA GeForce RTX 3090", Backend::Vulkan, DeviceType::DiscreteGpu, NVIDIA_3090), + pci_gpu("AMD Radeon(TM) Graphics", Backend::Vulkan, DeviceType::IntegratedGpu, AMD_IGPU), + pci_gpu("NVIDIA GeForce RTX 3090", Backend::Dx12, DeviceType::DiscreteGpu, NVIDIA_3090), + pci_gpu("AMD Radeon(TM) Graphics", Backend::Dx12, DeviceType::IntegratedGpu, AMD_IGPU), + pci_gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu, WARP), + pci_gpu("NVIDIA GeForce RTX 3090/PCIe/SSE2", Backend::Gl, DeviceType::Other, (NVIDIA_3090.0, 0)), + ] +} + +/// Power saving picked the integrated GPU, whose driver reset when Windows had to present its +/// frames on the monitors of the NVIDIA card. The GPU that drives the displays comes first, +/// through DX12; the integrated GPU stays the fallback a restart reaches. +#[test] +fn a_desktop_draws_on_the_gpu_that_drives_its_monitors() { + let c = ryzen_desktop(); + for displays in [vec![primary(NVIDIA_3090)], vec![monitor(NVIDIA_3090)]] { + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, &[]), [2, 0, 3, 1, 5, 4], "{displays:?}"); + assert_eq!(adapter_order(&c, PowerPreference::None, &displays, None, &[]), [2, 0, 3, 1, 5, 4], "{displays:?}"); + } + // Without the display information the order is power saving's. + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], None, &[]), [3, 1, 2, 0, 5, 4]); + // After the NVIDIA adapters failed, the integrated GPU is next. + let skip = vec![c[2].key.clone(), c[0].key.clone()]; + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[primary(NVIDIA_3090)], None, &skip), [3, 1, 5, 4]); +} + +/// A hybrid laptop: the panel (primary) on the integrated GPU, an external monitor on the +/// discrete one. Power saving and the display agree, in either enumeration order. +#[test] +fn a_hybrid_laptop_draws_on_the_integrated_gpu_that_drives_its_panel() { + let c = ryzen_desktop(); + for displays in [vec![primary(AMD_IGPU), monitor(NVIDIA_3090)], vec![monitor(NVIDIA_3090), primary(AMD_IGPU)], vec![primary(AMD_IGPU)]] { + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, &[]), [3, 1, 2, 0, 5, 4], "{displays:?}"); + } + // Linux names no primary unless there is a built-in panel: a monitor on each GPU keeps the + // system's order among them (Mesa puts the compositor's GPU first), and a GPU without one + // comes after both. + let linux = vec![ + pci_gpu("AMD Radeon Graphics (RADV RAPHAEL_MENDOCINO)", Backend::Vulkan, DeviceType::IntegratedGpu, AMD_IGPU), + pci_gpu("NVIDIA GeForce RTX 3090", Backend::Vulkan, DeviceType::DiscreteGpu, NVIDIA_3090), + pci_gpu("Intel(R) Arc(TM) A380", Backend::Vulkan, DeviceType::DiscreteGpu, INTEL_IGPU), + pci_gpu("llvmpipe (LLVM 20.1.8, 256 bits)", Backend::Vulkan, DeviceType::Cpu, (0x10005, 0)), + ]; + let both = [monitor(AMD_IGPU), monitor(NVIDIA_3090)]; + assert_eq!(adapter_order(&linux, PowerPreference::None, &both, None, &[]), [0, 1, 2, 3]); + assert_eq!(adapter_order(&linux, PowerPreference::None, &[monitor(INTEL_IGPU)], None, &[]), [2, 0, 1, 3]); + assert_eq!(adapter_order(&linux, PowerPreference::None, &[monitor(NVIDIA_3090), primary(AMD_IGPU)], None, &[]), [0, 1, 2, 3]); +} + +/// A Windows desktop with a monitor on each GPU: the window opens on the primary display, so +/// its GPU draws, whichever kind it is. +#[test] +fn the_primary_display_decides_between_gpus_that_both_drive_a_monitor() { + let c = ryzen_desktop(); + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[monitor(AMD_IGPU), primary(NVIDIA_3090)], None, &[]), [2, 0, 3, 1, 5, 4]); + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[primary(AMD_IGPU), monitor(NVIDIA_3090)], None, &[]), [3, 1, 2, 0, 5, 4]); + // The displays only matter among adapters that can show the window; a GPU driving no + // display still loses to one that does. + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[monitor(AMD_IGPU), monitor(INTEL_IGPU)], None, &[]), [3, 1, 2, 0, 5, 4]); +} + +/// Display GPUs none of the adapters match (another driver's ids, OpenGL's missing device id, +/// Metal's zeros) change nothing: power saving's order. +#[test] +fn displays_no_adapter_matches_leave_the_power_preference_in_charge() { + let c = ryzen_desktop(); + let low = adapter_order(&c, PowerPreference::LowPower, &[], None, &[]); + for displays in [vec![monitor(INTEL_IGPU)], vec![primary((0x8086, 0x1234)), monitor((0x8086, 0x5678))], vec![primary((0, 0))]] { + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, &[]), low, "{displays:?}"); + } + assert_eq!(names(&c, &adapter_order(&c, PowerPreference::HighPerformance, &[monitor(INTEL_IGPU)], None, &[]))[0], "NVIDIA GeForce RTX 3090"); + // The Wayland desktop's adapters report no ids in these tests: the primary display never matches. + let c = wayland_desktop(); + assert_eq!( + adapter_order(&c, PowerPreference::None, &[primary(NVIDIA_3090)], None, &[]), + adapter_order(&c, PowerPreference::None, &[], None, &[]) + ); + assert!(adapter_order(&[], PowerPreference::LowPower, &[primary(NVIDIA_3090)], None, &[]).is_empty()); +} + +/// `WGPU_ADAPTER_NAME` and "can't present" still come before the displays. +#[test] +fn a_named_adapter_comes_before_the_one_driving_the_display() { + let mut c = ryzen_desktop(); + let displays = [primary(NVIDIA_3090)]; + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, Some("radeon"), &[]), [3, 1, 2, 0, 5, 4]); + c[2].presents = false; + c[0].presents = false; + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, &[]), [3, 1, 5, 4]); +} + +/// `WGPU_POWER_PREF`, the user's own choice, is kept as it is; only without it do the displays +/// count. +#[test] +fn only_automatic_considers_which_gpu_drives_the_display() { + assert!(automatic(None)); + assert!(!automatic(Some(PowerPreference::None))); + assert!(!automatic(Some(PowerPreference::LowPower))); + assert!(preferred_displays(Some(PowerPreference::HighPerformance)).is_empty()); + // Automatic reports whatever this machine has (nothing on macOS, in CI, or under Remote Desktop). + assert_eq!(preferred_displays(None), display_gpus()); +} + +#[test] +fn pci_ids_come_from_windows_device_ids() { + assert_eq!(pci_ids("PCI\\VEN_10DE&DEV_2204&SUBSYS_40421458&REV_A1"), Some(NVIDIA_3090)); + assert_eq!(pci_ids("PCI\\VEN_1002&DEV_164E&SUBSYS_88771043&REV_C1"), Some(AMD_IGPU)); + assert_eq!(pci_ids("pci\\ven_1002&dev_164e"), Some(AMD_IGPU)); + // Remote Desktop and other non-PCI display devices, and malformed ids. + assert_eq!(pci_ids("ROOT\\BasicDisplay\\0000"), None); + assert_eq!(pci_ids("PCI\\VEN_10DE&SUBSYS_40421458"), None); + assert_eq!(pci_ids("PCI\\VEN_10DE&DEV_ZZZZ"), None); + assert_eq!(pci_ids("VEN_&DEV_"), None); + assert_eq!(pci_ids("PCI\\VEN_10DÉ&DEV_2204"), None); + assert_eq!(pci_ids(""), None); +} + +/// Whatever this machine has: every GPU listed is a PCI device, at most one drives the primary +/// display, and none is listed twice. (A headless CI runner may list none.) +#[cfg(windows)] +#[test] +fn windows_lists_each_gpu_with_a_monitor_once() { + let gpus = windows_display_gpus(); + assert!(gpus.iter().filter(|g| g.primary).count() <= 1, "{gpus:?}"); + for (i, g) in gpus.iter().enumerate() { + assert_ne!(g.pci.0, 0, "{gpus:?}"); + assert!(!gpus[..i].iter().any(|h| h.pci == g.pci), "{gpus:?}"); + } +} + +#[test] +fn internal_panels_are_edp_lvds_and_dsi_connectors() { + for name in ["eDP-1", "LVDS-1", "DSI-1"] { + assert!(is_internal_panel(name), "{name}"); + } + for name in ["DP-3", "HDMI-A-1", "DVI-D-1", "VGA-1", "Writeback-1", ""] { + assert!(!is_internal_panel(name), "{name}"); + } +} + +/// A sysfs like a hybrid laptop's with an external monitor: the panel and a disconnected port +/// on the integrated GPU (card0), the monitor on the discrete one (card1), and a card without +/// PCI ids. Each GPU is listed once, the one with the panel as primary. +#[test] +fn linux_display_gpus_come_from_the_connected_connectors() -> std::io::Result<()> { + let dir = std::env::temp_dir().join(format!("designcraft-drm-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let card = |name: &str, (vendor, device): (u32, u32)| -> std::io::Result<()> { + std::fs::create_dir_all(dir.join(name).join("device"))?; + std::fs::write(dir.join(name).join("device/vendor"), format!("{vendor:#06x}\n"))?; + std::fs::write(dir.join(name).join("device/device"), format!("{device:#06x}\n")) + }; + let connector = |name: &str, status: &str| -> std::io::Result<()> { + std::fs::create_dir_all(dir.join(name))?; + std::fs::write(dir.join(name).join("status"), format!("{status}\n")) + }; + card("card0", AMD_IGPU)?; + card("card1", NVIDIA_3090)?; + std::fs::create_dir_all(dir.join("card2"))?; + connector("card0-eDP-1", "connected")?; + connector("card0-HDMI-A-1", "disconnected")?; + connector("card1-DP-1", "connected")?; + connector("card1-DP-2", "connected")?; + connector("card2-Virtual-1", "connected")?; + std::fs::write(dir.join("renderD128"), "")?; + let mut gpus = linux_display_gpus(&dir); + gpus.sort_by_key(|g| g.pci); + assert_eq!(gpus, [primary(AMD_IGPU), monitor(NVIDIA_3090)]); + assert!(linux_display_gpus(&dir.join("no such dir")).is_empty()); + std::fs::remove_dir_all(&dir) +} + +#[test] +fn display_gpus_are_logged_as_pci_ids() { + assert_eq!(primary(NVIDIA_3090).to_string(), "10de:2204 (primary)"); + assert_eq!(monitor(AMD_IGPU).to_string(), "1002:164e"); + let mut gpus = vec![]; + add_display_gpu(&mut gpus, NVIDIA_3090, false); + add_display_gpu(&mut gpus, NVIDIA_3090, true); + add_display_gpu(&mut gpus, AMD_IGPU, false); + assert_eq!(gpus, [primary(NVIDIA_3090), monitor(AMD_IGPU)]); +} + +/// A dual-GPU Linux desktop as Vulkan lists it with Mesa's device-select layer: the GPU KDE's +/// compositor runs on (NVIDIA) first, then the Ryzen's integrated GPU, Mesa's software +/// renderer, and OpenGL. +fn wayland_desktop() -> Vec { + vec![ + gpu("NVIDIA GeForce RTX 5070", Backend::Vulkan, DeviceType::DiscreteGpu), + gpu("AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", Backend::Vulkan, DeviceType::IntegratedGpu), + gpu("llvmpipe (LLVM 20.1.8, 256 bits)", Backend::Vulkan, DeviceType::Cpu), + gpu("NVIDIA GeForce RTX 5070/PCIe/SSE2", Backend::Gl, DeviceType::Other), + ] +} + +#[test] +fn automatic_keeps_the_systems_order_on_linux_and_saves_power_elsewhere() { + let c = wayland_desktop(); + let order = adapter_order(&c, power_preference(None), &[], None, &[]); + let first = &c[order[0]].name; + if cfg!(any(windows, target_os = "macos")) { + assert_eq!(first, "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)"); + } else { + // The integrated GPU couldn't show frames to a compositor on the NVIDIA GPU. + assert_eq!(first, "NVIDIA GeForce RTX 5070"); + } +} + +#[test] +fn each_preference_orders_the_adapters_then_falls_back_to_opengl_and_software() { + let c = wayland_desktop(); + let order = |power| names(&c, &adapter_order(&c, power, &[], None, &[])); + assert_eq!( + order(PowerPreference::None), + [ + "NVIDIA GeForce RTX 5070", + "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", + "NVIDIA GeForce RTX 5070/PCIe/SSE2", + "llvmpipe (LLVM 20.1.8, 256 bits)" + ] + ); + assert_eq!( + order(PowerPreference::LowPower), + [ + "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", + "NVIDIA GeForce RTX 5070", + "NVIDIA GeForce RTX 5070/PCIe/SSE2", + "llvmpipe (LLVM 20.1.8, 256 bits)" + ] + ); + assert_eq!( + order(PowerPreference::HighPerformance), + [ + "NVIDIA GeForce RTX 5070", + "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", + "NVIDIA GeForce RTX 5070/PCIe/SSE2", + "llvmpipe (LLVM 20.1.8, 256 bits)" + ] + ); +} + +/// A restart leaves out the adapter the window failed on, and once every adapter failed there +/// is nothing to try. A dual-GPU Mac reports both GPUs under Metal with no PCI ids: their keys +/// still differ, so when the integrated GPU fails, the restart renders on the discrete one. +#[test] +fn gpus_without_pci_ids_are_told_apart_by_name() { + let mac = |name: &str, device_type| Candidate { + key: key(Backend::Metal, 0, 0, name), + name: name.into(), + backend: Backend::Metal, + device_type, + pci: (0, 0), + presents: true, + }; + let c = vec![mac("NVIDIA GeForce GT 750M", DeviceType::DiscreteGpu), mac("Intel Iris Pro Graphics", DeviceType::IntegratedGpu)]; + assert_eq!((c[0].key.as_str(), c[1].key.as_str()), ("Metal:NVIDIA GeForce GT 750M", "Metal:Intel Iris Pro Graphics")); + let first = adapter_order(&c, PowerPreference::LowPower, &[], None, &[]); + assert_eq!(names(&c, &first)[0], "Intel Iris Pro Graphics"); + let skip = parse_skip(&c[first[0]].key); + assert_eq!(names(&c, &adapter_order(&c, PowerPreference::LowPower, &[], None, &skip)), ["NVIDIA GeForce GT 750M"]); + // A comma in a name can't split the list. + assert_eq!(parse_skip(&key(Backend::Gl, 0, 0, "Mesa, llvmpipe")), ["Gl:Mesa llvmpipe"]); +} + +fn sample(frames: u64, busy: bool, leaves: u64, hidden: bool) -> Sample { + Sample { frames, busy, leaves, hidden } +} + +/// Runs `ticks` looks with the same sample; the verdicts other than `Wait`. +fn run(w: &mut Watch, s: Sample, ticks: u32) -> Vec<(u32, Verdict)> { + (1..=ticks).filter_map(|t| Some((t, w.step(s, WATCH_TICK))).filter(|(_, v)| *v != Verdict::Wait)).collect() +} + +const TICKS_PER_S: u32 = 4; + +/// The UI thread stuck in the graphics stack (not in the app's code, no progress) with the +/// window not known to be hidden: late after 20 s, hung after 60 s. +#[test] +fn a_ui_thread_stuck_in_the_graphics_stack_hangs_its_adapter() { + let mut w = Watch::default(); + let v = run(&mut w, sample(1, false, 1, false), 61 * TICKS_PER_S); + assert_eq!(v.first(), Some(&(20 * TICKS_PER_S, Verdict::Late))); + // `leaves` moved from 0 to 1 on the first look, so the stuck time starts one look later. + assert_eq!(v.get(1), Some(&(60 * TICKS_PER_S + 1, Verdict::Hung))); + // The second frame began: the first was presented. + assert_eq!(Watch::default().step(sample(2, false, 9, false), WATCH_TICK), Verdict::Shown); +} + +/// A busy UI thread (a large document composing on a slow machine) is only ever late. +#[test] +fn a_busy_ui_thread_never_hangs_its_adapter() { + let mut w = Watch::default(); + let v = run(&mut w, sample(0, true, 0, false), 600 * TICKS_PER_S); + assert_eq!(v, [(20 * TICKS_PER_S, Verdict::Late)]); + // Nor does one that keeps coming back from the graphics stack. + let mut w = Watch::default(); + for t in 0..(600 * TICKS_PER_S) { + assert_ne!(w.step(sample(1, false, u64::from(t), false), WATCH_TICK), Verdict::Hung); + } +} + +/// A window known to be hidden (locked screen, another workspace, minimized) may have its frames +/// held back by the compositor: that time counts for nothing and starts the stuck time over. +#[test] +fn time_while_the_window_is_hidden_does_not_count() { + let mut w = Watch::default(); + assert!(run(&mut w, sample(1, false, 0, true), 600 * TICKS_PER_S).is_empty()); + assert!(run(&mut w, sample(1, false, 0, false), 50 * TICKS_PER_S).iter().all(|(_, v)| *v == Verdict::Late)); + assert!(run(&mut w, sample(1, false, 0, true), 10 * TICKS_PER_S).is_empty()); + assert!(run(&mut w, sample(1, false, 0, false), 59 * TICKS_PER_S).is_empty()); + assert_eq!(run(&mut w, sample(1, false, 0, false), TICKS_PER_S), [(TICKS_PER_S, Verdict::Hung)]); +} + +#[test] +fn a_hung_adapter_is_left_out_once() { + let skip = vec!["Dx12:10de:28a0".to_string()]; + assert_eq!(restart_without(true, 1, Some("Dx12:10de:28a0"), &skip), None); + assert_eq!(restart_without(true, 1, Some("Dx12:8086:a7a8"), &skip), Some("Dx12:8086:a7a8")); +} + +/// Windows, a hardware DX12 adapter failing: the software renderer (WARP) is passed over while +/// another backend is left, the restarts go through OpenGL and Vulkan, and only then back to +/// every backend with software allowed. The chain ends. +#[test] +fn software_renderers_come_after_every_backends_hardware() { + use backend::Backend as B; + let c = ryzen_desktop(); + let order = adapter_order(&c, PowerPreference::LowPower, &[], None, &[]); + assert_eq!(without_software(order.clone(), &c, false), (order.clone(), false)); + let (hardware, dropped) = without_software(order, &c, true); + assert!(dropped && !hardware.contains(&4), "{hardware:?}"); + + let dx12 = "Dx12:10de:2204".to_string(); + // The adapter that failed first. + let (skip, _) = next_start(&[], Some(&dx12), Some((B::Dx12, Some(B::Gl))), true).unwrap(); + assert_eq!(skip, [dx12.as_str()]); + // Only WARP left on DX12 (passed over, nothing picked): OpenGL next. + let (skip, what) = next_start(&skip, None, Some((B::Dx12, Some(B::Gl))), true).unwrap(); + assert_eq!(skip, [dx12.as_str(), "Dx12", SOFTWARE_LEFT]); + assert!(what.contains("OpenGL"), "{what}"); + // OpenGL has no adapter, Vulkan next; then Vulkan has none and no backend is left. + let (skip, _) = next_start(&skip, None, Some((B::Gl, Some(B::Vulkan))), false).unwrap(); + assert_eq!(skip, [dx12.as_str(), "Dx12", SOFTWARE_LEFT, "Gl"]); + let (skip, what) = next_start(&skip, None, Some((B::Vulkan, None)), false).unwrap(); + assert_eq!(skip, [dx12.as_str(), SOFTWARE], "{what}"); + assert!(backend::skipped_backends(&skip).is_empty()); + // With software allowed and nothing left: the end. + assert_eq!(next_start(&skip, None, Some((B::Dx12, None)), false), None); + // Nothing was passed over (a Mac whose GPUs all failed): no software round. + assert_eq!(next_start(&[], None, Some((B::Metal, None)), false), None); + // Without the backend fallback (`WGPU_BACKEND`), only adapters. + assert_eq!(next_start(&[], None, None, false), None); +} + +/// The skip list travels as an argument, which processes the app starts don't inherit. +#[test] +fn the_skip_list_is_an_argument() { + assert_eq!(skip_arg("--gpu-skip=Dx12:10de:2204,Dx12"), Some(vec!["Dx12:10de:2204".to_string(), "Dx12".to_string()])); + assert_eq!(skip_arg("--gpu-skip="), Some(vec![])); + assert_eq!(skip_arg("--gpu-skip"), None); + assert_eq!(skip_arg("layout.designcraft"), None); +} + +#[test] +fn a_restart_tries_the_next_adapter() { + let c = wayland_desktop(); + let mut skip = vec![]; + let mut tried = vec![]; + while let Some(&i) = adapter_order(&c, PowerPreference::LowPower, &[], None, &skip).first() { + tried.push(c[i].name.clone()); + skip.push(c[i].key.clone()); + } + assert_eq!( + tried, + [ + "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)", + "NVIDIA GeForce RTX 5070", + "NVIDIA GeForce RTX 5070/PCIe/SSE2", + "llvmpipe (LLVM 20.1.8, 256 bits)" + ] + ); +} + +#[test] +fn adapters_that_cannot_present_to_the_window_are_never_tried() { + let mut c = wayland_desktop(); + c[0].presents = false; + c[3].presents = false; + for power in [PowerPreference::None, PowerPreference::LowPower, PowerPreference::HighPerformance] { + let order = adapter_order(&c, power, &[], None, &[]); + assert!(!order.contains(&0) && !order.contains(&3), "{power:?}: {order:?}"); + assert_eq!(order.len(), 2); + } + for p in &mut c { + p.presents = false; + } + assert!(adapter_order(&c, PowerPreference::None, &[], None, &[]).is_empty()); + assert!(adapter_order(&[], PowerPreference::HighPerformance, &[], None, &[]).is_empty()); +} + +#[test] +fn wgpu_adapter_name_picks_an_adapter_first_and_the_rest_stay_as_fallbacks() { + let c = wayland_desktop(); + let order = adapter_order(&c, PowerPreference::HighPerformance, &[], Some("radv"), &[]); + assert_eq!(c[order[0]].name, "AMD Ryzen 9 7900X (RADV RAPHAEL_MENDOCINO)"); + assert_eq!(order.len(), 4); + // Any case; every match comes before the rest, natively first. + let order = adapter_order(&c, PowerPreference::LowPower, &[], Some("GeForce"), &[]); + assert_eq!(names(&c, &order[..2]), ["NVIDIA GeForce RTX 5070", "NVIDIA GeForce RTX 5070/PCIe/SSE2"]); + // An empty name or one nothing matches changes nothing. + for named in ["", "no such gpu"] { + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], Some(named), &[]), adapter_order(&c, PowerPreference::LowPower, &[], None, &[])); + } + // A named adapter that failed is left out like any other. + let skip = vec![c[1].key.clone()]; + assert_eq!(adapter_order(&c, PowerPreference::None, &[], Some("radv"), &skip)[0], 0); +} + +/// A Windows machine lists every GPU twice when Vulkan and DX12 are both in the instance: +/// power saving still takes the integrated GPU first, each GPU through DX12 first. +#[test] +fn hybrid_laptop_on_windows_renders_on_the_integrated_gpu_with_power_saving() { + let c = vec![ + gpu("Intel(R) Arc(TM) A370M", Backend::Vulkan, DeviceType::DiscreteGpu), + gpu("Intel(R) Iris(R) Xe Graphics", Backend::Vulkan, DeviceType::IntegratedGpu), + gpu("Intel(R) Arc(TM) A370M", Backend::Dx12, DeviceType::DiscreteGpu), + gpu("Intel(R) Iris(R) Xe Graphics", Backend::Dx12, DeviceType::IntegratedGpu), + gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu), + ]; + let order = adapter_order(&c, PowerPreference::LowPower, &[], None, &[]); + assert_eq!(order, [3, 1, 2, 0, 4]); + assert_eq!(adapter_order(&c, PowerPreference::HighPerformance, &[], None, &[]), [2, 0, 3, 1, 4]); +} + +/// One Intel GPU whose Vulkan driver made the window flicker black: DX12 comes first whatever +/// the preference; Vulkan stays a fallback, and `WGPU_ADAPTER_NAME` or a restart leaving DX12 +/// out still reach it. +#[test] +fn a_windows_gpu_renders_through_dx12_before_vulkan() { + let c = vec![ + gpu("Intel(R) Graphics", Backend::Vulkan, DeviceType::IntegratedGpu), + gpu("Intel(R) Graphics", Backend::Dx12, DeviceType::IntegratedGpu), + gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu), + gpu("Intel(R) Graphics", Backend::Gl, DeviceType::Other), + ]; + for power in [PowerPreference::LowPower, PowerPreference::HighPerformance, PowerPreference::None] { + assert_eq!(adapter_order(&c, power, &[], None, &[]), [1, 0, 3, 2], "{power:?}"); + } + assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], None, &[c[1].key.clone()]), [0, 3, 2], "DX12 failed: Vulkan next"); +} + +#[test] +fn power_preference_follows_the_environment() { + assert_eq!(power_preference(None), AUTOMATIC); + for p in [PowerPreference::LowPower, PowerPreference::HighPerformance, PowerPreference::None] { + assert_eq!(power_preference(Some(p)), p); + } + assert_eq!(AUTOMATIC, if cfg!(any(windows, target_os = "macos")) { PowerPreference::LowPower } else { PowerPreference::None }); +} + +/// The Intel Mac of #334 lost its Metal device compiling the compute shader of wgpu's indirect +/// call check: off unless `WGPU_VALIDATION_INDIRECT_CALL` turns it on. +#[test] +fn the_indirect_call_check_is_off() { + if std::env::var_os("WGPU_VALIDATION_INDIRECT_CALL").is_none() { + assert!(!instance_flags().contains(wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL)); + } + // The other flags of the build stay as wgpu sets them. + let others = wgpu::InstanceFlags::from_build_config() - wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL; + if std::env::vars_os().all(|(k, _)| !k.to_string_lossy().starts_with("WGPU_")) { + assert_eq!(instance_flags(), others); + } +} + +#[test] +fn skipped_adapters_parse_from_a_comma_separated_list() { + assert_eq!(parse_skip("Vulkan:1002:164e, Gl:10de:2f04,,"), ["Vulkan:1002:164e", "Gl:10de:2f04"]); + assert!(parse_skip("").is_empty()); + assert_eq!(key(Backend::Vulkan, 0x1002, 0x164e, "AMD Radeon"), "Vulkan:1002:164e"); + assert_eq!(key(Backend::Gl, 0x10de, 0x2f04, "NVIDIA"), "Gl:10de:2f04"); + // A whole backend left out is its bare name; it never matches an adapter's key. + let skip = parse_skip("Dx12,Dx12:10de:2204"); + assert_eq!(backend::skipped_backends(&skip), [backend::Backend::Dx12]); + let c = vec![pci_gpu("NVIDIA GeForce RTX 3090", Backend::Dx12, DeviceType::DiscreteGpu, NVIDIA_3090)]; + assert!(adapter_order(&c, PowerPreference::None, &[], None, &skip).is_empty()); + assert_eq!(adapter_order(&c, PowerPreference::None, &[], None, &skip[..1]), [0]); +} + +#[test] +fn panics_in_wgpu_egui_wgpu_and_naga_are_graphics_failures() { + for file in [ + "/home/u/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/egui-wgpu-0.36.2/src/winit.rs", + r"C:\Users\u\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\wgpu-core-30.0.1\src\device\mod.rs", + "/rustc/deps/wgpu-hal-30.0.1/src/vulkan/adapter.rs", + "naga-30.0.0/src/back/spv/writer.rs", + ] { + assert!(in_graphics_stack(file), "{file}"); + } + for file in + ["apps/designcraft/src/main.rs", "/home/wgpu/designcraft/crates/ui-egui/src/lib.rs", "egui-0.36.2/src/context.rs", "winit-0.30.12/src/x.rs"] + { + assert!(!in_graphics_stack(file), "{file}"); + } +} + +#[test] +fn only_a_graphics_failure_while_starting_up_restarts_without_the_adapter() { + let amd = Some("Vulkan:1002:164e"); + assert_eq!(restart_without(true, 0, amd, &[]), amd); + assert_eq!(restart_without(true, STARTUP_FRAMES - 1, amd, &[]), amd); + // After start-up the adapter has shown frames; a lost window is something else. + assert_eq!(restart_without(true, STARTUP_FRAMES, amd, &[]), None); + // A bug outside the graphics, or no adapter picked: nothing another adapter would change. + assert_eq!(restart_without(false, 0, amd, &[]), None); + assert_eq!(restart_without(true, 0, None, &[]), None); + // Never the same adapter twice, so restarts end. + assert_eq!(restart_without(true, 0, amd, &["Vulkan:1002:164e".into()]), None); +} + +/// Without a backend fallback (`WGPU_BACKEND` set), a start whose graphics failed with no +/// adapter to leave out ends with the error and the hint; a bug ends with its message. +#[test] +fn a_normal_exit_or_another_failure_does_not_restart() { + let startup = Startup::new(None, Vec::new()); + assert_eq!(finish(Ok(Ok(())), &startup), Ok(())); + // No adapter was picked (none can show the window): the error is returned, nothing restarts. + let none = eframe::Error::Wgpu(eframe::egui_wgpu::WgpuError::CustomNativeAdapterSelectionError("no graphics adapter can show the window".into())); + assert_eq!( + finish(Ok(Err(none)), &startup), + Err(format!("WGPU error: Adapter selection failed: no graphics adapter can show the window ({HELP})")) + ); + let _ = startup.adapter.set("Vulkan:1002:164e".into()); + GRAPHICS_PANIC.store(false, Ordering::Relaxed); + assert_eq!(finish(Err("a bug".into()), &startup), Err("a bug".into())); + // Nothing to record without a fallback. + startup.presented(); + startup.started_with(wgpu::Backend::Gl); + assert_eq!(startup.status_line(), None); +}