fix: a rejected upload must not latch the slot as up to date

The drawing vanished from Model and only came back with REGENALL. That is
this bug's exact signature, and the guard added two commits ago was
incomplete.

`cached_wire_id` gates the entire wire upload block — line 620 skips it
whenever the slot's id already matches the viewport's content id — and it
was stamped unconditionally at the end of that block, whether or not the
device had accepted what was uploaded. So a rejected allocation left the
slot holding invalid buffers *and* claiming to be current. Nothing
rebuilt it. REGENALL worked because it mints a new content id, which is
the one thing that makes the ids differ again.

`wire_buffer_cache` and `wire_arena_id` were guarded; this third latch,
the one that gates them both, was missed. Guarding it individually would
leave the same question for `cached_text_source`, `cached_hatch_source`
and the rest, each stamped after its own upload.

So the check moves to where it covers all of them: if the device reported
anything during this viewport's turn, the slot forgets every cache key it
holds and the next frame rebuilds it. The counter is device-wide, so an
error raised by one viewport also invalidates its neighbours in that
frame. That is deliberate — the cost is one slow frame, and the
alternative is the failure this fixes.

The narrower guards stay: `wire_buffer_cache` is shared across slots, so
keeping a bad entry out of it protects viewports that were healthy.

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 0118b28d10
commit deebc14012
1 file changed
+15
+15
View File
@@ -373,6 +373,12 @@ impl shader::Primitive for Primitive {
for (i, vp) in self.viewports.iter().enumerate() {
let inner = &mut pipeline.inners[slots[i]];
// Compared at the end of this viewport's turn. Every "we hold this
// now" latch below is stamped after its upload, whether or not the
// device accepted it — and the wire latch gates the whole upload
// block, so one rejected allocation could leave a slotpermanently empty with
// nothing that would ever rebuild it.
let slot_errors_before = crate::scene::pipeline::gpu_errors_seen();
// Pipeline slots are addressed by list index, but off-canvas
// viewports are dropped from the list — so a slot can be reused by a
// DIFFERENT viewport across frames (e.g. the first viewport scrolls
@@ -1180,6 +1186,15 @@ impl shader::Primitive for Primitive {
self.viewcube_text_color,
);
}
// A rejected allocation leaves buffers that are invalid but
// indistinguishable from good ones at every cache key. Forget
// them all, so the next frame rebuilds this slot instead of
// drawing nothing for the rest of the session — which is what
// sent a user to REGENALL to get the model back.
if crate::scene::pipeline::gpu_errors_seen() != slot_errors_before {
inner.forget_cached_keys();
}
}
let prepare_ms = nav_prepare_started.elapsed().as_secs_f64() * 1000.0;
if let Some(sample) = self.nav_perf {