mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Fix #502: no startup crash on dual-GPU Linux desktops under Wayland
0.5.0 asked wgpu for the low-power adapter everywhere (#306). On a Linux desktop whose KDE Wayland compositor runs on an NVIDIA GPU, wgpu sorted the Ryzen's integrated RADV GPU first: it reports that it can present to the window, but KWin can't import the buffers it allocates ("importing the supplied dmabufs failed"), which is a fatal Wayland protocol error. The next surface configuration then got ERROR_SURFACE_LOST_KHR and egui-wgpu panicked ("The surface isn't supported by this adapter", winit.rs:129). - Preferences > Performance > Graphics Processor gains Automatic, the new default: power saving on Windows and macOS as #306 wanted, and on Linux the system's order, where Mesa's device-select layer puts the GPU the desktop runs on first. Values are now automatic / lowPower / highPerformance; 0.5.0 saved powerSaving for everyone, which reads as automatic. - The app picks the window's adapter itself (apps/vectorcraft/src/gpu.rs, eframe's native_adapter_selector): only adapters that present to the surface, WGPU_ADAPTER_NAME first, hardware before software, native backends before OpenGL, then the power preference. Every adapter and the pick are logged. - run_native runs under the engine guard. When the graphics fail during the first frames (a wgpu error, or a panic in wgpu, egui-wgpu or naga), the app starts again without that adapter (exec on Unix, so an AppImage stays mounted; a new process on Windows), passing the failed adapters in VECTORCRAFT_GPU_SKIP, and says so in the status bar. When none is left it logs why and exits with an error, never a panic. - docs/development.md, README and ROADMAP: the new default, the restart and the escape hatches (WGPU_POWER_PREF, WGPU_ADAPTER_NAME, WGPU_BACKEND, MESA_VK_DEVICE_SELECT, DRI_PRIME). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
99a5318fe4
commit
d12d5195da
10 files changed
+628
-133
No files matched your search
@@ -0,0 +1,470 @@
|
||||
//! Which graphics adapter the window renders with (#306, #502), and what happens when it can't
|
||||
//! show the window.
|
||||
//!
|
||||
//! 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, and the power
|
||||
//! preference ([`power_preference`]) among the rest. eframe is handed [`selector`], which takes
|
||||
//! the first of that order.
|
||||
//!
|
||||
//! 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
|
||||
//! configuring the surface (#502). Nothing in the process can show a window after that, so when
|
||||
//! the graphics fail while the window is starting up, [`finish`] starts the app again without
|
||||
//! that adapter, as the next one in the order would be tried. Each restart leaves out one more
|
||||
//! adapter, so it ends when none is left.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use eframe::egui_wgpu::NativeAdapterSelectorMethod;
|
||||
use eframe::wgpu::{self, Backend, DeviceType, PowerPreference};
|
||||
|
||||
/// The adapters a restart leaves out, those the window failed on, as comma-separated
|
||||
/// `backend:vendor:device` keys (`Vulkan:1002:164e`). Set by the app itself when it starts again.
|
||||
pub const SKIP_ENV: &str = "VECTORCRAFT_GPU_SKIP";
|
||||
|
||||
/// wgpu's variable for choosing an adapter by (part of) its name, any case.
|
||||
const NAME_ENV: &str = "WGPU_ADAPTER_NAME";
|
||||
|
||||
/// Where to read how to choose another adapter, for the error that ends the app.
|
||||
const HELP: &str =
|
||||
"to choose a graphics adapter, see https://github.com/storytold/vectorcraft/blob/main/docs/development.md#desktop-graphics-processor";
|
||||
|
||||
/// Frames the UI ran before the failure, below which it was still starting up. The failure in
|
||||
/// #502 came on the first or second frame; later, the adapter has proved itself, and a lost
|
||||
/// window is something else (such as the compositor restarting).
|
||||
const STARTUP_FRAMES: u64 = 10;
|
||||
|
||||
/// The preference used when Preferences say Automatic. Windows and macOS show frames from any GPU
|
||||
/// (the integrated one avoids the flicker of #306). Elsewhere, the system's own order: Mesa's
|
||||
/// device-select layer puts the GPU the desktop runs on first (the integrated one on hybrid
|
||||
/// laptops), and a Wayland compositor may not show frames from another GPU (#502).
|
||||
pub const AUTOMATIC: PowerPreference = if cfg!(any(windows, target_os = "macos")) { PowerPreference::LowPower } else { PowerPreference::None };
|
||||
|
||||
/// The power preference that orders the adapters: the `WGPU_POWER_PREF` environment variable
|
||||
/// (`low`, `high`, `none`) when set, otherwise Preferences › Performance › Graphics Processor
|
||||
/// (`gpuPreference`). Automatic, and any value this version doesn't know (0.5.0's default
|
||||
/// `powerSaving`, a newer or damaged preference file), is [`AUTOMATIC`].
|
||||
pub fn power_preference(pref: Option<&str>, env: Option<PowerPreference>) -> PowerPreference {
|
||||
match (env, pref) {
|
||||
(Some(p), _) => p,
|
||||
(None, Some("highPerformance")) => PowerPreference::HighPerformance,
|
||||
(None, Some("lowPower")) => PowerPreference::LowPower,
|
||||
(None, _) => AUTOMATIC,
|
||||
}
|
||||
}
|
||||
|
||||
/// One adapter, as [`adapter_order`] sees it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Candidate {
|
||||
/// Identifies the adapter across a restart: `backend:vendor:device` ([`key`]).
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub backend: Backend,
|
||||
pub device_type: DeviceType,
|
||||
/// Reports that it can present to the window's surface.
|
||||
pub presents: bool,
|
||||
}
|
||||
|
||||
impl Candidate {
|
||||
fn new(adapter: &wgpu::Adapter, surface: Option<&wgpu::Surface<'_>>) -> Self {
|
||||
let info = adapter.get_info();
|
||||
Self {
|
||||
key: key(info.backend, info.vendor, info.device),
|
||||
presents: surface.is_none_or(|s| adapter.is_surface_supported(s)),
|
||||
name: info.name,
|
||||
backend: info.backend,
|
||||
device_type: info.device_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `backend:vendor:device`, e.g. `Vulkan:1002:164e` (PCI ids, as `MESA_VK_DEVICE_SELECT` takes them).
|
||||
fn key(backend: Backend, vendor: u32, device: u32) -> String {
|
||||
format!("{backend:?}:{vendor:04x}:{device:04x}")
|
||||
}
|
||||
|
||||
/// 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; then by `power`, keeping the system's order among
|
||||
/// equals (all of it for [`PowerPreference::None`]).
|
||||
pub fn adapter_order(candidates: &[Candidate], power: PowerPreference, 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.
|
||||
order.sort_by_key(|(_, c)| {
|
||||
let unnamed = named.as_ref().is_some_and(|n| !c.name.to_lowercase().contains(n));
|
||||
(unnamed, c.device_type == DeviceType::Cpu, c.backend == Backend::Gl, power_rank(c.device_type, power))
|
||||
});
|
||||
order.into_iter().map(|(i, _)| i).collect()
|
||||
}
|
||||
|
||||
/// wgpu's own ranking for a power preference: the preferred kind of GPU, the other kind, unknown,
|
||||
/// virtual, software.
|
||||
fn power_rank(t: DeviceType, power: PowerPreference) -> u8 {
|
||||
match (power, t) {
|
||||
(PowerPreference::None, _) => 0,
|
||||
(PowerPreference::LowPower, DeviceType::IntegratedGpu) | (PowerPreference::HighPerformance, DeviceType::DiscreteGpu) => 0,
|
||||
(_, DeviceType::IntegratedGpu | DeviceType::DiscreteGpu) => 1,
|
||||
(_, DeviceType::Other) => 2,
|
||||
(_, DeviceType::VirtualGpu) => 3,
|
||||
(_, DeviceType::Cpu) => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// The adapters [`SKIP_ENV`] lists.
|
||||
pub fn skipped() -> Vec<String> {
|
||||
std::env::var(SKIP_ENV).map(|v| parse_skip(&v)).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn parse_skip(v: &str) -> Vec<String> {
|
||||
v.split(',').map(str::trim).filter(|k| !k.is_empty()).map(String::from).collect()
|
||||
}
|
||||
|
||||
/// What the window's start-up leaves for [`finish`].
|
||||
#[derive(Default)]
|
||||
pub struct Startup {
|
||||
/// The key of the adapter [`selector`] picked.
|
||||
adapter: OnceLock<String>,
|
||||
/// The UI's context, which counts the frames run.
|
||||
ui: OnceLock<egui::Context>,
|
||||
}
|
||||
|
||||
impl Startup {
|
||||
/// The app was created on `ctx`.
|
||||
pub fn created(&self, ctx: &egui::Context) {
|
||||
// Set once; the window is created once.
|
||||
let _ = self.ui.set(ctx.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// eframe's adapter choice: the first of [`adapter_order`] for `power`, `WGPU_ADAPTER_NAME` and the
|
||||
/// adapters a restart left out. Every adapter and the choice are logged, for bug reports.
|
||||
pub fn selector(power: PowerPreference, 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, named.as_deref(), &skip);
|
||||
let listed: Vec<String> = candidates
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let note = if !c.presents {
|
||||
", can't show the window"
|
||||
} else if skip.contains(&c.key) {
|
||||
", failed before"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{} ({:?}, {:?}, {}{note})", c.name.trim(), c.backend, c.device_type, c.key)
|
||||
})
|
||||
.collect();
|
||||
log::info!("graphics adapters: {}", listed.join("; "));
|
||||
let first = order.first().and_then(|&i| adapters.get(i).zip(candidates.get(i)));
|
||||
let Some((adapter, chosen)) = first else {
|
||||
return Err(format!("no graphics adapter can show the window (power preference {power:?}, {} adapters)", adapters.len()));
|
||||
};
|
||||
// Set once; eframe picks the adapter once.
|
||||
let _ = startup.adapter.set(chosen.key.clone());
|
||||
Ok(adapter.clone())
|
||||
})
|
||||
}
|
||||
|
||||
/// The last panic came from the graphics stack (see [`watch_panics`]).
|
||||
static GRAPHICS_PANIC: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Note whether each panic comes from the graphics stack, so [`finish`] can tell a failing adapter
|
||||
/// from a bug. The default hook still prints it.
|
||||
pub fn watch_panics() {
|
||||
let default = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
GRAPHICS_PANIC.store(info.location().is_some_and(|l| in_graphics_stack(l.file())), Ordering::Relaxed);
|
||||
default(info);
|
||||
}));
|
||||
}
|
||||
|
||||
/// A source file of wgpu (`wgpu-core-30.0.1/src/…`, `wgpu-hal-…`), egui-wgpu or naga.
|
||||
fn in_graphics_stack(file: &str) -> bool {
|
||||
file.split(['/', '\\']).any(|dir| ["wgpu-", "egui-wgpu-", "naga-"].iter().any(|p| dir.starts_with(p)))
|
||||
}
|
||||
|
||||
/// The adapter to start again without: the one picked, when the graphics failed while the window
|
||||
/// was starting up and it wasn't already left out.
|
||||
fn restart_without<'a>(graphics_failed: bool, frames: u64, picked: Option<&'a str>, skip: &[String]) -> Option<&'a str> {
|
||||
picked.filter(|p| graphics_failed && frames < STARTUP_FRAMES && !skip.iter().any(|s| s == p))
|
||||
}
|
||||
|
||||
/// After the window's run: `Ok` when it closed normally or the app started again (see the module
|
||||
/// docs), otherwise why it failed. `outcome` is eframe's result, or the message of a panic that
|
||||
/// ended it.
|
||||
pub fn finish(outcome: Result<eframe::Result, String>, startup: &Startup) -> Result<(), String> {
|
||||
let (why, graphics_failed) = match outcome {
|
||||
Ok(Ok(())) => return Ok(()),
|
||||
Ok(Err(e @ eframe::Error::Wgpu(_))) => (e.to_string(), true),
|
||||
Ok(Err(e)) => (e.to_string(), false),
|
||||
Err(panic) => (panic, GRAPHICS_PANIC.load(Ordering::Relaxed)),
|
||||
};
|
||||
let frames = startup.ui.get().map_or(0, egui::Context::cumulative_frame_nr);
|
||||
let mut skip = skipped();
|
||||
let Some(failed) = restart_without(graphics_failed, frames, startup.adapter.get().map(String::as_str), &skip) else {
|
||||
return Err(if graphics_failed { format!("{why} ({HELP})") } else { why });
|
||||
};
|
||||
log::error!("the window failed on graphics adapter {failed} ({why}): starting again without it");
|
||||
skip.push(failed.to_string());
|
||||
restart(&skip.join(",")).map_err(|e| format!("{why}; starting again failed: {e}"))
|
||||
}
|
||||
|
||||
/// Start the app again with the same arguments, leaving out the adapters in `skip`. On Unix the
|
||||
/// new app replaces this process (same process id, so an AppImage keeps its files mounted), and
|
||||
/// this returns only on failure; on Windows it starts beside this one, which then exits.
|
||||
fn restart(skip: &str) -> std::io::Result<()> {
|
||||
log::logger().flush();
|
||||
let mut c = std::process::Command::new(std::env::current_exe()?);
|
||||
c.args(std::env::args_os().skip(1)).env(SKIP_ENV, skip);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt as _;
|
||||
Err(c.exec())
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
c.spawn().map(|_| ())
|
||||
}
|
||||
|
||||
#[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, presents: true }
|
||||
}
|
||||
|
||||
fn names(c: &[Candidate], order: &[usize]) -> Vec<String> {
|
||||
order.iter().map(|&i| c[i].name.clone()).collect()
|
||||
}
|
||||
|
||||
/// The machine of #502 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 issue_502() -> Vec<Candidate> {
|
||||
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 = issue_502();
|
||||
let order = adapter_order(&c, power_preference(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)");
|
||||
} else {
|
||||
// #502: 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 = issue_502();
|
||||
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, so Power Saving on the machine of
|
||||
/// #502 ends up on the NVIDIA GPU, and once every adapter failed there is nothing to try.
|
||||
#[test]
|
||||
fn a_restart_tries_the_next_adapter() {
|
||||
let c = issue_502();
|
||||
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 = issue_502();
|
||||
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 = issue_502();
|
||||
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 (Vulkan and DX12): power saving still takes the
|
||||
/// integrated GPU first, as wgpu's own choice did (#306).
|
||||
#[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, [1, 3, 0, 2, 4]);
|
||||
assert_eq!(adapter_order(&c, PowerPreference::HighPerformance, None, &[]), [0, 2, 1, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_preference_follows_the_environment_then_the_preference() {
|
||||
assert_eq!(power_preference(None, None), AUTOMATIC);
|
||||
assert_eq!(power_preference(Some("automatic"), None), AUTOMATIC);
|
||||
assert_eq!(power_preference(Some("lowPower"), None), PowerPreference::LowPower);
|
||||
assert_eq!(power_preference(Some("highPerformance"), None), PowerPreference::HighPerformance);
|
||||
// 0.5.0's default, and values this version doesn't know (a newer or damaged file).
|
||||
for old in ["powerSaving", "turbo", ""] {
|
||||
assert_eq!(power_preference(Some(old), None), AUTOMATIC, "{old}");
|
||||
}
|
||||
// WGPU_POWER_PREF wins over the preference, either way.
|
||||
assert_eq!(power_preference(Some("lowPower"), Some(PowerPreference::HighPerformance)), PowerPreference::HighPerformance);
|
||||
assert_eq!(power_preference(Some("highPerformance"), Some(PowerPreference::LowPower)), PowerPreference::LowPower);
|
||||
assert_eq!(power_preference(Some("lowPower"), Some(PowerPreference::None)), PowerPreference::None);
|
||||
assert_eq!(AUTOMATIC, if cfg!(any(windows, target_os = "macos")) { PowerPreference::LowPower } else { PowerPreference::None });
|
||||
}
|
||||
|
||||
/// The preference the engine saves is the one the app reads back before the window opens.
|
||||
#[test]
|
||||
fn the_saved_engine_preference_is_found() {
|
||||
let mut prefs = vectorcraft_engine::Prefs::default();
|
||||
let saved = prefs.to_json();
|
||||
assert_eq!(saved.get("gpuPreference").and_then(serde_json::Value::as_str), Some("automatic"));
|
||||
for (value, power) in [("lowPower", PowerPreference::LowPower), ("highPerformance", PowerPreference::HighPerformance)] {
|
||||
prefs.gpu_preference = value.into();
|
||||
let saved = prefs.to_json();
|
||||
assert_eq!(power_preference(saved.get("gpuPreference").and_then(serde_json::Value::as_str), None), power);
|
||||
}
|
||||
// Every choice Preferences offer means something here.
|
||||
for (value, _) in vectorcraft_engine::cmd::prefscmds::GPU_PREFERENCES {
|
||||
assert!(["automatic", "lowPower", "highPerformance"].contains(value), "{value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[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), "Vulkan:1002:164e");
|
||||
assert_eq!(key(Backend::Gl, 0x10de, 0x2f04), "Gl:10de:2f04");
|
||||
}
|
||||
|
||||
#[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/vectorcraft/src/main.rs",
|
||||
"/home/wgpu/vectorcraft/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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_normal_exit_or_another_failure_does_not_restart() {
|
||||
let startup = Startup::default();
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ compile_error!("the win7 target requires --no-default-features --features window
|
||||
|
||||
mod clipboard;
|
||||
mod control_server;
|
||||
#[cfg(feature = "wgpu")]
|
||||
mod gpu;
|
||||
mod logging;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
@@ -297,23 +299,6 @@ fn app_icon() -> egui::IconData {
|
||||
eframe::icon_data::from_png_bytes(png).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The power preference wgpu picks the window's graphics adapter with (#306): the
|
||||
/// `WGPU_POWER_PREF` environment variable (`low`, `high`, `none`) when set, otherwise Preferences ›
|
||||
/// Performance › Graphics Processor (`gpuPreference`). Power saving unless the preference asks for
|
||||
/// high performance: the canvas is rasterized on the CPU and the GPU only composites it, which the
|
||||
/// integrated GPU of a hybrid-graphics laptop does easily, while presenting frames rendered on the
|
||||
/// discrete GPU through the integrated one made the window flicker on some laptops. With a single
|
||||
/// GPU both preferences pick it.
|
||||
#[cfg(feature = "wgpu")]
|
||||
fn power_preference(pref: Option<&str>, env: Option<eframe::wgpu::PowerPreference>) -> eframe::wgpu::PowerPreference {
|
||||
use eframe::wgpu::PowerPreference;
|
||||
match (env, pref) {
|
||||
(Some(p), _) => p,
|
||||
(None, Some("highPerformance")) => PowerPreference::HighPerformance,
|
||||
(None, _) => PowerPreference::LowPower,
|
||||
}
|
||||
}
|
||||
|
||||
/// "name (backend, kind)" of the adapter the window renders with, for Help › About and bug reports.
|
||||
#[cfg(feature = "wgpu")]
|
||||
fn adapter_summary(info: &eframe::wgpu::AdapterInfo) -> String {
|
||||
@@ -324,7 +309,7 @@ fn adapter_summary(info: &eframe::wgpu::AdapterInfo) -> String {
|
||||
/// macOS keeps its traffic lights over the integrated title strip.
|
||||
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
fn main() -> std::process::ExitCode {
|
||||
// First, so every start-up warning is recorded (`logging`).
|
||||
let logger = logging::install();
|
||||
vectorcraft_ui_egui::i18n::detect_system_lang_in_background();
|
||||
@@ -336,7 +321,7 @@ fn main() -> eframe::Result {
|
||||
"--control" => control_port = args.next().and_then(|p| p.parse().ok()),
|
||||
"--version" => {
|
||||
println!("vectorcraft {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
return std::process::ExitCode::SUCCESS;
|
||||
}
|
||||
_ => files.push(a),
|
||||
}
|
||||
@@ -360,7 +345,9 @@ fn main() -> eframe::Result {
|
||||
#[cfg(feature = "wgpu")]
|
||||
let gpu_pref = saved.as_ref().and_then(|ui| ui.engine_prefs.get("gpuPreference")).and_then(serde_json::Value::as_str);
|
||||
#[cfg(feature = "wgpu")]
|
||||
let power = power_preference(gpu_pref, eframe::wgpu::PowerPreference::from_env());
|
||||
let power = gpu::power_preference(gpu_pref, eframe::wgpu::PowerPreference::from_env());
|
||||
#[cfg(feature = "wgpu")]
|
||||
let startup = std::sync::Arc::new(gpu::Startup::default());
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("VectorCraft")
|
||||
@@ -383,86 +370,94 @@ fn main() -> eframe::Result {
|
||||
// One frame queued, not two: the canvas is rasterized on the CPU and the GPU only
|
||||
// composites it, so the window answers the pointer a frame sooner (#444).
|
||||
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.power_preference = power;
|
||||
create.native_adapter_selector = Some(gpu::selector(power, startup.clone()));
|
||||
}
|
||||
options
|
||||
};
|
||||
#[cfg(feature = "wgpu")]
|
||||
gpu::watch_panics();
|
||||
// Files opened from Finder and the Dock arrive as events, not arguments.
|
||||
#[cfg(target_os = "macos")]
|
||||
open_documents::install();
|
||||
eframe::run_native(
|
||||
"VectorCraft",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
let mut app = VectorcraftApp::new(Session::new(), services());
|
||||
load_prefs(&mut app, saved);
|
||||
// Fit the window to its monitor, or put it back where it was (still hidden).
|
||||
if let Some(w) = cc.winit_window() {
|
||||
app.ui.window = Some(window::restore(w, saved_window));
|
||||
}
|
||||
// User Defined swatch and graphic style libraries live next to the preferences.
|
||||
let swatches = prefs_path().and_then(|p| Some(p.parent()?.join("Swatches").to_string_lossy().to_string()));
|
||||
app.session.swatch_libraries.set_user_dir(swatches);
|
||||
let styles = prefs_path().and_then(|p| Some(p.parent()?.join("Graphic Styles").to_string_lossy().to_string()));
|
||||
app.session.style_libraries.set_user_dir(styles);
|
||||
// Data Recovery copies live next to the preferences too (none for runs without
|
||||
// preferences, such as agents' test runs, unless the recoveryFolder preference is set).
|
||||
if std::env::var_os("VECTORCRAFT_NO_PREFS").is_none() {
|
||||
let recovery = prefs_path().and_then(|p| Some(p.parent()?.join("Data Recovery").to_string_lossy().to_string()));
|
||||
app.session.recovery.set_default_folder(recovery);
|
||||
}
|
||||
let graphics_loss = GraphicsLoss::default();
|
||||
#[cfg(feature = "wgpu")]
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
let summary = adapter_summary(&rs.adapter.get_info());
|
||||
log::info!("rendering with {summary} (power preference {power:?})");
|
||||
app.graphics_adapter = Some(summary);
|
||||
let (loss, ctx) = (graphics_loss.clone(), cc.egui_ctx.clone());
|
||||
rs.device.set_device_lost_callback(move |reason, msg| loss.report(&ctx, format!("{reason:?}: {msg}")));
|
||||
}
|
||||
#[cfg(feature = "windows7")]
|
||||
{
|
||||
app.graphics_adapter = Some("OpenGL (Windows 7 compatibility)".into());
|
||||
}
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||
if let Some(port) = control_port {
|
||||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
app = app.with_control(rx);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
open_documents::set_ui(&cc.egui_ctx);
|
||||
open_files(&mut app, files);
|
||||
Ok(Box::new(App {
|
||||
app,
|
||||
graphics_loss,
|
||||
graphics_lost: false,
|
||||
#[cfg(feature = "wgpu")]
|
||||
let created = startup.clone();
|
||||
// A panic that ends the window (egui-wgpu's own, #502) is caught here: never a crash.
|
||||
let outcome = vectorcraft_engine::guard::catch_panic(|| {
|
||||
eframe::run_native(
|
||||
"VectorCraft",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
let mut app = VectorcraftApp::new(Session::new(), services());
|
||||
load_prefs(&mut app, saved);
|
||||
// Fit the window to its monitor, or put it back where it was (still hidden).
|
||||
if let Some(w) = cc.winit_window() {
|
||||
app.ui.window = Some(window::restore(w, saved_window));
|
||||
}
|
||||
// User Defined swatch and graphic style libraries live next to the preferences.
|
||||
let swatches = prefs_path().and_then(|p| Some(p.parent()?.join("Swatches").to_string_lossy().to_string()));
|
||||
app.session.swatch_libraries.set_user_dir(swatches);
|
||||
let styles = prefs_path().and_then(|p| Some(p.parent()?.join("Graphic Styles").to_string_lossy().to_string()));
|
||||
app.session.style_libraries.set_user_dir(styles);
|
||||
// Data Recovery copies live next to the preferences too (none for runs without
|
||||
// preferences, such as agents' test runs, unless the recoveryFolder preference is set).
|
||||
if std::env::var_os("VECTORCRAFT_NO_PREFS").is_none() {
|
||||
let recovery = prefs_path().and_then(|p| Some(p.parent()?.join("Data Recovery").to_string_lossy().to_string()));
|
||||
app.session.recovery.set_default_folder(recovery);
|
||||
}
|
||||
let graphics_loss = GraphicsLoss::default();
|
||||
#[cfg(feature = "wgpu")]
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
let summary = adapter_summary(&rs.adapter.get_info());
|
||||
log::info!("rendering with {summary} (power preference {power:?})");
|
||||
created.created(&cc.egui_ctx);
|
||||
if !gpu::skipped().is_empty() {
|
||||
app.status(format!("The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary}"));
|
||||
}
|
||||
app.graphics_adapter = Some(summary);
|
||||
let (loss, ctx) = (graphics_loss.clone(), cc.egui_ctx.clone());
|
||||
rs.device.set_device_lost_callback(move |reason, msg| loss.report(&ctx, format!("{reason:?}: {msg}")));
|
||||
}
|
||||
#[cfg(feature = "windows7")]
|
||||
{
|
||||
app.graphics_adapter = Some("OpenGL (Windows 7 compatibility)".into());
|
||||
}
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||
if let Some(port) = control_port {
|
||||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
app = app.with_control(rx);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
menu: None,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
open_documents::set_ui(&cc.egui_ctx);
|
||||
open_files(&mut app, files);
|
||||
Ok(Box::new(App {
|
||||
app,
|
||||
graphics_loss,
|
||||
graphics_lost: false,
|
||||
#[cfg(target_os = "macos")]
|
||||
menu: None,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
});
|
||||
#[cfg(feature = "wgpu")]
|
||||
let outcome = gpu::finish(outcome, &startup);
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
let outcome = outcome.and_then(|r| r.map_err(|e| e.to_string()));
|
||||
match outcome {
|
||||
Ok(()) => std::process::ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
log::error!("VectorCraft stopped: {e}");
|
||||
std::process::ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "wgpu"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use eframe::wgpu::PowerPreference;
|
||||
|
||||
#[test]
|
||||
fn power_saving_unless_the_preference_or_the_environment_says_otherwise() {
|
||||
assert_eq!(power_preference(None, None), PowerPreference::LowPower);
|
||||
assert_eq!(power_preference(Some("powerSaving"), None), PowerPreference::LowPower);
|
||||
assert_eq!(power_preference(Some("highPerformance"), None), PowerPreference::HighPerformance);
|
||||
// A value this version doesn't know (a newer or damaged preference file) is the default.
|
||||
assert_eq!(power_preference(Some("turbo"), None), PowerPreference::LowPower);
|
||||
assert_eq!(power_preference(Some(""), None), PowerPreference::LowPower);
|
||||
// WGPU_POWER_PREF wins over the preference, either way.
|
||||
assert_eq!(power_preference(Some("powerSaving"), Some(PowerPreference::HighPerformance)), PowerPreference::HighPerformance);
|
||||
assert_eq!(power_preference(Some("highPerformance"), Some(PowerPreference::LowPower)), PowerPreference::LowPower);
|
||||
assert_eq!(power_preference(None, Some(PowerPreference::None)), PowerPreference::None);
|
||||
}
|
||||
|
||||
/// The file extensions the macOS bundle declares: its document types and its own exported type.
|
||||
fn plist_extensions(plist: &str) -> Vec<&str> {
|
||||
@@ -492,16 +487,4 @@ mod tests {
|
||||
assert!(types > 1 && plist.matches("<key>LSHandlerRank</key>").count() == types, "every document type has a rank");
|
||||
assert_eq!(plist.matches("<string>Owner</string>").count(), 2, "only VectorCraft documents and templates are owned");
|
||||
}
|
||||
|
||||
/// The preference the engine saves is the one the app reads back before the window opens.
|
||||
#[test]
|
||||
fn the_saved_engine_preference_is_found() {
|
||||
let mut prefs = vectorcraft_engine::Prefs::default();
|
||||
let saved = prefs.to_json();
|
||||
assert_eq!(saved.get("gpuPreference").and_then(serde_json::Value::as_str), Some("powerSaving"));
|
||||
prefs.gpu_preference = "highPerformance".into();
|
||||
let saved = prefs.to_json();
|
||||
let pref = saved.get("gpuPreference").and_then(serde_json::Value::as_str);
|
||||
assert_eq!(power_preference(pref, None), PowerPreference::HighPerformance);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user