diff --git a/all-is-cubes-gpu/benches/wgpu.rs b/all-is-cubes-gpu/benches/wgpu.rs index 296a9b690..5302ee62e 100644 --- a/all-is-cubes-gpu/benches/wgpu.rs +++ b/all-is-cubes-gpu/benches/wgpu.rs @@ -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) { +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 = @@ -74,8 +59,7 @@ fn render_benches(runtime: &Runtime, c: &mut Criterion, adapter: &Arc, + instance: &wgpu::Instance, ) -> ( Universe, universe::Handle, @@ -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() @@ -129,13 +114,18 @@ async fn create_updated_renderer( } /// Benchmarks for internal components -fn module_benches(runtime: &Runtime, c: &mut Criterion, adapter: &Arc) { +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| { diff --git a/all-is-cubes-gpu/src/in_wgpu/headless.rs b/all-is-cubes-gpu/src/in_wgpu/headless.rs index f3db33465..f598323f1 100644 --- a/all-is-cubes-gpu/src/in_wgpu/headless.rs +++ b/all-is-cubes-gpu/src/in_wgpu/headless.rs @@ -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, @@ -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, - ) -> Result { + pub async fn from_adapter(adapter: wgpu::Adapter) -> Result { let (device, queue) = adapter .request_device( &in_wgpu::EverythingRenderer::::device_descriptor(adapter.limits()), @@ -37,7 +38,7 @@ impl Builder { Ok(Self { device: Arc::new(device), queue: Arc::new(queue), - adapter, + adapter: Arc::new(adapter), executor: Arc::new(()), }) } diff --git a/all-is-cubes-gpu/src/in_wgpu/init.rs b/all-is-cubes-gpu/src/in_wgpu/init.rs index 2ddf37133..04fce56d5 100644 --- a/all-is-cubes-gpu/src/in_wgpu/init.rs +++ b/all-is-cubes-gpu/src/in_wgpu/init.rs @@ -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) { - 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 { // For WebGL we need a Surface. #[cfg(target_family = "wasm")] let surface = { @@ -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::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." @@ -90,7 +122,7 @@ pub async fn create_instance_and_adapter_for_test( )); } - (instance, adapter) + adapter } #[allow(dead_code)] // conditionally used diff --git a/all-is-cubes-gpu/tests/shaders/harness.rs b/all-is-cubes-gpu/tests/shaders/harness.rs index f0da463f9..1c42d91b4 100644 --- a/all-is-cubes-gpu/tests/shaders/harness.rs +++ b/all-is-cubes-gpu/tests/shaders/harness.rs @@ -1,6 +1,3 @@ -use std::io::Write as _; -use std::sync::Arc; - use half::f16; use all_is_cubes::camera; @@ -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 { - static CELL: tokio::sync::OnceCell> = 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 = 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]); diff --git a/all-is-cubes-wasm/tests/browser/render.rs b/all-is-cubes-wasm/tests/browser/render.rs index 0cc476389..67fcdc0f4 100644 --- a/all-is-cubes-wasm/tests/browser/render.rs +++ b/all-is-cubes-wasm/tests/browser/render.rs @@ -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; @@ -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 }; @@ -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(); diff --git a/test-renderers/tests/wgpu-render.rs b/test-renderers/tests/wgpu-render.rs index 24afe4aab..e002fda4e 100644 --- a/test-renderers/tests/wgpu-render.rs +++ b/test-renderers/tests/wgpu-render.rs @@ -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; @@ -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. @@ -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> = OnceCell::const_new(); +/// but we can use just one [`wgpu::Instance`] to create all of them. +static WGPU_INSTANCE: OnceCell = OnceCell::const_new(); -async fn get_factory() -> WgpuFactory { +async fn get_factory() -> Result> { // Temporary workaround for : // 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)]