Files
cadcraft/crates/ui-egui/tests/gpu_faults.rs
2026-10-11 20:42:45 -04:00

91 lines
3.9 KiB
Rust

//! A wgpu error or device loss no longer closes the app (#602): it is recorded, unsaved drawings
//! are autosaved at once instead of SAVETIME minutes later, and a notice says what happened.
//! SAVEFILEPATH is process-wide, so everything runs in one test.
use cadcraft_engine::Session;
use cadcraft_engine::cmd::file::{IoHooks, set_io};
use cadcraft_engine::doc::Drawing;
use cadcraft_ui_egui::gpu::GpuFaults;
use cadcraft_ui_egui::{CadApp, Services};
use egui_wgpu::wgpu::DeviceLostReason;
/// The text the notice paints (the second frame: a new window sizes itself in the first).
fn painted_text(app: &mut CadApp, ctx: &egui::Context) -> String {
fn collect(shape: &egui::Shape, text: &mut String) {
match shape {
egui::Shape::Text(t) => {
text.push_str(t.galley.text());
text.push('\n');
}
egui::Shape::Vec(v) => v.iter().for_each(|s| collect(s, text)),
_ => {}
}
}
let mut text = String::new();
for _ in 0..2 {
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| cadcraft_ui_egui::autosave::gpu_notice(app, ui.ctx()));
out.textures_delta.clear(); // no renderer here
text.clear();
out.shapes.iter().for_each(|s| collect(&s.shape, &mut text));
}
text
}
#[test]
fn faults_are_recorded_autosaved_and_shown() {
// The registry reports each new batch once; dropping the device on purpose is not a fault.
let f = GpuFaults::default();
assert_eq!(f.take(), None);
f.error("first".into());
f.error("second".into());
let got = f.take().unwrap();
assert_eq!((got.errors, got.lost, got.message.as_str()), (2, false, "second"));
assert_eq!(f.take(), None);
f.device_lost(DeviceLostReason::Destroyed, "dropped".into());
assert_eq!(f.take(), None);
f.device_lost(DeviceLostReason::Unknown, "driver reset".into());
assert!(f.take().is_some_and(|g| g.lost && g.message == "driver reset"));
set_io(IoHooks { read: |_, _| Ok(Drawing::new_metric()), write: |_, _| Ok(b"0\nEOF\n".to_vec()), plot: None });
let dir = std::env::temp_dir().join(format!("cadcraft-gpu-faults-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let mut app = CadApp::new(Session::new(), Services::default());
app.enable_autosave(&dir);
app.run("circle", serde_json::json!({"center": [0, 0], "radius": 1})).unwrap();
let ctx = egui::Context::default();
app.gpu_fault_tick(&ctx);
assert!(app.gpu_notice.is_none(), "no fault, no notice");
app.gpu_faults.error("Buffer size 302884104 is greater than the maximum buffer size".into());
app.gpu_fault_tick(&ctx);
let n = app.gpu_notice.clone().unwrap();
assert_eq!((n.fault.errors, n.fault.lost, n.autosaved), (1, false, 1));
// The change was autosaved now, not SAVETIME minutes from now.
let written = || std::fs::read_dir(&dir).map(|d| d.flatten().filter(|e| e.path().extension().is_some_and(|x| x == "sv$")).count()).unwrap_or(0);
for _ in 0..500 {
if written() == 1 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert_eq!(written(), 1);
let text = painted_text(&mut app, &ctx);
assert!(text.contains("graphics driver reported an error") && text.contains("recovery copy"), "{text}");
// The same error again (an invalid buffer used every frame): counted, nothing new to save.
app.gpu_faults.error("again".into());
app.gpu_fault_tick(&ctx);
let n = app.gpu_notice.clone().unwrap();
assert_eq!((n.fault.errors, n.autosaved), (2, 1));
assert!(painted_text(&mut app, &ctx).contains("(2 times)"));
app.gpu_faults.device_lost(DeviceLostReason::Unknown, "lost".into());
app.gpu_fault_tick(&ctx);
assert!(app.gpu_notice.as_ref().is_some_and(|n| n.fault.lost));
assert!(painted_text(&mut app, &ctx).contains("can't draw again until it restarts"));
app.shutdown_autosave();
let _ = std::fs::remove_dir_all(&dir);
}