graphics: draw with the newer driver when Windows lists one GPU through two versions of it (#609)
A PC whose GPUs were installed from different driver packages of one vendor (an ASUS G513QY: the integrated Radeon on the 2021 package, the RX 6800M on a 2026 one) gives the Vulkan loader two versions of that vendor's driver, and the RX 6800M is listed once through each. wgpu's request_adapter takes the first discrete entry, the 2021 driver's, and the window drawn through it dies inside amdvlk64.dll (0xc0000005) before it paints: the main thread is in the 2021 driver, the fault on a thread of the 2026 one, no Rust frame anywhere. Left to the #820 ladder, two crashed launches end in DirectX 12 only. filmcraft_gpu::adapter::choose is wgpu's own rule (the adapters that can draw to the surface, by device type for WGPU_POWER_PREF, then as enumerated) with one addition: when the adapter that rule picks is listed again by the same driver (same backend, device type, PCI vendor and device and driver name; the same bus id when both report one), the entry exposing the most wgpu features is used, i.e. the newer version (68 against 60 here, a strict superset). Without such a duplicate the choice is wgpu's own, and entries of one GPU from different drivers (RADV and AMDVLK, a vendor's driver and Dozen) are left alone. graphics::apply installs it as egui-wgpu's native_adapter_selector in the default mode on Windows, as #609 was asked to be rebased onto the #827 start-up. DirectX 12 only, software mode and the other platforms keep their adapter choice, and the ladder is untouched. The adapters and the choice go to the log. Tests: the laptop's listing (wgpu's rule picks the entry that crashed, choose the other one), listings without two versions of one driver over every power preference (choose equals wgpu's rule), surface, bus id and driver cases, and one against the adapters of the machine running the tests (the rule is what request_adapter returns there). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
654b2b8362
commit
1d1cec80b1
5 files changed
+406
-3
No files matched your search
@@ -347,6 +347,13 @@ Remove-Item Env:WGPU_BACKEND # restore the default for later
|
||||
An explicit `gl` override can bring the driver crash back on affected systems. The macOS, Linux
|
||||
and web backend defaults are unchanged.
|
||||
|
||||
A PC whose GPUs were installed from different driver packages of one vendor (a laptop with an
|
||||
integrated and a discrete AMD GPU, for example) can list one GPU twice, once through each installed
|
||||
version of the Vulkan driver, and a window drawn through the stale one crashes inside the driver
|
||||
(`amdvlk64.dll`, exit code `0xc0000005`). FilmCraft draws with the newer one and says so in
|
||||
`%APPDATA%\FilmCraft\Logs\filmcraft.log`; installing one driver package for every GPU removes the
|
||||
duplicate. With one driver per GPU the adapter is the one wgpu picks, as before.
|
||||
|
||||
### macOS
|
||||
|
||||
| Build | File | Notes |
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
//! `igvk64.dll`, #512 #413; AMD's OpenGL driver `atio6axx.dll`, #477), where no Rust error
|
||||
//! handling runs, so every launch died the same way until the user set `WGPU_BACKEND` by hand.
|
||||
//!
|
||||
//! # The adapter
|
||||
//! In the default mode on Windows the adapter is wgpu's own choice, except for a GPU that Vulkan
|
||||
//! lists through two installed versions of one driver: there the newer one draws, because a
|
||||
//! window drawn through the stale one dies inside the driver (`amdvlk64.dll`; see
|
||||
//! `filmcraft_gpu::adapter`). That case is avoided up front instead of left to the fallback
|
||||
//! below, which would end in DirectX 12 only.
|
||||
//!
|
||||
//! # When a start counts as failed
|
||||
//! The data folder keeps the mode to start with and the failed starts in a row in it
|
||||
//! ([`STATE_FILE`]). Each start also leaves its own attempt file (`graphics-start/<pid>-<n>.json`,
|
||||
@@ -435,7 +442,16 @@ fn apply_for(options: &mut NativeOptions, mode: Mode, backend_override: Option<B
|
||||
// Vulkan and DX12 both: forcing DX12 alone sends shaders through FXC (which rejects
|
||||
// fx.wgsl, silently falling back to the CPU) and leaves machines without DX12 with no
|
||||
// backend at all.
|
||||
Mode::Default if windows => create.instance_descriptor.backends = backend_override.unwrap_or(Backends::PRIMARY),
|
||||
Mode::Default if windows => {
|
||||
create.instance_descriptor.backends = backend_override.unwrap_or(Backends::PRIMARY);
|
||||
// wgpu's own choice of adapter, except for a GPU that Windows lists through two
|
||||
// versions of one driver: the first entry can be a stale one that crashes inside the
|
||||
// driver (`filmcraft_gpu::adapter`). A selector replaces `power_preference`
|
||||
// (`WGPU_POWER_PREF`), so it takes that along.
|
||||
let power = create.power_preference;
|
||||
create.native_adapter_selector =
|
||||
Some(Arc::new(move |adapters: &[wgpu::Adapter], surface: Option<&wgpu::Surface<'_>>| filmcraft_gpu::adapter::pick(adapters, surface, power)));
|
||||
}
|
||||
Mode::Default => {}
|
||||
// Without Vulkan, whose driver is what crashes; GPU effects may fall back to the CPU.
|
||||
Mode::Dx12 => create.instance_descriptor.backends = Backends::DX12,
|
||||
@@ -495,7 +511,19 @@ mod tests {
|
||||
assert_eq!(backends, Backends::PRIMARY);
|
||||
assert!(!backends.contains(Backends::GL));
|
||||
assert!(backends.contains(Backends::DX12) && backends.contains(Backends::VULKAN));
|
||||
assert!(!selector);
|
||||
// and chooses the adapter itself: a GPU listed through two versions of one driver
|
||||
assert!(selector);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_windows_default_and_software_modes_choose_the_adapter() {
|
||||
for backend_override in [None, Some(Backends::VULKAN), Some(Backends::DX12)] {
|
||||
assert!(configured(Mode::Default, backend_override, true).1);
|
||||
// elsewhere, and in DirectX 12 only mode, the adapter is wgpu's own choice
|
||||
assert!(!configured(Mode::Default, backend_override, false).1);
|
||||
assert!(!configured(Mode::Dx12, backend_override, true).1);
|
||||
}
|
||||
assert!(configured(Mode::Software, None, true).1 && configured(Mode::Software, None, false).1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
//! Which adapter to draw with when one GPU is listed through two versions of one driver.
|
||||
//!
|
||||
//! On Windows the Vulkan loader takes one driver from each display adapter's driver package. A PC
|
||||
//! whose GPUs were installed from different packages of one vendor (a laptop with an integrated
|
||||
//! and a discrete AMD GPU, the first on the manufacturer's old package and the second on a current
|
||||
//! one) loads two versions of that driver, and the newer GPU is listed once through each. wgpu
|
||||
//! takes the first entry of the device type it prefers, which is whichever version the loader
|
||||
//! listed first; a window drawn through the stale one kills the process inside the driver
|
||||
//! (`amdvlk64.dll`, 0xc0000005), where no Rust error handling runs.
|
||||
//!
|
||||
//! [`choose`] is wgpu's own rule (`Instance::request_adapter`: the adapters that can draw to the
|
||||
//! surface, by device type for the power preference, then as enumerated) with one addition: when
|
||||
//! the adapter that rule picks is listed again by the same driver, the entry exposing the most
|
||||
//! features is used, i.e. the newer version. Without such a duplicate the choice is wgpu's own.
|
||||
//! Entries of one GPU from different drivers (RADV and AMDVLK, a vendor's driver and Dozen) are
|
||||
//! left alone: those are installed side by side on purpose, and wgpu's order between them stays.
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::fmt;
|
||||
|
||||
use wgpu::{Adapter, Backend, DeviceType, PowerPreference, Surface};
|
||||
|
||||
/// One enumerated adapter as plain data, so that [`choose`] is a pure function tests run without
|
||||
/// a GPU.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Candidate {
|
||||
pub name: String,
|
||||
pub backend: Backend,
|
||||
pub device_type: DeviceType,
|
||||
/// PCI vendor and device ids; both zero when the backend reports none (OpenGL).
|
||||
pub vendor: u32,
|
||||
pub device: u32,
|
||||
/// PCI bus id such as `0000:03:00.0`; empty when the driver reports none.
|
||||
pub pci_bus_id: String,
|
||||
/// The driver's name (Vulkan) or version (DirectX 12).
|
||||
pub driver: String,
|
||||
/// More about the driver, for the log.
|
||||
pub driver_info: String,
|
||||
/// How many wgpu features the driver exposes; a newer version of one driver exposes more.
|
||||
pub features: u32,
|
||||
/// Whether the surface can be drawn through this adapter (true without a surface).
|
||||
pub usable: bool,
|
||||
}
|
||||
|
||||
impl Candidate {
|
||||
pub fn of(adapter: &Adapter, surface: Option<&Surface<'_>>) -> Self {
|
||||
let info = adapter.get_info();
|
||||
Candidate {
|
||||
name: info.name,
|
||||
backend: info.backend,
|
||||
device_type: info.device_type,
|
||||
vendor: info.vendor,
|
||||
device: info.device,
|
||||
pci_bus_id: info.device_pci_bus_id,
|
||||
driver: info.driver,
|
||||
driver_info: info.driver_info,
|
||||
features: adapter.features().bits().0.iter().map(|word| word.count_ones()).sum(),
|
||||
usable: surface.is_none_or(|s| adapter.is_surface_supported(s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Candidate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let kind = match self.device_type {
|
||||
DeviceType::DiscreteGpu => "discrete GPU",
|
||||
DeviceType::IntegratedGpu => "integrated GPU",
|
||||
DeviceType::VirtualGpu => "virtual GPU",
|
||||
DeviceType::Cpu => "software",
|
||||
DeviceType::Other => "unknown type",
|
||||
};
|
||||
let driver = format!("{} {}", self.driver, self.driver_info);
|
||||
write!(f, "\"{}\" ({:?}, {kind}, driver {}, {} features", self.name, self.backend, driver.trim(), self.features)?;
|
||||
if !self.pci_bus_id.is_empty() {
|
||||
write!(f, ", bus {}", self.pci_bus_id)?;
|
||||
}
|
||||
if !self.usable {
|
||||
write!(f, ", cannot draw to the window")?;
|
||||
}
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `a` and `b` are one GPU listed twice by one driver, i.e. through two installed
|
||||
/// versions of it: the same backend, device type, PCI vendor and device and driver name, and the
|
||||
/// same bus id when both report one. Adapters without PCI ids (OpenGL reports zeros) are never
|
||||
/// the same: two of them cannot be told apart.
|
||||
fn same_gpu_and_driver(a: &Candidate, b: &Candidate) -> bool {
|
||||
let identified = a.vendor != 0 || a.device != 0;
|
||||
let same_bus = a.pci_bus_id.is_empty() || b.pci_bus_id.is_empty() || a.pci_bus_id == b.pci_bus_id;
|
||||
identified && a.backend == b.backend && a.device_type == b.device_type && a.vendor == b.vendor && a.device == b.device && a.driver == b.driver && same_bus
|
||||
}
|
||||
|
||||
/// wgpu's device type order for `power` (wgpu-core's `Instance::request_adapter`): lower first.
|
||||
/// Without a preference the adapters stay as enumerated.
|
||||
fn type_order(device_type: DeviceType, power: PowerPreference) -> u8 {
|
||||
let prefer_integrated = match power {
|
||||
PowerPreference::LowPower => true,
|
||||
PowerPreference::HighPerformance => false,
|
||||
PowerPreference::None => return 0,
|
||||
};
|
||||
match device_type {
|
||||
DeviceType::DiscreteGpu if prefer_integrated => 2,
|
||||
DeviceType::IntegratedGpu if prefer_integrated => 1,
|
||||
DeviceType::DiscreteGpu => 1,
|
||||
DeviceType::IntegratedGpu => 2,
|
||||
DeviceType::Other => 3,
|
||||
DeviceType::VirtualGpu => 4,
|
||||
DeviceType::Cpu => 5,
|
||||
}
|
||||
}
|
||||
|
||||
/// The index of the adapter wgpu's `request_adapter` picks: the first usable one of the best
|
||||
/// device type for `power`. `None` without a usable adapter.
|
||||
fn wgpu_choice(candidates: &[Candidate], power: PowerPreference) -> Option<usize> {
|
||||
let usable = candidates.iter().enumerate().filter(|(_, c)| c.usable);
|
||||
usable.min_by_key(|(index, c)| (type_order(c.device_type, power), *index)).map(|(index, _)| index)
|
||||
}
|
||||
|
||||
/// The index of the adapter to draw with: [`wgpu_choice`], or the entry exposing the most
|
||||
/// features among those that are the same GPU listed by the same driver (the first of them when
|
||||
/// several expose as many). `None` without a usable adapter.
|
||||
pub fn choose(candidates: &[Candidate], power: PowerPreference) -> Option<usize> {
|
||||
let first = wgpu_choice(candidates, power)?;
|
||||
let Some(preferred) = candidates.get(first) else {
|
||||
return Some(first);
|
||||
};
|
||||
let versions = candidates.iter().enumerate().filter(|(_, c)| c.usable && same_gpu_and_driver(preferred, c));
|
||||
versions.max_by_key(|(index, c)| (c.features, Reverse(*index))).map(|(index, _)| index).or(Some(first))
|
||||
}
|
||||
|
||||
/// [`choose`] over enumerated adapters, as egui-wgpu's `native_adapter_selector` for the window
|
||||
/// (`surface`). The adapters and the choice go to the log.
|
||||
pub fn pick(adapters: &[Adapter], surface: Option<&Surface<'_>>, power: PowerPreference) -> Result<Adapter, String> {
|
||||
let candidates: Vec<Candidate> = adapters.iter().map(|a| Candidate::of(a, surface)).collect();
|
||||
for candidate in &candidates {
|
||||
log::info!("graphics adapter available: {candidate}");
|
||||
}
|
||||
let listed = || candidates.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
|
||||
let index = choose(&candidates, power).ok_or_else(|| format!("no usable graphics adapter among {}: {}", candidates.len(), listed()))?;
|
||||
let (Some(chosen), Some(adapter)) = (candidates.get(index), adapters.get(index)) else {
|
||||
return Err(format!("graphics adapter {index} of {} is missing", adapters.len()));
|
||||
};
|
||||
let others = candidates.iter().enumerate().filter(|(i, c)| *i != index && c.usable && same_gpu_and_driver(chosen, c)).count();
|
||||
if others > 0 {
|
||||
log::warn!(
|
||||
"{:?} lists \"{}\" {} times: several versions of its driver are installed, and a window drawn through a stale one can crash. Using the one with the most features; installing one driver package for every GPU removes the others.",
|
||||
chosen.backend,
|
||||
chosen.name,
|
||||
others.saturating_add(1)
|
||||
);
|
||||
}
|
||||
log::info!("graphics adapter chosen: {chosen}");
|
||||
Ok(adapter.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wgpu::Backend::{Dx12, Gl, Vulkan};
|
||||
use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other};
|
||||
use wgpu::PowerPreference::{HighPerformance, LowPower};
|
||||
|
||||
const AMD: u32 = 0x1002;
|
||||
const RX_6800M: u32 = 0x73df;
|
||||
const RADEON_IGPU: u32 = 0x1638;
|
||||
const POWERS: [PowerPreference; 3] = [PowerPreference::None, LowPower, HighPerformance];
|
||||
|
||||
#[expect(clippy::too_many_arguments, reason = "one line per adapter in the listings below")]
|
||||
fn gpu(
|
||||
name: &str,
|
||||
backend: Backend,
|
||||
device_type: DeviceType,
|
||||
vendor: u32,
|
||||
device: u32,
|
||||
bus: &str,
|
||||
driver: &str,
|
||||
driver_info: &str,
|
||||
features: u32,
|
||||
) -> Candidate {
|
||||
Candidate {
|
||||
name: name.into(),
|
||||
backend,
|
||||
device_type,
|
||||
vendor,
|
||||
device,
|
||||
pci_bus_id: bus.into(),
|
||||
driver: driver.into(),
|
||||
driver_info: driver_info.into(),
|
||||
features,
|
||||
usable: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// An ASUS G513QY as wgpu 30 lists it: the integrated Radeon is on the manufacturer's 2021
|
||||
/// driver package and the RX 6800M on a 2026 one, so the Vulkan loader has both versions of
|
||||
/// AMD's driver, and the 2021 one lists the RX 6800M first. A window drawn through that entry
|
||||
/// died inside `amdvlk64.dll` (0xc0000005) on every launch.
|
||||
fn g513qy() -> Vec<Candidate> {
|
||||
let amd = "AMD proprietary driver";
|
||||
vec![
|
||||
gpu("AMD Radeon RX 6800M", Vulkan, DiscreteGpu, AMD, RX_6800M, "", amd, "26.9.2", 60),
|
||||
gpu("AMD Radeon RX 6800M", Vulkan, DiscreteGpu, AMD, RX_6800M, "", amd, "26.9.2 (AMD proprietary shader compiler)", 68),
|
||||
gpu("AMD Radeon(TM) Graphics", Vulkan, IntegratedGpu, AMD, RADEON_IGPU, "", amd, "21.30.02.13", 56),
|
||||
gpu("AMD Radeon RX 6800M", Dx12, DiscreteGpu, AMD, RX_6800M, "0000:03:00.0", "32.0.21045.11001", "", 36),
|
||||
gpu("AMD Radeon(TM) Graphics", Dx12, IntegratedGpu, AMD, RADEON_IGPU, "0000:08:00.0", "30.0.13002.13003", "", 36),
|
||||
gpu("Microsoft Basic Render Driver", Dx12, Cpu, 0x1414, 0x8c, "", "10.0.26100.9549", "", 36),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gpu_listed_through_two_versions_of_its_driver_draws_with_the_newer_one() {
|
||||
let adapters = g513qy();
|
||||
// the window asks for high performance; `WGPU_POWER_PREF=none` for no preference
|
||||
for power in [HighPerformance, PowerPreference::None] {
|
||||
assert_eq!(wgpu_choice(&adapters, power), Some(0), "wgpu takes the stale entry, which crashed");
|
||||
assert_eq!(choose(&adapters, power), Some(1));
|
||||
}
|
||||
// whichever version the loader lists first
|
||||
let mut newer_first = adapters.clone();
|
||||
newer_first.swap(0, 1);
|
||||
assert_eq!(choose(&newer_first, HighPerformance), Some(0));
|
||||
// two entries exposing as much: wgpu's, the first
|
||||
let mut equal = adapters.clone();
|
||||
if let Some(c) = equal.get_mut(1) {
|
||||
c.features = 60;
|
||||
}
|
||||
assert_eq!(choose(&equal, HighPerformance), Some(0));
|
||||
// three versions: the one exposing the most
|
||||
let mut three = adapters.clone();
|
||||
three.insert(1, gpu("AMD Radeon RX 6800M", Vulkan, DiscreteGpu, AMD, RX_6800M, "", "AMD proprietary driver", "24.1.1", 64));
|
||||
assert_eq!(choose(&three, HighPerformance), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_newer_version_never_overtakes_another_gpu_or_backend() {
|
||||
// low power: the integrated GPU, as wgpu picks, whatever the discrete one is listed as
|
||||
assert_eq!(choose(&g513qy(), LowPower), Some(2));
|
||||
// DirectX 12 enumerated first: its entry of the GPU stays the choice
|
||||
let mut dx12_first = g513qy();
|
||||
dx12_first.rotate_left(3);
|
||||
for power in [HighPerformance, PowerPreference::None] {
|
||||
assert_eq!(choose(&dx12_first, power), Some(0));
|
||||
}
|
||||
assert_eq!(choose(&dx12_first, LowPower), wgpu_choice(&dx12_first, LowPower));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_two_versions_of_one_driver_the_choice_is_wgpus() {
|
||||
let nvidia = |backend, bus, driver, features| gpu("NVIDIA GeForce RTX 4070", backend, DiscreteGpu, 0x10de, 0x2786, bus, driver, "", features);
|
||||
let intel = |backend, bus, driver, features| gpu("Intel(R) UHD Graphics 770", backend, IntegratedGpu, 0x8086, 0x4680, bus, driver, "", features);
|
||||
let radeon = |bus, driver, features| gpu("AMD Radeon RX 7900 XT", Vulkan, DiscreteGpu, AMD, 0x744c, bus, driver, "", features);
|
||||
let gl = |name: &str, features| gpu(name, Gl, Other, 0, 0, "", "", "", features);
|
||||
let without = |skip: usize| g513qy().into_iter().enumerate().filter(|(i, _)| *i != skip).map(|(_, c)| c).collect::<Vec<_>>();
|
||||
let listings: Vec<Vec<Candidate>> = vec![
|
||||
vec![],
|
||||
vec![nvidia(Vulkan, "0000:01:00.0", "NVIDIA", 70)],
|
||||
// a laptop with two GPUs, through Vulkan and DirectX 12
|
||||
vec![
|
||||
intel(Vulkan, "0000:00:02.0", "Intel Corporation", 60),
|
||||
nvidia(Vulkan, "0000:01:00.0", "NVIDIA", 70),
|
||||
nvidia(Dx12, "0000:01:00.0", "32.0.15.6094", 36),
|
||||
intel(Dx12, "0000:00:02.0", "31.0.101.5186", 36),
|
||||
gpu("Microsoft Basic Render Driver", Dx12, Cpu, 0x1414, 0x8c, "", "10.0.26100.1", "", 36),
|
||||
],
|
||||
// one GPU through two different drivers, the second exposing more: Linux with AMDVLK and RADV
|
||||
vec![radeon("0000:03:00.0", "AMD open-source driver", 58), radeon("0000:03:00.0", "radv", 71)],
|
||||
// a vendor's driver and Dozen (Vulkan on DirectX 12)
|
||||
vec![nvidia(Vulkan, "", "Dozen", 75), nvidia(Vulkan, "0000:01:00.0", "NVIDIA", 70)],
|
||||
// two cards of one model, on their own buses
|
||||
vec![radeon("0000:03:00.0", "AMD proprietary driver", 60), radeon("0000:04:00.0", "AMD proprietary driver", 68)],
|
||||
// OpenGL reports no ids: two adapters cannot be told apart
|
||||
vec![gl("ANGLE", 20), gl("ANGLE", 30)],
|
||||
vec![gpu("llvmpipe (LLVM 19.1.0, 256 bits)", Vulkan, Cpu, 0x10005, 0, "", "llvmpipe", "", 50)],
|
||||
// the laptop above with either version of the driver alone
|
||||
without(0),
|
||||
without(1),
|
||||
];
|
||||
for adapters in &listings {
|
||||
for power in POWERS {
|
||||
assert_eq!(choose(adapters, power), wgpu_choice(adapters, power), "{power:?} over {adapters:#?}");
|
||||
}
|
||||
}
|
||||
assert_eq!(choose(&[], HighPerformance), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_entries_are_one_gpu_only_with_the_same_ids_type_driver_and_bus() {
|
||||
let stale = g513qy().swap_remove(0);
|
||||
let newer = g513qy().swap_remove(1);
|
||||
assert!(same_gpu_and_driver(&stale, &newer) && same_gpu_and_driver(&stale, &stale));
|
||||
// a bus id only one version reports does not tell them apart; two different ones do
|
||||
let on_bus = |bus: &str| Candidate { pci_bus_id: bus.into(), ..newer.clone() };
|
||||
assert!(same_gpu_and_driver(&stale, &on_bus("0000:03:00.0")));
|
||||
assert!(same_gpu_and_driver(&on_bus("0000:03:00.0"), &on_bus("0000:03:00.0")));
|
||||
assert!(!same_gpu_and_driver(&on_bus("0000:03:00.0"), &on_bus("0000:04:00.0")));
|
||||
for other in [
|
||||
Candidate { backend: Dx12, ..newer.clone() },
|
||||
Candidate { device_type: IntegratedGpu, ..newer.clone() },
|
||||
Candidate { vendor: 0x10de, ..newer.clone() },
|
||||
Candidate { device: RADEON_IGPU, ..newer.clone() },
|
||||
Candidate { driver: "radv".into(), ..newer.clone() },
|
||||
] {
|
||||
assert!(!same_gpu_and_driver(&stale, &other), "{other}");
|
||||
}
|
||||
let unidentified = Candidate { vendor: 0, device: 0, ..newer };
|
||||
assert!(!same_gpu_and_driver(&unidentified, &unidentified));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapters_that_cannot_draw_to_the_window_are_left_out() {
|
||||
let unusable = |which: &dyn Fn(usize, &Candidate) -> bool| {
|
||||
let mut adapters = g513qy();
|
||||
for (i, c) in adapters.iter_mut().enumerate() {
|
||||
c.usable = !which(i, c);
|
||||
}
|
||||
adapters
|
||||
};
|
||||
// the newer version cannot: the other entry is all there is of that GPU through Vulkan
|
||||
assert_eq!(choose(&unusable(&|i, _| i == 1), HighPerformance), Some(0));
|
||||
assert_eq!(choose(&unusable(&|i, _| i == 0), HighPerformance), Some(1));
|
||||
// Vulkan cannot at all: DirectX 12
|
||||
assert_eq!(choose(&unusable(&|_, c| c.backend == Vulkan), HighPerformance), Some(3));
|
||||
assert_eq!(choose(&unusable(&|_, _| true), HighPerformance), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidates_describe_themselves_for_the_log() {
|
||||
let adapters = g513qy();
|
||||
let text = adapters.get(1).map(ToString::to_string).unwrap_or_default();
|
||||
assert_eq!(text, "\"AMD Radeon RX 6800M\" (Vulkan, discrete GPU, driver AMD proprietary driver 26.9.2 (AMD proprietary shader compiler), 68 features)");
|
||||
let offscreen = adapters.get(4).map(|c| Candidate { usable: false, ..c.clone() }.to_string()).unwrap_or_default();
|
||||
assert_eq!(
|
||||
offscreen,
|
||||
"\"AMD Radeon(TM) Graphics\" (Dx12, integrated GPU, driver 30.0.13002.13003, 36 features, bus 0000:08:00.0, cannot draw to the window)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Against the adapters of the machine running the tests: [`wgpu_choice`] is what wgpu's
|
||||
/// `request_adapter` returns, and [`pick`] returns that adapter too, or a version of it
|
||||
/// exposing more features.
|
||||
#[test]
|
||||
fn on_this_host_the_choice_is_wgpus_unless_its_driver_is_listed_twice() {
|
||||
if wgpu::Instance::enabled_backend_features().is_empty() {
|
||||
return;
|
||||
}
|
||||
// no OpenGL: the app leaves it out on Windows, where creating it can crash a driver
|
||||
let backends = wgpu::Backends::PRIMARY;
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends, ..wgpu::InstanceDescriptor::new_without_display_handle() });
|
||||
let adapters = pollster::block_on(instance.enumerate_adapters(backends));
|
||||
let candidates: Vec<Candidate> = adapters.iter().map(|a| Candidate::of(a, None)).collect();
|
||||
for power in POWERS {
|
||||
let options = wgpu::RequestAdapterOptions { power_preference: power, ..Default::default() };
|
||||
let wgpu_own = pollster::block_on(instance.request_adapter(&options)).ok().map(|a| Candidate::of(&a, None));
|
||||
assert_eq!(wgpu_choice(&candidates, power).and_then(|i| candidates.get(i)), wgpu_own.as_ref(), "{power:?} over {candidates:#?}");
|
||||
let picked = pick(&adapters, None, power).ok().map(|a| Candidate::of(&a, None));
|
||||
match (&wgpu_own, &picked) {
|
||||
(Some(wgpu_own), Some(picked)) if wgpu_own != picked => {
|
||||
assert!(same_gpu_and_driver(wgpu_own, picked) && picked.features > wgpu_own.features, "{power:?}: {picked} instead of {wgpu_own}");
|
||||
}
|
||||
_ => assert_eq!(picked, wgpu_own, "{power:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ use filmcraft_frame::{Chroma, PixelData, VideoFrame};
|
||||
use filmcraft_render::Blend;
|
||||
use filmcraft_render::plan::{FramePlan, PlanLayer, PlanStep};
|
||||
|
||||
pub mod adapter;
|
||||
pub mod export_renderer;
|
||||
pub mod fx;
|
||||
pub mod lut;
|
||||
|
||||
+3
-1
@@ -85,7 +85,9 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
(Windows), then a software adapter (WARP, lavapipe, llvmpipe) with CPU compositing, and keeps
|
||||
the mode that works; a start-up error wgpu reports moves on in the same process. A fallback mode
|
||||
shows a notice with Use Default Graphics; `--graphics default|dx12|software` picks a mode by
|
||||
hand. An explicit `WGPU_BACKEND` stays in charge.
|
||||
hand. An explicit `WGPU_BACKEND` stays in charge. On Windows, a GPU that Vulkan lists through two
|
||||
installed versions of one driver (GPUs on different driver packages) draws with the newer one
|
||||
instead of dying inside the stale one (`filmcraft_gpu::adapter`).
|
||||
- **Missing:** a launch that never fails, and CI that runs the gates.
|
||||
- **Evidence:** open issues: macOS 12 Monterey aborts on a missing VideoToolbox symbol (#468,
|
||||
#661, #655), Intel UHD graphics (`igvk64.dll`, #512), Windows 11 does not run or fails after setup
|
||||
|
||||
Reference in new issue
Block a user