Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,41 @@ jobs:

cargo --locked test --doc

miri:
# runtime is normally 2 minutes
timeout-minutes: 10

name: Miri (limited scope)
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v6

# Miri is *only* available on nightly.
- name: Install nightly toolchain

run: |
rustup toolchain install nightly --no-self-update --profile=minimal --component miri
rustup override set nightly
cargo -V

- name: Caching
uses: Swatinem/rust-cache@v2
with:
key: miri-${{ env.CACHE_SUFFIX }}

- name: Run Miri on selected tests
shell: bash
run: |
set -e

# Note: this is a *smaller* set of tests than `cargo xtask miri` runs;
# it is ones that are both important and cheap.
# Consider expanding it to include some API tests with the noop backend.
cargo miri test --package=wgpu --no-default-features -- write_only


fmt:
# runtime is normally 15 seconds
timeout-minutes: 2
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ depth_stencil: Some(wgpu::DepthStencilState::stencil(
- Split the `TransferError::BufferOverrun` variant into new `BufferStartOffsetOverrun` and `BufferEndOffsetOverrun` variants.
- 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).
- 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).
- 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).

#### Metal

Expand Down
5 changes: 4 additions & 1 deletion tests/tests/wgpu-gpu/binding_array/buffers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ async fn binding_array_buffers(
size: 16,
mapped_at_creation: true,
});
buffer.slice(..).get_mapped_range_mut()[0..4].copy_from_slice(&data.0);
buffer
.get_mapped_range_mut(..)
.slice(0..4)
.copy_from_slice(&data.0);
buffer.unmap();
buffers.push(buffer);
}
Expand Down
6 changes: 3 additions & 3 deletions tests/tests/wgpu-gpu/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async fn test_empty_buffer_range(ctx: &TestingContext, buffer_size: u64, label:

{
let view = b1.slice(0..0).get_mapped_range_mut();
assert!(view.is_empty());
assert_eq!(view.len(), 0);
}

b1.unmap();
Expand Down Expand Up @@ -148,8 +148,8 @@ static MAP_OFFSET: GpuTestConfiguration = GpuTestConfiguration::new()
{
let slice = write_buf.slice(32..48);
let mut view = slice.get_mapped_range_mut();
for byte in &mut view[..] {
*byte = 2;
for byte in view.slice(..) {
byte.write(2);
}
}

Expand Down
41 changes: 30 additions & 11 deletions tests/tests/wgpu-validation/api/buffer_mapping.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
fn mapping_is_zeroed(array: &[u8]) {
for (i, &byte) in array.iter().enumerate() {
// FIXME: Now that MAP_WRITE mappings are write-only,
// the “mut” and “immutable” terminology is incorrect.
Comment on lines +1 to +2
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to tackle this here? Otherwise should be an issue.


fn read_mapping_is_zeroed(slice: &[u8]) {
for (i, &byte) in slice.iter().enumerate() {
assert_eq!(byte, 0, "Byte at index {i} is not zero");
}
}
fn write_mapping_is_zeroed(mut slice: wgpu::WriteOnly<'_, [u8]>) {
let ptr = slice.as_raw_ptr().cast::<u8>();
for i in 0..slice.len() {
assert_eq!(
// SAFETY: it is not, in general, safe to read from a write mapping, but our goal here
// is specifically to verify the internally provided zeroedness.
//
// FIXME: Is the goal of these tests to ensure that zeroes are what is exposed to Rust,
// and not to ensure that zeroes get into the GPU buffer? If so, then we can delete
// them, or perhaps replace them with tests of mapping without writing, then reading.
unsafe { ptr.add(i).read() },
0,
"Byte at index {i} is not zero"
);
}
}

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

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

mapping_is_zeroed(&mapping);
read_mapping_is_zeroed(&mapping);

drop(mapping);

Expand All @@ -40,9 +59,9 @@ fn full_mut_binding() {
mapped_at_creation: true,
});

let mapping = buffer.slice(..).get_mapped_range_mut();
let mut mapping = buffer.slice(..).get_mapped_range_mut();

mapping_is_zeroed(&mapping);
write_mapping_is_zeroed(mapping.slice(..));

drop(mapping);

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

mapping_is_zeroed(&mapping0);
mapping_is_zeroed(&mapping1);
read_mapping_is_zeroed(&mapping0);
read_mapping_is_zeroed(&mapping1);

drop(mapping0);
drop(mapping1);
Expand All @@ -88,11 +107,11 @@ fn split_mut_binding() {
mapped_at_creation: true,
});

let mapping0 = buffer.slice(0..512).get_mapped_range_mut();
let mapping1 = buffer.slice(512..1024).get_mapped_range_mut();
let mut mapping0 = buffer.slice(0..512).get_mapped_range_mut();
let mut mapping1 = buffer.slice(512..1024).get_mapped_range_mut();

mapping_is_zeroed(&mapping0);
mapping_is_zeroed(&mapping1);
write_mapping_is_zeroed(mapping0.slice(..));
write_mapping_is_zeroed(mapping1.slice(..));

drop(mapping0);
drop(mapping1);
Expand Down
3 changes: 2 additions & 1 deletion tests/tests/wgpu-validation/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ fn staging_belt_random_test() {
offset,
wgpu::BufferSize::new(size).unwrap(),
);
slice[0] = 1; // token amount of actual writing, just in case it makes a difference
// token amount of actual writing, just in case it makes a difference
slice.slice(..1).copy_from_slice(&[1]);
}

belt.finish();
Expand Down
116 changes: 67 additions & 49 deletions wgpu/src/api/buffer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use alloc::{boxed::Box, sync::Arc, vec::Vec};
use core::{
error, fmt,
ops::{Bound, Deref, DerefMut, Range, RangeBounds},
ops::{Bound, Deref, Range, RangeBounds},
};

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

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

let index = self
Expand Down Expand Up @@ -894,43 +894,16 @@ pub struct BufferView {
inner: dispatch::DispatchBufferMappedRange,
}

#[cfg(webgpu)]
impl BufferView {
/// Provides the same data as dereferencing the view, but as a `Uint8Array` in js.
/// This can be MUCH faster than dereferencing the view which copies the data into
/// the Rust / wasm heap.
pub fn as_uint8array(&self) -> &js_sys::Uint8Array {
self.inner.as_uint8array()
}
}

impl core::ops::Deref for BufferView {
type Target = [u8];

#[inline]
fn deref(&self) -> &[u8] {
self.inner.slice()
}
}

impl AsRef<[u8]> for BufferView {
#[inline]
fn as_ref(&self) -> &[u8] {
self.inner.slice()
}
}

/// A write-only view of a mapped buffer's bytes.
///
/// To get a `BufferViewMut`, first [map] the buffer, and then
/// call `buffer.slice(range).get_mapped_range_mut()`.
///
/// `BufferViewMut` dereferences to `&mut [u8]`, so you can use all the usual
/// Rust slice methods to access the buffer's contents. It also implements
/// `AsMut<[u8]>`, if that's more convenient.
///
/// It is possible to read the buffer using this view, but doing so is not
/// recommended, as it is likely to be slow.
/// Because Rust has no write-only reference type
/// (`&[u8]` is read-only and `&mut [u8]` is read-write),
/// this type does not dereference to a slice in the way that [`BufferView`] does.
/// Instead, [`.slice()`][BufferViewMut::slice] returns a special [`WriteOnly`] pointer type,
/// and there are also a few convenience methods such as [`BufferViewMut::copy_from_slice()`].
///
/// Before the buffer can be unmapped, all `BufferViewMut`s observing it
/// must be dropped. Otherwise, the call to [`Buffer::unmap`] will panic.
Expand All @@ -947,24 +920,25 @@ pub struct BufferViewMut {
inner: dispatch::DispatchBufferMappedRange,
}

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

impl Deref for BufferViewMut {
impl core::ops::Deref for BufferView {
type Target = [u8];

fn deref(&self) -> &Self::Target {
self.inner.slice()
#[inline]
fn deref(&self) -> &[u8] {
// SAFETY: this is a read mapping
unsafe { self.inner.read_slice() }
}
}

impl DerefMut for BufferViewMut {
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner.slice_mut()
impl AsRef<[u8]> for BufferView {
#[inline]
fn as_ref(&self) -> &[u8] {
self
}
}

Expand All @@ -986,6 +960,50 @@ impl Drop for BufferViewMut {
}
}

#[cfg(webgpu)]
impl BufferView {
/// Provides the same data as dereferencing the view, but as a `Uint8Array` in js.
/// This can be MUCH faster than dereferencing the view which copies the data into
/// the Rust / wasm heap.
pub fn as_uint8array(&self) -> &js_sys::Uint8Array {
self.inner.as_uint8array()
}
}

/// These methods are equivalent to the methods of the same names on [`WriteOnly`].
impl BufferViewMut {
/// Returns the length of this view; the number of bytes to be written.
pub fn len(&self) -> usize {
// cannot fail because we can't actually map more than isize::MAX bytes
usize::try_from(self.size.get()).unwrap()
}

/// Returns `true` if the view has a length of 0.
///
/// Note that this is currently impossible.
pub fn is_empty(&self) -> bool {
self.len() == 0
}

/// Returns a [`WriteOnly`] reference to a portion of this.
///
/// `.slice(..)` can be used to access the whole data.
pub fn slice<'a, S: RangeBounds<usize>>(&'a mut self, bounds: S) -> WriteOnly<'a, [u8]> {
// SAFETY: this is a write mapping
unsafe { self.inner.write_slice() }.into_slice(bounds)
}

/// Copies all elements from src into `self`.
///
/// The length of `src` must be the same as `self`.
///
/// This method is equivalent to
/// [`self.slice(..).copy_from_slice(src)`][WriteOnly::copy_from_slice].
pub fn copy_from_slice(&mut self, src: &[u8]) {
self.slice(..).copy_from_slice(src)
}
}

#[track_caller]
fn check_buffer_bounds(
buffer_size: BufferAddress,
Expand Down
Loading
Loading