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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ Bottom level categories:

### New Features

#### General

- It is now possible to create a dummy `wgpu` device even when no GPU is available. This may be useful for testing of code which manages graphics resources. Currently, it supports reading and writing buffers, and other resource types can be created but do nothing.

To use it, enable the `noop` feature of `wgpu`, and add `NoopBackendOptions { enable: true }` to the backend options; this is an additional safeguard beyond the `Backends` bits.

By @kpreid in [#7063](https://github.com/gfx-rs/wgpu/pull/7063).

#### Naga

- Support @must_use attribute on function declarations. By @turbocrime in [#6801](https://github.com/gfx-rs/wgpu/pull/6801).
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ wgpu = { version = "24.0.0", path = "./wgpu", default-features = false, features
"metal",
"static-dxc",
"webgl",
"noop", # This should be removed if we ever have non-test crates that depend on wgpu
] }
wgpu-core = { version = "24.0.0", path = "./wgpu-core" }
wgpu-hal = { version = "24.0.0", path = "./wgpu-hal" }
Expand Down
1 change: 1 addition & 0 deletions deno_webgpu/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ pub fn op_webgpu_request_adapter(
shader_compiler: wgpu_types::Dx12Compiler::Fxc,
},
gl: wgpu_types::GlBackendOptions::default(),
noop: wgpu_types::NoopBackendOptions::default(),
},
},
)));
Expand Down
6 changes: 2 additions & 4 deletions player/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! and run the tests through them.
//!
//! Test requirements:
//! - all IDs have the backend `Empty`
//! - all IDs have the backend `Noop`
//! - all expected buffers have `MAP_READ` usage
//! - last action is `Submit`
//! - no swapchain use
Expand Down Expand Up @@ -74,9 +74,7 @@ impl Test<'_> {
wgt::Backend::Gl => "Gl",
_ => unreachable!(),
};
let string = read_to_string(&path)
.unwrap()
.replace("Empty", backend_name);
let string = read_to_string(&path).unwrap().replace("Noop", backend_name);
ron::de::from_str(&string)
.unwrap_or_else(|e| panic!("{path:?}:{} {}", e.position.line, e.code))
}
Expand Down
7 changes: 6 additions & 1 deletion tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ name = "wgpu-compile-test"
path = "compile_tests/root.rs"
harness = true

[[test]]
name = "wgpu-validation-test"
path = "validation_tests/root.rs"
harness = true

[features]
webgl = ["wgpu/webgl"]

[dependencies]
wgpu.workspace = true
wgpu = { workspace = true, features = ["noop"] }
wgpu-macros.workspace = true

anyhow.workspace = true
Expand Down
2 changes: 2 additions & 0 deletions tests/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub fn initialize_instance(backends: wgpu::Backends, force_fxc: bool) -> Instanc
shader_compiler: dx12_shader_compiler,
},
gl: wgpu::GlBackendOptions::from_env_or_default(),
// TODO(https://github.com/gfx-rs/wgpu/issues/7119): Enable noop backend?
noop: wgpu::NoopBackendOptions::default(),
},
})
}
Expand Down
61 changes: 61 additions & 0 deletions tests/validation_tests/noop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! Tests of [`wgpu::Backend::Noop`].

use std::sync::atomic::{AtomicBool, Ordering::Relaxed};
use std::sync::Arc;

#[test]
fn device_is_not_available_by_default() {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::NOOP,
..Default::default()
});

assert_eq!(
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())),
None,
"noop backend adapter present when it should not be"
);
}

#[test]
fn device_and_buffers() {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::NOOP,
backend_options: wgpu::BackendOptions {
noop: wgpu::NoopBackendOptions { enable: true },
..Default::default()
},
..Default::default()
});
let adapter =
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.expect("adapter");
let (device, queue) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None))
.expect("device");

assert_eq!(adapter.get_info().backend, wgpu::Backend::Noop);

// Demonstrate that creating and *writing* to a buffer succeeds.
// This also involves creation of a staging buffer.
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("hello world"),
size: 8,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
assert_eq!(buffer.size(), 8);
queue.write_buffer(&buffer, 0, &[1, 2, 3, 4]);
queue.write_buffer(&buffer, 4, &[5, 6, 7, 8]);

