Skip to content

Commit e910b48

Browse files
committed
[wgpu] add convience functions for deferring mapping/callbacks
1 parent 2996c92 commit e910b48

File tree

12 files changed

+531
-40
lines changed

12 files changed

+531
-40
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering::SeqCst};
2+
use std::sync::Arc;
3+
4+
/// Helper to create a small mappable buffer for READ tests.
5+
fn make_read_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
6+
device.create_buffer(&wgpu::BufferDescriptor {
7+
label: Some("read buffer"),
8+
size,
9+
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
10+
mapped_at_creation: false,
11+
})
12+
}
13+
14+
/// map_buffer_on_submit defers mapping until submit, then invokes the callback after polling.
15+
#[test]
16+
fn encoder_map_buffer_on_submit_defers_until_submit() {
17+
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
18+
let buffer = make_read_buffer(&device, 16);
19+
20+
let fired = Arc::new(AtomicBool::new(false));
21+
let fired_cl = Arc::clone(&fired);
22+
23+
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
24+
label: Some("encoder"),
25+
});
26+
27+
// Register deferred map.
28+
encoder.map_buffer_on_submit(&buffer, wgpu::MapMode::Read, 0..4, move |_| {
29+
fired_cl.store(true, SeqCst);
30+
});
31+
// Include a trivial command that uses the buffer.
32+
encoder.clear_buffer(&buffer, 0, None);
33+
34+
// Polling before submit should not trigger the callback.
35+
_ = device.poll(wgpu::PollType::Poll);
36+
assert!(!fired.load(SeqCst));
37+
38+
// Submit and wait; callback should fire.
39+
queue.submit([encoder.finish()]);
40+
_ = device.poll(wgpu::PollType::Wait);
41+
assert!(fired.load(SeqCst));
42+
}
43+
44+
/// Empty ranges panic immediately when registering the deferred map.
45+
#[test]
46+
#[should_panic = "buffer slices can not be empty"]
47+
fn encoder_map_buffer_on_submit_empty_range_panics_immediately() {
48+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
49+
let buffer = make_read_buffer(&device, 16);
50+
51+
let encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
52+
53+
// This panics inside map_buffer_on_submit (range_to_offset_size).
54+
encoder.map_buffer_on_submit(&buffer, wgpu::MapMode::Read, 8..8, |_| {});
55+
}
56+
57+
/// Out-of-bounds ranges panic during submit (when the deferred map executes).
58+
#[test]
59+
#[should_panic = "is out of range for buffer of size"]
60+
fn encoder_map_buffer_on_submit_out_of_bounds_panics_on_submit() {
61+
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
62+
let buffer = make_read_buffer(&device, 16);
63+
64+
let mut encoder =
65+
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
66+
// 12..24 overflows the 16-byte buffer (size=12, end=24).
67+
encoder.map_buffer_on_submit(&buffer, wgpu::MapMode::Read, 12..24, |_| {});
68+
encoder.clear_buffer(&buffer, 0, None);
69+
70+
// Panic happens inside submit when executing deferred actions.
71+
queue.submit([encoder.finish()]);
72+
}
73+
74+
/// If the buffer is already mapped when the deferred mapping executes, it panics during submit.
75+
#[test]
76+
#[should_panic = "Buffer with 'read buffer' label is still mapped"]
77+
fn encoder_map_buffer_on_submit_panics_if_already_mapped_on_submit() {
78+
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
79+
let buffer = make_read_buffer(&device, 16);
80+
81+
// Start a mapping now so the buffer is considered mapped.
82+
buffer.slice(0..4).map_async(wgpu::MapMode::Read, |_| {});
83+
84+
let mut encoder =
85+
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
86+
// Deferred mapping of an already-mapped buffer will panic when executed on submit or be rejected by submit.
87+
encoder.map_buffer_on_submit(&buffer, wgpu::MapMode::Read, 0..4, |_| {});
88+
// Include any trivial work; using the same buffer ensures core validation catches the mapped hazard.
89+
encoder.clear_buffer(&buffer, 0, None);
90+
91+
queue.submit([encoder.finish()]);
92+
}
93+
94+
/// on_submitted_work_done is deferred until submit.
95+
#[test]
96+
fn encoder_on_submitted_work_done_defers_until_submit() {
97+
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
98+
99+
let fired = Arc::new(AtomicBool::new(false));
100+
let fired_cl = Arc::clone(&fired);
101+
102+
let mut encoder =
103+
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
104+
105+
encoder.on_submitted_work_done(move || {
106+
fired_cl.store(true, SeqCst);
107+
});
108+
109+
// Include a trivial command so the command buffer isn't completely empty.
110+
let dummy = make_read_buffer(&device, 4);
111+
encoder.clear_buffer(&dummy, 0, None);
112+
113+
// Without submission, polling shouldn't invoke the callback.
114+
_ = device.poll(wgpu::PollType::Poll);
115+
assert!(!fired.load(SeqCst));
116+
117+
queue.submit([encoder.finish()]);
118+
_ = device.poll(wgpu::PollType::Wait);
119+
assert!(fired.load(SeqCst));
120+
}
121+
122+
/// Both kinds of deferred callbacks are enqueued and eventually invoked.
123+
#[test]
124+
fn encoder_both_callbacks_fire_after_submit() {
125+
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
126+
let buffer = make_read_buffer(&device, 16);
127+
128+
let map_fired = Arc::new(AtomicBool::new(false));
129+
let map_fired_cl = Arc::clone(&map_fired);
130+
let queue_fired = Arc::new(AtomicBool::new(false));
131+
let queue_fired_cl = Arc::clone(&queue_fired);
132+
133+
let mut encoder =
134+
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
135+
encoder.map_buffer_on_submit(&buffer, wgpu::MapMode::Read, 0..4, move |_| {
136+
map_fired_cl.store(true, SeqCst);
137+
});
138+
encoder.on_submitted_work_done(move || {
139+
queue_fired_cl.store(true, SeqCst);
140+
});
141+
encoder.clear_buffer(&buffer, 0, None);
142+
143+
queue.submit([encoder.finish()]);
144+
_ = device.poll(wgpu::PollType::Wait);
145+
146+
assert!(map_fired.load(SeqCst));
147+
assert!(queue_fired.load(SeqCst));
148+
}
149+
150+
/// Registering multiple deferred mappings works; all callbacks fire after submit.
151+
#[test]
152+
fn encoder_multiple_map_buffer_on_submit_callbacks_fire() {
153+
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
154+
let buffer1 = make_read_buffer(&device, 32);
155+
let buffer2 = make_read_buffer(&device, 32);
156+
157+
let counter = Arc::new(AtomicU32::new(0));
158+
let c1 = Arc::clone(&counter);
159+
let c2 = Arc::clone(&counter);
160+
161+
let mut encoder =
162+
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
163+
encoder.map_buffer_on_submit(&buffer1, wgpu::MapMode::Read, 0..4, move |_| {
164+
c1.fetch_add(1, SeqCst);
165+
});
166+
encoder.map_buffer_on_submit(&buffer2, wgpu::MapMode::Read, 8..12, move |_| {
167+
c2.fetch_add(1, SeqCst);
168+
});
169+
encoder.clear_buffer(&buffer1, 0, None);
170+
171+
queue.submit([encoder.finish()]);
172+
_ = device.poll(wgpu::PollType::Wait);
173+
174+
assert_eq!(counter.load(SeqCst), 2);
175+
}
176+
177+
/// Mapping with a buffer lacking MAP_* usage should panic when executed on submit.
178+
#[test]
179+
#[should_panic]
180+
fn encoder_map_buffer_on_submit_panics_if_usage_invalid_on_submit() {
181+
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
182+
let unmappable = device.create_buffer(&wgpu::BufferDescriptor {
183+
label: Some("unmappable buffer"),
184+
size: 16,
185+
usage: wgpu::BufferUsages::COPY_DST, // No MAP_READ or MAP_WRITE
186+
mapped_at_creation: false,
187+
});
188+
189+
let mut encoder =
190+
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
191+
encoder.map_buffer_on_submit(&unmappable, wgpu::MapMode::Read, 0..4, |_| {});
192+
193+
// Add unrelated work so the submission isn't empty.
194+
let dummy = make_read_buffer(&device, 4);
195+
encoder.clear_buffer(&dummy, 0, None);
196+
197+
// Panic expected when deferred mapping executes.
198+
queue.submit([encoder.finish()]);
199+
}
200+
201+
/// Deferred map callbacks run before on_submitted_work_done for the same submission.
202+
#[test]
203+
fn encoder_deferred_map_runs_before_on_submitted_work_done() {
204+
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
205+
let buffer = make_read_buffer(&device, 16);
206+
207+
#[derive(Default)]
208+
struct Order {
209+
map_order: AtomicU32,
210+
queue_order: AtomicU32,
211+
counter: AtomicU32,
212+
}
213+
let order = Arc::new(Order::default());
214+
let o_map = Arc::clone(&order);
215+
let o_queue = Arc::clone(&order);
216+
217+
let mut encoder =
218+
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
219+
encoder.map_buffer_on_submit(&buffer, wgpu::MapMode::Read, 0..4, move |_| {
220+
let v = o_map.counter.fetch_add(1, SeqCst);
221+
o_map.map_order.store(v, SeqCst);
222+
});
223+
encoder.on_submitted_work_done(move || {
224+
let v = o_queue.counter.fetch_add(1, SeqCst);
225+
o_queue.queue_order.store(v, SeqCst);
226+
});
227+
encoder.clear_buffer(&buffer, 0, None);
228+
229+
queue.submit([encoder.finish()]);
230+
_ = device.poll(wgpu::PollType::Wait);
231+
232+
assert_eq!(order.counter.load(SeqCst), 2);
233+
assert_eq!(order.map_order.load(SeqCst), 0);
234+
assert_eq!(order.queue_order.load(SeqCst), 1);
235+
}
236+
237+
/// Multiple on_submitted_work_done callbacks registered on encoder all fire after submit.
238+
#[test]
239+
fn encoder_multiple_on_submitted_callbacks_fire() {
240+
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
241+
let buffer = make_read_buffer(&device, 4);
242+
243+
let counter = Arc::new(AtomicU32::new(0));
244+
let c1 = Arc::clone(&counter);
245+
let c2 = Arc::clone(&counter);
246+
247+
let mut encoder =
248+
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
249+
encoder.on_submitted_work_done(move || {
250+
c1.fetch_add(1, SeqCst);
251+
});
252+
encoder.on_submitted_work_done(move || {
253+
c2.fetch_add(1, SeqCst);
254+
});
255+
encoder.clear_buffer(&buffer, 0, None);
256+
257+
queue.submit([encoder.finish()]);
258+
_ = device.poll(wgpu::PollType::Wait);
259+
260+
assert_eq!(counter.load(SeqCst), 2);
261+
}

