Skip to content

Commit

Permalink
Use a new wgpu::Adapter for each wgpu::Device.
Browse files Browse the repository at this point in the history
This obeys the rules for `requestDevice()` in the WebGPU specification,
which `wgpu` does not yet enforce but may in a future version.

Only tests and `headless::Builder` are affected.
  • Loading branch information
kpreid committed Jun 1, 2024
1 parent 7557dbf commit 6b67bcf
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 111 deletions.
40 changes: 15 additions & 25 deletions all-is-cubes-gpu/benches/wgpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,22 @@ fn main() {

let mut criterion: Criterion<_> = Criterion::default().configure_from_args();

let (_, adapter) = runtime.block_on(init::create_instance_and_adapter_for_test(|msg| {
eprintln!("{msg}")
}));
let adapter = match adapter {
Some(adapter) => Arc::new(adapter),
None => {
// TODO: kludge; would be better if we could get the mode out of Criterion
if cfg!(test) || std::env::args().any(|s| s == "--test") {
eprintln!("GPU not available; skipping actually testing bench functions");
return;
} else {
panic!("benches/wgpu requires a GPU, but no adapter was found");
}
}
};
let instance = runtime.block_on(init::create_instance_for_test_or_exit());

render_benches(&runtime, &mut criterion, &adapter);
module_benches(&runtime, &mut criterion, &adapter);
render_benches(&runtime, &mut criterion, &instance);
module_benches(&runtime, &mut criterion, &instance);

criterion.final_summary();
}

