Skip to content

Commit 8334940

Browse files
committed
Add WriteOnly type to replace giving &mut [u8] access to mapped buffers.
Fixes <#8897>.
1 parent 7705c84 commit 8334940

15 files changed

Lines changed: 1391 additions & 120 deletions

File tree

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,41 @@ jobs:
743743
744744
cargo --locked test --doc
745745
746+
miri:
747+
# TODO: note typical run time
748+
timeout-minutes: 10
749+
750+
name: Miri (limited scope)
751+
runs-on: ubuntu-latest
752+
753+
steps:
754+
- name: Checkout repo
755+
uses: actions/checkout@v6
756+
757+
# Miri is *only* available on nightly.
758+
- name: Install nightly toolchain
759+
760+
run: |
761+
rustup toolchain install nightly --no-self-update --profile=minimal --component miri
762+
rustup override set nightly
763+
cargo -V
764+
765+
- name: Caching
766+
uses: Swatinem/rust-cache@v2
767+
with:
768+
key: miri-${{ env.CACHE_SUFFIX }}
769+
770+
- name: Run Miri on selected tests
771+
shell: bash
772+
run: |
773+
set -e
774+
775+
# Note: this is a *smaller* set of tests than `cargo xtask miri` runs;
776+
# it is ones that are both important and cheap.
777+
# Consider expanding it to include some API tests with the noop backend.
778+
cargo miri test --package=wgpu --no-default-features -- write_only
779+
780+
746781
fmt:
747782
# runtime is normally 15 seconds
748783
timeout-minutes: 2

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ By @cwfitzgerald in [#8999](https://github.com/gfx-rs/wgpu/pull/8999).
9898
- Split the `OutOfBoundsOverrun` variant into new `OutOfBoundsStartOffsetOverrun` and `OutOfBoundsEndOffsetOverrun` variants.
9999
- Removed the `NegativeRange` variant in favor of new `MapStartOffsetUnderrun` and `MapStartOffsetOverrun` variants.
100100
- Split the `TransferError::BufferOverrun` variant into new `BufferStartOffsetOverrun` and `BufferEndOffsetOverrun` variants.
101+
- To ensure memory safety in all circumstances, `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 will likely be needed, particularly including replacing `view[start..end]` with `view.slice(start..end)`.
101102

102103
#### Metal
103104

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,

0 commit comments

Comments
 (0)