From c4b8f96d3c90ef6ba180d25fed5b697da4efb7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Mon, 19 Oct 2026 11:31:04 -0400 Subject: [PATCH] metal: take a drawable only in frames that draw to the screen `commit_frame` asked the view for its `currentDrawable` every frame and presented it. `currentDrawable` waits in `-[CAMetalLayer nextDrawable]` until the display lets go of one of the layer's drawables, so every frame waited for the display, even one drawn only into render targets. A frame now takes a drawable only when it begins a pass on the screen, and `commit_frame` presents one only then. An app that draws its frames off-screen and reads them back, such as a tool saving them to files, can then draw them as fast as the GPU does. A frame with no pass at all commits nothing, where it panicked on the missing command buffer. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --- src/graphics/metal.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/graphics/metal.rs b/src/graphics/metal.rs index e18ec9f75..5fa93db93 100644 --- a/src/graphics/metal.rs +++ b/src/graphics/metal.rs @@ -293,6 +293,9 @@ pub struct MetalContext { // / `end_render_pass`. current_pass_target: Option>, pending_end_encoder: bool, + /// Whether this frame drew to the screen, which takes one of the + /// view's drawables, so `commit_frame` presents it. + drew_to_screen: bool, view: ObjcId, device: ObjcId, current_frame_index: usize, @@ -388,6 +391,7 @@ impl MetalContext { current_pass_height: 0, current_pass_target: None, pending_end_encoder: false, + drew_to_screen: false, view, device, buffers: vec![], @@ -1312,6 +1316,7 @@ impl RenderingBackend for MetalContext { let (descriptor, width_px, height_px) = match pass { None => { + self.drew_to_screen = true; // Read dimensions from the actual pass attachment // texture, not `screen_size()`. MTKView's // `drawableSize` and our cached screen size can @@ -1454,12 +1459,18 @@ impl RenderingBackend for MetalContext { } unsafe { assert!(!self.command_queue.is_null()); - let drawable: ObjcId = msg_send!(self.view, currentDrawable); - //msg_send_![drawable, retain]; - msg_send_![self.command_buffer.unwrap(), presentDrawable: drawable]; - msg_send_![self.command_buffer.unwrap(), commit]; - msg_send_![self.command_buffer.unwrap(), waitUntilCompleted]; + if let Some(command_buffer) = self.command_buffer { + // A frame drawn only off-screen leaves the drawables alone: + // `currentDrawable` waits until the display is done with one. + if self.drew_to_screen { + let drawable: ObjcId = msg_send!(self.view, currentDrawable); + msg_send_![command_buffer, presentDrawable: drawable]; + } + msg_send_![command_buffer, commit]; + msg_send_![command_buffer, waitUntilCompleted]; + } } + self.drew_to_screen = false; for buffer in &mut self.buffers { buffer.next_value = 0; }