From 5f0088211e6609f6116af55510e39b54950098fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Tue, 13 Oct 2026 20:04:01 -0400 Subject: [PATCH] metal: intersect the scissor rect with the pass apply_scissor_rect clamped the flipped y and the height into the pass one at a time. A rect reaching past both the top and the bottom of the pass clamped to the whole pass, so it didn't clip at all along that axis, and one starting left of the pass kept its full width from x = 0. OpenGL clips both to the part of the rect inside the target. Co-Authored-By: Claude Opus 5.5 --- src/graphics/metal.rs | 93 +++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/src/graphics/metal.rs b/src/graphics/metal.rs index e18ec9f75..68c2dc665 100644 --- a/src/graphics/metal.rs +++ b/src/graphics/metal.rs @@ -416,6 +416,34 @@ impl MetalContext { } } +/// Flips a bottom-left-origin scissor rect to Metal's top-left origin +/// against the active pass, not the screen (they diverge for off-screen +/// targets at high DPI), and intersects it with the pass, as Metal +/// validation rejects a scissor that reaches past it. Callers (macroquad) +/// can pass one that does, e.g. sized against a screen size that lags +/// the drawable's by a frame mid-rotation. +fn scissor_within_pass( + x: i32, + y: i32, + w: i32, + h: i32, + pass_width: i32, + pass_height: i32, +) -> MTLScissorRect { + let left = x.clamp(0, pass_width); + let right = x.saturating_add(w).clamp(0, pass_width).max(left); + let top = pass_height + .saturating_sub(y.saturating_add(h)) + .clamp(0, pass_height); + let bottom = pass_height.saturating_sub(y).clamp(0, pass_height).max(top); + MTLScissorRect { + x: left as u64, + y: top as u64, + width: (right - left) as u64, + height: (bottom - top) as u64, + } +} + impl RenderingBackend for MetalContext { fn info(&self) -> ContextInfo { ContextInfo { @@ -450,26 +478,14 @@ impl RenderingBackend for MetalContext { fn apply_scissor_rect(&mut self, x: i32, y: i32, w: i32, h: i32) { assert!(self.render_encoder.is_some()); - // Flip against the active pass height, not the screen — - // they diverge for off-screen targets at high DPI. Then - // clamp into `[0, pass_size]` on both axes: callers - // (macroquad) can briefly request a scissor sized against - // a stale screen cache (e.g. mid-rotation, where the - // caller's `screen_size()` lags MTKView's drawable - // dimensions by a frame), and an unclamped scissor would - // trip Metal validation. - let pass_width = self.current_pass_width as i32; - let pass_height = self.current_pass_height as i32; - let x_clamped = x.clamp(0, pass_width); - let y_metal = (pass_height - (y + h)).clamp(0, pass_height); - let w_clamped = (pass_width - x_clamped).clamp(0, w.max(0)); - let h_clamped = (pass_height - y_metal).clamp(0, h.max(0)); - let r = MTLScissorRect { - x: x_clamped as u64, - y: y_metal as u64, - width: w_clamped as u64, - height: h_clamped as u64, - }; + let r = scissor_within_pass( + x, + y, + w, + h, + self.current_pass_width as i32, + self.current_pass_height as i32, + ); unsafe { msg_send_![self.render_encoder.unwrap(), setScissorRect: r] }; } fn texture_set_min_filter( @@ -1471,3 +1487,40 @@ impl RenderingBackend for MetalContext { } } } + +#[cfg(test)] +mod tests { + use super::scissor_within_pass; + + fn scissor(x: i32, y: i32, w: i32, h: i32) -> (u64, u64, u64, u64) { + let r = scissor_within_pass(x, y, w, h, 804, 1748); + (r.x, r.y, r.width, r.height) + } + + #[test] + fn flips_a_rect_inside_the_pass() { + assert_eq!(scissor(10, 20, 100, 200), (10, 1528, 100, 200)); + } + + #[test] + fn keeps_the_part_of_a_taller_rect_inside_the_pass() { + assert_eq!(scissor(0, -6992, 804, 8304), (0, 436, 804, 1312)); + assert_eq!(scissor(0, 436, 804, 8304), (0, 0, 804, 1312)); + } + + #[test] + fn keeps_the_part_of_a_wider_rect_inside_the_pass() { + assert_eq!(scissor(-100, 0, 300, 1748), (0, 0, 200, 1748)); + assert_eq!(scissor(700, 0, 300, 1748), (700, 0, 104, 1748)); + } + + #[test] + fn empties_a_rect_outside_the_pass() { + assert_eq!(scissor(-300, 0, 200, 1748).2, 0); + assert_eq!(scissor(900, 0, 200, 1748).2, 0); + assert_eq!(scissor(0, -300, 804, 200).3, 0); + assert_eq!(scissor(0, 1800, 804, 200).3, 0); + let (_, _, width, height) = scissor(10, 20, -5, -5); + assert_eq!((width, height), (0, 0)); + } +}