mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
Linux: draw on the GPU the Wayland compositor draws with (#1088)
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 panel rule (#8) picked it, and the window stayed empty: the compositor took the frames and showed nothing, with no error anywhere. 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 pick_adapter ranks that adapter first. No answer (X11, an older compositor, a GPU no adapter matches) leaves the existing rules in charge, and WGPU_POWER_PREF still overrides. wayland-client and wayland-protocols are already in the tree through winit; they become direct Linux-only dependencies.
This commit is contained in:
1 parent
a7e24b9512
commit
86e74ec121
5 files changed
+230
-24
No files matched your search
Generated
+2
@@ -3564,6 +3564,8 @@ dependencies = [
|
||||
"serde_json",
|
||||
"uds_windows",
|
||||
"ureq",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"winresource",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
@@ -28,6 +28,12 @@ log = { workspace = true }
|
||||
# behind a safe API. Same version as PhotoCraft. Pinned exactly: review the diff before bumping.
|
||||
fmv-macos-events = "=0.1.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# Asks the Wayland compositor which GPU it draws with (linux-dmabuf feedback), so the window is
|
||||
# drawn on that one (src/compositor_gpu.rs). Both are already in the tree through winit.
|
||||
wayland-client = "0.31"
|
||||
wayland-protocols = { version = "0.32", features = ["client"] }
|
||||
|
||||
[dev-dependencies]
|
||||
# Waits for wgpu's adapter, device and error-scope futures in tests (already in the tree through wgpu).
|
||||
pollster = "1.0"
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//! The GPU the Wayland compositor draws with.
|
||||
//!
|
||||
//! 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 (#96), or take them and
|
||||
//! show nothing, with no error on either side. 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!("pdfcraft-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
-23
@@ -31,6 +31,7 @@ use pdfcraft_ui_egui::PdfCraftApp;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod apple_events;
|
||||
mod compositor_gpu;
|
||||
mod logging;
|
||||
mod single_instance;
|
||||
mod text_scale;
|
||||
@@ -458,6 +459,10 @@ fn write_control_file(path: &str, port: u16, token: &str) -> std::io::Result<()>
|
||||
/// desktop with a monitor on each GPU, issue #445) the discrete one wins, as the integrated one
|
||||
/// may fail to present there. Where the system doesn't say which GPU drives a display (macOS,
|
||||
/// containers, remote sessions) the power preference alone decides.
|
||||
/// - Under Wayland, the GPU the compositor draws with comes before all of that: the compositor
|
||||
/// says which one it is ([`compositor_gpu::compositor_gpu`]), and frames from another GPU may
|
||||
/// never reach the screen, even from the GPU behind the built-in panel when the compositor runs
|
||||
/// on the discrete one.
|
||||
/// - 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
|
||||
@@ -477,7 +482,8 @@ fn configure_gpu(native: &mut eframe::NativeOptions) {
|
||||
if std::env::var_os("WGPU_POWER_PREF").is_none() {
|
||||
setup.power_preference = eframe::wgpu::PowerPreference::LowPower;
|
||||
let displays = display_gpus();
|
||||
if !displays.is_empty() {
|
||||
let compositor = compositor_gpu::compositor_gpu();
|
||||
if !displays.is_empty() || compositor.is_some() {
|
||||
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
|
||||
@@ -487,7 +493,7 @@ fn configure_gpu(native: &mut eframe::NativeOptions) {
|
||||
(info.vendor, info.device, info.device_type)
|
||||
})
|
||||
.collect();
|
||||
let picked = pick_adapter(&infos, &displays).and_then(|i| usable.get(i)).map(|a| (*a).clone());
|
||||
let picked = pick_adapter(&infos, &displays, compositor).and_then(|i| usable.get(i)).map(|a| (*a).clone());
|
||||
match &picked {
|
||||
// Which GPU draws is the first question when a window stays black (#8, #378, #445).
|
||||
Some(a) => {
|
||||
@@ -620,12 +626,12 @@ fn linux_display_gpus(drm: &std::path::Path) -> Vec<DisplayGpu> {
|
||||
gpus
|
||||
}
|
||||
|
||||
/// Index of the adapter to draw with: one that drives a display (by PCI ids) first. Among several
|
||||
/// of those, the one driving the primary display (a hybrid laptop's panel, #8; the primary display
|
||||
/// on Windows, #378), or else, with no primary display known (a Linux desktop with a monitor on
|
||||
/// each GPU, #445), the discrete one. Otherwise the most frugal kind: integrated, discrete, other,
|
||||
/// virtual, software.
|
||||
fn pick_adapter(adapters: &[(u32, u32, eframe::wgpu::DeviceType)], displays: &[DisplayGpu]) -> Option<usize> {
|
||||
/// Index of the adapter to draw with: the GPU the Wayland compositor draws with (`compositor`, by
|
||||
/// PCI ids) first, then one that drives a display. Among several of those, the one driving the
|
||||
/// primary display (a hybrid laptop's panel, #8; the primary display on Windows, #378), or else,
|
||||
/// with no primary display known (a Linux desktop with a monitor on each GPU, #445), the discrete
|
||||
/// one. Otherwise the most frugal kind: integrated, discrete, other, virtual, software.
|
||||
fn pick_adapter(adapters: &[(u32, u32, eframe::wgpu::DeviceType)], displays: &[DisplayGpu], compositor: Option<(u32, u32)>) -> Option<usize> {
|
||||
use eframe::wgpu::DeviceType;
|
||||
let display = |pci: (u32, u32)| displays.iter().find(|g| g.pci == pci);
|
||||
let multi_gpu_desktop = displays.len() > 1 && !displays.iter().any(|g| g.primary);
|
||||
@@ -646,7 +652,7 @@ fn pick_adapter(adapters: &[(u32, u32, eframe::wgpu::DeviceType)], displays: &[D
|
||||
DeviceType::VirtualGpu => 4,
|
||||
DeviceType::Cpu => 5,
|
||||
};
|
||||
(!drives_display, !drives_primary, rank)
|
||||
(compositor != Some((*vendor, *device)), !drives_display, !drives_primary, rank)
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
}
|
||||
@@ -967,17 +973,17 @@ mod tests {
|
||||
#[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(), &[monitor(NVIDIA)]), Some(0));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor(NVIDIA)], None), Some(0));
|
||||
// Issue #378: the same on Windows, where the Ryzen integrated GPU without a monitor crashed
|
||||
// the display driver. Windows also says which display is the primary one.
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(NVIDIA)]), Some(0));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(NVIDIA)], None), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_adapter_prefers_the_gpu_driving_the_primary_display() {
|
||||
// A Windows desktop with a monitor on each GPU: the window opens on the primary display.
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU), panel(NVIDIA)]), Some(0));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU), monitor(NVIDIA)]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU), panel(NVIDIA)], None), Some(0));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU), monitor(NVIDIA)], None), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1009,10 +1015,10 @@ mod tests {
|
||||
#[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(), &[monitor(NVIDIA), panel(AMD_IGPU)]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU), monitor(NVIDIA)]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU)]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU)]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor(NVIDIA), panel(AMD_IGPU)], None), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU), monitor(NVIDIA)], None), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU)], None), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU)], None), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1021,17 +1027,32 @@ mod tests {
|
||||
use eframe::wgpu::DeviceType;
|
||||
let desktop =
|
||||
[(INTEL_IGPU.0, INTEL_IGPU.1, DeviceType::IntegratedGpu), (NVIDIA.0, NVIDIA.1, DeviceType::DiscreteGpu), (0, 0, DeviceType::Cpu)];
|
||||
assert_eq!(super::pick_adapter(&desktop, &[monitor(INTEL_IGPU), monitor(NVIDIA)]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&desktop, &[monitor(NVIDIA), monitor(INTEL_IGPU)]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&desktop, &[monitor(INTEL_IGPU), monitor(NVIDIA)], None), Some(1));
|
||||
assert_eq!(super::pick_adapter(&desktop, &[monitor(NVIDIA), monitor(INTEL_IGPU)], None), Some(1));
|
||||
// A GPU driving no display still loses to one that does.
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU), monitor(INTEL_IGPU)]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU), monitor(INTEL_IGPU)], None), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_adapter_prefers_the_gpu_the_compositor_draws_with() {
|
||||
// A hybrid laptop whose Wayland compositor runs on the discrete GPU (Hyprland with
|
||||
// `AQ_DRM_DEVICES` naming it first): the integrated GPU drives the built-in panel, yet the
|
||||
// compositor shows nothing it draws, and reports no error either.
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU)], Some(NVIDIA)), Some(0));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU), monitor(NVIDIA)], Some(NVIDIA)), Some(0));
|
||||
// The usual hybrid laptop, with the compositor on the integrated GPU: as before (#8).
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU), monitor(NVIDIA)], Some(AMD_IGPU)), Some(1));
|
||||
// No GPU drives a display as far as sysfs says (a container), but the compositor knows.
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[], Some(NVIDIA)), Some(0));
|
||||
// A compositor GPU no adapter matches (a virtual one) leaves the displays in charge.
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU)], Some((0x1af4, 0x1050))), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_adapter_falls_back_to_low_power_without_a_match() {
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor((0x8086, 0x1234))]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor((0x8086, 0x1234)), monitor((0x8086, 0x5678))]), Some(1));
|
||||
assert_eq!(super::pick_adapter(&[], &[monitor(NVIDIA)]), None);
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor((0x8086, 0x1234))], None), Some(1));
|
||||
assert_eq!(super::pick_adapter(&adapters(), &[monitor((0x8086, 0x1234)), monitor((0x8086, 0x5678))], None), Some(1));
|
||||
assert_eq!(super::pick_adapter(&[], &[monitor(NVIDIA)], None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ OpenGL can't start either, PdfCraft exits with both errors in the log. The fallb
|
||||
| `RUST_LOG` | Log levels for standard error and the log file (see [Logs](#logs)) |
|
||||
| `RUST_BACKTRACE` | `1` adds a backtrace to the report of an internal error |
|
||||
| `XDG_DATA_HOME` | Linux/FreeBSD: base of the settings folder (`pdfcraft/`), the log folder and crash recovery |
|
||||
| `WGPU_POWER_PREF` | GPU choice; by default PdfCraft draws on the GPU that drives the primary display (Windows) or the built-in panel (Linux), else the low-power (integrated) GPU. The log says which one was chosen |
|
||||
| `WGPU_POWER_PREF` | GPU choice; by default PdfCraft draws on the GPU the Wayland compositor draws with (it says so through linux-dmabuf feedback), else the GPU that drives the primary display (Windows) or the built-in panel (Linux), else the low-power (integrated) GPU. The log says which one was chosen |
|
||||
| `WGPU_BACKEND` | Graphics backend; by default Windows uses Direct3D 12, falling back to OpenGL |
|
||||
| `PDFCRAFT_RENDERER` | `gl` starts with OpenGL (glow) and never loads wgpu; `wgpu` reports a wgpu failure instead of retrying with OpenGL; unset, wgpu is retried with OpenGL when it can't start (see [Renderer fallback](#renderer-fallback)) |
|
||||
| `CRAFT_FONTS_DIR` | Build time: a [craft-fonts](https://github.com/storytold/craft-fonts) checkout to embed (Japanese fonts) |
|
||||
|
||||
Reference in new issue
Block a user