Skip to main content

corundum_framework_gui/
gpu.rs

1//! Renderer setup: backend selection and device-loss reporting.
2//!
3//! On Windows the app asks for Vulkan rather than letting wgpu pick: the DX12
4//! device is removed whenever a Remote Desktop session reconnects or the display
5//! configuration changes, and egui-wgpu turns that into an unrecoverable panic
6//! (see [`device_lost_explanation`]). Every other OS gets wgpu's primary
7//! backends. `WGPU_BACKEND` overrides the choice everywhere, so
8//! `WGPU_BACKEND=dx12` is the escape hatch on Windows when Vulkan misbehaves
9//! (for example, a Vulkan overlay layer that crashes the app).
10//!
11//! The device asks for wgpu's default limits only as far as the adapter has them: some
12//! adapters fall short of the defaults (the iOS simulator's Metal one), and asking for
13//! more than the adapter has fails the device's creation, and with it the window.
14//!
15//! On Android a debug build leaves out wgpu's debug labels ([`instance_flags`]): the
16//! emulator's Vulkan driver crashes the app when wgpu names its objects.
17
18use std::sync::Arc;
19
20use eframe::{egui_wgpu, wgpu};
21
22/// Renderer configuration for [`eframe::NativeOptions`].
23pub fn wgpu_options() -> egui_wgpu::WgpuConfiguration {
24    let mut options = egui_wgpu::WgpuConfiguration::default();
25    if let egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
26        setup.instance_descriptor.backends = backends();
27        setup.instance_descriptor.flags = instance_flags(setup.instance_descriptor.flags);
28        let egui_default = Arc::clone(&setup.device_descriptor);
29        setup.device_descriptor = Arc::new(move |adapter| {
30            let mut descriptor = egui_default(adapter);
31            descriptor.required_limits = descriptor
32                .required_limits
33                .or_worse_values_from(&adapter.limits());
34            descriptor
35        });
36    }
37    options
38}
39
40/// `flags` (wgpu's, from the build and the `WGPU_*` variables), less the debug labels on
41/// Android: the emulator's Vulkan driver (`vulkan.ranchu`) dies with SIGSEGV in
42/// `vkSetDebugUtilsObjectNameEXT`, which wgpu calls for each label. Validation stays.
43fn instance_flags(flags: wgpu::InstanceFlags) -> wgpu::InstanceFlags {
44    if cfg!(target_os = "android") {
45        flags - wgpu::InstanceFlags::DEBUG
46    } else {
47        flags
48    }
49}
50
51/// [`default_backends`], unless `WGPU_BACKEND` says otherwise.
52fn backends() -> wgpu::Backends {
53    wgpu::Backends::from_env().unwrap_or_else(default_backends)
54}
55
56/// Vulkan on Windows, where DX12 loses its device on every Remote Desktop
57/// reconnect; wgpu's primary backends (Vulkan, Metal, DX12, browser WebGPU)
58/// everywhere else, so macOS gets Metal and Linux picks what it has. Android adds GLES.
59fn default_backends() -> wgpu::Backends {
60    if cfg!(windows) {
61        wgpu::Backends::VULKAN
62    } else if cfg!(target_os = "android") {
63        // Vulkan where the device has a usable driver, GLES where it does not (older
64        // phones, and emulators whose host GPU is not passed through).
65        wgpu::Backends::VULKAN | wgpu::Backends::GL
66    } else {
67        wgpu::Backends::PRIMARY
68    }
69}
70
71/// Report which adapter was chosen and arm the device-loss callback.
72///
73/// The startup line is how you confirm which backend actually took effect.
74pub fn on_render_state(render_state: Option<&egui_wgpu::RenderState>) {
75    let Some(render_state) = render_state else {
76        tracing::warn!("no wgpu render state; skipping GPU diagnostics");
77        return;
78    };
79
80    let info = render_state.adapter.get_info();
81    tracing::info!(
82        backend = ?info.backend,
83        adapter = %info.name,
84        device_type = ?info.device_type,
85        driver = %info.driver_info,
86        "wgpu renderer ready"
87    );
88
89    // Fires before egui-wgpu notices, so this is the only place the real reason
90    // is available.
91    render_state
92        .device
93        .set_device_lost_callback(|reason, message| {
94            tracing::error!(?reason, %message, "GPU device lost");
95        });
96}
97
98/// Install a panic hook that translates egui-wgpu's staging-buffer panic.
99///
100/// The hook only adds an explanation; the previous hook still runs, so the
101/// original message and any backtrace are preserved.
102pub fn install_panic_hook() {
103    let previous = std::panic::take_hook();
104    std::panic::set_hook(Box::new(move |info| {
105        if let Some(explanation) = panic_message(info).and_then(device_lost_explanation) {
106            tracing::error!("{explanation}");
107        }
108        previous(info);
109    }));
110}
111
112fn panic_message<'a>(info: &'a std::panic::PanicHookInfo<'_>) -> Option<&'a str> {
113    let payload = info.payload();
114    payload
115        .downcast_ref::<&str>()
116        .copied()
117        .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
118}
119
120const DEVICE_LOST_EXPLANATION: &str = concat!(
121    "The GPU device was lost. egui-wgpu reports this as a staging buffer failure, ",
122    "but the buffer size is not the problem: wgpu swallows a lost device and returns ",
123    "None, and egui-wgpu panics on that None. On Windows this usually means a Remote ",
124    "Desktop reconnect or a display change removed the graphics device. Restart the app.",
125);
126
127/// Recognise egui-wgpu's staging-buffer panic and say what it actually means.
128///
129/// Both the index-data and vertex-data branches of `Renderer::update_buffers`
130/// share the same opening, so one substring covers them. Returns `None` for any
131/// other panic message.
132fn device_lost_explanation(panic_msg: &str) -> Option<&'static str> {
133    panic_msg
134        .contains("Failed to create staging buffer for ")
135        .then_some(DEVICE_LOST_EXPLANATION)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    /// The real text from egui-wgpu 0.34.3 `renderer.rs`, index-buffer branch.
143    const INDEX_PANIC: &str = "Failed to create staging buffer for index data. \
144        Index count: 16113. Required index buffer size: 64452. \
145        Actual size 99168 and capacity: 99168 (bytes)";
146
147    /// The vertex-buffer branch of the same function panics with parallel wording.
148    const VERTEX_PANIC: &str = "Failed to create staging buffer for vertex data. \
149        Vertex count: 4028. Required vertex buffer size: 128896. \
150        Actual size 198336 and capacity: 198336 (bytes)";
151
152    #[test]
153    fn explains_the_index_buffer_panic() {
154        assert!(device_lost_explanation(INDEX_PANIC).is_some());
155    }
156
157    #[test]
158    fn explains_the_vertex_buffer_panic() {
159        assert!(device_lost_explanation(VERTEX_PANIC).is_some());
160    }
161
162    #[test]
163    fn explanation_names_the_real_cause() {
164        let msg = device_lost_explanation(INDEX_PANIC).unwrap();
165        assert!(
166            msg.contains("device"),
167            "should say the device was lost: {msg}"
168        );
169        assert!(
170            msg.contains("Remote Desktop"),
171            "should name the usual Windows trigger: {msg}"
172        );
173    }
174
175    #[test]
176    fn ignores_unrelated_panics() {
177        assert_eq!(
178            device_lost_explanation("index out of bounds: len is 3"),
179            None
180        );
181        assert_eq!(device_lost_explanation(""), None);
182    }
183
184    #[cfg(windows)]
185    #[test]
186    fn defaults_to_vulkan_on_windows_without_an_env_override() {
187        // `backends()` reads WGPU_BACKEND, which is not set in the test harness.
188        assert_eq!(backends(), wgpu::Backends::VULKAN);
189    }
190
191    #[cfg(target_os = "android")]
192    #[test]
193    fn defaults_to_vulkan_and_gl_on_android_without_an_env_override() {
194        // `backends()` reads WGPU_BACKEND, which is not set in the test harness.
195        assert_eq!(backends(), wgpu::Backends::VULKAN | wgpu::Backends::GL);
196    }
197
198    #[cfg(not(any(windows, target_os = "android")))]
199    #[test]
200    fn defaults_to_the_primary_backends_elsewhere_without_an_env_override() {
201        // `backends()` reads WGPU_BACKEND, which is not set in the test harness.
202        assert_eq!(backends(), wgpu::Backends::PRIMARY);
203    }
204
205    #[test]
206    fn android_drops_the_debug_labels_and_keeps_validation() {
207        let flags = instance_flags(wgpu::InstanceFlags::debugging());
208        assert_eq!(
209            flags.contains(wgpu::InstanceFlags::DEBUG),
210            !cfg!(target_os = "android")
211        );
212        assert!(flags.contains(wgpu::InstanceFlags::VALIDATION));
213    }
214
215    #[test]
216    fn the_default_is_vulkan_on_windows_and_adds_gl_on_android() {
217        assert_eq!(
218            default_backends(),
219            if cfg!(windows) {
220                wgpu::Backends::VULKAN
221            } else if cfg!(target_os = "android") {
222                wgpu::Backends::VULKAN | wgpu::Backends::GL
223            } else {
224                wgpu::Backends::PRIMARY
225            }
226        );
227    }
228}