fix: never cache a buffer the device rejected

After an out-of-memory the viewport stopped updating for the rest of the
session — both Model and Layout frozen, the UI still responsive. The log
shows why: `block-wire-geometry` prints *after* the errors it provoked.
The failed uploads were cached.

A rejected allocation still yields a `Buffer`. It is invalid, every later
use reports again, and it is indistinguishable from a good one at the
cache key. So the entry keeps hitting, nothing ever rebuilds, and the
frame stays empty even once memory frees up. Sharing `block_geometry`
across slots in the previous commit made that worse, not better: one
poisoned entry now serves every viewport.

The device's uncaptured-error counter was already there, private to the
handler that throttles the log. It is now readable, and the three caches
that can retain a bad upload — block definition geometry, the shared wire
buffer cache, and the arena's content id — snapshot it across their build
and decline to record what it condemns. The degraded frame still draws;
the next frame tries again.

Best effort by construction: wgpu surfaces some errors asynchronously, so
a moved counter means something failed, while an unmoved one is not proof
that nothing did. That asymmetry is the right way round here — believing
a bad buffer costs a frozen viewport, rebuilding a good one costs a frame.

`block-wire-geometry` gains `poisoned=` so a run says whether this
happened rather than leaving it to be inferred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-05 18:53:09 +03:00
1 parent 6599b82776
commit d9c998fa17
3 files changed
+62 -11

No files matched your search

+24 -4
View File
@@ -714,6 +714,9 @@ impl shader::Primitive for Primitive {
})
})
};
// Snapshot before any arena allocation, so a rejected
// upload cannot claim this content id below.
let arena_errors_before = crate::scene::pipeline::gpu_errors_seen();
let reg_ok = base_ok
&& if let Some(arena) = inner.wire_arena.as_mut() {
let patch = patch.unwrap();
@@ -868,7 +871,15 @@ impl shader::Primitive for Primitive {
inner.wire_handle_index =
wire_arena::build_handle_index(&vp_wires[..]);
}
inner.wire_arena_id = vp.wire_content_id;
// Only claim the content id when the device accepted
// the upload; otherwise the slot believes it holds this
// content and never rebuilds it.
inner.wire_arena_id =
if crate::scene::pipeline::gpu_errors_seen() == arena_errors_before {
vp.wire_content_id
} else {
u64::MAX
};
arena_served = true;
} else {
inner.wire_arena = None;
@@ -922,6 +933,8 @@ impl shader::Primitive for Primitive {
);
}
let t_upload = _perf.then(iced::time::Instant::now);
let errors_before =
crate::scene::pipeline::gpu_errors_seen();
let entry =
inner.build_wire_buffers(device, queue, &vp_wires[..], &draw_depths);
if let Some(start) = t_upload {
@@ -933,9 +946,16 @@ impl shader::Primitive for Primitive {
);
}
pipeline
.wire_buffer_cache
.insert(vp.wire_content_id, entry.clone());
// Buffers the device rejected are still `Buffer`s.
// Cached under a content id that keeps matching,
// they render nothing for the rest of the session
// and nothing ever rebuilds them. Draw the degraded
// frame, but let the next one try again.
if crate::scene::pipeline::gpu_errors_seen() == errors_before {
pipeline
.wire_buffer_cache
.insert(vp.wire_content_id, entry.clone());
}
entry
}
};