// Demonstrate that we can read back data from the buffer.
// This also involves copy_buffer_to_buffer().
let done: Arc<AtomicBool> = Arc::default();
let done2 = done.clone();
wgpu::util::DownloadBuffer::read_buffer(&device, &queue, &buffer.slice(..), move |result| {
assert_eq!(*result.unwrap(), [1, 2, 3, 4, 5, 6, 7, 8],);
done.store(true, Relaxed);
});
device.poll(wgpu::Maintain::Wait);
assert!(done2.load(Relaxed));
}
3 changes: 3 additions & 0 deletions tests/validation_tests/root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//! Tests of the [`wgpu`] library API that are not run against a particular GPU.

mod noop;
4 changes: 4 additions & 0 deletions wgpu-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ gles = ["wgpu-hal/gles"]
## Enable the `dx12` backend.
dx12 = ["wgpu-hal/dx12"]

## Enable the `noop` backend.
# TODO(https://github.com/gfx-rs/wgpu/issues/7120): there should be a hal feature
noop = []

[dependencies]
naga.workspace = true
wgpu-hal.workspace = true
Expand Down
4 changes: 2 additions & 2 deletions wgpu-core/src/hal_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ pub trait HalApi: hal::Api + 'static + WasmNotSendSync {
const VARIANT: Backend;
}

