Skip to content

Commit a97a49d

Browse files
kpreidcwfitzgerald
authored andcommitted
Replace &mut [u8] access to mapped buffers with WriteOnly.
When accessing a buffer for writing, do not expose (or internally create) any `&'a mut [u8]` references to that memory, so as to avoid exposing write-combining memory that might visibly misbehave in atomic operations. Instead, expose the new pointer type `WriteOnly<'a, [u8]>` which offers only write operations. This can be sliced and reborrowed, allowing it to be used in most ways which `&mut [u8]` would be, hopefully with near-zero overhead. This pointer type might also allow users to opt in to uninitialized memory for performance, while confining the consequences to GPU UB rather than Rust UB. However, that is a future possibility and is not implemented in this commit other than as a consideration in defining the API of the `WriteOnly` type. This change is not a complete solution to the problem of write-combining memory because it is still possible to read after writing by calling `get_mapped_range()` after `get_mapped_range_mut()`. However, that is solvable separately, perhaps with a barrier or a validation change, and is not particularly coupled with this part of the solution.
1 parent e6899b5 commit a97a49d

12 files changed

Lines changed: 213 additions & 119 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ depth_stencil: Some(wgpu::DepthStencilState::stencil(
175175
- Split the `TransferError::BufferOverrun` variant into new `BufferStartOffsetOverrun` and `BufferEndOffsetOverrun` variants.
176176
- The various "max resources per stage" limits are now capped at 100, so that their total remains below `max_bindings_per_bind_group`, as required by WebGPU. By @andyleiserson in [#9118](https://github.com/gfx-rs/wgpu/pull/9118).
177177
- The `max_uniform_buffer_binding_size` and `max_storage_buffer_binding_size` limits are now `u64` instead of `u32`, to match WebGPU. By @wingertge in [#9146](https://github.com/gfx-rs/wgpu/pull/9146).
178+
- To ensure memory safety when accessing mapped memory, `MAP_WRITE` buffer mappings are no longer exposed as Rust `&mut [u8]`, but the new type `WriteOnly<[u8]>`, which does not allow reading. Similar methods are provided where possible, but changes to your code will likely be needed, particularly including replacing `view[start..end]` with `view.slice(start..end)`. By @kpreid in [#9042](https://github.com/gfx-rs/wgpu/pull/9042).
178179

179180
#### Metal
180181

tests/tests/wgpu-gpu/binding_array/buffers.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ async fn binding_array_buffers(
158158
size: 16,
159159
mapped_at_creation: true,
160160
});
161-
buffer.slice(..).get_mapped_range_mut()[0..4].copy_from_slice(&data.0);
161+
buffer
162+
.get_mapped_range_mut(..)
163+
.slice(0..4)
164+
.copy_from_slice(&data.0);
162165
buffer.unmap();
163166
buffers.push(buffer);
164167
}

tests/tests/wgpu-gpu/buffer.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async fn test_empty_buffer_range(ctx: &TestingContext, buffer_size: u64, label:
8989

9090
{
9191
let view = b1.slice(0..0).get_mapped_range_mut();
92-
assert!(view.is_empty());
92+
assert_eq!(view.len(), 0);
9393
}
9494

9595
b1.unmap();
@@ -148,8 +148,8 @@ static MAP_OFFSET: GpuTestConfiguration = GpuTestConfiguration::new()
148148
{
149149
let slice = write_buf.slice(32..48);
150150
let mut view = slice.get_mapped_range_mut();
151-
for byte in &mut view[..] {
152-
*byte = 2;
151+
for byte in view.slice(..) {
152+
byte.write(2);
153153
}
154154
}
155155

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

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
1-
fn mapping_is_zeroed(array: &[u8]) {
2-
for (i, &byte) in array.iter().enumerate() {
1+
// FIXME: Now that MAP_WRITE mappings are write-only,
2+
// the “mut” and “immutable” terminology is incorrect.
3+
4+
fn read_mapping_is_zeroed(slice: &[u8]) {
5+
for (i, &byte) in slice.iter().enumerate() {
36
assert_eq!(byte, 0, "Byte at index {i} is not zero");
47
}
58
}
9+
fn write_mapping_is_zeroed(mut slice: wgpu::WriteOnly<'_, [u8]>) {
10+
let ptr = slice.as_raw_ptr().cast::<u8>();
11+
for i in 0..slice.len() {
12+
assert_eq!(
13+
// SAFETY: it is not, in general, safe to read from a write mapping, but our goal here
14+
// is specifically to verify the internally provided zeroedness.
15+
//
16+
// FIXME: Is the goal of these tests to ensure that zeroes are what is exposed to Rust,
17+
// and not to ensure that zeroes get into the GPU buffer? If so, then we can delete
18+
// them, or perhaps replace them with tests of mapping without writing, then reading.
19+
unsafe { ptr.add(i).read() },
20+
0,
21+
"Byte at index {i} is not zero"
22+
);
23+
}
24+
}
625

726
// Ensure that a simple immutable mapping works and it is zeroed.
827
#[test]
@@ -21,7 +40,7 @@ fn full_immutable_binding() {
2140

2241
let mapping = buffer.slice(..).get_mapped_range();
2342

24-
mapping_is_zeroed(&mapping);
43+
read_mapping_is_zeroed(&mapping);
2544

2645
drop(mapping);
2746

@@ -40,9 +59,9 @@ fn full_mut_binding() {
4059
mapped_at_creation: true,
4160
});
4261

43-
let mapping = buffer.slice(..).get_mapped_range_mut();
62+
let mut mapping = buffer.slice(..).get_mapped_range_mut();
4463

45-
mapping_is_zeroed(&mapping);
64+
write_mapping_is_zeroed(mapping.slice(..));
4665

4766
drop(mapping);
4867

@@ -67,8 +86,8 @@ fn split_immutable_binding() {
6786
let mapping0 = buffer.slice(0..512).get_mapped_range();
6887
let mapping1 = buffer.slice(512..1024).get_mapped_range();
6988

70-
mapping_is_zeroed(&mapping0);
71-
mapping_is_zeroed(&mapping1);
89+
read_mapping_is_zeroed(&mapping0);
90+
read_mapping_is_zeroed(&mapping1);
7291

7392
drop(mapping0);
7493
drop(mapping1);
@@ -88,11 +107,11 @@ fn split_mut_binding() {
88107
mapped_at_creation: true,
89108
});
90109

91-
let mapping0 = buffer.slice(0..512).get_mapped_range_mut();
92-
let mapping1 = buffer.slice(512..1024).get_mapped_range_mut();
110+
let mut mapping0 = buffer.slice(0..512).get_mapped_range_mut();
111+
let mut mapping1 = buffer.slice(512..1024).get_mapped_range_mut();
93112

94-
mapping_is_zeroed(&mapping0);
95-
mapping_is_zeroed(&mapping1);
113+
write_mapping_is_zeroed(mapping0.slice(..));
114+
write_mapping_is_zeroed(mapping1.slice(..));
96115

97116
drop(mapping0);
98117
drop(mapping1);

tests/tests/wgpu-validation/util.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ fn staging_belt_random_test() {
3131
offset,
3232
wgpu::BufferSize::new(size).unwrap(),
3333
);
34-
slice[0] = 1; // token amount of actual writing, just in case it makes a difference
34+
// token amount of actual writing, just in case it makes a difference
35+
slice.slice(..1).copy_from_slice(&[1]);
3536
}
3637

3738
belt.finish();

wgpu/src/api/buffer.rs

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use alloc::{boxed::Box, sync::Arc, vec::Vec};
22
use core::{
33
error, fmt,
4-
ops::{Bound, Deref, DerefMut, Range, RangeBounds},
4+
ops::{Bound, Deref, Range, RangeBounds},
55
};
66

77
use crate::util::Mutex;
@@ -140,8 +140,8 @@ use crate::*;
140140
/// buffer.map_async(wgpu::MapMode::Write, .., move |result| {
141141
/// if result.is_ok() {
142142
/// let mut view = capturable.get_mapped_range_mut(..);
143-
/// let floats: &mut [f32] = bytemuck::cast_slice_mut(&mut view);
144-
/// floats.fill(42.0);
143+
/// let mut floats: wgpu::WriteOnly<[[u8; 4]]> = view.slice(..).into_chunks::<4>().0;
144+
/// floats.fill(42.0f32.to_ne_bytes());
145145
/// drop(view);
146146
/// capturable.unmap();
147147
/// }
@@ -613,7 +613,7 @@ impl<'a> BufferSlice<'a> {
613613
}
614614
}
615615

616-
/// Gain write access to the bytes of a [mapped] [`Buffer`].
616+
/// Gain write-only access to the bytes of a [mapped] [`Buffer`].
617617
///
618618
/// Returns a [`BufferViewMut`] referring to the buffer range represented by
619619
/// `self`. See the documentation for [`BufferViewMut`] for more details.
@@ -825,7 +825,7 @@ impl MapContext {
825825
///
826826
/// This panics if the given range does not exactly match one previously
827827
/// passed to [`MapContext::validate_and_add`].
828-
fn remove(&mut self, offset: BufferAddress, size: BufferSize) {
828+
pub(crate) fn remove(&mut self, offset: BufferAddress, size: BufferSize) {
829829
let end = offset + size.get();
830830

831831
let index = self
@@ -894,43 +894,16 @@ pub struct BufferView {
894894
inner: dispatch::DispatchBufferMappedRange,
895895
}
896896

897-
#[cfg(webgpu)]
898-
impl BufferView {
899-
/// Provides the same data as dereferencing the view, but as a `Uint8Array` in js.
900-
/// This can be MUCH faster than dereferencing the view which copies the data into
901-
/// the Rust / wasm heap.
902-
pub fn as_uint8array(&self) -> &js_sys::Uint8Array {
903-
self.inner.as_uint8array()
904-
}
905-
}
906-
907-
impl core::ops::Deref for BufferView {
908-
type Target = [u8];
909-
910-
#[inline]
911-
fn deref(&self) -> &[u8] {
912-
self.inner.slice()
913-
}
914-
}
915-
916-
impl AsRef<[u8]> for BufferView {
917-
#[inline]
918-
fn as_ref(&self) -> &[u8] {
919-
self.inner.slice()
920-
}
921-
}
922-
923897
/// A write-only view of a mapped buffer's bytes.
924898
///
925899
/// To get a `BufferViewMut`, first [map] the buffer, and then
926900
/// call `buffer.slice(range).get_mapped_range_mut()`.
927901
///
928-
/// `BufferViewMut` dereferences to `&mut [u8]`, so you can use all the usual
929-
/// Rust slice methods to access the buffer's contents. It also implements
930-
/// `AsMut<[u8]>`, if that's more convenient.
931-
///
932-
/// It is possible to read the buffer using this view, but doing so is not
933-
/// recommended, as it is likely to be slow.
902+
/// Because Rust has no write-only reference type
903+
/// (`&[u8]` is read-only and `&mut [u8]` is read-write),
904+
/// this type does not dereference to a slice in the way that [`BufferView`] does.
905+
/// Instead, [`.slice()`][BufferViewMut::slice] returns a special [`WriteOnly`] pointer type,
906+
/// and there are also a few convenience methods such as [`BufferViewMut::copy_from_slice()`].
934907
///
935908
/// Before the buffer can be unmapped, all `BufferViewMut`s observing it
936909
/// must be dropped. Otherwise, the call to [`Buffer::unmap`] will panic.
@@ -947,24 +920,25 @@ pub struct BufferViewMut {
947920
inner: dispatch::DispatchBufferMappedRange,
948921
}
949922

950-
impl AsMut<[u8]> for BufferViewMut {
951-
#[inline]
952-
fn as_mut(&mut self) -> &mut [u8] {
953-
self.inner.slice_mut()
954-
}
955-
}
923+
// `BufferView` simply dereferences. `BufferViewMut` cannot, because mapped memory may be
924+
// write-combining memory <https://en.wikipedia.org/wiki/Write_combining>,
925+
// and not support the expected behavior of atomic accesses.
926+
// Further context: <https://github.com/gfx-rs/wgpu/issues/8897>
956927

957-
impl Deref for BufferViewMut {
928+
impl core::ops::Deref for BufferView {
958929
type Target = [u8];
959930

960-
fn deref(&self) -> &Self::Target {
961-
self.inner.slice()
931+
#[inline]
932+
fn deref(&self) -> &[u8] {
933+
// SAFETY: this is a read mapping
934+
unsafe { self.inner.read_slice() }
962935
}
963936
}
964937

965-
impl DerefMut for BufferViewMut {
966-
fn deref_mut(&mut self) -> &mut Self::Target {
967-
self.inner.slice_mut()
938+
impl AsRef<[u8]> for BufferView {
939+
#[inline]
940+
fn as_ref(&self) -> &[u8] {
941+
self
968942
}
969943
}
970944

@@ -986,6 +960,50 @@ impl Drop for BufferViewMut {
986960
}
987961
}
988962

963+
#[cfg(webgpu)]
964+
impl BufferView {
965+
/// Provides the same data as dereferencing the view, but as a `Uint8Array` in js.
966+
/// This can be MUCH faster than dereferencing the view which copies the data into
967+
/// the Rust / wasm heap.
968+
pub fn as_uint8array(&self) -> &js_sys::Uint8Array {
969+
self.inner.as_uint8array()
970+
}
971+
}
972+
973+
/// These methods are equivalent to the methods of the same names on [`WriteOnly`].
974+
impl BufferViewMut {
975+
/// Returns the length of this view; the number of bytes to be written.
976+
pub fn len(&self) -> usize {
977+
// cannot fail because we can't actually map more than isize::MAX bytes
978+
usize::try_from(self.size.get()).unwrap()
979+
}
980+
981+
/// Returns `true` if the view has a length of 0.
982+
///
983+
/// Note that this is currently impossible.
984+
pub fn is_empty(&self) -> bool {
985+
self.len() == 0
986+
}
987+
988+
/// Returns a [`WriteOnly`] reference to a portion of this.
989+
///
990+
/// `.slice(..)` can be used to access the whole data.
991+
pub fn slice<'a, S: RangeBounds<usize>>(&'a mut self, bounds: S) -> WriteOnly<'a, [u8]> {
992+
// SAFETY: this is a write mapping
993+
unsafe { self.inner.write_slice() }.into_slice(bounds)
994+
}
995+
996+
/// Copies all elements from src into `self`.
997+
///
998+
/// The length of `src` must be the same as `self`.
999+
///
1000+
/// This method is equivalent to
1001+
/// [`self.slice(..).copy_from_slice(src)`][WriteOnly::copy_from_slice].
1002+
pub fn copy_from_slice(&mut self, src: &[u8]) {
1003+
self.slice(..).copy_from_slice(src)
1004+
}
1005+
}
1006+
9891007
#[track_caller]
9901008
fn check_buffer_bounds(
9911009
buffer_size: BufferAddress,

wgpu/src/api/queue.rs

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use alloc::boxed::Box;
2-
use core::ops::{Deref, DerefMut};
2+
use core::ops::{Deref, RangeBounds};
33

44
use crate::{api::DeferredCommandBufferActions, *};
55

@@ -55,9 +55,7 @@ static_assertions::assert_impl_all!(PollType: Send, Sync);
5555

5656
/// A write-only view into a staging buffer.
5757
///
58-
/// Reading into this buffer won't yield the contents of the buffer from the
59-
/// GPU and is likely to be slow. Because of this, although [`AsMut`] is
60-
/// implemented for this type, [`AsRef`] is not.
58+
/// This type is what [`Queue::write_buffer_with()`] returns.
6159
pub struct QueueWriteBufferView {
6260
queue: Queue,
6361
buffer: Buffer,
@@ -75,31 +73,44 @@ impl QueueWriteBufferView {
7573
}
7674
}
7775

78-
impl Deref for QueueWriteBufferView {
79-
type Target = [u8];
80-
81-
fn deref(&self) -> &Self::Target {
82-
self.inner.slice()
76+
impl Drop for QueueWriteBufferView {
77+
fn drop(&mut self) {
78+
self.queue
79+
.inner
80+
.write_staging_buffer(&self.buffer.inner, self.offset, &self.inner);
8381
}
8482
}
8583

86-
impl DerefMut for QueueWriteBufferView {
87-
fn deref_mut(&mut self) -> &mut Self::Target {
88-
self.inner.slice_mut()
84+
/// These methods are equivalent to the methods of the same names on [`WriteOnly`].
85+
impl QueueWriteBufferView {
86+
/// Returns the length of this view; the number of bytes to be written.
87+
pub fn len(&self) -> usize {
88+
self.inner.len()
8989
}
90-
}
9190

92-
impl AsMut<[u8]> for QueueWriteBufferView {
93-
fn as_mut(&mut self) -> &mut [u8] {
94-
self.inner.slice_mut()
91+
/// Returns `true` if the view has a length of 0.
92+
pub fn is_empty(&self) -> bool {
93+
self.len() == 0
9594
}
96-
}
9795

98-
impl Drop for QueueWriteBufferView {
99-
fn drop(&mut self) {
100-
self.queue
101-
.inner
102-
.write_staging_buffer(&self.buffer.inner, self.offset, &self.inner);
96+
/// Returns a [`WriteOnly`] reference to a portion of this.
97+
///
98+
/// `.slice(..)` can be used to access the whole data.
99+
pub fn slice<'a, S: RangeBounds<usize>>(&'a mut self, bounds: S) -> WriteOnly<'a, [u8]> {
100+
// SAFETY:
101+
// * this is a write mapping
102+
// * function signature ensures no aliasing
103+
unsafe { self.inner.write_slice() }.into_slice(bounds)
104+
}
105+
106+
/// Copies all elements from src into `self`.
107+
///
108+
/// The length of `src` must be the same as `self`.
109+
///
110+
/// This method is equivalent to
111+
/// [`self.slice(..).copy_from_slice(src)`][WriteOnly::copy_from_slice].
112+
pub fn copy_from_slice(&mut self, src: &[u8]) {
113+
self.slice(..).copy_from_slice(src)
103114
}
104115
}
105116

0 commit comments

Comments
 (0)