Files
solidcraft/crates/ui-egui/src/surface_errors_tests.rs
T
momysnowandClaude Sonnet 5.5 10b5cc5914 GPU: an uncaught error on the window's device is logged, not a panic (#5486) (#5667)
wgpu's default uncaptured-error handler panics ("wgpu error: Out of Memory").
The headless picture devices already report instead; the window's device had
no handler. Errors the code expects stay under error scopes.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 10:42:44 +00:00

200 lines
10 KiB
Rust

//! A surface the driver fails to reconfigure was a crash (#1935): on D3D12, `Surface::configure`
//! reported "Failed to wait for GPU to come idle before reconfiguring the Surface", and the
//! device's default uncaptured-error handler panicked on it. The vendored egui-wgpu configures
//! its surfaces under `capture_errors`, logs the error and tries again once the retry is due. A
//! driver's refusal can't be provoked here, so these check the capture on a real device with an
//! error the validation always reports, and the retry policy with a stand-in clock and device.
//! An error no scope catches at all is reported by the device's handler
//! (`gpu::log_uncaptured_errors`) instead of panicking (#5486).
use egui_wgpu::{Attempt, ConfigureRetry, SURFACE_RECREATE_AFTER, capture_errors, wgpu};
use std::time::Duration;
use web_time::Instant;
#[test]
fn an_error_under_capture_is_returned_not_a_panic() {
let Some((_, device, _)) = crate::gpu::offscreen_device() else { return };
// Mapped at creation with a size that isn't a multiple of 4: a validation error.
let (_, error) = capture_errors(&device, || {
device.create_buffer(&wgpu::BufferDescriptor { label: Some("bad"), size: 3, usage: wgpu::BufferUsages::MAP_WRITE, mapped_at_creation: true })
});
assert!(error.is_some_and(|e| matches!(e, wgpu::Error::Validation { .. })), "the validation error is captured");
// The device goes on working, and a clean call reports nothing.
let (buffer, error) = capture_errors(&device, || {
device.create_buffer(&wgpu::BufferDescriptor { label: Some("good"), size: 4, usage: wgpu::BufferUsages::COPY_DST, mapped_at_creation: false })
});
assert!(error.is_none(), "{error:?}");
assert_eq!(buffer.size(), 4);
}
/// An error no scope caught on the window's device used to reach wgpu's default handler, which
/// panics ("wgpu error: Out of Memory", #5486). With the handler installed it is reported and
/// the device goes on working.
#[test]
fn an_uncaught_gpu_error_is_not_a_panic() {
let Some((_, device, queue)) = crate::gpu::offscreen_device() else { return };
crate::gpu::log_uncaptured_errors(&device);
// No error scope around it: mapped at creation with a size that isn't a multiple of 4.
let _bad =
device.create_buffer(&wgpu::BufferDescriptor { label: Some("bad"), size: 3, usage: wgpu::BufferUsages::MAP_WRITE, mapped_at_creation: true });
let (buffer, error) = capture_errors(&device, || {
device.create_buffer(&wgpu::BufferDescriptor { label: Some("good"), size: 4, usage: wgpu::BufferUsages::COPY_DST, mapped_at_creation: false })
});
assert!(error.is_none(), "{error:?}");
queue.submit([]);
assert_eq!(buffer.size(), 4);
}
/// A surface that keeps refusing its configuration waits 16 ms before the next try, doubling with
/// each refusal in a row up to 1 s, and every few refusals the surface is recreated (asked in
/// review of #1966). Only the first refusal of the streak is worth a warning.
#[test]
fn refused_configures_back_off_and_recreate_the_surface() {
let start = Instant::now();
let mut retry = ConfigureRetry::default();
let plans: Vec<_> = (0..9).map(|_| retry.refused(start)).collect();
let delays: Vec<u128> = plans.iter().map(|plan| plan.delay.as_millis()).collect();
assert_eq!(delays, [16, 32, 64, 128, 256, 512, 1000, 1000, 1000], "capped at a second");
assert_eq!(plans.iter().map(|plan| plan.failures).collect::<Vec<_>>(), (1..=9).collect::<Vec<_>>());
assert_eq!(plans.iter().filter(|plan| plan.first).count(), 1);
assert!(plans.first().is_some_and(|plan| plan.first));
let recreate: Vec<u32> = plans.iter().filter(|plan| plan.recreate).map(|plan| plan.failures).collect();
assert_eq!(recreate, [SURFACE_RECREATE_AFTER, 2 * SURFACE_RECREATE_AFTER, 3 * SURFACE_RECREATE_AFTER]);
// The last refusal's try is due a second later.
assert_eq!(retry.wait(start), Some(Duration::from_secs(1)));
assert!(retry.is_due(start + Duration::from_secs(1)));
}
/// The review of #1966: a repaint that comes before the retry is due (pointer input, an
/// animation, another UI update) must not configure or recreate the surface. Here the window
/// repaints every frame at 60 Hz for 10 s on a device that refuses every configure, and the
/// frames follow `paint_and_update_textures`: recreate when one is pending and due (which
/// `recreate_surface` marks done), then configure.
#[test]
fn repaints_before_the_retry_is_due_neither_configure_nor_recreate() {
let start = Instant::now();
// Just after a refusal, each early repaint is told how long is left and tries nothing.
let mut retry = ConfigureRetry::default();
retry.refused(start);
for ms in 0..16 {
let mut tried = false;
let attempt = retry.attempt(start + Duration::from_millis(ms), || {
tried = true;
Ok::<(), ()>(())
});
assert!(matches!(attempt, Attempt::Waiting(left) if left == Duration::from_millis(16 - ms)), "{ms} ms: {attempt:?}");
assert!(!tried, "{ms} ms: configured before the retry was due");
}
assert_eq!(retry.failures(), 1, "waiting doesn't count as a refusal");
let frame = Duration::from_micros(16_667);
let frames = 600;
let mut retry = ConfigureRetry::default();
let (mut configures, mut recreates) = (0u32, 0u32);
let mut refusals: Vec<(Instant, Duration)> = Vec::new();
for n in 0..frames {
let now = start + frame * n;
if retry.recreate_due(now) {
assert!(retry.is_due(now), "recreated before the retry was due");
recreates += 1;
retry.recreated();
} else if retry.recreate_pending() {
assert!(!retry.is_due(now), "a due recreate was left pending");
}
let attempt = retry.attempt(now, || {
configures += 1;
Err::<(), _>("refused")
});
match attempt {
Attempt::Refused(plan, _) => {
assert_eq!(plan.recreate, retry.recreate_pending(), "{plan:?}");
refusals.push((now, plan.delay));
}
Attempt::Waiting(left) => assert!(left <= Duration::from_secs(1), "{left:?}"),
Attempt::Took(_) => panic!("the device refuses every configure"),
}
}
// The tries ramp up through 16 ms .. 512 ms (7 tries in the first second), then one a second:
// 15 configures in 600 frames, not one a frame.
assert_eq!(configures, 15, "{refusals:?}");
assert_eq!(refusals.len(), 15);
// No try comes before the delay the previous refusal set.
for pair in refusals.windows(2) {
let [(at, delay), (next, _)] = pair else { continue };
assert!(next.duration_since(*at) >= *delay, "{pair:?}");
}
// A recreate before the try after every third refusal (the 3rd, 6th, 9th and 12th; the
// 15th's try falls after the 10 s), not every third frame.
assert_eq!(recreates, (configures - 1) / SURFACE_RECREATE_AFTER);
assert_eq!(recreates, 4);
assert!(retry.recreate_pending(), "the 15th refusal asked for a recreate, still waiting for its retry");
}
/// A configure the device takes ends the streak: the next refusal is a first one again, warned
/// about and retried after 16 ms. A first configure is never held back.
#[test]
fn a_configure_the_device_takes_ends_the_streak() {
let start = Instant::now();
let mut retry = ConfigureRetry::default();
assert!(retry.is_due(start), "nothing refused yet");
for _ in 0..4 {
retry.refused(start);
}
let due = start + Duration::from_millis(128);
assert!(!retry.is_due(due - Duration::from_millis(1)));
let attempt = retry.attempt(due, || Ok::<(), ()>(()));
assert!(matches!(attempt, Attempt::Took(4)), "{attempt:?}");
assert_eq!(retry, ConfigureRetry::default());
assert_eq!(retry.failures(), 0);
assert_eq!(retry.wait(due), None, "no deadline left");
let plan = retry.refused(due);
assert!(plan.first && plan.failures == 1 && !plan.recreate, "{plan:?}");
assert_eq!(plan.delay, Duration::from_millis(16));
assert_eq!(retry.wait(due), Some(Duration::from_millis(16)));
// `took` alone does the same, and reports an empty streak as 0.
assert_eq!(retry.took(), 1);
assert_eq!(retry.took(), 0);
}
/// A configure the device takes after the refusal that asked for a recreate (a resize at the
/// retry's deadline, before the next frame) ends the streak with its recreate: the surface took
/// its configuration, so the next frame doesn't recreate it.
#[test]
fn a_configure_taken_cancels_the_pending_recreate() {
let start = Instant::now();
let mut retry = ConfigureRetry::default();
let plans: Vec<_> = (0..SURFACE_RECREATE_AFTER).map(|_| retry.refused(start)).collect();
assert!(plans.last().is_some_and(|plan| plan.recreate));
let due = start + Duration::from_millis(64);
assert!(retry.recreate_pending());
assert!(!retry.recreate_due(due - Duration::from_millis(1)), "not before the deadline");
assert!(retry.recreate_due(due));
let attempt = retry.attempt(due, || Ok::<(), ()>(()));
assert!(matches!(attempt, Attempt::Took(n) if n == SURFACE_RECREATE_AFTER), "{attempt:?}");
assert!(!retry.recreate_pending());
assert!(!retry.recreate_due(due + Duration::from_secs(10)));
assert_eq!(retry, ConfigureRetry::default());
}
/// `recreate_surface` hands the old surface's retry to the new one with the recreate marked
/// done: the deadline still holds, the new surface doesn't recreate itself again, and the streak
/// goes on rather than starting again.
#[test]
fn the_retry_survives_a_recreate() {
let start = Instant::now();
let mut old = ConfigureRetry::default();
let plans: Vec<_> = (0..SURFACE_RECREATE_AFTER).map(|_| old.refused(start)).collect();
assert!(plans.last().is_some_and(|plan| plan.recreate));
let mut carried = old;
carried.recreated();
assert!(old.recreate_pending() && !carried.recreate_pending());
assert!(!carried.recreate_due(start + Duration::from_secs(10)));
assert_eq!(carried.failures(), old.failures());
let soon = start + Duration::from_millis(10);
assert_eq!(carried.wait(soon), old.wait(soon));
assert_eq!(carried.wait(soon), Some(Duration::from_millis(54)));
let next = carried.refused(start + Duration::from_millis(64));
assert!(!next.first, "{next:?}");
assert_eq!((next.failures, next.delay), (SURFACE_RECREATE_AFTER + 1, Duration::from_millis(128)));
}