From d08be97d05e9104c8cc6b00556e3cfcf5ec4738f Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:09:33 +0000 Subject: [PATCH] gpui: Fix native object accumulation in headless Metal rendering (#64099) # Objective Heavy headless rendering benchmarks accumulated native temporary objects across frames because they could run for a long time without an autorelease pool draining. This PR adds a scoped autorelease pool around each headless rendering call so those temporary objects are released promptly. GPU submission remains asynchronous, and image readback retains its existing completion wait. Release Notes: - N/A --- Cargo.lock | 1 + crates/gpui_apple/Cargo.toml | 1 + crates/gpui_apple/src/metal_renderer.rs | 103 +++++++++++++----------- 3 files changed, 57 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c026a85ee6..501c1d097c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7578,6 +7578,7 @@ dependencies = [ "log", "metal", "objc", + "objc2 0.6.3", "parking_lot", ] diff --git a/crates/gpui_apple/Cargo.toml b/crates/gpui_apple/Cargo.toml index fc04bccf64..d88611fb24 100644 --- a/crates/gpui_apple/Cargo.toml +++ b/crates/gpui_apple/Cargo.toml @@ -34,6 +34,7 @@ image.workspace = true log.workspace = true metal.workspace = true objc.workspace = true +objc2.workspace = true parking_lot.workspace = true [target.'cfg(target_os = "macos")'.build-dependencies] diff --git a/crates/gpui_apple/src/metal_renderer.rs b/crates/gpui_apple/src/metal_renderer.rs index 112717b234..2350718d1b 100644 --- a/crates/gpui_apple/src/metal_renderer.rs +++ b/crates/gpui_apple/src/metal_renderer.rs @@ -575,35 +575,40 @@ impl MetalRenderer { anyhow::bail!("Invalid size for render_scene_to_image: {:?}", size); } - // Update path intermediate textures for this size - self.update_path_intermediate_textures(size); + // Headless callers do not have a Cocoa event-loop pool to release + // autoreleased command buffers and render-pass descriptors. + objc2::rc::autoreleasepool(|_| { + // Update path intermediate textures for this size + self.update_path_intermediate_textures(size); - // Create an offscreen texture as render target - let texture_descriptor = metal::TextureDescriptor::new(); - texture_descriptor.set_width(size.width.0 as u64); - texture_descriptor.set_height(size.height.0 as u64); - texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm); - texture_descriptor - .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead); - texture_descriptor.set_storage_mode(metal::MTLStorageMode::Managed); - let target_texture = self.device.new_texture(&texture_descriptor); + // Create an offscreen texture as render target + let texture_descriptor = metal::TextureDescriptor::new(); + texture_descriptor.set_width(size.width.0 as u64); + texture_descriptor.set_height(size.height.0 as u64); + texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm); + texture_descriptor.set_usage( + metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead, + ); + texture_descriptor.set_storage_mode(metal::MTLStorageMode::Managed); + let target_texture = self.device.new_texture(&texture_descriptor); - let command_buffer = self.render_frame(scene, &target_texture, size)?; + let command_buffer = self.render_frame(scene, &target_texture, size)?; - // On discrete GPUs (non-unified memory), Managed textures require an - // explicit blit synchronize before the CPU can read back the rendered - // data. Without this, get_bytes returns stale zeros. - if !self.is_unified_memory { - let blit = command_buffer.new_blit_command_encoder(); - blit.synchronize_resource(&target_texture); - blit.end_encoding(); - } + // On discrete GPUs (non-unified memory), Managed textures require an + // explicit blit synchronize before the CPU can read back the rendered + // data. Without this, get_bytes returns stale zeros. + if !self.is_unified_memory { + let blit = command_buffer.new_blit_command_encoder(); + blit.synchronize_resource(&target_texture); + blit.end_encoding(); + } - // Commit and wait for completion - command_buffer.commit(); - command_buffer.wait_until_completed(); + // Commit and wait for completion + command_buffer.commit(); + command_buffer.wait_until_completed(); - read_texture_to_image(&target_texture) + read_texture_to_image(&target_texture) + }) } /// Renders a scene to a reused offscreen texture without reading pixels @@ -619,33 +624,35 @@ impl MetalRenderer { anyhow::bail!("Invalid size for render_scene: {:?}", size); } - self.update_path_intermediate_textures(size); + objc2::rc::autoreleasepool(|_| { + self.update_path_intermediate_textures(size); - let needs_new_target = self.headless_render_target.as_ref().is_none_or(|texture| { - texture.width() != size.width.0 as u64 || texture.height() != size.height.0 as u64 - }); - if needs_new_target { - let texture_descriptor = metal::TextureDescriptor::new(); - texture_descriptor.set_width(size.width.0 as u64); - texture_descriptor.set_height(size.height.0 as u64); - texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm); - texture_descriptor.set_usage( - metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead, - ); - texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private); - self.headless_render_target = Some(self.device.new_texture(&texture_descriptor)); - } - let target_texture = self - .headless_render_target - .clone() - .expect("just ensured the render target exists"); + let needs_new_target = self.headless_render_target.as_ref().is_none_or(|texture| { + texture.width() != size.width.0 as u64 || texture.height() != size.height.0 as u64 + }); + if needs_new_target { + let texture_descriptor = metal::TextureDescriptor::new(); + texture_descriptor.set_width(size.width.0 as u64); + texture_descriptor.set_height(size.height.0 as u64); + texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm); + texture_descriptor.set_usage( + metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead, + ); + texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private); + self.headless_render_target = Some(self.device.new_texture(&texture_descriptor)); + } + let target_texture = self + .headless_render_target + .clone() + .expect("just ensured the render target exists"); - let command_buffer = self.render_frame(scene, &target_texture, size)?; + let command_buffer = self.render_frame(scene, &target_texture, size)?; - // Commit without waiting, mirroring presentation to a real window where - // the CPU doesn't block on the GPU. - command_buffer.commit(); - Ok(()) + // Commit without waiting, mirroring presentation to a real window where + // the CPU doesn't block on the GPU. + command_buffer.commit(); + Ok(()) + }) } fn draw_primitives_to_texture(