diff --git a/wgpu-hal/src/metal/command.rs b/wgpu-hal/src/metal/command.rs index 97f4c8b59c1..af01f8760bf 100644 --- a/wgpu-hal/src/metal/command.rs +++ b/wgpu-hal/src/metal/command.rs @@ -332,6 +332,10 @@ impl crate::CommandEncoder for super::CommandEncoder { self.raw_cmd_buf = Some(raw); + // Clear resource tracking for new command buffer + self.used_buffers.clear(); + self.used_textures.clear(); + Ok(()) } @@ -346,6 +350,10 @@ impl crate::CommandEncoder for super::CommandEncoder { encoder.end_encoding(); } self.raw_cmd_buf = None; + + // Clear resource tracking since we're discarding + self.used_buffers.clear(); + self.used_textures.clear(); } unsafe fn end_encoding(&mut self) -> Result { @@ -362,6 +370,9 @@ impl crate::CommandEncoder for super::CommandEncoder { Ok(super::CommandBuffer { raw: self.raw_cmd_buf.take().unwrap(), + // Transfer resource references to keep them alive until GPU completion + used_buffers: core::mem::take(&mut self.used_buffers), + used_textures: core::mem::take(&mut self.used_textures), }) } @@ -387,6 +398,9 @@ impl crate::CommandEncoder for super::CommandEncoder { unsafe fn clear_buffer(&mut self, buffer: &super::Buffer, range: crate::MemoryRange) { let encoder = self.enter_blit(); encoder.fill_buffer(&buffer.raw, conv::map_range(&range), 0); + + // Retain buffer reference until command buffer completes + self.used_buffers.push(buffer.raw.clone()); } unsafe fn copy_buffer_to_buffer( @@ -407,6 +421,10 @@ impl crate::CommandEncoder for super::CommandEncoder { copy.size.get(), ); } + + // Retain buffer references until command buffer completes + self.used_buffers.push(src.raw.clone()); + self.used_buffers.push(dst.raw.clone()); } unsafe fn copy_texture_to_texture( @@ -444,6 +462,10 @@ impl crate::CommandEncoder for super::CommandEncoder { dst_origin, ); } + + // Retain texture references until command buffer completes + self.used_textures.push(src.raw.clone()); + self.used_textures.push(dst.raw.clone()); } unsafe fn copy_buffer_to_texture( @@ -486,6 +508,10 @@ impl crate::CommandEncoder for super::CommandEncoder { conv::get_blit_option(dst.format, copy.texture_base.aspect), ); } + + // Retain resource references until command buffer completes + self.used_buffers.push(src.raw.clone()); + self.used_textures.push(dst.raw.clone()); } unsafe fn copy_texture_to_buffer( @@ -523,6 +549,10 @@ impl crate::CommandEncoder for super::CommandEncoder { conv::get_blit_option(src.format, copy.texture_base.aspect), ); } + + // Retain resource references until command buffer completes + self.used_textures.push(src.raw.clone()); + self.used_buffers.push(dst.raw.clone()); } unsafe fn copy_acceleration_structure_to_acceleration_structure( @@ -822,6 +852,17 @@ impl crate::CommandEncoder for super::CommandEncoder { self.state.render = Some(encoder.to_owned()); }); + // Retain texture references for render attachments until command buffer completes + for at in desc.color_attachments.iter().flatten() { + self.used_textures.push(at.target.view.raw.clone()); + if let Some(ref resolve) = at.resolve_target { + self.used_textures.push(resolve.view.raw.clone()); + } + } + if let Some(ref at) = desc.depth_stencil_attachment { + self.used_textures.push(at.target.view.raw.clone()); + } + Ok(()) } @@ -1127,6 +1168,9 @@ impl crate::CommandEncoder for super::CommandEncoder { stride, raw_type, }); + + // Retain buffer reference until command buffer completes + self.used_buffers.push(binding.buffer.raw.clone()); } unsafe fn set_vertex_buffer<'a>( @@ -1158,6 +1202,9 @@ impl crate::CommandEncoder for super::CommandEncoder { sizes.as_ptr().cast(), ); } + + // Retain buffer reference until command buffer completes + self.used_buffers.push(binding.buffer.raw.clone()); } unsafe fn set_viewport(&mut self, rect: &crate::Rect, depth_range: Range) { @@ -1300,6 +1347,9 @@ impl crate::CommandEncoder for super::CommandEncoder { encoder.draw_primitives_indirect(self.state.raw_primitive_type, &buffer.raw, offset); offset += size_of::() as wgt::BufferAddress; } + + // Retain indirect buffer reference until command buffer completes + self.used_buffers.push(buffer.raw.clone()); } unsafe fn draw_indexed_indirect( @@ -1321,6 +1371,9 @@ impl crate::CommandEncoder for super::CommandEncoder { ); offset += size_of::() as wgt::BufferAddress; } + + // Retain indirect buffer reference until command buffer completes + self.used_buffers.push(buffer.raw.clone()); } unsafe fn draw_mesh_tasks_indirect( @@ -1505,6 +1558,9 @@ impl crate::CommandEncoder for super::CommandEncoder { offset, self.state.stage_infos.cs.raw_wg_size, ); + + // Retain indirect buffer reference until command buffer completes + self.used_buffers.push(buffer.raw.clone()); } unsafe fn build_acceleration_structures<'a, T>( diff --git a/wgpu-hal/src/metal/device.rs b/wgpu-hal/src/metal/device.rs index 82d06e61501..acc9a836abb 100644 --- a/wgpu-hal/src/metal/device.rs +++ b/wgpu-hal/src/metal/device.rs @@ -643,6 +643,8 @@ impl crate::Device for super::Device { state: super::CommandState::default(), temp: super::Temp::default(), counters: Arc::clone(&self.counters), + used_buffers: Vec::new(), + used_textures: Vec::new(), }) } diff --git a/wgpu-hal/src/metal/mod.rs b/wgpu-hal/src/metal/mod.rs index 0d404c146cf..de0647bd2a2 100644 --- a/wgpu-hal/src/metal/mod.rs +++ b/wgpu-hal/src/metal/mod.rs @@ -323,19 +323,11 @@ struct PrivateDisabilities { broken_layered_clear_image: bool, } -#[derive(Debug)] +#[derive(Debug, Default)] struct Settings { retain_command_buffer_references: bool, } -impl Default for Settings { - fn default() -> Self { - Self { - retain_command_buffer_references: true, - } - } -} - struct AdapterShared { device: Mutex, disabilities: PrivateDisabilities, @@ -1022,6 +1014,11 @@ pub struct CommandEncoder { state: CommandState, temp: Temp, counters: Arc, + /// Buffers used during encoding of the current command buffer. + /// These are transferred to the CommandBuffer in end_encoding(). + used_buffers: Vec, + /// Textures used during encoding of the current command buffer. + used_textures: Vec, } impl fmt::Debug for CommandEncoder { @@ -1039,6 +1036,16 @@ unsafe impl Sync for CommandEncoder {} #[derive(Debug)] pub struct CommandBuffer { raw: metal::CommandBuffer, + /// Metal buffer handles used by this command buffer. + /// + /// When `retain_command_buffer_references` is false, Metal's command buffer + /// doesn't automatically retain resources. We keep these handles alive + /// until the command buffer completes execution to prevent use-after-free. + #[expect(dead_code, reason = "Keeps strong references to resources")] + used_buffers: Vec, + /// Metal texture handles used by this command buffer. + #[expect(dead_code, reason = "Keeps strong references to resources")] + used_textures: Vec, } impl crate::DynCommandBuffer for CommandBuffer {}