Skip to content

Commit fe9eb2d

Browse files
committed
[core] defer buffer/texture destruction while used by a command buffer
1 parent 0ac2da4 commit fe9eb2d

File tree

5 files changed

+147
-11
lines changed

5 files changed

+147
-11
lines changed

wgpu-core/src/device/resource.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use core::{
99
fmt,
1010
mem::{self, ManuallyDrop},
1111
num::NonZeroU32,
12-
sync::atomic::{AtomicBool, Ordering},
12+
sync::atomic::{AtomicBool, AtomicU32, Ordering},
1313
};
1414
use hal::ShouldBeNonZeroExt;
1515

@@ -278,6 +278,8 @@ pub struct Device {
278278
pub(crate) enum DeferredDestroy {
279279
TextureViews(WeakVec<TextureView>),
280280
BindGroups(WeakVec<BindGroup>),
281+
Buffer(Weak<Buffer>),
282+
Texture(Weak<Texture>),
281283
}
282284

283285
impl fmt::Debug for Device {
@@ -705,6 +707,18 @@ impl Device {
705707
}
706708
}
707709
}
710+
DeferredDestroy::Buffer(buffer) => {
711+
// Call destroy() now that we're in a safe context (no locks held).
712+
if let Some(buffer) = buffer.upgrade() {
713+
buffer.destroy();
714+
}
715+
}
716+
DeferredDestroy::Texture(texture) => {
717+
// Call destroy() now that we're in a safe context (no locks held).
718+
if let Some(texture) = texture.upgrade() {
719+
texture.destroy();
720+
}
721+
}
708722
}
709723
}
710724
}
@@ -1051,6 +1065,8 @@ impl Device {
10511065
bind_groups: Mutex::new(rank::BUFFER_BIND_GROUPS, WeakVec::new()),
10521066
timestamp_normalization_bind_group,
10531067
indirect_validation_bind_groups,
1068+
destroyed: AtomicBool::new(false),
1069+
in_flight_count: AtomicU32::new(0),
10541070
};
10551071

10561072
let buffer = Arc::new(buffer);
@@ -1234,6 +1250,8 @@ impl Device {
12341250
bind_groups: Mutex::new(rank::BUFFER_BIND_GROUPS, WeakVec::new()),
12351251
timestamp_normalization_bind_group,
12361252
indirect_validation_bind_groups,
1253+
destroyed: AtomicBool::new(false),
1254+
in_flight_count: AtomicU32::new(0),
12371255
};
12381256

12391257
let buffer = Arc::new(buffer);

wgpu-core/src/resource.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use core::{
66
num::NonZeroU64,
77
ops::Range,
88
ptr::NonNull,
9+
sync::atomic::{AtomicBool, AtomicU32, Ordering},
910
};
1011
use smallvec::SmallVec;
1112
use thiserror::Error;
@@ -438,6 +439,14 @@ pub struct Buffer {
438439
pub(crate) bind_groups: Mutex<WeakVec<BindGroup>>,
439440
pub(crate) timestamp_normalization_bind_group: Snatchable<TimestampNormalizationBindGroup>,
440441
pub(crate) indirect_validation_bind_groups: Snatchable<crate::indirect_validation::BindGroups>,
442+
/// Set to true when destroy() is called. This makes check_destroyed() fail immediately,
443+
/// but the actual raw resource cleanup may be deferred if in_flight_count > 0.
444+
pub(crate) destroyed: AtomicBool,
445+
/// Count of command buffer trackers that currently hold a reference to this buffer.
446+
/// Incremented when inserted into a command buffer's tracker, decremented when the
447+
/// tracker is cleared (on command buffer submission or drop).
448+
/// When destroy() is called and this is > 0, raw resource cleanup is deferred to drop().
449+
pub(crate) in_flight_count: AtomicU32,
441450
}
442451

