GPU: the frame guard contains validation errors and drops the failed frame's textures (refs #1319)

This commit is contained in:
tylerjenningsw
2026-10-13 13:36:04 -04:00
parent ec29d78ca2
commit babdcd3b9f
4 changed files with 70 additions and 10 deletions
+12
View File
@@ -980,6 +980,18 @@ impl GpuContext {
s.clear();
}
}
/// After a failed frame: forget every texture and buffer kept for reuse, since any of them
/// may be one the device rejected (#1319). Later frames allocate afresh.
pub(crate) fn discard_kept(&self) {
self.clear_uploads();
if let Ok(mut z) = self.zeros.lock() {
z.clear();
}
if let Ok(mut c) = self.particle_states.lock() {
c.clear();
}
}
}
/// Records GPU work for one render (a command encoder submitted at readback or the end).
+17 -10
View File
@@ -264,10 +264,13 @@ impl Gpu {
}
}
/// Run work in native thread-local out-of-memory and internal-error scopes. A failed
/// result is discarded and cached allocations are released; callers can retry with a fresh
/// CPU renderer. Some drivers report a failed allocation as an internal error rather than
/// out-of-memory (Adreno's `ERROR_INITIALIZATION_FAILED`, #591), so both count.
/// Run work in native thread-local out-of-memory, internal-error and validation scopes. A
/// failed result is discarded and the textures and buffers kept for reuse are released;
/// callers can retry with a fresh CPU renderer. Some drivers report a failed allocation as an
/// internal error rather than out-of-memory (Adreno's `ERROR_INITIALIZATION_FAILED`, #591),
/// so both count. A failed allocation leaves an invalid texture whose use is then a
/// validation error (`Texture::create_view`, #1319): caught here, it fails this frame only
/// instead of retiring the GPU compositor for the session as an uncaptured error.
/// Browser callers use the host/owned-device failure bridge and bounded readbacks instead
/// of blocking their event loop on an error-scope future.
pub fn within_memory<T>(&self, f: impl FnOnce() -> T) -> Result<T, String> {
@@ -276,16 +279,20 @@ impl Gpu {
{
let oom = self.ctx.device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
let internal = self.ctx.device.push_error_scope(wgpu::ErrorFilter::Internal);
let validation = self.ctx.device.push_error_scope(wgpu::ErrorFilter::Validation);
let out = f();
let validation = pollster::block_on(validation.pop());
let internal = pollster::block_on(internal.pop());
let oom = pollster::block_on(oom.pop());
match internal.or(oom) {
Some(e) => {
self.ctx.clear_uploads();
Err(e.to_string())
}
None => self.ctx.check_health().map(|()| out),
// The allocation failure first: a validation error is often its consequence.
let errors: Vec<String> = [oom, internal, validation].into_iter().flatten().map(|e| e.to_string()).collect();
if errors.is_empty() {
return self.ctx.check_health().map(|()| out);
}
// Its textures go back to the pool when dropped: drop them before emptying it.
drop(out);
self.ctx.discard_kept();
Err(errors.join("; then: "))
}
#[cfg(target_arch = "wasm32")]
{
+7
View File
@@ -33,6 +33,13 @@ pub(crate) struct Checkpoints {
clock: u64,
}
impl Checkpoints {
/// Forget every kept state (after a failed frame, whose buffers may be invalid).
pub(crate) fn clear(&mut self) {
self.list.clear();
}
}
fn pipes(g: &GpuContext) -> &Pipes {
g.particles.get_or_init(|| {
let d = &g.device;
+34
View File
@@ -746,6 +746,40 @@ fn within_memory_returns_the_work_when_there_is_room() {
assert_eq!((img.width, img.height), (64, 32));
}
/// A texture the device rejects, then used in the frame guard (its view: "Texture with '' label
/// is invalid", #1319), fails that frame only. The validation error doesn't reach the device's
/// handler (which retires the compositor for the session, and panics in tests), the frame's
/// textures don't go back to the pool, and the next frame renders on the GPU.
#[test]
fn a_rejected_texture_fails_the_frame_not_the_compositor() {
hold_gpu_lock();
let Some(g) = Gpu::headless() else { return };
let too_wide = g.ctx.device.limits().max_texture_dimension_2d.saturating_add(1);
let err = g
.within_memory(|| {
let mut e = crate::context::Enc::new(&g.ctx);
let kept = e.image(64, 32);
let rejected = g.ctx.device.create_texture(&wgpu::TextureDescriptor {
label: Some("deliberately too wide"),
size: wgpu::Extent3d { width: too_wide, height: 1, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
(kept, rejected.create_view(&Default::default()))
})
.expect_err("the rejected texture fails the frame");
assert!(err.contains("deliberately too wide"), "{err}");
assert!(g.ctx.check_health().is_ok(), "the compositor stays on: {:?}", g.ctx.check_health());
let (hits, _) = g.ctx.pool_stats();
let img = g.within_memory(|| crate::context::Enc::new(&g.ctx).image(64, 32)).expect("the next frame renders");
assert_eq!((img.width, img.height), (64, 32));
assert_eq!(g.ctx.pool_stats().0, hits, "the failed frame's texture wasn't reused");
}
/// A buffer whose allocation fails (here: over the device's buffer size limit; on Adreno an
/// internal `ERROR_INITIALIZATION_FAILED`) is reported to the caller's error scope instead of
/// panicking in `create_buffer_init`'s mapped range on a frame thread (#591).