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:
1 parent
b8aee8f9d7
commit
d08be97d05
3 files changed
+57
-48
No files matched your search
Generated
+1
@@ -7578,6 +7578,7 @@ dependencies = [
|
||||
"log",
|
||||
"metal",
|
||||
"objc",
|
||||
"objc2 0.6.3",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user