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
This commit is contained in:
Anthony Eid authored and GitHub committed 2026-09-11 21:09:33 +00:00
1 parent b8aee8f9d7
commit d08be97d05
3 files changed
+57 -48

No files matched your search

Generated
+1
View File
@@ -7578,6 +7578,7 @@ dependencies = [
"log",
"metal",
"objc",
"objc2 0.6.3",
"parking_lot",
]
+1
View File
@@ -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]
+55 -48
View File
@@ -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(