fix(paper): skip GPU for off-canvas inactive viewports

Zooming the paper view in PSPACE grows each floating viewport's screen
rectangle until its edges run past the canvas. The per-viewport blit
computes its surface offset via `(screen_rect.x * cw) as u32`, which
saturates negative values to 0 — so a viewport hanging off the left
edge was being painted at the canvas top-left instead of its actual
position, producing a drifting ghost copy on top of the CPU projection
that PaperCanvas was already drawing correctly clipped.

`active_viewports` for paper now skips the GPU pass for any inactive
content viewport whose screen rect would extend beyond the canvas. The
CPU projection already handles those, so only one (correct) copy is
visible. The active viewport keeps the GPU path so its per-viewport
render mode (Hidden Line, Gouraud, …) still applies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-28 10:38:56 +03:00
1 parent dac835fe33
commit 600717c1ec
1 file changed
+19 -1
+19 -1
View File
@@ -4644,12 +4644,30 @@ impl Scene {
) else {
continue;
};
let is_active = self.active_viewport == Some(h);
// Skip the GPU pass for inactive viewports whose screen rect
// extends beyond the canvas — the per-viewport pipeline blits
// the full MSAA at a u32-saturated (≥0) surface offset, so a
// negative offset would paint the content at the canvas
// corner instead of where the viewport actually sits. The CPU
// projection in PaperCanvas already handles those correctly
// clipped to the canvas, so the user simply sees that copy.
// The active viewport always renders through the GPU so its
// own render mode (Hidden Line, Gouraud, …) still applies.
if !is_active
&& (screen_rect.x < 0.0
|| screen_rect.y < 0.0
|| screen_rect.x + screen_rect.width > canvas_w
|| screen_rect.y + screen_rect.height > canvas_h)
{
continue;
}
out.push(ViewportInstance {
handle: h,
screen_rect,
camera,
render_mode: vp.render_mode,
active: self.active_viewport == Some(h),
active: is_active,
});
}
out