impl HalApi for hal::api::Empty {
const VARIANT: Backend = Backend::Empty;
impl HalApi for hal::api::Noop {
const VARIANT: Backend = Backend::Noop;
}

#[cfg(vulkan)]
Expand Down
8 changes: 4 additions & 4 deletions wgpu-core/src/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,19 @@ impl RawId {
/// any backend, but the corresponding resource types ([`Texture<A>`], for
/// example) are always parameterized by a specific backend `A`.
///
/// So the `T` in `Id<T>` is usually a resource type like `Texture<Empty>`,
/// where [`Empty`] is the `wgpu_hal` dummy back end. These empty types are
/// So the `T` in `Id<T>` is usually a resource type like `Texture<Noop>`,
/// where [`Noop`] is the `wgpu_hal` dummy back end. These empty types are
/// never actually used, beyond just making sure you access each `Storage` with
/// the right kind of identifier. The members of [`Hub<A>`] pair up each
/// `X<Empty>` type with the resource type `X<A>`, for some specific backend
/// `X<Noop>` type with the resource type `X<A>`, for some specific backend
/// `A`.
///
/// [`Global`]: crate::global::Global
/// [`Hub`]: crate::hub::Hub
/// [`Hub<A>`]: crate::hub::Hub
/// [`Texture<A>`]: crate::resource::Texture
/// [`Registry`]: crate::hub::Registry
/// [`Empty`]: hal::api::Empty
/// [`Noop`]: hal::api::Noop
#[repr(transparent)]
#[cfg_attr(any(feature = "serde", feature = "trace"), derive(serde::Serialize))]
#[cfg_attr(any(feature = "serde", feature = "replay"), derive(serde::Deserialize))]
Expand Down
2 changes: 2 additions & 0 deletions wgpu-core/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ impl Instance {
init(hal::api::Dx12, instance_desc, &mut instance_per_backend);
#[cfg(gles)]
init(hal::api::Gles, instance_desc, &mut instance_per_backend);
#[cfg(feature = "noop")]
init(hal::api::Noop, instance_desc, &mut instance_per_backend);

Self {
name: name.to_string(),
Expand Down
2 changes: 1 addition & 1 deletion wgpu-hal/examples/halmark/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ cfg_if::cfg_if! {
}
// Fallback
else {
type Api = hal::api::Empty;
type Api = hal::api::Noop;
}
}

Expand Down
2 changes: 1 addition & 1 deletion wgpu-hal/examples/ray-traced-triangle/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,7 @@ cfg_if::cfg_if! {
}
// Fallback
else {
type Api = hal::api::Empty;
type Api = hal::api::Noop;
}
}

Expand Down
9 changes: 6 additions & 3 deletions wgpu-hal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,15 @@ extern crate wgpu_types as wgt;
/// DirectX12 API internals.
#[cfg(dx12)]
pub mod dx12;
/// A dummy API implementation.
pub mod empty;
/// GLES API internals.
#[cfg(gles)]
pub mod gles;
/// Metal API internals.
#[cfg(metal)]
pub mod metal;
/// A dummy API implementation.
// TODO(https://github.com/gfx-rs/wgpu/issues/7120): this should have a cfg
pub mod noop;
/// Vulkan API internals.
#[cfg(vulkan)]
pub mod vulkan;
Expand All @@ -255,11 +256,11 @@ pub mod auxil;
pub mod api {
#[cfg(dx12)]
pub use super::dx12::Api as Dx12;
pub use super::empty::Api as Empty;
#[cfg(gles)]
pub use super::gles::Api as Gles;
#[cfg(metal)]
pub use super::metal::Api as Metal;
pub use super::noop::Api as Noop;
#[cfg(vulkan)]
pub use super::vulkan::Api as Vulkan;
}
Expand Down Expand Up @@ -936,6 +937,8 @@ pub trait Device: WasmNotSendSync {
/// Calling `wait` with a lower [`FenceValue`] than `fence`'s current value
/// returns immediately.
///
/// Returns `Ok(true)` on success and `Ok(false)` on timeout.
///
/// [`Fence`]: Api::Fence
/// [`FencePool`]: vulkan/enum.Fence.html#variant.FencePool
unsafe fn wait(
Expand Down
82 changes: 82 additions & 0 deletions wgpu-hal/src/noop/buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use core::cell::UnsafeCell;
use core::ops::Range;
use core::ptr;
use std::sync::Arc;

#[derive(Clone, Debug)]
pub struct Buffer {
/// This data is potentially accessed mutably in arbitrary non-overlapping slices,
/// so we must store it in `UnsafeCell` to avoid making any too-strong no-aliasing claims.
storage: Arc<UnsafeCell<[u8]>>,

/// Size of the allocation.
///
/// This is redundant with `storage.get().len()`, but that method is not
/// available until our MSRV is 1.79 or greater.
size: usize,
}

/// SAFETY:
/// This shared mutable data will not be accessed in a way which causes data races;
/// the obligation to do so is on the caller of the HAL API.
/// For safe code, `wgpu-core` validation manages appropriate access.
unsafe impl Send for Buffer {}
unsafe impl Sync for Buffer {}

impl Buffer {
pub(super) fn new(desc: &crate::BufferDescriptor) -> Result<Self, crate::DeviceError> {
let &crate::BufferDescriptor {
label: _,
size,
usage: _,
memory_flags: _,
} = desc;

let size = usize::try_from(size).map_err(|_| crate::DeviceError::OutOfMemory)?;

let mut vector: Vec<u8> = Vec::new();
vector
.try_reserve_exact(size)
.map_err(|_| crate::DeviceError::OutOfMemory)?;
vector.resize(size, 0);
let storage: Arc<[u8]> = Arc::from(vector);
debug_assert_eq!(storage.len(), size);

// SAFETY: `UnsafeCell<[u8]>` and `[u8]` have the same layout.
// This is just adding a wrapper type without changing any layout,
// because there is not currently a safe language/`std` way to accomplish this.
let storage: Arc<UnsafeCell<[u8]>> =
unsafe { Arc::from_raw(Arc::into_raw(storage) as *mut UnsafeCell<[u8]>) };

Ok(Buffer { storage, size })
}

/// Returns a pointer to the memory owned by this buffer within the given `range`.
///
/// This may be used to create any number of simultaneous pointers;
/// aliasing is only a concern when actually reading, writing, or converting the pointer
/// to a reference.
pub(super) fn get_slice_ptr(&self, range: crate::MemoryRange) -> *mut [u8] {
let base_ptr = self.storage.get();
let range = range_to_usize(range, self.size);

// We must obtain a slice pointer without ever creating a slice reference
// that could alias with another slice.
ptr::slice_from_raw_parts_mut(
// SAFETY: `range_to_usize` bounds checks this addition.
unsafe { base_ptr.cast::<u8>().add(range.start) },
range.len(),
)
}
}

/// Convert a [`crate::MemoryRange`] to `Range<usize>` and bounds check it.
fn range_to_usize(range: crate::MemoryRange, upper_bound: usize) -> Range<usize> {
// Note: these assertions should be impossible to trigger from safe code.
// We're doing them anyway since this entire backend is for testing
// (except for when it is an unused placeholder)
let start = usize::try_from(range.start).expect("range too large");
let end = usize::try_from(range.end).expect("range too large");
assert!(start <= end && end <= upper_bound, "range out of bounds");
start..end
}
Loading
Loading