From deebc1401225848534a01ad0d5928e8cf60b70c9 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:10:20 +0300 Subject: [PATCH] fix: a rejected upload must not latch the slot as up to date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/scene/view/render.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 60343204a..7994a84dc 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -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 {