#[allow(clippy::await_holding_lock)]
fn render_benches(runtime: &Runtime, c: &mut Criterion, adapter: &Arc<wgpu::Adapter>) {
fn render_benches(runtime: &Runtime, c: &mut Criterion, instance: &wgpu::Instance) {
let mut g = c.benchmark_group("render");

// Benchmark for running update() only. Insofar as this touches the GPU it will
// naturally fill up the pipeline as Criterion iterates it.
g.bench_function("update-only", |b| {
let (mut universe, space, renderer) =
runtime.block_on(create_updated_renderer(adapter.clone()));
let (mut universe, space, renderer) = runtime.block_on(create_updated_renderer(instance));

let [block] = make_some_blocks();
let txn1 =
Expand All @@ -74,8 +59,7 @@ fn render_benches(runtime: &Runtime, c: &mut Criterion, adapter: &Arc<wgpu::Adap
// latency since it must wait for the image to be fetched. TODO: Figure out how to
// improve that.
g.bench_function("draw-only", |b| {
let (_universe, _space, renderer) =
runtime.block_on(create_updated_renderer(adapter.clone()));
let (_universe, _space, renderer) = runtime.block_on(create_updated_renderer(instance));

b.to_async(runtime).iter_with_large_drop(move || {
let renderer = renderer.clone();
Expand All @@ -94,7 +78,7 @@ fn render_benches(runtime: &Runtime, c: &mut Criterion, adapter: &Arc<wgpu::Adap
}

async fn create_updated_renderer(
adapter: Arc<wgpu::Adapter>,
instance: &wgpu::Instance,
) -> (
Universe,
universe::Handle<Space>,
Expand All @@ -113,6 +97,7 @@ async fn create_updated_renderer(
.insert("character".into(), Character::spawn_default(space.clone()))
.unwrap();

let adapter = init::create_adapter_for_test(instance).await;
let mut renderer = headless::Builder::from_adapter(adapter)
.await
.unwrap()
Expand All @@ -129,13 +114,18 @@ async fn create_updated_renderer(
}

/// Benchmarks for internal components
fn module_benches(runtime: &Runtime, c: &mut Criterion, adapter: &Arc<wgpu::Adapter>) {
fn module_benches(runtime: &Runtime, c: &mut Criterion, instance: &wgpu::Instance) {
let mut g = c.benchmark_group("mod");
g.sample_size(400); // increase sample size from default 100 to reduce noise
g.measurement_time(Duration::from_secs(10));

let (device, queue) = runtime
.block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None))
.block_on(async {
let adapter = init::create_adapter_for_test(instance).await;
adapter
.request_device(&wgpu::DeviceDescriptor::default(), None)
.await
})
.unwrap();

g.bench_function("light-update", |b| {
Expand Down
11 changes: 6 additions & 5 deletions all-is-cubes-gpu/src/in_wgpu/headless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use all_is_cubes::util::Executor;
use crate::common::{AdaptedInstant, FrameBudget};
use crate::in_wgpu::{self, init};

/// Builder for the headless [`Renderer`].
/// Builder for configuring a headless [`Renderer`].
///
/// The builder owns a `wgpu::Device`; all created renderers will share this device.
/// If the device is lost, a new `Builder` must be created.
#[derive(Clone, Debug)]
pub struct Builder {
executor: Arc<dyn Executor>,
Expand All @@ -25,9 +28,7 @@ pub struct Builder {
impl Builder {
/// Create a [`Builder`] by obtaining a new [`wgpu::Device`] from the given adapter.
#[cfg_attr(target_family = "wasm", allow(clippy::arc_with_non_send_sync))]
pub async fn from_adapter(
adapter: Arc<wgpu::Adapter>,
) -> Result<Self, wgpu::RequestDeviceError> {
pub async fn from_adapter(adapter: wgpu::Adapter) -> Result<Self, wgpu::RequestDeviceError> {
let (device, queue) = adapter
.request_device(
&in_wgpu::EverythingRenderer::<AdaptedInstant>::device_descriptor(adapter.limits()),
Expand All @@ -37,7 +38,7 @@ impl Builder {
Ok(Self {
device: Arc::new(device),
queue: Arc::new(queue),
adapter,
adapter: Arc::new(adapter),
executor: Arc::new(()),
})
}
Expand Down
80 changes: 56 additions & 24 deletions all-is-cubes-gpu/src/in_wgpu/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,70 @@
//! for downstream users of the libraries.
use std::future::Future;
use std::io::Write as _;
use std::sync::Arc;

use all_is_cubes::camera::{self, Rendering};
use all_is_cubes::math::area_usize;

/// Create a [`wgpu::Instance`] and [`wgpu::Adapter`] controlled by environment variables,
/// Create a [`wgpu::Instance`] controlled by environment variables.
/// Then, check if the instance has any usable adapters,
/// and exit the process if it does not.
/// Print status information to stderr.
#[doc(hidden)]
pub async fn create_instance_for_test_or_exit() -> wgpu::Instance {
let stderr = &mut std::io::stderr();
let backends = wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all);

let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends,
..Default::default()
});

// Report adapters that we *could* pick
_ = writeln!(
stderr,
"Available adapters (backend filter = {backends:?}):"
);
for adapter in instance.enumerate_adapters(wgpu::Backends::all()) {
_ = writeln!(stderr, " {}", shortened_adapter_info(&adapter.get_info()));
}

let adapter = try_create_adapter_for_test(&instance, |m| _ = writeln!(stderr, "{m}")).await;
if adapter.is_none() {
let _ = writeln!(
stderr,
"Skipping rendering tests due to lack of suitable wgpu::Adapter."
);
std::process::exit(0);
}

instance
}

/// Create a [`wgpu::Adapter`] controlled by environment variables.
///
/// Panics if creation fails.
/// This should not happen unless the `Instance` was not obtained from
/// [`create_instance_for_test_or_exit()`], or an adapter becomes unavailable
/// while the test is running.
#[doc(hidden)]
pub async fn create_adapter_for_test(instance: &wgpu::Instance) -> wgpu::Adapter {
try_create_adapter_for_test(instance, |_| {})
.await
.expect("adapter creation unexpectedly failed")
}

/// Create a [`wgpu::Adapter`] controlled by environment variables,
/// and print information about the decision made.
///
/// `log` receives whole lines with no trailing newlines, as suitable for logging or
/// printing using [`println!()`].
#[doc(hidden)]
pub async fn create_instance_and_adapter_for_test(
pub async fn try_create_adapter_for_test(
instance: &wgpu::Instance,
mut log: impl FnMut(std::fmt::Arguments<'_>),
) -> (wgpu::Instance, Option<wgpu::Adapter>) {
let requested_backends =
wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: requested_backends,
..Default::default()
});

) -> Option<wgpu::Adapter> {
// For WebGL we need a Surface.
#[cfg(target_family = "wasm")]
let surface = {
Expand All @@ -33,29 +76,18 @@ pub async fn create_instance_and_adapter_for_test(
match instance.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas)) {
Ok(surface) => Some(surface),
// If we can't make a surface then we can't make an adapter.
Err(_) => return (instance, None),
Err(_) => return None,
}
};
#[cfg(not(target_family = "wasm"))]
let surface = None;

// Report adapters that we *could* pick
log(format_args!(
"Available adapters (backend filter = {requested_backends:?}):"
));
for adapter in instance.enumerate_adapters(wgpu::Backends::all()) {
log(format_args!(
" {}",
shortened_adapter_info(&adapter.get_info())
));
}

// Pick an adapter.
// TODO: Replace this with
// wgpu::util::initialize_adapter_from_env_or_default(&instance, wgpu::Backends::all(), None)
// (which defaults to low-power) or even better, test on *all* available adapters?
let mut adapter: Option<wgpu::Adapter> =
wgpu::util::initialize_adapter_from_env(&instance, surface.as_ref());
wgpu::util::initialize_adapter_from_env(instance, surface.as_ref());
if adapter.is_none() {
log(format_args!(
"No adapter specified via WGPU_ADAPTER_NAME; picking automatically."
Expand Down Expand Up @@ -90,7 +122,7 @@ pub async fn create_instance_and_adapter_for_test(
));
}

(instance, adapter)
adapter
}

#[allow(dead_code)] // conditionally used
Expand Down
32 changes: 6 additions & 26 deletions all-is-cubes-gpu/tests/shaders/harness.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
use std::io::Write as _;
use std::sync::Arc;

use half::f16;

use all_is_cubes::camera;
Expand All @@ -12,33 +9,16 @@ use all_is_cubes_gpu::in_wgpu::shader_testing;
///
/// We don't share the [`wgpu::Device`] because it can enter failure states,
/// but we can use just one [`wgpu::Adapter`] to create all of them.
pub(crate) async fn adapter() -> Arc<wgpu::Adapter> {
static CELL: tokio::sync::OnceCell<Arc<wgpu::Adapter>> = tokio::sync::OnceCell::const_new();

CELL.get_or_init(|| async {
let (_instance, adapter) =
init::create_instance_and_adapter_for_test(|msg| eprintln!("{msg}")).await;
match adapter {
Some(adapter) => Arc::new(adapter),
None => {
// don't use eprintln! so test harness does not capture it
let _ = writeln!(
std::io::stderr(),
"Skipping rendering tests due to lack of wgpu::Adapter."
);
// Exit the process to skip redundant reports and make it clear that the
// tests aren't just passing
std::process::exit(0);
}
}
})
.await
.clone()
pub(crate) async fn instance() -> &'static wgpu::Instance {
static CELL: tokio::sync::OnceCell<wgpu::Instance> = tokio::sync::OnceCell::const_new();
CELL.get_or_init(init::create_instance_for_test_or_exit)
.await
}

/// TODO: image probably isn't the best output format, just what I prototyped with
pub(crate) async fn run_shader_test(test_wgsl: &str) -> image::Rgba32FImage {
let adapter = adapter().await;
let instance = instance().await;
let adapter = init::create_adapter_for_test(instance).await;

// 32 is the minimum viewport width that will satisfy copy alignment
let output_viewport = camera::Viewport::with_scale(1.0, [32, 32]);
Expand Down
14 changes: 6 additions & 8 deletions all-is-cubes-wasm/tests/browser/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
//! TODO: Consider expanding this out to running all of test-renderers. This will need more work.
use core::time::Duration;
use std::sync::Arc;

use wasm_bindgen_test::wasm_bindgen_test;

Expand All @@ -18,8 +17,8 @@ use all_is_cubes_wasm::AdaptedInstant as Instant;

#[wasm_bindgen_test]
async fn renderer_test() {
let (_instance, adapter) =
init::create_instance_and_adapter_for_test(|msg| eprintln!("{msg}")).await;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::default());
let adapter = init::try_create_adapter_for_test(&instance, |msg| eprintln!("{msg}")).await;

// Skip this test if no adapter available
let Some(adapter) = adapter else { return };
Expand All @@ -43,11 +42,10 @@ async fn renderer_test() {
);
let world_space = cameras.world_space().snapshot().unwrap();

let mut renderer =
all_is_cubes_gpu::in_wgpu::headless::Builder::from_adapter(Arc::new(adapter))
.await
.unwrap()
.build(cameras.clone());
let mut renderer = all_is_cubes_gpu::in_wgpu::headless::Builder::from_adapter(adapter)
.await
.unwrap()
.build(cameras.clone());
renderer.update(None).await.unwrap();
let image = renderer.draw("").await.unwrap();

Expand Down
34 changes: 11 additions & 23 deletions test-renderers/tests/wgpu-render.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
//! Runs [`test_renderers::harness_main`] against [`all_is_cubes_gpu::in_wgpu`].
use std::process::ExitCode;
use std::sync::Arc;

use clap::Parser as _;
use tokio::sync::OnceCell;

Expand All @@ -14,14 +11,9 @@ use test_renderers::{RendererFactory, RendererId};
async fn main() -> test_renderers::HarnessResult {
test_renderers::initialize_logging();

let (_instance, adapter) =
init::create_instance_and_adapter_for_test(|msg| eprintln!("{msg}")).await;
if let Some(adapter) = adapter {
WGPU_ADAPTER.set(Arc::new(adapter)).unwrap();
} else {
eprintln!("Skipping rendering tests due to lack of wgpu::Adapter.");
return ExitCode::SUCCESS;
};
WGPU_INSTANCE
.set(init::create_instance_for_test_or_exit().await)
.unwrap();

let parallelism = if option_env!("CI").is_some() && cfg!(target_os = "macos") {
// Workaround for limited available memory on macOS CI.
Expand All @@ -35,29 +27,25 @@ async fn main() -> test_renderers::HarnessResult {
RendererId::Wgpu,
test_renderers::SuiteId::Renderers,
test_renderers::test_cases::all_tests,
get_factory,
move || async move { get_factory().await.unwrap() },
parallelism,
)
.await
}

/// We don't share the [`wgpu::Device`] because it can enter failure states,
/// but we can use just one [`wgpu::Adapter`] to create all of them.
/// TODO: Should we bother not making this global, but threading it through
/// the test harness? Probably, in the form of some `impl TestRenderer`.
static WGPU_ADAPTER: OnceCell<Arc<wgpu::Adapter>> = OnceCell::const_new();
/// but we can use just one [`wgpu::Instance`] to create all of them.
static WGPU_INSTANCE: OnceCell<wgpu::Instance> = OnceCell::const_new();

async fn get_factory() -> WgpuFactory {
async fn get_factory() -> Result<WgpuFactory, Box<dyn std::error::Error + Send + Sync>> {
// Temporary workaround for <https://github.com/gfx-rs/wgpu/issues/3498>:
// Create a new adapter every time, rather than sharing one.
// TODO: Either remove this or keep it and remove WGPU_ADAPTER.
let (_instance, adapter) = init::create_instance_and_adapter_for_test(|_| {}).await;
let adapter = Arc::new(adapter.expect("failed to obtain wgpu::Adapter"));
let adapter =
init::create_adapter_for_test(WGPU_INSTANCE.get().expect("instance not initialized")).await;

let builder = headless::Builder::from_adapter(adapter)
.await
.expect("Adapter::request_device() failed");
WgpuFactory { builder }
let builder = headless::Builder::from_adapter(adapter).await?;
Ok(WgpuFactory { builder })
}

#[derive(Clone, Debug)]
Expand Down

0 comments on commit 6b67bcf

Please sign in to comment.