443452
impl Drop for Buffer {
@@ -472,6 +481,11 @@ impl Buffer {
472481
&self,
473482
guard: &SnatchGuard,
474483
) -> Result<(), DestroyedResourceError> {
484+
// Check the destroyed flag first - this is set by destroy() even if
485+
// the raw resource hasn't been snatched yet (deferred cleanup case).
486+
if self.destroyed.load(Ordering::Acquire) {
487+
return Err(DestroyedResourceError(self.error_ident()));
488+
}
475489
self.raw
476490
.get(guard)
477491
.map(|_| ())
@@ -903,8 +917,23 @@ impl Buffer {
903917
}
904918

905919
pub fn destroy(self: &Arc<Self>) {
920+
// Mark as destroyed. This makes check_destroyed() fail,
921+
// so any future use of this buffer will return an error.
922+
self.destroyed.store(true, Ordering::Release);
923+
906924
let device = &self.device;
907925

926+
// Check if this buffer is currently in any command buffer's tracker.
927+
// If so, we should NOT snatch the raw resource yet - the tracker's Drop
928+
// will call destroy() again when the count reaches 0.
929+
if self.in_flight_count.load(Ordering::Acquire) > 0 {
930+
// Buffer is in a recording command buffer.
931+
// The raw resource will be cleaned up when the command buffer is
932+
// submitted/dropped and the tracker calls destroy() again.
933+
return;
934+
}
935+
936+
// Not in any command buffer tracker, safe to snatch and cleanup now.
908937
let temp = {
909938
let mut snatch_guard = device.snatchable_lock.write();
910939

@@ -1255,6 +1284,14 @@ pub struct Texture {
12551284
pub(crate) clear_mode: RwLock<TextureClearMode>,
12561285
pub(crate) views: Mutex<WeakVec<TextureView>>,
12571286
pub(crate) bind_groups: Mutex<WeakVec<BindGroup>>,
1287+
/// Set to true when destroy() is called. This makes check_destroyed() fail immediately,
1288+
/// but the actual raw resource cleanup may be deferred if in_flight_count > 0.
1289+
pub(crate) destroyed: AtomicBool,
1290+
/// Count of command buffer trackers that currently hold a reference to this texture.
1291+
/// Incremented when inserted into a command buffer's tracker, decremented when the
1292+
/// tracker is cleared (on command buffer submission or drop).
1293+
/// When destroy() is called and this is > 0, raw resource cleanup is deferred to drop().
1294+
pub(crate) in_flight_count: AtomicU32,
12581295
}
12591296

12601297
impl Texture {
@@ -1290,6 +1327,8 @@ impl Texture {
12901327
clear_mode: RwLock::new(rank::TEXTURE_CLEAR_MODE, clear_mode),
12911328
views: Mutex::new(rank::TEXTURE_VIEWS, WeakVec::new()),
12921329
bind_groups: Mutex::new(rank::TEXTURE_BIND_GROUPS, WeakVec::new()),
1330+
destroyed: AtomicBool::new(false),
1331+
in_flight_count: AtomicU32::new(0),
12931332
}
12941333
}
12951334

@@ -1353,13 +1392,31 @@ impl RawResourceAccess for Texture {
13531392
fn raw<'a>(&'a self, guard: &'a SnatchGuard) -> Option<&'a Self::DynResource> {
13541393
self.inner.get(guard).map(|t| t.raw())
13551394
}
1395+
1396+
fn try_raw<'a>(
1397+
&'a self,
1398+
guard: &'a SnatchGuard,
1399+
) -> Result<&'a Self::DynResource, DestroyedResourceError> {
1400+
// Check the destroyed flag first - this is set by destroy() even if
1401+
// the raw resource hasn't been snatched yet (deferred cleanup case).
1402+
if self.destroyed.load(Ordering::Acquire) {
1403+
return Err(DestroyedResourceError(self.error_ident()));
1404+
}
1405+
self.raw(guard)
1406+
.ok_or_else(|| DestroyedResourceError(self.error_ident()))
1407+
}
13561408
}
13571409

13581410
impl Texture {
13591411
pub(crate) fn try_inner<'a>(
13601412
&'a self,
13611413
guard: &'a SnatchGuard,
13621414
) -> Result<&'a TextureInner, DestroyedResourceError> {
1415+
// Check the destroyed flag first - this is set by destroy() even if
1416+
// the raw resource hasn't been snatched yet (deferred cleanup case).
1417+
if self.destroyed.load(Ordering::Acquire) {
1418+
return Err(DestroyedResourceError(self.error_ident()));
1419+
}
13631420
self.inner
13641421
.get(guard)
13651422
.ok_or_else(|| DestroyedResourceError(self.error_ident()))
@@ -1369,6 +1426,11 @@ impl Texture {
13691426
&self,
13701427
guard: &SnatchGuard,
13711428
) -> Result<(), DestroyedResourceError> {
1429+
// Check the destroyed flag first - this is set by destroy() even if
1430+
// the raw resource hasn't been snatched yet (deferred cleanup case).
1431+
if self.destroyed.load(Ordering::Acquire) {
1432+
return Err(DestroyedResourceError(self.error_ident()));
1433+
}
13721434
self.inner
13731435
.get(guard)
13741436
.map(|_| ())
@@ -1405,8 +1467,23 @@ impl Texture {
14051467
}
14061468

14071469
pub fn destroy(self: &Arc<Self>) {
1470+
// Mark as destroyed. This makes check_destroyed() fail,
1471+
// so any future use of this texture will return an error.
1472+
self.destroyed.store(true, Ordering::Release);
1473+
14081474
let device = &self.device;
14091475

1476+
// Check if this texture is currently in any command buffer's tracker.
1477+
// If so, we should NOT snatch the raw resource yet - the tracker's Drop
1478+
// will call destroy() again when the count reaches 0.
1479+
if self.in_flight_count.load(Ordering::Acquire) > 0 {
1480+
// Texture is in a recording command buffer.
1481+
// The raw resource will be cleaned up when the command buffer is
1482+
// submitted/dropped and the tracker calls destroy() again.
1483+
return;
1484+
}
1485+
1486+
// Not in any command buffer tracker, safe to snatch and cleanup now.
14101487
let temp = {
14111488
let raw = match self.inner.snatch(&mut device.snatchable_lock.write()) {
14121489
Some(TextureInner::Native { raw }) => raw,

wgpu-core/src/track/buffer.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use alloc::{
88
sync::{Arc, Weak},
99
vec::Vec,
1010
};
11+
use core::sync::atomic::Ordering;
1112

1213
use hal::BufferBarrier;
1314
use wgt::{strict_assert, strict_assert_eq, BufferUses};
@@ -530,6 +531,11 @@ impl BufferTracker {
530531
let currently_owned = unsafe { self.metadata.contains_unchecked(index) };
531532

532533
if !currently_owned {
534+
// Increment the in_flight_count for the buffer being inserted.
535+
// This is decremented when the tracker is dropped.
536+
let buffer = unsafe { metadata_provider.get(index) };
537+
buffer.in_flight_count.fetch_add(1, Ordering::Release);
538+
533539
unsafe {
534540
insert(
535541
Some(&mut self.start),
@@ -552,6 +558,25 @@ impl BufferTracker {
552558
}
553559
}
554560

561+
impl Drop for BufferTracker {
562+
fn drop(&mut self) {
563+
// Decrement in_flight_count for all buffers in this tracker.
564+
// If this was the last command buffer using a destroyed buffer,
565+
// queue it for deferred destruction (we can't call destroy() directly
566+
// here as it might cause lock recursion).
567+
for buffer in self.metadata.owned_resources() {
568+
let prev_count = buffer.in_flight_count.fetch_sub(1, Ordering::Release);
569+
if prev_count == 1 && buffer.destroyed.load(Ordering::Acquire) {
570+
// This was the last command buffer using this destroyed buffer.
571+
// Queue it for deferred destruction.
572+
buffer.device.deferred_destroy.lock().push(
573+
crate::device::resource::DeferredDestroy::Buffer(Arc::downgrade(buffer)),
574+
);
575+
}
576+
}
577+
}
578+
}
579+
555580
/// Stores all buffer state within a device.
556581
pub(crate) struct DeviceBufferTracker {
557582
current_states: Vec<BufferUses>,

wgpu-core/src/track/texture.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use alloc::{
3838
sync::{Arc, Weak},
3939
vec::{Drain, Vec},
4040
};
41-
use core::iter;
41+
use core::{iter, sync::atomic::Ordering};
4242

4343
impl ResourceUses for TextureUses {
4444
const EXCLUSIVE: Self = Self::EXCLUSIVE;
@@ -667,6 +667,25 @@ impl TextureTrackerSetSingle for TextureTracker {
667667
}
668668
}
669669

670+
impl Drop for TextureTracker {
671+
fn drop(&mut self) {
672+
// Decrement in_flight_count for all textures in this tracker.
673+
// If this was the last command buffer using a destroyed texture,
674+
// queue it for deferred destruction (we can't call destroy() directly
675+
// here as it might cause lock recursion).
676+
for texture in self.metadata.owned_resources() {
677+
let prev_count = texture.in_flight_count.fetch_sub(1, Ordering::Release);
678+
if prev_count == 1 && texture.destroyed.load(Ordering::Acquire) {
679+
// This was the last command buffer using this destroyed texture.
680+
// Queue it for deferred destruction.
681+
texture.device.deferred_destroy.lock().push(
682+
crate::device::resource::DeferredDestroy::Texture(Arc::downgrade(texture)),
683+
);
684+
}
685+
}
686+
}
687+
}
688+
670689
/// Stores all texture state within a device.
671690
pub(crate) struct DeviceTextureTracker {
672691
current_state_set: TextureStateSet,
@@ -1038,6 +1057,11 @@ unsafe fn insert_or_barrier_update(
10381057
let currently_owned = unsafe { resource_metadata.contains_unchecked(index) };
10391058

10401059
if !currently_owned {
1060+
// Increment the in_flight_count for the texture being inserted.
1061+
// This is decremented when the tracker is dropped.
1062+
let texture = unsafe { metadata_provider.get(index) };
1063+
texture.in_flight_count.fetch_add(1, Ordering::Release);
1064+
10411065
unsafe {
10421066
insert(
10431067
Some(texture_selector),

wgpu-hal/src/metal/mod.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -323,19 +323,11 @@ struct PrivateDisabilities {
323323
broken_layered_clear_image: bool,
324324
}
325325

326-
#[derive(Debug)]
326+
#[derive(Debug, Default)]
327327
struct Settings {
328328
retain_command_buffer_references: bool,
329329
}
330330

331-
impl Default for Settings {
332-
fn default() -> Self {
333-
Self {
334-
retain_command_buffer_references: true,
335-
}
336-
}
337-
}
338-
339331
struct AdapterShared {
340332
device: Mutex<metal::Device>,
341333
disabilities: PrivateDisabilities,

0 commit comments

Comments
 (0)