Linux: Automatic draws on the GPU the Wayland compositor draws with

On a hybrid laptop whose Wayland compositor runs on the discrete GPU
(Hyprland with AQ_DRM_DEVICES naming the NVIDIA card first), the built-in
panel still hangs off the integrated GPU, so the display rule put it first,
and the window stayed empty: the compositor took the frames and showed
nothing, with no error, so neither the restart on a failed adapter (#502)
nor the first-frame watch (#964) noticed.

Which GPU drives a display stands in for which GPU the compositor draws
with. Under Wayland the compositor says so itself: the main_device of its
linux-dmabuf feedback (version 4), the device it imports client buffers on,
which Mesa's Vulkan device-select layer reads for the same purpose.
compositor_gpu.rs asks for it on a short-lived connection, maps the dev_t
to PCI ids through /sys/dev/char, and under Automatic adapter_order ranks
that adapter ahead of the display GPUs. No answer (X11, an older compositor,
a GPU no adapter matches) leaves the existing rules in charge; a user's own
choice, WGPU_ADAPTER_NAME and the restart skip list still come first.

wayland-client and wayland-protocols are already in the tree through winit;
they become direct Linux-only dependencies.
This commit is contained in:
Atos Lins authored and bflatastic committed 2026-10-11 22:18:06 +07:00
1 parent cfe0a779f0
commit 9c40a2643f
6 files changed
+304 -47

No files matched your search

+6
View File
@@ -67,6 +67,12 @@ objc2-core-text = { version = "0.3", default-features = false, features = ["std"
# already in the build through eframe.
pollster = "1"
[target.'cfg(target_os = "linux")'.dependencies]
# Asks the Wayland compositor which GPU it draws with (linux-dmabuf feedback, src/compositor_gpu.rs),
# so Automatic draws on that one. Both are already in the build through winit.
wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client"] }
[build-dependencies]
# Icon + VERSIONINFO for vectorcraft.exe; build.rs only uses it when targeting Windows.
winresource = "0.1"
+178
View File
@@ -0,0 +1,178 @@
//! The GPU the Wayland compositor draws with ([`crate::gpu`] ranks it first under Automatic).
//!
//! A window drawn on another GPU reaches the compositor as that GPU's buffers, and the compositor
//! may not show them: it can refuse them and end the window's connection (#502), or take them and
//! show nothing, with no error on either side, so neither the restart on a failed adapter nor
//! the first-frame watch notices. The second happened on a hybrid laptop whose Hyprland runs on
//! the NVIDIA GPU (`AQ_DRM_DEVICES`) while the built-in panel hangs off the Intel one: the panel
//! rule picked Intel and the window stayed empty. Which GPU drives a display is a guess at the
//! compositor's GPU; the compositor states it, as the `main_device` of its linux-dmabuf feedback
//! (protocol version 4): the device it imports client buffers on.
#[cfg(target_os = "linux")]
use wayland_client::globals::{GlobalListContents, registry_queue_init};
#[cfg(target_os = "linux")]
use wayland_client::protocol::wl_registry::{self, WlRegistry};
#[cfg(target_os = "linux")]
use wayland_client::{Connection, Dispatch, QueueHandle};
#[cfg(target_os = "linux")]
use wayland_protocols::wp::linux_dmabuf::zv1::client::zwp_linux_dmabuf_feedback_v1::{self, ZwpLinuxDmabufFeedbackV1};
#[cfg(target_os = "linux")]
use wayland_protocols::wp::linux_dmabuf::zv1::client::zwp_linux_dmabuf_v1::{self, ZwpLinuxDmabufV1};
/// PCI `(vendor, device)` ids of the GPU the Wayland compositor draws with. `None` outside a
/// Wayland session, when the compositor doesn't say (linux-dmabuf older than version 4), when its
/// device isn't a PCI GPU, and on other systems.
#[cfg(target_os = "linux")]
pub(crate) fn compositor_gpu() -> Option<(u32, u32)> {
let device = match main_device() {
Ok(device) => device,
Err(why) => {
log::debug!("the compositor's GPU is unknown: {why}");
return None;
}
};
let pci = pci_ids(std::path::Path::new("/sys/dev/char"), device);
match pci {
Some((vendor, id)) => log::info!("the compositor draws with {vendor:04x}:{id:04x}"),
None => log::debug!("the compositor's device {:?} has no PCI ids", major_minor(device)),
}
pci
}
#[cfg(not(target_os = "linux"))]
pub(crate) fn compositor_gpu() -> Option<(u32, u32)> {
None
}
/// The `main_device` of the compositor's default linux-dmabuf feedback, asked on a connection of
/// its own that is closed again before the window opens.
#[cfg(target_os = "linux")]
fn main_device() -> Result<u64, String> {
let conn = Connection::connect_to_env().map_err(|e| format!("no Wayland compositor: {e}"))?;
let (globals, mut queue) = registry_queue_init::<Feedback>(&conn).map_err(|e| e.to_string())?;
let qh = queue.handle();
// Version 4 brought the feedback; older compositors only list formats.
let dmabuf: ZwpLinuxDmabufV1 = globals.bind(&qh, 4..=4, ()).map_err(|e| format!("linux-dmabuf version 4: {e}"))?;
let _feedback = dmabuf.get_default_feedback(&qh, ());
let mut state = Feedback::default();
// The compositor sends the whole feedback, ending with `done`, in reply; the bound only keeps
// a compositor that never ends it from holding up the start.
for _ in 0..4 {
if state.done {
break;
}
queue.roundtrip(&mut state).map_err(|e| e.to_string())?;
}
// Dropping the connection releases the objects.
let device = state.main_device.ok_or("the compositor named no main device")?;
// A `dev_t`, in this machine's byte order.
let device: [u8; 8] = device.try_into().map_err(|d: Vec<u8>| format!("a {}-byte device number", d.len()))?;
Ok(u64::from_ne_bytes(device))
}
/// What [`main_device`] collects from the feedback events.
#[cfg(target_os = "linux")]
#[derive(Default)]
struct Feedback {
main_device: Option<Vec<u8>>,
done: bool,
}
#[cfg(target_os = "linux")]
impl Dispatch<WlRegistry, GlobalListContents> for Feedback {
fn event(_: &mut Self, _: &WlRegistry, _: wl_registry::Event, _: &GlobalListContents, _: &Connection, _: &QueueHandle<Self>) {}
}
#[cfg(target_os = "linux")]
impl Dispatch<ZwpLinuxDmabufV1, ()> for Feedback {
// Version 4 sends no format or modifier events: the feedback replaces them.
fn event(_: &mut Self, _: &ZwpLinuxDmabufV1, _: zwp_linux_dmabuf_v1::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {}
}
#[cfg(target_os = "linux")]
impl Dispatch<ZwpLinuxDmabufFeedbackV1, ()> for Feedback {
fn event(
state: &mut Self,
_: &ZwpLinuxDmabufFeedbackV1,
event: zwp_linux_dmabuf_feedback_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zwp_linux_dmabuf_feedback_v1::Event::MainDevice { device } => state.main_device = Some(device),
zwp_linux_dmabuf_feedback_v1::Event::Done => state.done = true,
_ => {}
}
}
}
/// The major and minor numbers of a Linux `dev_t`, as glibc's and musl's `major()` and `minor()`
/// split it.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn major_minor(device: u64) -> (u64, u64) {
let major = ((device & 0x0000_0000_000f_ff00) >> 8) | ((device & 0xffff_f000_0000_0000) >> 32);
let minor = (device & 0x0000_0000_0000_00ff) | ((device & 0x0000_0fff_fff0_0000) >> 12);
(major, minor)
}
/// The PCI ids of the GPU behind DRM device `device` (its `cardN` or `renderDN` node), from
/// `<sys_dev_char>/MAJOR:MINOR/device/{vendor,device}`.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn pci_ids(sys_dev_char: &std::path::Path, device: u64) -> Option<(u32, u32)> {
let read_hex = |p: std::path::PathBuf| -> Option<u32> {
let s = std::fs::read_to_string(p).ok()?;
u32::from_str_radix(s.trim().trim_start_matches("0x"), 16).ok()
};
let (major, minor) = major_minor(device);
let gpu = sys_dev_char.join(format!("{major}:{minor}")).join("device");
Some((read_hex(gpu.join("vendor"))?, read_hex(gpu.join("device"))?))
}
#[cfg(test)]
mod tests {
/// glibc's `makedev()`.
fn makedev(major: u64, minor: u64) -> u64 {
((major & 0x0000_0fff) << 8) | ((major & 0xffff_f000) << 32) | (minor & 0x0000_00ff) | ((minor & 0xffff_ff00) << 12)
}
#[test]
fn device_numbers_split_into_major_and_minor() {
// DRM nodes: card1 is 226:1, renderD128 is 226:128.
assert_eq!(super::major_minor(0xe201), (226, 1));
assert_eq!(super::major_minor(0xe280), (226, 128));
for (major, minor) in [(0, 0), (226, 129), (511, 1_048_575), (0xfff, 0xff), (0x1000, 0x100), (0xffff_ffff, 0xffff_ffff)] {
assert_eq!(super::major_minor(makedev(major, minor)), (major, minor));
}
}
#[cfg(target_os = "linux")]
#[test]
fn the_gpu_behind_a_device_number_comes_from_sysfs() -> std::io::Result<()> {
let dir = std::env::temp_dir().join(format!("vectorcraft-dev-char-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
// /sys/dev/char/226:128 links to the render node, whose `device` is the PCI GPU.
std::fs::create_dir_all(dir.join("226:128/device"))?;
std::fs::write(dir.join("226:128/device/vendor"), "0x10de\n")?;
std::fs::write(dir.join("226:128/device/device"), "0x25ac\n")?;
// A device that isn't a PCI GPU (a virtual one) has no ids.
std::fs::create_dir_all(dir.join("226:0/device"))?;
let render = super::pci_ids(&dir, makedev(226, 128));
let virtual_gpu = super::pci_ids(&dir, makedev(226, 0));
let missing = super::pci_ids(&dir, makedev(226, 1));
std::fs::remove_dir_all(&dir)?;
assert_eq!(render, Some((0x10de, 0x25ac)));
assert_eq!(virtual_gpu, None);
assert_eq!(missing, None);
Ok(())
}
#[test]
fn the_compositor_names_a_pci_gpu_or_nothing() {
// Whatever this session has: no compositor (CI), one that doesn't say, or a PCI GPU.
if let Some((vendor, _)) = super::compositor_gpu() {
assert_ne!(vendor, 0);
}
}
}
+110 -45
View File
@@ -4,8 +4,9 @@
//! The canvas is rasterized on the CPU; the GPU only composites it and draws the UI, so any
//! adapter that can present to the window will do. [`adapter_order`] ranks the adapters: those that
//! can't present to the window's surface never, then the one `WGPU_ADAPTER_NAME` names, hardware
//! before software, the native backends (Vulkan, Metal, DX12) before OpenGL, the GPU that drives a
//! display and the one that drives the primary display ([`display_gpus`]), the power preference
//! before software, the native backends (Vulkan, Metal, DX12) before OpenGL, the GPU the Wayland
//! compositor draws with ([`crate::compositor_gpu`]), the GPU that drives a display and the one
//! that drives the primary display ([`display_gpus`]), the power preference
//! ([`power_preference`]) among the rest, and a GPU's DX12 adapter before its Vulkan one (#545).
//! eframe is handed [`selector`], which takes the first of that order.
//!
@@ -19,6 +20,10 @@
//! matches, the order is the power preference's as before. Power Saving and High Performance,
//! chosen by the user, order by kind as they always did.
//!
//! Under Wayland, the compositor's own GPU comes before the displays: the compositor says which
//! GPU it draws with, and frames from another one may never reach the screen, with no error, even
//! from the GPU behind the built-in panel when the compositor runs on the discrete one.
//!
//! An adapter can report that it presents to the window and still fail once it does: on a hybrid
//! Linux desktop under Wayland, the compositor runs on one GPU and may refuse the frame buffers
//! another GPU allocates, which kills the window's Wayland connection; egui-wgpu then panics while
@@ -93,6 +98,12 @@ pub fn preferred_displays(pref: Option<&str>, env: Option<PowerPreference>) -> V
if automatic(pref, env) { display_gpus() } else { Vec::new() }
}
/// The GPU to rank first: the one the Wayland compositor draws with, when the choice is
/// [`automatic`].
pub fn preferred_compositor(pref: Option<&str>, env: Option<PowerPreference>) -> Option<(u32, u32)> {
if automatic(pref, env) { crate::compositor_gpu::compositor_gpu() } else { None }
}
/// The graphics backends the window starts with: on Windows Direct3D 12, falling back to OpenGL,
/// unless `WGPU_BACKEND` names others (`env_set`; `default` then holds them). Creating a Vulkan
/// instance loads every installed Vulkan driver into the process, and a faulty one (Intel's
@@ -130,6 +141,11 @@ impl Candidate {
}
}
/// Whether it is the GPU the compositor draws with (`compositor`, by PCI ids).
fn composites(&self, compositor: Option<(u32, u32)>) -> bool {
compositor == Some(self.pci)
}
/// Whether it drives one of `displays`, and whether that is the primary one.
fn drives(&self, displays: &[DisplayGpu]) -> (bool, bool) {
let shown = displays.iter().find(|g| g.pci == self.pci);
@@ -266,15 +282,22 @@ fn key(backend: Backend, vendor: u32, device: u32, name: &str) -> String {
/// The order to try `candidates` in (their indices): only those that present to the window and
/// aren't in `skip`; the one whose name contains `named` (any case) first; hardware before
/// software; native backends before OpenGL; the GPUs that drive one of `displays`, the primary
/// display's first; then by `power`; then DX12 before Vulkan, keeping the system's order among
/// software; native backends before OpenGL; the GPU the compositor draws with (`compositor`); the
/// GPUs that drive one of `displays`, the primary display's first; then by `power`; then DX12 before Vulkan, keeping the system's order among
/// equals (all of it for [`PowerPreference::None`]). Windows lists each GPU under both, and Intel's
/// Vulkan driver made the whole window flicker black where DX12 and OpenGL didn't (#545);
/// elsewhere there is no DX12 adapter, so the order is unchanged.
///
/// With no `displays`, or none a candidate's PCI ids match (OpenGL reports no device id, Metal no
/// ids at all), the order is the power preference's alone, as it was.
pub fn adapter_order(candidates: &[Candidate], power: PowerPreference, displays: &[DisplayGpu], named: Option<&str>, skip: &[String]) -> Vec<usize> {
pub fn adapter_order(
candidates: &[Candidate],
power: PowerPreference,
displays: &[DisplayGpu],
compositor: Option<(u32, u32)>,
named: Option<&str>,
skip: &[String],
) -> Vec<usize> {
let named = named.map(str::to_lowercase).filter(|n| !n.is_empty());
let mut order: Vec<(usize, &Candidate)> = candidates.iter().enumerate().filter(|(_, c)| c.presents && !skip.contains(&c.key)).collect();
// Stable, so equals keep the system's order.
@@ -285,6 +308,7 @@ pub fn adapter_order(candidates: &[Candidate], power: PowerPreference, displays:
unnamed,
c.device_type == DeviceType::Cpu,
c.backend == Backend::Gl,
!c.composites(compositor),
!drives_display,
!drives_primary,
power_rank(c.device_type, power),
@@ -400,15 +424,21 @@ pub fn watch_first_frame(startup: Arc<Startup>) {
}
}
/// eframe's adapter choice: the first of [`adapter_order`] for `power`, the GPUs that drive a
/// `displays`, `WGPU_ADAPTER_NAME` and the adapters a restart left out. Every adapter, the displays
/// eframe's adapter choice: the first of [`adapter_order`] for `power`, the GPU the `compositor`
/// draws with, the GPUs that drive a `displays`, `WGPU_ADAPTER_NAME` and the adapters a restart
/// left out. Every adapter, the displays
/// and the choice are logged: which GPU draws is the first question in every black-window report.
pub fn selector(power: PowerPreference, displays: Vec<DisplayGpu>, startup: Arc<Startup>) -> NativeAdapterSelectorMethod {
pub fn selector(
power: PowerPreference,
displays: Vec<DisplayGpu>,
compositor: Option<(u32, u32)>,
startup: Arc<Startup>,
) -> NativeAdapterSelectorMethod {
let named = std::env::var(NAME_ENV).ok();
let skip = skipped();
Arc::new(move |adapters, surface| {
let candidates: Vec<Candidate> = adapters.iter().map(|a| Candidate::new(a, surface)).collect();
let order = adapter_order(&candidates, power, &displays, named.as_deref(), &skip);
let order = adapter_order(&candidates, power, &displays, compositor, named.as_deref(), &skip);
let listed: Vec<String> = candidates
.iter()
.map(|c| {
@@ -417,6 +447,8 @@ pub fn selector(power: PowerPreference, displays: Vec<DisplayGpu>, startup: Arc<
", can't show the window"
} else if skip.contains(&c.key) {
", failed before"
} else if c.composites(compositor) {
", the compositor draws with it"
} else if drives_primary {
", drives the primary display"
} else if drives_display {
@@ -433,6 +465,7 @@ pub fn selector(power: PowerPreference, displays: Vec<DisplayGpu>, startup: Arc<
return Err(format!("no graphics adapter can show the window (power preference {power:?}, {} adapters)", adapters.len()));
};
let why = match chosen.drives(&displays) {
_ if chosen.composites(compositor) => "the compositor draws with it".to_string(),
(_, true) => "it drives the primary display".to_string(),
(true, false) => "it drives a display".to_string(),
(false, false) if displays.is_empty() => format!("power preference {power:?}"),
@@ -555,14 +588,14 @@ mod tests {
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:?}");
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, None, &[]), [2, 0, 3, 1, 5, 4], "{displays:?}");
assert_eq!(adapter_order(&c, PowerPreference::None, &displays, None, None, &[]), [2, 0, 3, 1, 5, 4], "{displays:?}");
}
// Without the display information the order is power saving's, as before.
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], None, &[]), [3, 1, 2, 0, 5, 4]);
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], None, 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]);
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[primary(NVIDIA_3090)], None, None, &skip), [3, 1, 5, 4]);
}
/// A hybrid laptop: the panel (primary) on the integrated GPU, an external monitor on the
@@ -571,7 +604,7 @@ mod tests {
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:?}");
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, 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
@@ -583,9 +616,35 @@ mod tests {
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]);
assert_eq!(adapter_order(&linux, PowerPreference::None, &both, None, None, &[]), [0, 1, 2, 3]);
assert_eq!(adapter_order(&linux, PowerPreference::None, &[monitor(INTEL_IGPU)], None, None, &[]), [2, 0, 1, 3]);
assert_eq!(adapter_order(&linux, PowerPreference::None, &[monitor(NVIDIA_3090), primary(AMD_IGPU)], None, None, &[]), [0, 1, 2, 3]);
}
/// A hybrid laptop whose Wayland compositor runs on the discrete GPU (Hyprland with
/// `AQ_DRM_DEVICES` naming it first), as Vulkan lists it there: the integrated GPU drives the
/// built-in panel, yet the compositor shows nothing it draws, and reports no error. The GPU the
/// compositor draws with comes first; without it, the panel's, as before.
#[test]
fn the_gpu_the_compositor_draws_with_comes_before_the_panels() {
const RTX_3050: (u32, u32) = (0x10de, 0x25ac);
const INTEL_RPL: (u32, u32) = (0x8086, 0xa7a8);
let mut c = vec![
pci_gpu("NVIDIA GeForce RTX 3050 6GB Laptop GPU", Backend::Vulkan, DeviceType::DiscreteGpu, RTX_3050),
pci_gpu("Intel(R) Graphics (RPL-P)", Backend::Vulkan, DeviceType::IntegratedGpu, INTEL_RPL),
pci_gpu("NVIDIA GeForce RTX 3050 6GB Laptop GPU/PCIe/SSE2", Backend::Gl, DeviceType::Other, (RTX_3050.0, 0)),
];
c[2].presents = false;
let panel = [primary(INTEL_RPL)];
assert_eq!(adapter_order(&c, PowerPreference::None, &panel, Some(RTX_3050), None, &[]), [0, 1]);
assert_eq!(adapter_order(&c, PowerPreference::None, &panel, None, None, &[]), [1, 0]);
// The usual hybrid laptop, with the compositor on the integrated GPU: the panel's GPU.
assert_eq!(adapter_order(&c, PowerPreference::None, &[primary(INTEL_RPL), monitor(RTX_3050)], Some(INTEL_RPL), None, &[]), [1, 0]);
// `WGPU_ADAPTER_NAME` still comes first, and a restart after a failure still moves on.
assert_eq!(adapter_order(&c, PowerPreference::None, &panel, Some(RTX_3050), Some("intel"), &[]), [1, 0]);
assert_eq!(adapter_order(&c, PowerPreference::None, &panel, Some(RTX_3050), None, &[c[0].key.clone()]), [1]);
// A compositor GPU no adapter matches (a virtual one) leaves the displays in charge.
assert_eq!(adapter_order(&c, PowerPreference::None, &panel, Some((0x1af4, 0x1050)), None, &[]), [1, 0]);
}
/// A Windows desktop with a monitor on each GPU: the window opens on the primary display, so
@@ -593,11 +652,11 @@ mod tests {
#[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]);
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[monitor(AMD_IGPU), primary(NVIDIA_3090)], None, None, &[]), [2, 0, 3, 1, 5, 4]);
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[primary(AMD_IGPU), monitor(NVIDIA_3090)], None, 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]);
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[monitor(AMD_IGPU), monitor(INTEL_IGPU)], None, None, &[]), [3, 1, 2, 0, 5, 4]);
}
/// Display GPUs none of the adapters match (another driver's ids, OpenGL's missing device id,
@@ -605,18 +664,21 @@ mod tests {
#[test]
fn displays_no_adapter_matches_leave_the_power_preference_in_charge() {
let c = ryzen_desktop();
let low = adapter_order(&c, PowerPreference::LowPower, &[], None, &[]);
let low = adapter_order(&c, PowerPreference::LowPower, &[], None, 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!(adapter_order(&c, PowerPreference::LowPower, &displays, None, None, &[]), low, "{displays:?}");
}
assert_eq!(names(&c, &adapter_order(&c, PowerPreference::HighPerformance, &[monitor(INTEL_IGPU)], None, &[]))[0], "NVIDIA GeForce RTX 3090");
assert_eq!(
names(&c, &adapter_order(&c, PowerPreference::HighPerformance, &[monitor(INTEL_IGPU)], None, None, &[]))[0],
"NVIDIA GeForce RTX 3090"
);
// The #502 machine's adapters report no ids in these tests: the primary display never matches.
let c = issue_502();
assert_eq!(
adapter_order(&c, PowerPreference::None, &[primary(NVIDIA_3090)], None, &[]),
adapter_order(&c, PowerPreference::None, &[], None, &[])
adapter_order(&c, PowerPreference::None, &[primary(NVIDIA_3090)], None, None, &[]),
adapter_order(&c, PowerPreference::None, &[], None, None, &[])
);
assert!(adapter_order(&[], PowerPreference::LowPower, &[primary(NVIDIA_3090)], None, &[]).is_empty());
assert!(adapter_order(&[], PowerPreference::LowPower, &[primary(NVIDIA_3090)], None, None, &[]).is_empty());
}
/// `WGPU_ADAPTER_NAME` and "can't present" still come before the displays.
@@ -624,10 +686,10 @@ mod tests {
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]);
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, 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]);
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &displays, None, None, &[]), [3, 1, 5, 4]);
}
/// A user's own choice (Preferences or `WGPU_POWER_PREF`) is kept as it is; only Automatic
@@ -645,6 +707,9 @@ mod tests {
assert!(preferred_displays(None, 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, None), display_gpus());
assert_eq!(preferred_compositor(Some("lowPower"), None), None);
assert_eq!(preferred_compositor(None, Some(PowerPreference::HighPerformance)), None);
assert_eq!(preferred_compositor(None, None), crate::compositor_gpu::compositor_gpu());
}
#[test]
@@ -741,7 +806,7 @@ mod tests {
#[test]
fn automatic_keeps_the_systems_order_on_linux_and_saves_power_elsewhere() {
let c = issue_502();
let order = adapter_order(&c, power_preference(None, None), &[], None, &[]);
let order = adapter_order(&c, power_preference(None, None), &[], 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)");
@@ -754,7 +819,7 @@ mod tests {
#[test]
fn each_preference_orders_the_adapters_then_falls_back_to_opengl_and_software() {
let c = issue_502();
let order = |power| names(&c, &adapter_order(&c, power, &[], None, &[]));
let order = |power| names(&c, &adapter_order(&c, power, &[], None, None, &[]));
assert_eq!(
order(PowerPreference::None),
[
@@ -800,10 +865,10 @@ mod tests {
};
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, &[]);
let first = adapter_order(&c, PowerPreference::LowPower, &[], None, 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"]);
assert_eq!(names(&c, &adapter_order(&c, PowerPreference::LowPower, &[], None, 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"]);
}
@@ -831,7 +896,7 @@ mod tests {
let c = issue_502();
let mut skip = vec![];
let mut tried = vec![];
while let Some(&i) = adapter_order(&c, PowerPreference::LowPower, &[], None, &skip).first() {
while let Some(&i) = adapter_order(&c, PowerPreference::LowPower, &[], None, None, &skip).first() {
tried.push(c[i].name.clone());
skip.push(c[i].key.clone());
}
@@ -852,36 +917,36 @@ mod tests {
c[0].presents = false;
c[3].presents = false;
for power in [PowerPreference::None, PowerPreference::LowPower, PowerPreference::HighPerformance] {
let order = adapter_order(&c, power, &[], None, &[]);
let order = adapter_order(&c, power, &[], None, 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());
assert!(adapter_order(&c, PowerPreference::None, &[], None, None, &[]).is_empty());
assert!(adapter_order(&[], PowerPreference::HighPerformance, &[], None, None, &[]).is_empty());
}
#[test]
fn wgpu_adapter_name_picks_an_adapter_first_and_the_rest_stay_as_fallbacks() {
let c = issue_502();
let order = adapter_order(&c, PowerPreference::HighPerformance, &[], Some("radv"), &[]);
let order = adapter_order(&c, PowerPreference::HighPerformance, &[], None, 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"), &[]);
let order = adapter_order(&c, PowerPreference::LowPower, &[], None, 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, &[])
adapter_order(&c, PowerPreference::LowPower, &[], None, Some(named), &[]),
adapter_order(&c, PowerPreference::LowPower, &[], None, 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);
assert_eq!(adapter_order(&c, PowerPreference::None, &[], None, Some("radv"), &skip)[0], 0);
}
/// A Windows machine lists every GPU twice (Vulkan and DX12): power saving still takes the
@@ -895,9 +960,9 @@ mod tests {
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, &[]);
let order = adapter_order(&c, PowerPreference::LowPower, &[], None, None, &[]);
assert_eq!(order, [3, 1, 2, 0, 4]);
assert_eq!(adapter_order(&c, PowerPreference::HighPerformance, &[], None, &[]), [2, 0, 3, 1, 4]);
assert_eq!(adapter_order(&c, PowerPreference::HighPerformance, &[], None, None, &[]), [2, 0, 3, 1, 4]);
}
/// The machine of #545: one Intel GPU, whose Vulkan driver made the window flicker black.
@@ -912,9 +977,9 @@ mod tests {
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, power, &[], None, 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");
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], None, None, &[c[1].key.clone()]), [0, 3, 2], "DX12 failed: Vulkan next");
}
#[test]
+6 -1
View File
@@ -23,6 +23,8 @@ compile_error!("the win7 target requires --no-default-features --features window
mod automation_args;
mod clipboard;
#[cfg(feature = "wgpu")]
mod compositor_gpu;
mod control_server;
#[cfg(feature = "wgpu")]
mod gpu;
@@ -670,6 +672,9 @@ fn main() -> std::process::ExitCode {
// every black-window report.
#[cfg(feature = "wgpu")]
let displays = gpu::preferred_displays(gpu_pref, power_env);
// Under Wayland, the GPU the compositor draws with comes even before those (src/compositor_gpu.rs).
#[cfg(feature = "wgpu")]
let compositor = gpu::preferred_compositor(gpu_pref, power_env);
#[cfg(feature = "wgpu")]
let chosen_gpu = !gpu::automatic(gpu_pref, power_env);
#[cfg(feature = "wgpu")]
@@ -710,7 +715,7 @@ fn main() -> std::process::ExitCode {
options.wgpu_options.surface = eframe::egui_wgpu::SurfaceConfig::LOW_LATENCY;
// Only adapters that can show the window, in the order `gpu` gives (#306, #502).
if let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup {
create.native_adapter_selector = Some(gpu::selector(power, displays, startup.clone()));
create.native_adapter_selector = Some(gpu::selector(power, displays, compositor, startup.clone()));
// Nothing draws or dispatches indirectly, so wgpu's check of indirect arguments only
// costs a compute shader at start-up, one some drivers can't compile (OCLP-patched
// Metal on an Iris Pro, #651). `WGPU_VALIDATION_INDIRECT_CALL=1` turns it back on.