Skip to main content

ravenrs_platform_native/
diagnostics.rs

1//! Describes the GPU and the machine an app runs on, from its panel or from the
2//! command line.
3
4use std::sync::Arc;
5
6use ravenrs_core::{DiagnosticReport, SurfaceReport, SystemReport};
7use ravenrs_render::{GpuContext, gpu_report, preferred_surface_format, surface_report, wgpu};
8use winit::application::ApplicationHandler;
9use winit::event::WindowEvent;
10use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
11use winit::window::{Window, WindowId};
12
13/// The variables that change which GPU is used or how it is reached.
14const OVERRIDES: [&str; 9] = [
15    "WGPU_BACKEND",
16    "WGPU_ADAPTER_NAME",
17    "WGPU_POWER_PREF",
18    "WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER",
19    "VK_ICD_FILENAMES",
20    "VK_DRIVER_FILES",
21    "LIBGL_ALWAYS_SOFTWARE",
22    "MESA_LOADER_DRIVER_OVERRIDE",
23    "GALLIUM_DRIVER",
24];
25
26/// The overrides whose values are lists of paths, which may be in a person's folder.
27const PATH_OVERRIDES: [&str; 2] = ["VK_ICD_FILENAMES", "VK_DRIVER_FILES"];
28
29/// An instance for `display`, which lets the GL backend reach the window system.
30/// `permissive` also accepts drivers that are not conformant.
31pub(crate) fn new_instance(display: Option<&OwnedDisplayHandle>, permissive: bool) -> wgpu::Instance {
32    let mut descriptor = match display {
33        Some(display) => {
34            wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(display.clone()))
35        }
36        None => wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
37    };
38    if permissive {
39        descriptor.flags |= wgpu::InstanceFlags::ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER;
40    }
41    wgpu::Instance::new(descriptor)
42}
43
44/// The report for an app that ended up on `adapter`.
45pub(crate) fn gather(
46    display: Option<&OwnedDisplayHandle>,
47    adapter: &wgpu::Adapter,
48    surface: Option<SurfaceReport>,
49) -> DiagnosticReport {
50    let (instance, permissive) = (new_instance(display, false), new_instance(display, true));
51    let gpu = pollster::block_on(gpu_report(adapter, &instance, &permissive, surface));
52    DiagnosticReport { gpu: Ok(gpu), system: system_report() }
53}
54
55fn system_report() -> SystemReport {
56    SystemReport {
57        version: env!("CARGO_PKG_VERSION").into(),
58        os: os_name(),
59        session: std::env::var("XDG_SESSION_TYPE").ok(),
60        desktop: std::env::var("XDG_CURRENT_DESKTOP").ok(),
61        vulkan_drivers: vulkan_drivers(),
62        overrides: OVERRIDES
63            .iter()
64            .filter_map(|name| Some(override_entry(name, &std::env::var(name).ok()?)))
65            .collect(),
66        optimized: !cfg!(debug_assertions),
67    }
68}
69
70/// `NAME=value`, with only the file names of a list of paths: the report is made to
71/// be shared, and which driver file is enough to tell what happened.
72fn override_entry(name: &str, value: &str) -> String {
73    if !PATH_OVERRIDES.contains(&name) {
74        return format!("{name}={value}");
75    }
76    let files: Vec<_> = std::env::split_paths(value)
77        .filter_map(|path| Some(path.file_name()?.to_string_lossy().into_owned()))
78        .collect();
79    format!("{name}={}", files.join(","))
80}
81
82/// The distribution's name on Linux; the system's elsewhere.
83fn os_name() -> String {
84    let release = std::fs::read_to_string("/etc/os-release").unwrap_or_default();
85    pretty_name(&release).unwrap_or_else(|| std::env::consts::OS.to_owned())
86}
87
88fn pretty_name(os_release: &str) -> Option<String> {
89    let line = os_release.lines().find_map(|line| line.strip_prefix("PRETTY_NAME="))?;
90    Some(line.trim_matches('"').to_owned())
91}
92
93/// The driver files the Vulkan loader looks at on Linux.
94fn vulkan_drivers() -> Vec<String> {
95    let mut drivers: Vec<String> = ["/usr/share/vulkan/icd.d", "/etc/vulkan/icd.d"]
96        .iter()
97        .filter_map(|dir| std::fs::read_dir(dir).ok())
98        .flatten()
99        .filter_map(|entry| Some(entry.ok()?.file_name().to_string_lossy().into_owned()))
100        .collect();
101    drivers.sort();
102    drivers
103}
104
105/// Finds the GPU the way a window would, and describes it.
106fn probe(display: Option<&OwnedDisplayHandle>, window: Option<Arc<Window>>) -> DiagnosticReport {
107    let instance = new_instance(display, false);
108    let surface = window.and_then(|window| instance.create_surface(window).ok());
109    let failed = |e: String| DiagnosticReport { gpu: Err(e), system: system_report() };
110    let gpu = match pollster::block_on(GpuContext::new(&instance, surface.as_ref())) {
111        Ok(gpu) => gpu,
112        Err(e) => return failed(e.to_string()),
113    };
114    let described = surface.as_ref().and_then(|surface| {
115        let capabilities = surface.get_capabilities(&gpu.adapter);
116        let format = preferred_surface_format(&capabilities.formats)?;
117        Some(surface_report(&capabilities, format, wgpu::PresentMode::AutoVsync))
118    });
119    gather(display, &gpu.adapter, described)
120}
121
122struct Probe {
123    display: OwnedDisplayHandle,
124    report: Option<DiagnosticReport>,
125}
126
127impl ApplicationHandler for Probe {
128    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
129        if self.report.is_some() {
130            return;
131        }
132        // Never shown: a surface needs a window, and which GPU works depends on it.
133        let attributes = Window::default_attributes().with_visible(false);
134        let window = event_loop.create_window(attributes).ok().map(Arc::new);
135        self.report = Some(probe(Some(&self.display), window));
136        event_loop.exit();
137    }
138
139    fn window_event(&mut self, _event_loop: &ActiveEventLoop, _id: WindowId, _event: WindowEvent) {}
140}
141
142/// Finds the GPU the way a window would and describes it, along with the machine
143/// and the build, without showing anything. For a command line option such as
144/// `--diagnostics`, which still answers when a window would not open or would be
145/// too slow to use. Printing the result gives text to paste into a bug report.
146///
147/// Call it instead of running the app: a process has only one event loop.
148pub fn diagnose() -> DiagnosticReport {
149    let Ok(event_loop) = EventLoop::new() else {
150        // No window system at all, as over SSH: adapters can still be listed.
151        return probe(None, None);
152    };
153    let mut probing = Probe { display: event_loop.owned_display_handle(), report: None };
154    match event_loop.run_app(&mut probing) {
155        Ok(()) => probing.report.unwrap_or_else(|| probe(None, None)),
156        Err(e) => {
157            tracing::warn!(error = %e, "could not run the event loop");
158            probe(None, None)
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[cfg(unix)]
168    #[test]
169    fn overrides_with_paths_keep_only_the_file_names() {
170        assert_eq!(
171            override_entry("VK_DRIVER_FILES", "/home/ana/icd/radeon.json:/opt/lvp.json"),
172            "VK_DRIVER_FILES=radeon.json,lvp.json"
173        );
174        assert_eq!(override_entry("WGPU_BACKEND", "gl"), "WGPU_BACKEND=gl");
175    }
176
177    #[test]
178    fn the_distribution_name_comes_from_os_release() {
179        let release =
180            "NAME=\"Fedora Linux\"\nPRETTY_NAME=\"Fedora Linux 44 (Workstation Edition)\"\nID=fedora\n";
181        assert_eq!(pretty_name(release).as_deref(), Some("Fedora Linux 44 (Workstation Edition)"));
182        assert_eq!(pretty_name("ID=fedora\n"), None);
183    }
184}