mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
* Issue #96: on Linux, draw on the GPU that drives the monitors Preferring the low-power GPU (issue #8) picked the integrated GPU on desktops whose monitors are all on the discrete one, so the window was black (Hyprland) or the app panicked with "surface isn't supported" (GNOME). On Linux, pick an adapter that supports the window's surface and whose PCI ids match a GPU with a connected monitor (from /sys/class/drm); integrated still wins among those, so hybrid laptops keep the issue #8 behaviour. WGPU_POWER_PREF still overrides. * Use the PdfCraft name for the fake sysfs test folder Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 file changed
+127
@@ -180,6 +180,9 @@ fn write_control_file(path: &str, port: u16, token: &str) -> std::io::Result<()>
|
||||
/// for a discrete GPU, and on hybrid-graphics laptops (NVIDIA Optimus) the discrete one can lose
|
||||
/// or corrupt its memory across suspend and screen lock, leaving the window illegible (issue #8).
|
||||
/// It also saves battery. Machines with one GPU are unaffected.
|
||||
/// - On Linux, draw on a GPU that a monitor is plugged into. On a desktop whose monitors all hang
|
||||
/// off the discrete GPU, drawing on the integrated one leaves the window black under Wayland
|
||||
/// compositors on NVIDIA. Among the GPUs that drive a display, the integrated one still wins.
|
||||
/// - On Windows, use Direct3D 12, falling back to OpenGL, and never load Vulkan drivers unless
|
||||
/// `WGPU_BACKEND` asks for them. Creating a Vulkan instance loads every installed Vulkan driver
|
||||
/// into the process, and a faulty one (an Intel driver in issue #37) crashed PdfCraft before
|
||||
@@ -188,12 +191,83 @@ fn configure_gpu(native: &mut eframe::NativeOptions) {
|
||||
let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut native.wgpu_options.wgpu_setup else { return };
|
||||
if std::env::var_os("WGPU_POWER_PREF").is_none() {
|
||||
setup.power_preference = eframe::wgpu::PowerPreference::LowPower;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let displays = linux_display_gpus(std::path::Path::new("/sys/class/drm"));
|
||||
// Without sysfs (containers, remote sessions) the power preference alone decides.
|
||||
if !displays.is_empty() {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(move |adapters, surface| {
|
||||
let usable: Vec<&eframe::wgpu::Adapter> = adapters.iter().filter(|a| surface.is_none_or(|s| a.is_surface_supported(s))).collect();
|
||||
let infos: Vec<(u32, u32, eframe::wgpu::DeviceType)> = usable
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let info = a.get_info();
|
||||
(info.vendor, info.device, info.device_type)
|
||||
})
|
||||
.collect();
|
||||
pick_adapter(&infos, &displays)
|
||||
.and_then(|i| usable.get(i))
|
||||
.map(|a| (*a).clone())
|
||||
.ok_or_else(|| "no GPU can draw to this window".to_string())
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
if cfg!(target_os = "windows") && std::env::var_os("WGPU_BACKEND").is_none() {
|
||||
setup.instance_descriptor.backends = eframe::wgpu::Backends::DX12 | eframe::wgpu::Backends::GL;
|
||||
}
|
||||
}
|
||||
|
||||
/// PCI `(vendor, device)` ids of the GPUs with a connected monitor, read from the DRM connectors
|
||||
/// under `drm` (`card1-DP-3/status` is `connected`, `card1/device/{vendor,device}` hold `0x10de`).
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_display_gpus(drm: &std::path::Path) -> Vec<(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 mut gpus = Vec::new();
|
||||
let Ok(entries) = std::fs::read_dir(drm) else { return gpus };
|
||||
// A machine has a handful of connectors; the cap only bounds a pathological sysfs.
|
||||
for entry in entries.flatten().take(256) {
|
||||
let name = entry.file_name();
|
||||
let Some((card, _connector)) = name.to_str().and_then(|n| n.split_once('-')) else { continue };
|
||||
let connected = std::fs::read_to_string(entry.path().join("status")).is_ok_and(|s| s.trim() == "connected");
|
||||
if !connected {
|
||||
continue;
|
||||
}
|
||||
let device = drm.join(card).join("device");
|
||||
if let (Some(v), Some(d)) = (read_hex(device.join("vendor")), read_hex(device.join("device")))
|
||||
&& !gpus.contains(&(v, d))
|
||||
{
|
||||
gpus.push((v, d));
|
||||
}
|
||||
}
|
||||
gpus
|
||||
}
|
||||
|
||||
/// Index of the adapter to draw with: one that drives a display (by PCI ids) first, then the most
|
||||
/// frugal kind — integrated, discrete, other, virtual, software.
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
fn pick_adapter(adapters: &[(u32, u32, eframe::wgpu::DeviceType)], displays: &[(u32, u32)]) -> Option<usize> {
|
||||
use eframe::wgpu::DeviceType;
|
||||
adapters
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_, (vendor, device, kind))| {
|
||||
let drives_display = displays.contains(&(*vendor, *device));
|
||||
let frugality = match kind {
|
||||
DeviceType::IntegratedGpu => 0,
|
||||
DeviceType::DiscreteGpu => 1,
|
||||
DeviceType::Other => 2,
|
||||
DeviceType::VirtualGpu => 3,
|
||||
DeviceType::Cpu => 4,
|
||||
};
|
||||
(!drives_display, frugality)
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
@@ -212,4 +286,57 @@ mod tests {
|
||||
assert!(!backends.contains(eframe::wgpu::Backends::VULKAN), "issue #37: {backends:?}");
|
||||
}
|
||||
}
|
||||
|
||||
const NVIDIA: (u32, u32) = (0x10de, 0x2684);
|
||||
const AMD_IGPU: (u32, u32) = (0x1002, 0x164e);
|
||||
|
||||
fn adapters() -> Vec<(u32, u32, eframe::wgpu::DeviceType)> {
|
||||
use eframe::wgpu::DeviceType;
|
||||
vec![(NVIDIA.0, NVIDIA.1, DeviceType::DiscreteGpu), (AMD_IGPU.0, AMD_IGPU.1, DeviceType::IntegratedGpu), (0, 0, DeviceType::Cpu)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_adapter_prefers_the_gpu_driving_the_monitors() {
|
||||
// A desktop whose monitors are all on the discrete GPU: the integrated one shows black.
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[NVIDIA]), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_adapter_keeps_the_integrated_gpu_on_hybrid_laptops() {
|
||||
// Issue #8: the panel is on the integrated GPU, an external monitor on the discrete one.
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[NVIDIA, AMD_IGPU]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[AMD_IGPU]), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_adapter_falls_back_to_low_power_without_a_match() {
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[(0x8086, 0x1234)]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&[], &[NVIDIA]), None);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn linux_display_gpus_reads_connected_connectors() -> std::io::Result<()> {
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-drm-{}", std::process::id()));
|
||||
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("card1", NVIDIA)?;
|
||||
card("card2", AMD_IGPU)?;
|
||||
connector("card1-DP-3", "connected")?;
|
||||
connector("card1-DP-4", "connected")?;
|
||||
connector("card2-HDMI-A-1", "disconnected")?;
|
||||
connector("card2-Writeback-1", "unknown")?;
|
||||
let gpus = super::linux_display_gpus(&dir);
|
||||
std::fs::remove_dir_all(&dir)?;
|
||||
assert_eq!(gpus, vec![NVIDIA]);
|
||||
assert!(super::linux_display_gpus(std::path::Path::new("/nonexistent/drm")).is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user