Native crash capture on Windows, and startup steps in crash reports (#1215)
A crash inside a graphics driver left only "an unclean exit between commands": no backtrace, no module, no hint that it was at startup. - The crash journal now keeps the startup step the app is in (window, graphics instance, surface, adapter request, adapter chosen, device creation, renderer and viewport shader compiles, surface configure, first frame and its present, running) and the renderer as soon as the adapter is picked. egui-wgpu tells the app about its steps through a small hook. An unclean exit before the first frame names the step. - Windows: an unhandled-exception filter (src/native_crash.rs, the one Windows-only module allowed unsafe) writes the exception code, the faulting module and offset, and a minidump next to the journal; the next start puts them into the report. - The Windows CI job runs a child that faults on purpose and checks the report. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ddd660089
commit
d20b0fab65
15 files changed
+703
-30
No files matched your search
Vendored
+41
@@ -41,6 +41,35 @@ use std::sync::Arc;
|
||||
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
/// SolidCraft: what [`set_startup_hook`] is told before each step of getting the graphics
|
||||
/// device and the window's surface up, so a crash inside the driver (with no Rust panic) can
|
||||
/// still say which step it died in. `detail` is empty except for [`STEP_ADAPTER_CHOSEN`].
|
||||
pub type StartupHook = fn(step: &'static str, detail: &str);
|
||||
|
||||
static STARTUP_HOOK: std::sync::OnceLock<StartupHook> = std::sync::OnceLock::new();
|
||||
|
||||
/// SolidCraft: the steps [`StartupHook`] is told about, in order.
|
||||
pub const STEP_INSTANCE: &str = "graphics instance";
|
||||
pub const STEP_SURFACE: &str = "window surface";
|
||||
pub const STEP_ADAPTER: &str = "adapter request";
|
||||
/// The adapter was picked; `detail` names it, its backend and driver.
|
||||
pub const STEP_ADAPTER_CHOSEN: &str = "adapter chosen";
|
||||
pub const STEP_DEVICE: &str = "device creation";
|
||||
pub const STEP_RENDERER: &str = "egui renderer (shader compile)";
|
||||
pub const STEP_SURFACE_CONFIGURE: &str = "surface configure";
|
||||
pub const STEP_FIRST_PRESENT: &str = "first frame (present)";
|
||||
|
||||
/// SolidCraft: install the startup hook (once; later calls are ignored).
|
||||
pub fn set_startup_hook(hook: StartupHook) {
|
||||
let _ = STARTUP_HOOK.set(hook);
|
||||
}
|
||||
|
||||
pub(crate) fn startup_step(step: &'static str, detail: &str) {
|
||||
if let Some(hook) = STARTUP_HOOK.get() {
|
||||
hook(step, detail);
|
||||
}
|
||||
}
|
||||
|
||||
/// An error produced by egui-wgpu.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum WgpuError {
|
||||
@@ -226,6 +255,7 @@ impl RenderState {
|
||||
wgpu::Backends::all()
|
||||
};
|
||||
|
||||
startup_step(STEP_ADAPTER, ""); // SolidCraft
|
||||
instance.enumerate_adapters(backends).await
|
||||
};
|
||||
|
||||
@@ -257,6 +287,16 @@ impl RenderState {
|
||||
}
|
||||
}?;
|
||||
|
||||
// SolidCraft: name the adapter before the driver is asked for a device.
|
||||
let info = adapter.get_info();
|
||||
startup_step(
|
||||
STEP_ADAPTER_CHOSEN,
|
||||
&format!(
|
||||
"{:?} {} ({:?}, driver {} {})",
|
||||
info.backend, info.name, info.device_type, info.driver, info.driver_info
|
||||
),
|
||||
);
|
||||
startup_step(STEP_DEVICE, "");
|
||||
let (device, queue) = {
|
||||
profiling::scope!("request_device");
|
||||
adapter
|
||||
@@ -303,6 +343,7 @@ impl RenderState {
|
||||
msaa_samples,
|
||||
..options
|
||||
};
|
||||
startup_step(STEP_RENDERER, ""); // SolidCraft
|
||||
let renderer = Renderer::new(&device, target_format, options);
|
||||
|
||||
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
|
||||
|
||||
Vendored
+10
@@ -27,6 +27,9 @@ struct SurfaceState {
|
||||
needs_recreate: bool,
|
||||
}
|
||||
|
||||
/// SolidCraft: whether a frame was presented yet (`crate::STEP_FIRST_PRESENT`).
|
||||
static FIRST_PRESENT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
||||
///
|
||||
/// Alternatively you can use [`crate::Renderer`] directly.
|
||||
@@ -70,6 +73,7 @@ impl Painter {
|
||||
options: RendererOptions,
|
||||
) -> Self {
|
||||
let (capture_tx, capture_rx) = capture_channel();
|
||||
crate::startup_step(crate::STEP_INSTANCE, ""); // SolidCraft
|
||||
let instance = config.wgpu_setup.new_instance().await;
|
||||
|
||||
Self {
|
||||
@@ -206,6 +210,7 @@ impl Painter {
|
||||
if let Some(window) = window {
|
||||
let size = window.inner_size();
|
||||
if !self.surfaces.contains_key(&viewport_id) {
|
||||
crate::startup_step(crate::STEP_SURFACE, ""); // SolidCraft
|
||||
let surface = self.instance.create_surface(window)?;
|
||||
self.add_surface(surface, viewport_id, size).await?;
|
||||
}
|
||||
@@ -259,6 +264,7 @@ impl Painter {
|
||||
self.options.msaa_samples = render_state.msaa_samples;
|
||||
self.render_state = Some(render_state);
|
||||
}
|
||||
crate::startup_step(crate::STEP_SURFACE_CONFIGURE, ""); // SolidCraft
|
||||
self.install_surface(surface, viewport_id, size.width, size.height, false);
|
||||
Ok(())
|
||||
}
|
||||
@@ -628,6 +634,10 @@ impl Painter {
|
||||
profiling::scope!("get_current_texture");
|
||||
// This is what vsync-waiting happens on my Mac.
|
||||
let start = web_time::Instant::now();
|
||||
// SolidCraft: the first frame is the last startup step.
|
||||
if !FIRST_PRESENT.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||
crate::startup_step(crate::STEP_FIRST_PRESENT, "");
|
||||
}
|
||||
let output_frame = surface_state.surface.get_current_texture();
|
||||
vsync_sec += start.elapsed().as_secs_f32();
|
||||
output_frame
|
||||
|
||||
Reference in new issue
Block a user