Skip to content

Commit c6ca2a7

Browse files
authored
Always free staging buffers (#2961)
* Have `prepare_staging_buffer` take a raw hal Device. This helps the borrow checker understand that we can borrow `pending_writes`'s encoder and the raw Device at the same time. * Always free staging buffers. We must ensure that the staging buffer is not leaked when an error occurs, so allocate it as late as possible, and free it explicitly when those fallible operations we can't move it past go awry. Fixes #2959. * Some tests for texture and buffer copies. * Add CHANGELOG.md entry.
1 parent 33d313c commit c6ca2a7

File tree

5 files changed

+176
-45
lines changed

5 files changed

+176
-45
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ the same every time it is rendered, we now warn if it is missing.
6363
### Bug Fixes
6464

6565
#### General
66+
- Free `StagingBuffers` even when an error occurs in the operation that consumes them. By @jimblandy in [#2961](https://github.com/gfx-rs/wgpu/pull/2961)
6667
- Improve the validation and error reporting of buffer mappings by @nical in [#2848](https://github.com/gfx-rs/wgpu/pull/2848)
6768
- Fix compilation errors when using wgpu-core in isolation while targetting `wasm32-unknown-unknown` by @Seamooo in [#2922](https://github.com/gfx-rs/wgpu/pull/2922)
6869
- Fixed opening of RenderDoc library by @abuffseagull in [#2930](https://github.com/gfx-rs/wgpu/pull/2930)

wgpu-core/src/device/queue.rs

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -243,30 +243,28 @@ impl<A: hal::Api> PendingWrites<A> {
243243
}
244244
}
245245

246-
impl<A: HalApi> super::Device<A> {
247-
fn prepare_staging_buffer(
248-
&mut self,
249-
size: wgt::BufferAddress,
250-
) -> Result<(StagingBuffer<A>, *mut u8), DeviceError> {
251-
profiling::scope!("prepare_staging_buffer");
252-
let stage_desc = hal::BufferDescriptor {
253-
label: Some("(wgpu internal) Staging"),
254-
size,
255-
usage: hal::BufferUses::MAP_WRITE | hal::BufferUses::COPY_SRC,
256-
memory_flags: hal::MemoryFlags::TRANSIENT,
257-
};
258-
259-
let buffer = unsafe { self.raw.create_buffer(&stage_desc)? };
260-
let mapping = unsafe { self.raw.map_buffer(&buffer, 0..size) }?;
261-
262-
let staging_buffer = StagingBuffer {
263-
raw: buffer,
264-
size,
265-
is_coherent: mapping.is_coherent,
266-
};
267-
268-
Ok((staging_buffer, mapping.ptr.as_ptr()))
269-
}
246+
fn prepare_staging_buffer<A: HalApi>(
247+
device: &mut A::Device,
248+
size: wgt::BufferAddress,
249+
) -> Result<(StagingBuffer<A>, *mut u8), DeviceError> {
250+
profiling::scope!("prepare_staging_buffer");
251+
let stage_desc = hal::BufferDescriptor {
252+
label: Some("(wgpu internal) Staging"),
253+
size,
254+
usage: hal::BufferUses::MAP_WRITE | hal::BufferUses::COPY_SRC,
255+
memory_flags: hal::MemoryFlags::TRANSIENT,
256+
};
257+
258+
let buffer = unsafe { device.create_buffer(&stage_desc)? };
259+
let mapping = unsafe { device.map_buffer(&buffer, 0..size) }?;
260+
261+
let staging_buffer = StagingBuffer {
262+
raw: buffer,
263+
size,
264+
is_coherent: mapping.is_coherent,
265+
};
266+
267+
Ok((staging_buffer, mapping.ptr.as_ptr()))
270268
}
271269

272270
impl<A: hal::Api> StagingBuffer<A> {
@@ -350,21 +348,31 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
350348
return Ok(());
351349
}
352350

353-
let (staging_buffer, staging_buffer_ptr) = device.prepare_staging_buffer(data_size)?;
351+
// Platform validation requires that the staging buffer always be
352+
// freed, even if an error occurs. All paths from here must call
353+
// `device.pending_writes.consume`.
354+
let (staging_buffer, staging_buffer_ptr) =
355+
prepare_staging_buffer(&mut device.raw, data_size)?;
354356

355-
unsafe {
357+
if let Err(flush_error) = unsafe {
356358
profiling::scope!("copy");
357359
ptr::copy_nonoverlapping(data.as_ptr(), staging_buffer_ptr, data.len());
358-
staging_buffer.flush(&device.raw)?;
359-
};
360+
staging_buffer.flush(&device.raw)
361+
} {
362+
device.pending_writes.consume(staging_buffer);
363+
return Err(flush_error.into());
364+
}
360365

361-
self.queue_write_staging_buffer_impl(
366+
let result = self.queue_write_staging_buffer_impl(
362367
device,
363368
device_token,
364-
staging_buffer,
369+
&staging_buffer,
365370
buffer_id,
366371
buffer_offset,
367-
)
372+
);
373+
374+
device.pending_writes.consume(staging_buffer);
375+
result
368376
}
369377

370378
pub fn queue_create_staging_buffer<A: HalApi>(
@@ -382,7 +390,7 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
382390
.map_err(|_| DeviceError::Invalid)?;
383391

384392
let (staging_buffer, staging_buffer_ptr) =
385-
device.prepare_staging_buffer(buffer_size.get())?;
393+
prepare_staging_buffer(&mut device.raw, buffer_size.get())?;
386394

387395
let fid = hub.staging_buffers.prepare(id_in);
388396
let id = fid.assign(staging_buffer, device_token);
@@ -413,15 +421,25 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
413421
.0
414422
.ok_or(TransferError::InvalidBuffer(buffer_id))?;
415423

416-
unsafe { staging_buffer.flush(&device.raw)? };
424+
// At this point, we have taken ownership of the staging_buffer from the
425+
// user. Platform validation requires that the staging buffer always
426+
// be freed, even if an error occurs. All paths from here must call
427+
// `device.pending_writes.consume`.
428+
if let Err(flush_error) = unsafe { staging_buffer.flush(&device.raw) } {
429+
device.pending_writes.consume(staging_buffer);
430+
return Err(flush_error.into());
431+
}
417432

418-
self.queue_write_staging_buffer_impl(
433+
let result = self.queue_write_staging_buffer_impl(
419434
device,
420435
device_token,
421-
staging_buffer,
436+
&staging_buffer,
422437
buffer_id,
423438
buffer_offset,
424-
)
439+
);
440+
441+
device.pending_writes.consume(staging_buffer);
442+
result
425443
}
426444

427445
pub fn queue_validate_write_buffer<A: HalApi>(
@@ -481,7 +499,7 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
481499
&self,
482500
device: &mut super::Device<A>,
483501
device_token: &mut Token<super::Device<A>>,
484-
staging_buffer: StagingBuffer<A>,
502+
staging_buffer: &StagingBuffer<A>,
485503
buffer_id: id::BufferId,
486504
buffer_offset: u64,
487505
) -> Result<(), QueueWriteError> {
@@ -520,7 +538,6 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
520538
encoder.copy_buffer_to_buffer(&staging_buffer.raw, dst_raw, region.into_iter());
521539
}
522540

523-
device.pending_writes.consume(staging_buffer);
524541
device.pending_writes.dst_buffers.insert(buffer_id);
525542

526543
// Ensure the overwritten bytes are marked as initialized so they don't need to be nulled prior to mapping or binding.
@@ -613,7 +630,6 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
613630
let block_rows_in_copy =
614631
(size.depth_or_array_layers - 1) * block_rows_per_image + height_blocks;
615632
let stage_size = stage_bytes_per_row as u64 * block_rows_in_copy as u64;
616-
let (staging_buffer, staging_buffer_ptr) = device.prepare_staging_buffer(stage_size)?;
617633

618634
let dst = texture_guard.get_mut(destination.texture).unwrap();
619635
if !dst.desc.usage.contains(wgt::TextureUsages::COPY_DST) {
@@ -676,12 +692,23 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
676692
validate_texture_copy_range(destination, &dst.desc, CopySide::Destination, size)?;
677693
dst.life_guard.use_at(device.active_submission_index + 1);
678694

695+
let dst_raw = dst
696+
.inner
697+
.as_raw()
698+
.ok_or(TransferError::InvalidTexture(destination.texture))?;
699+
679700
let bytes_per_row = if let Some(bytes_per_row) = data_layout.bytes_per_row {
680701
bytes_per_row.get()
681702
} else {
682703
width_blocks * format_desc.block_size as u32
683704
};
684705

706+
// Platform validation requires that the staging buffer always be
707+
// freed, even if an error occurs. All paths from here must call
708+
// `device.pending_writes.consume`.
709+
let (staging_buffer, staging_buffer_ptr) =
710+
prepare_staging_buffer(&mut device.raw, stage_size)?;
711+
685712
if stage_bytes_per_row == bytes_per_row {
686713
profiling::scope!("copy aligned");
687714
// Fast path if the data is already being aligned optimally.
@@ -715,7 +742,10 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
715742
}
716743
}
717744

718-
unsafe { staging_buffer.flush(&device.raw) }?;
745+
if let Err(e) = unsafe { staging_buffer.flush(&device.raw) } {
746+
device.pending_writes.consume(staging_buffer);
747+
return Err(e.into());
748+
}
719749

720750
let regions = (0..array_layer_count).map(|rel_array_layer| {
721751
let mut texture_base = dst_base.clone();
@@ -737,11 +767,6 @@ impl<G: GlobalIdentityHandlerFactory> Global<G> {
737767
usage: hal::BufferUses::MAP_WRITE..hal::BufferUses::COPY_SRC,
738768
};
739769

740-
let dst_raw = dst
741-
.inner
742-
.as_raw()
743-
.ok_or(TransferError::InvalidTexture(destination.texture))?;
744-
745770
unsafe {
746771
encoder
747772
.transition_textures(transition.map(|pending| pending.into_hal(dst)).into_iter());

wgpu/tests/buffer_copy.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! Tests for buffer copy validation.
2+
3+
use wgt::BufferAddress;
4+
5+
use crate::common::{initialize_test, TestParameters};
6+
7+
#[test]
8+
fn copy_alignment() {
9+
fn try_copy(offset: BufferAddress, size: BufferAddress, should_panic: bool) {
10+
let mut parameters = TestParameters::default();
11+
if should_panic {
12+
parameters = parameters.failure();
13+
}
14+
15+
initialize_test(parameters, |ctx| {
16+
let buffer = ctx.device.create_buffer(&BUFFER_DESCRIPTOR);
17+
let data = vec![255; size as usize];
18+
ctx.queue.write_buffer(&buffer, offset, &data);
19+
});
20+
}
21+
22+
try_copy(0, 0, false);
23+
try_copy(4, 16 + 1, true);
24+
try_copy(64, 20 + 2, true);
25+
try_copy(256, 44 + 3, true);
26+
try_copy(1024, 8 + 4, false);
27+
28+
try_copy(0, 4, false);
29+
try_copy(4 + 1, 8, true);
30+
try_copy(64 + 2, 12, true);
31+
try_copy(256 + 3, 16, true);
32+
try_copy(1024 + 4, 4, false);
33+
}
34+
35+
const BUFFER_SIZE: BufferAddress = 1234;
36+
37+
const BUFFER_DESCRIPTOR: wgpu::BufferDescriptor = wgpu::BufferDescriptor {
38+
label: None,
39+
size: BUFFER_SIZE,
40+
usage: wgpu::BufferUsages::COPY_SRC.union(wgpu::BufferUsages::COPY_DST),
41+
mapped_at_creation: false,
42+
};

wgpu/tests/root.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// All files containing tests
22
mod common;
33

4+
mod buffer_copy;
45
mod clear_texture;
56
mod device;
67
mod example_wgsl;
78
mod instance;
89
mod poll;
910
mod resource_descriptor_accessor;
1011
mod shader_primitive_index;
12+
mod texture_bounds;
1113
mod vertex_indices;
1214
mod zero_init_texture_after_discard;

wgpu/tests/texture_bounds.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! Tests for texture copy bounds checks.
2+
3+
use crate::common::{initialize_test, TestParameters};
4+
use std::num::NonZeroU32;
5+
6+
#[test]
7+
fn bad_copy_origin() {
8+
fn try_origin(origin: wgpu::Origin3d, should_panic: bool) {
9+
let mut parameters = TestParameters::default();
10+
if should_panic {
11+
parameters = parameters.failure();
12+
}
13+
14+
initialize_test(parameters, |ctx| {
15+
let texture = ctx.device.create_texture(&TEXTURE_DESCRIPTOR);
16+
let data = vec![255; BUFFER_SIZE as usize];
17+
ctx.queue.write_texture(
18+
wgpu::ImageCopyTexture {
19+
texture: &texture,
20+
mip_level: 0,
21+
origin,
22+
aspect: wgpu::TextureAspect::All,
23+
},
24+
&data,
25+
BUFFER_COPY_LAYOUT,
26+
TEXTURE_SIZE,
27+
);
28+
});
29+
}
30+
31+
try_origin(wgpu::Origin3d { x: 0, y: 0, z: 0 }, false);
32+
try_origin(wgpu::Origin3d { x: 1, y: 0, z: 0 }, true);
33+
try_origin(wgpu::Origin3d { x: 0, y: 1, z: 0 }, true);
34+
try_origin(wgpu::Origin3d { x: 0, y: 0, z: 1 }, true);
35+
}
36+
37+
const TEXTURE_SIZE: wgpu::Extent3d = wgpu::Extent3d {
38+
width: 64,
39+
height: 64,
40+
depth_or_array_layers: 1,
41+
};
42+
43+
const TEXTURE_DESCRIPTOR: wgpu::TextureDescriptor = wgpu::TextureDescriptor {
44+
label: Some("CopyOrigin"),
45+
size: TEXTURE_SIZE,
46+
mip_level_count: 1,
47+
sample_count: 1,
48+
dimension: wgpu::TextureDimension::D2,
49+
format: wgpu::TextureFormat::Rgba8UnormSrgb,
50+
usage: wgpu::TextureUsages::COPY_DST.union(wgpu::TextureUsages::COPY_SRC),
51+
};
52+
53+
const BYTES_PER_PIXEL: u32 = 4;
54+
55+
const BUFFER_SIZE: u32 = TEXTURE_SIZE.width * TEXTURE_SIZE.height * BYTES_PER_PIXEL;
56+
57+
const BUFFER_COPY_LAYOUT: wgpu::ImageDataLayout = wgpu::ImageDataLayout {
58+
offset: 0,
59+
bytes_per_row: NonZeroU32::new(TEXTURE_SIZE.width * BYTES_PER_PIXEL),
60+
rows_per_image: None,
61+
};

0 commit comments

Comments
 (0)