tests/tests/wgpu-validation/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod binding_arrays;
22
mod buffer;
33
mod buffer_slice;
4+
mod command_buffer_actions;
45
mod device;
56
mod external_texture;
67
mod instance;

wgpu/src/api/buffer.rs

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -302,20 +302,28 @@ impl Buffer {
302302
self.usage
303303
}
304304

305-
/// Map the buffer to host (CPU) memory, making it available for reading or writing
306-
/// via [`get_mapped_range()`](Self::get_mapped_range).
307-
/// It is available once the `callback` is called with an [`Ok`] response.
305+
/// Map the buffer to host (CPU) memory, making it available for reading or writing via
306+
/// [`get_mapped_range()`](Self::get_mapped_range). The buffer becomes accessible once the
307+
/// `callback` is invoked with [`Ok`].
308308
///
309-
/// For the callback to complete, either `queue.submit(..)`, `instance.poll_all(..)`, or `device.poll(..)`
310-
/// must be called elsewhere in the runtime, possibly integrated into an event loop or run on a separate thread.
309+
/// Use this when you want to map the buffer immediately. If you need to submit GPU work that
310+
/// uses the buffer before mapping it, use `map_buffer_on_submit` on
311+
/// [`CommandEncoder`][CEmbos], [`CommandBuffer`][CBmbos], [`RenderPass`][RPmbos], or
312+
/// [`ComputePass`][CPmbos] to schedule the mapping after submission. This avoids extra calls to
313+
/// [`Buffer::map_async()`] or [`BufferSlice::map_async()`] and lets you initiate mapping from a
314+
/// more convenient place.
311315
///
312-
/// The callback will be called on the thread that first calls the above functions after the GPU work
313-
/// has completed. There are no restrictions on the code you can run in the callback, however on native the
314-
/// call to the function will not complete until the callback returns, so prefer keeping callbacks short
315-
/// and used to set flags, send messages, etc.
316+
/// For the callback to run, either [`queue.submit(..)`][q::s], [`instance.poll_all(..)`][i::p_a],
317+
/// or [`device.poll(..)`][d::p] must be called elsewhere in the runtime, possibly integrated into
318+
/// an event loop or run on a separate thread.
316319
///
317-
/// As long as a buffer is mapped, it is not available for use by any other commands;
318-
/// at all times, either the GPU or the CPU has exclusive access to the contents of the buffer.
320+
/// The callback runs on the thread that first calls one of the above functions after the GPU work
321+
/// completes. There are no restrictions on the code you can run in the callback; however, on native
322+
/// the polling call will not return until the callback finishes, so keep callbacks short (set flags,
323+
/// send messages, etc.).
324+
///
325+
/// While a buffer is mapped, it cannot be used by other commands; at any time, either the GPU or
326+
/// the CPU has exclusive access to the buffer’s contents.
319327
///
320328
/// This can also be performed using [`BufferSlice::map_async()`].
321329
///
@@ -326,6 +334,14 @@ impl Buffer {
326334
/// - If `bounds` is outside of the bounds of `self`.
327335
/// - If `bounds` has a length less than 1.
328336
/// - If the start and end of `bounds` are not be aligned to [`MAP_ALIGNMENT`].
337+
///
338+
/// [CEmbos]: CommandEncoder::map_buffer_on_submit
339+
/// [CBmbos]: CommandBuffer::map_buffer_on_submit
340+
/// [RPmbos]: RenderPass::map_buffer_on_submit
341+
/// [CPmbos]: ComputePass::map_buffer_on_submit
342+
/// [q::s]: Queue::submit
343+
/// [i::p_a]: Instance::poll_all
344+
/// [d::p]: Device::poll
329345
pub fn map_async<S: RangeBounds<BufferAddress>>(
330346
&self,
331347
mode: MapMode,
@@ -461,20 +477,28 @@ impl<'a> BufferSlice<'a> {
461477
}
462478
}
463479

464-
/// Map the buffer to host (CPU) memory, making it available for reading or writing
465-
/// via [`get_mapped_range()`](Self::get_mapped_range).
466-
/// It is available once the `callback` is called with an [`Ok`] response.
480+
/// Map the buffer to host (CPU) memory, making it available for reading or writing via
481+
/// [`get_mapped_range()`](Self::get_mapped_range). The buffer becomes accessible once the
482+
/// `callback` is invoked with [`Ok`].
467483
///
468-
/// For the callback to complete, either `queue.submit(..)`, `instance.poll_all(..)`, or `device.poll(..)`
469-
/// must be called elsewhere in the runtime, possibly integrated into an event loop or run on a separate thread.
484+
/// Use this when you want to map the buffer immediately. If you need to submit GPU work that
485+
/// uses the buffer before mapping it, use `map_buffer_on_submit` on
486+
/// [`CommandEncoder`][CEmbos], [`CommandBuffer`][CBmbos], [`RenderPass`][RPmbos], or
487+
/// [`ComputePass`][CPmbos] to schedule the mapping after submission. This avoids extra calls to
488+
/// [`Buffer::map_async()`] or [`BufferSlice::map_async()`] and lets you initiate mapping from a
489+
/// more convenient place.
470490
///
471-
/// The callback will be called on the thread that first calls the above functions after the GPU work
472-
/// has completed. There are no restrictions on the code you can run in the callback, however on native the
473-
/// call to the function will not complete until the callback returns, so prefer keeping callbacks short
474-
/// and used to set flags, send messages, etc.
491+
/// For the callback to run, either [`queue.submit(..)`][q::s], [`instance.poll_all(..)`][i::p_a],
492+
/// or [`device.poll(..)`][d::p] must be called elsewhere in the runtime, possibly integrated into
493+
/// an event loop or run on a separate thread.
475494
///
476-
/// As long as a buffer is mapped, it is not available for use by any other commands;
477-
/// at all times, either the GPU or the CPU has exclusive access to the contents of the buffer.
495+
/// The callback runs on the thread that first calls one of the above functions after the GPU work
496+
/// completes. There are no restrictions on the code you can run in the callback; however, on native
497+
/// the polling call will not return until the callback finishes, so keep callbacks short (set flags,
498+
/// send messages, etc.).
499+
///
500+
/// While a buffer is mapped, it cannot be used by other commands; at any time, either the GPU or
501+
/// the CPU has exclusive access to the buffer’s contents.
478502
///
479503
/// This can also be performed using [`Buffer::map_async()`].
480504
///
@@ -483,6 +507,14 @@ impl<'a> BufferSlice<'a> {
483507
/// - If the buffer is already mapped.
484508
/// - If the buffer’s [`BufferUsages`] do not allow the requested [`MapMode`].
485509
/// - If the endpoints of this slice are not aligned to [`MAP_ALIGNMENT`] within the buffer.
510+
///
511+
/// [CEmbos]: CommandEncoder::map_buffer_on_submit
512+
/// [CBmbos]: CommandBuffer::map_buffer_on_submit
513+
/// [RPmbos]: RenderPass::map_buffer_on_submit
514+
/// [CPmbos]: ComputePass::map_buffer_on_submit
515+
/// [q::s]: Queue::submit
516+
/// [i::p_a]: Instance::poll_all
517+
/// [d::p]: Device::poll
486518
pub fn map_async(
487519
&self,
488520
mode: MapMode,
@@ -856,7 +888,7 @@ fn check_buffer_bounds(
856888
}
857889

858890
#[track_caller]
859-
fn range_to_offset_size<S: RangeBounds<BufferAddress>>(
891+
pub(crate) fn range_to_offset_size<S: RangeBounds<BufferAddress>>(
860892
bounds: S,
861893
whole_size: BufferAddress,
862894
) -> (BufferAddress, BufferSize) {

0 commit comments

Comments
 (0)