Skip to content

Commit 54ea66f

Browse files
committed
[wgpu] Improve buffer mapping errors and allow multiple immutable borrows
1 parent 9a8dbfb commit 54ea66f

File tree

5 files changed

+227
-24
lines changed

5 files changed

+227
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ This allows using precompiled shaders without manually checking which backend's
100100
By @kpreid in [#8011](https://github.com/gfx-rs/wgpu/pull/8011).
101101
- Make a compacted hal acceleration structure inherit a label from the base BLAS. By @Vecvec in [#8103](https://github.com/gfx-rs/wgpu/pull/8103).
102102
- The limits requested for a device must now satisfy `min_subgroup_size <= max_subgroup_size`. By @andyleiserson in [#8085](https://github.com/gfx-rs/wgpu/pull/8085).
103+
- Improve errors when buffer mapping is done incorrectly. Allow aliasing immutable [`BufferViews`]. By @cwfitzgerald in [#8150](https://github.com/gfx-rs/wgpu/pull/8150).
103104

104105
#### Naga
105106

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
fn mapping_is_zeroed(array: &[u8]) {
2+
for (i, &byte) in array.iter().enumerate() {
3+
assert_eq!(byte, 0, "Byte at index {i} is not zero");
4+
}
5+
}
6+
7+
#[test]
8+
fn full_mut_binding() {
9+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
10+
11+
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
12+
label: None,
13+
size: 1024,
14+
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
15+
mapped_at_creation: true,
16+
});
17+
18+
let mapping = buffer.slice(..).get_mapped_range_mut();
19+
20+
mapping_is_zeroed(&mapping);
21+
22+
drop(mapping);
23+
24+
buffer.unmap();
25+
}
26+
27+
#[test]
28+
fn split_mut_binding() {
29+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
30+
31+
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
32+
label: None,
33+
size: 1024,
34+
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
35+
mapped_at_creation: true,
36+
});
37+
38+
let mapping0 = buffer.slice(0..512).get_mapped_range_mut();
39+
let mapping1 = buffer.slice(512..1024).get_mapped_range_mut();
40+
41+
mapping_is_zeroed(&mapping0);
42+
mapping_is_zeroed(&mapping1);
43+
44+
drop(mapping0);
45+
drop(mapping1);
46+
47+
buffer.unmap();
48+
}
49+
50+
#[test]
51+
fn overlapping_ref_binding() {
52+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
53+
54+
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
55+
label: None,
56+
size: 1024,
57+
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
58+
mapped_at_creation: true,
59+
});
60+
61+
let _mapping0 = buffer.slice(0..512).get_mapped_range();
62+
let _mapping1 = buffer.slice(256..768).get_mapped_range();
63+
}
64+
65+
#[test]
66+
#[should_panic(expected = "break Rust memory aliasing rules")]
67+
fn overlapping_mut_binding() {
68+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
69+
70+
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
71+
label: None,
72+
size: 1024,
73+
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
74+
mapped_at_creation: true,
75+
});
76+
77+
let _mapping0 = buffer.slice(0..512).get_mapped_range_mut();
78+
let _mapping1 = buffer.slice(256..768).get_mapped_range_mut();
79+
}
80+
81+
#[test]
82+
#[should_panic(expected = "an unmapped buffer")]
83+
fn not_mapped() {
84+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
85+
86+
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
87+
label: None,
88+
size: 1024,
89+
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
90+
mapped_at_creation: false,
91+
});
92+
93+
let _mapping = buffer.slice(..).get_mapped_range_mut();
94+
}
95+
96+
#[test]
97+
#[should_panic(expected = "Attempted to get range 512..1024, but the mapped range is 0..512")]
98+
fn partially_mapped() {
99+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
100+
101+
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
102+
label: None,
103+
size: 1024,
104+
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
105+
mapped_at_creation: false,
106+
});
107+
108+
buffer.map_async(wgpu::MapMode::Write, 0..512, |_| {});
109+
device.poll(wgpu::PollType::Wait).unwrap();
110+
111+
let _mapping0 = buffer.slice(0..512).get_mapped_range_mut();
112+
let _mapping1 = buffer.slice(512..1024).get_mapped_range_mut();
113+
}
114+
115+
#[test]
116+
#[should_panic(expected = "You cannot unmap a buffer that still has accessible mapped views")]
117+
fn unmap_while_visible() {
118+
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
119+
120+
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
121+
label: None,
122+
size: 1024,
123+
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
124+
mapped_at_creation: true,
125+
});
126+
127+
let _mapping0 = buffer.slice(..).get_mapped_range_mut();
128+
buffer.unmap();
129+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod binding_arrays;
22
mod buffer;
3+
mod buffer_mapping;
34
mod buffer_slice;
45
mod device;
56
mod external_texture;

wgpu/src/api/buffer.rs

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -396,9 +396,10 @@ impl Buffer {
396396
/// - If `bounds` has a length less than 1.
397397
/// - If the start and end of `bounds` are not aligned to [`MAP_ALIGNMENT`].
398398
/// - If the buffer to which `self` refers is not currently [mapped].
399-
/// - If you try to create overlapping views of a buffer, mutable or otherwise.
399+
/// - If you try to create a view which overlaps an existing [`BufferViewMut`].
400400
///
401401
/// [mapped]: Buffer#mapping-buffers
402+
#[track_caller]
402403
pub fn get_mapped_range<S: RangeBounds<BufferAddress>>(&self, bounds: S) -> BufferView {
403404
self.slice(bounds).get_mapped_range()
404405
}
@@ -419,9 +420,10 @@ impl Buffer {
419420
/// - If `bounds` has a length less than 1.
420421
/// - If the start and end of `bounds` are not aligned to [`MAP_ALIGNMENT`].
421422
/// - If the buffer to which `self` refers is not currently [mapped].
422-
/// - If you try to create overlapping views of a buffer, mutable or otherwise.
423+
/// - If you try to create a view which overlaps an existing [`BufferView`] or [`BufferViewMut`].
423424
///
424425
/// [mapped]: Buffer#mapping-buffers
426+
#[track_caller]
425427
pub fn get_mapped_range_mut<S: RangeBounds<BufferAddress>>(&self, bounds: S) -> BufferViewMut {
426428
self.slice(bounds).get_mapped_range_mut()
427429
}
@@ -534,9 +536,9 @@ impl<'a> BufferSlice<'a> {
534536
callback: impl FnOnce(Result<(), BufferAsyncError>) + WasmNotSend + 'static,
535537
) {
536538
let mut mc = self.buffer.map_context.lock();
537-
assert_eq!(mc.initial_range, 0..0, "Buffer is already mapped");
539+
assert_eq!(mc.mapped_range, 0..0, "Buffer is already mapped");
538540
let end = self.offset + self.size.get();
539-
mc.initial_range = self.offset..end;
541+
mc.mapped_range = self.offset..end;
540542

541543
self.buffer
542544
.inner
@@ -557,11 +559,16 @@ impl<'a> BufferSlice<'a> {
557559
///
558560
/// - If the endpoints of this slice are not aligned to [`MAP_ALIGNMENT`] within the buffer.
559561
/// - If the buffer to which `self` refers is not currently [mapped].
560-
/// - If you try to create overlapping views of a buffer, mutable or otherwise.
562+
/// - If you try to create a view which overlaps an existing [`BufferViewMut`].
561563
///
562564
/// [mapped]: Buffer#mapping-buffers
565+
#[track_caller]
563566
pub fn get_mapped_range(&self) -> BufferView {
564-
let end = self.buffer.map_context.lock().add(self.offset, self.size);
567+
let end = self
568+
.buffer
569+
.map_context
570+
.lock()
571+
.add(self.offset, self.size, RangeKind::Immutable);
565572
let range = self.buffer.inner.get_mapped_range(self.offset..end);
566573
BufferView {
567574
buffer: self.buffer.clone(),
@@ -585,11 +592,16 @@ impl<'a> BufferSlice<'a> {
585592
///
586593
/// - If the endpoints of this slice are not aligned to [`MAP_ALIGNMENT`].
587594
/// - If the buffer to which `self` refers is not currently [mapped].
588-
/// - If you try to create overlapping views of a buffer, mutable or otherwise.
595+
/// - If you try to create a view which overlaps an existing [`BufferView`] or [`BufferViewMut`].
589596
///
590597
/// [mapped]: Buffer#mapping-buffers
598+
#[track_caller]
591599
pub fn get_mapped_range_mut(&self) -> BufferViewMut {
592-
let end = self.buffer.map_context.lock().add(self.offset, self.size);
600+
let end = self
601+
.buffer
602+
.map_context
603+
.lock()
604+
.add(self.offset, self.size, RangeKind::Mutable);
593605
let range = self.buffer.inner.get_mapped_range(self.offset..end);
594606
BufferViewMut {
595607
buffer: self.buffer.clone(),
@@ -640,6 +652,28 @@ impl<'a> From<BufferSlice<'a>> for crate::BindingResource<'a> {
640652
}
641653
}
642654

655+
fn range_overlaps(a: &Range<BufferAddress>, b: &Range<BufferAddress>) -> bool {
656+
a.start < b.end && b.start < a.end
657+
}
658+
659+
#[derive(Debug)]
660+
enum RangeKind {
661+
Mutable,
662+
Immutable,
663+
}
664+
665+
impl RangeKind {
666+
fn compatible(&self, other: &Self) -> bool {
667+
matches!((self, other), (RangeKind::Immutable, RangeKind::Immutable))
668+
}
669+
}
670+
671+
#[derive(Debug)]
672+
struct Subrange {
673+
index: Range<BufferAddress>,
674+
kind: RangeKind,
675+
}
676+
643677
/// The mapped portion of a buffer, if any, and its outstanding views.
644678
///
645679
/// This ensures that views fall within the mapped range and don't overlap.
@@ -653,25 +687,25 @@ pub(crate) struct MapContext {
653687
/// *or has been requested to be* mapped.)
654688
///
655689
/// All [`BufferView`]s and [`BufferViewMut`]s must fall within this range.
656-
pub(crate) initial_range: Range<BufferAddress>,
690+
pub(crate) mapped_range: Range<BufferAddress>,
657691

658692
/// The ranges covered by all outstanding [`BufferView`]s and
659693
/// [`BufferViewMut`]s. These are non-overlapping, and are all contained
660-
/// within `initial_range`.
661-
sub_ranges: Vec<Range<BufferAddress>>,
694+
/// within `mapped_range`.
695+
sub_ranges: Vec<Subrange>,
662696
}
663697

664698
impl MapContext {
665699
pub(crate) fn new() -> Self {
666700
Self {
667-
initial_range: 0..0,
701+
mapped_range: 0..0,
668702
sub_ranges: Vec::new(),
669703
}
670704
}
671705

672706
/// Record that the buffer is no longer mapped.
673707
fn reset(&mut self) {
674-
self.initial_range = 0..0;
708+
self.mapped_range = 0..0;
675709

676710
assert!(
677711
self.sub_ranges.is_empty(),
@@ -686,19 +720,38 @@ impl MapContext {
686720
/// # Panics
687721
///
688722
/// This panics if the given range overlaps with any existing range.
689-
fn add(&mut self, offset: BufferAddress, size: BufferSize) -> BufferAddress {
723+
#[track_caller]
724+
fn add(&mut self, offset: BufferAddress, size: BufferSize, kind: RangeKind) -> BufferAddress {
690725
let end = offset + size.get();
691-
assert!(self.initial_range.start <= offset && end <= self.initial_range.end);
726+
727+
if self.mapped_range.is_empty() {
728+
panic!("Tried to call getMappedRange(Mut) on an unmapped buffer");
729+
}
730+
if !range_overlaps(&self.mapped_range, &(offset..end)) {
731+
panic!(
732+
"Tried to call getMappedRange(Mut) on a range that is not entirely mapped. \
733+
Attempted to get range {}..{}, but the mapped range is {}..{}",
734+
offset, end, self.mapped_range.start, self.mapped_range.end
735+
);
736+
}
737+
692738
// This check is essential for avoiding undefined behavior: it is the
693739
// only thing that ensures that `&mut` references to the buffer's
694740
// contents don't alias anything else.
695741
for sub in self.sub_ranges.iter() {
696-
assert!(
697-
end <= sub.start || offset >= sub.end,
698-
"Intersecting map range with {sub:?}"
699-
);
742+
if range_overlaps(&sub.index, &(offset..end)) && !sub.kind.compatible(&kind) {
743+
panic!(
744+
"Tried to call getMappedRange(Mut) on a range that has already \
745+
been mapped and would break Rust memory aliasing rules. Attempted \
746+
to get range {}..{} ({:?}), and the conflicting range is {}..{} ({:?})",
747+
offset, end, kind, sub.index.start, sub.index.end, sub.kind
748+
);
749+
}
700750
}
701-
self.sub_ranges.push(offset..end);
751+
self.sub_ranges.push(Subrange {
752+
index: offset..end,
753+
kind,
754+
});
702755
end
703756
}
704757

@@ -716,7 +769,7 @@ impl MapContext {
716769
let index = self
717770
.sub_ranges
718771
.iter()
719-
.position(|r| *r == (offset..end))
772+
.position(|r| r.index == (offset..end))
720773
.expect("unable to remove range from map context");
721774
self.sub_ranges.swap_remove(index);
722775
}
@@ -922,7 +975,9 @@ fn range_to_offset_size<S: RangeBounds<BufferAddress>>(
922975

923976
#[cfg(test)]
924977
mod tests {
925-
use super::{check_buffer_bounds, range_to_offset_size, BufferAddress, BufferSize};
978+
use super::{
979+
check_buffer_bounds, range_overlaps, range_to_offset_size, BufferAddress, BufferSize,
980+
};
926981

927982
fn bs(value: BufferAddress) -> BufferSize {
928983
BufferSize::new(value).unwrap()
@@ -972,4 +1027,21 @@ mod tests {
9721027
fn check_buffer_bounds_panics_for_end_wraparound() {
9731028
check_buffer_bounds(u64::MAX, 1, bs(u64::MAX));
9741029
}
1030+
1031+
#[test]
1032+
#[expect(clippy::bool_assert_comparison)] // This degrades readability significantly.
1033+
fn range_overlapping() {
1034+
// First range to the left
1035+
assert_eq!(range_overlaps(&(0..1), &(1..3)), false);
1036+
// First range overlaps left edge
1037+
assert_eq!(range_overlaps(&(0..2), &(1..3)), true);
1038+
// First range completely inside second
1039+
assert_eq!(range_overlaps(&(1..2), &(0..3)), true);
1040+
// First range completely surrounds second
1041+
assert_eq!(range_overlaps(&(0..3), &(1..2)), true);
1042+
// First range overlaps right edge
1043+
assert_eq!(range_overlaps(&(1..3), &(0..2)), true);
1044+
// First range entirely to the right
1045+
assert_eq!(range_overlaps(&(2..3), &(0..2)), false);
1046+
}
9751047
}

wgpu/src/api/device.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ impl Device {
264264
pub fn create_buffer(&self, desc: &BufferDescriptor<'_>) -> Buffer {
265265
let mut map_context = MapContext::new();
266266
if desc.mapped_at_creation {
267-
map_context.initial_range = 0..desc.size;
267+
map_context.mapped_range = 0..desc.size;
268268
}
269269

270270
let buffer = self.inner.create_buffer(desc);
@@ -373,7 +373,7 @@ impl Device {
373373
) -> Buffer {
374374
let mut map_context = MapContext::new();
375375
if desc.mapped_at_creation {
376-
map_context.initial_range = 0..desc.size;
376+
map_context.mapped_range = 0..desc.size;
377377
}
378378

379379
let buffer = unsafe {

0 commit comments

Comments
 (0)