use std::sync::Arc; use wgpu::{Instance, Surface}; use winit::{ dpi::PhysicalSize, event::{Event, KeyEvent, StartCause, WindowEvent}, event_loop::{EventLoop, EventLoopWindowTarget}, keyboard::{Key, NamedKey}, window::Window, }; pub trait Example: 'static + Sized { const SRGB: bool = true; fn optional_features() -> wgpu::Features { wgpu::Features::empty() } fn required_features() -> wgpu::Features { wgpu::Features::empty() } fn required_downlevel_capabilities() -> wgpu::DownlevelCapabilities { wgpu::DownlevelCapabilities { flags: wgpu::DownlevelFlags::empty(), shader_model: wgpu::ShaderModel::Sm5, ..wgpu::DownlevelCapabilities::default() } } fn required_limits() -> wgpu::Limits { wgpu::Limits::downlevel_webgl2_defaults() // These downlevel limits will allow the code to run on all possible hardware } fn init( config: &wgpu::SurfaceConfiguration, adapter: &wgpu::Adapter, device: &wgpu::Device, queue: &wgpu::Queue, ) -> Self; fn resize( &mut self, config: &wgpu::SurfaceConfiguration, device: &wgpu::Device, queue: &wgpu::Queue, ); fn update(&mut self, event: WindowEvent); fn render(&mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue); } // Initialize logging in platform dependant ways. fn init_logger() { cfg_if::cfg_if! { if #[cfg(target_arch = "wasm32")] { // As we don't have an environment to pull logging level from, we use the query string. let query_string = web_sys::window().unwrap().location().search().unwrap(); let query_level: Option = parse_url_query_string(&query_string, "RUST_LOG") .and_then(|x| x.parse().ok()); // We keep wgpu at Error level, as it's very noisy. let base_level = query_level.unwrap_or(log::LevelFilter::Info); let wgpu_level = query_level.unwrap_or(log::LevelFilter::Error); // On web, we use fern, as console_log doesn't have filtering on a per-module level. fern::Dispatch::new() .level(base_level) .level_for("wgpu_core", wgpu_level) .level_for("wgpu_hal", wgpu_level) .level_for("naga", wgpu_level) .chain(fern::Output::call(console_log::log)) .apply() .unwrap(); std::panic::set_hook(Box::new(console_error_panic_hook::hook)); } else { // parse_default_env will read the RUST_LOG environment variable and apply it on top // of these default filters. env_logger::builder() .filter_level(log::LevelFilter::Info) // We keep wgpu at Error level, as it's very noisy. .filter_module("wgpu_core", log::LevelFilter::Info) .filter_module("wgpu_hal", log::LevelFilter::Error) .filter_module("naga", log::LevelFilter::Error) .parse_default_env() .init(); } } } struct EventLoopWrapper { event_loop: EventLoop<()>, window: Arc, } impl EventLoopWrapper { pub fn new(title: &str) -> Self { let event_loop = EventLoop::new().unwrap(); let mut builder = winit::window::WindowBuilder::new(); #[cfg(target_arch = "wasm32")] { use wasm_bindgen::JsCast; use winit::platform::web::WindowBuilderExtWebSys; let canvas = web_sys::window() .unwrap() .document() .unwrap() .get_element_by_id("canvas") .unwrap() .dyn_into::() .unwrap(); builder = builder.with_canvas(Some(canvas)); } builder = builder.with_title(title); let window = Arc::new(builder.build(&event_loop).unwrap()); Self { event_loop, window } } } /// Wrapper type which manages the surface and surface configuration. /// /// As surface usage varies per platform, wrapping this up cleans up the event loop code. struct SurfaceWrapper { surface: Option>, config: Option, } impl SurfaceWrapper { /// Create a new surface wrapper with no surface or configuration. fn new() -> Self { Self { surface: None, config: None, } } /// Called after the instance is created, but before we request an adapter. /// /// On wasm, we need to create the surface here, as the WebGL backend needs /// a surface (and hence a canvas) to be present to create the adapter. /// /// We cannot unconditionally create a surface here, as Android requires /// us to wait until we receive the `Resumed` event to do so. fn pre_adapter(&mut self, instance: &Instance, window: Arc) { if cfg!(target_arch = "wasm32") { self.surface = Some(instance.create_surface(window).unwrap()); } } /// Check if the event is the start condition for the surface. fn start_condition(e: &Event<()>) -> bool { match e { // On all other platforms, we can create the surface immediately. Event::NewEvents(StartCause::Init) => !cfg!(target_os = "android"), // On android we need to wait for a resumed event to create the surface. Event::Resumed => cfg!(target_os = "android"), _ => false, } } /// Called when an event which matches [`Self::start_condition`] is received. /// /// On all native platforms, this is where we create the surface. /// /// Additionally, we configure the surface based on the (now valid) window size. fn resume(&mut self, context: &ExampleContext, window: Arc, srgb: bool) { // Window size is only actually valid after we enter the event loop. let window_size = window.inner_size(); let width = window_size.width.max(1); let height = window_size.height.max(1); log::info!("Surface resume {window_size:?}"); // We didn't create the surface in pre_adapter, so we need to do so now. if !cfg!(target_arch = "wasm32") { self.surface = Some(context.instance.create_surface(window).unwrap()); } // From here on, self.surface should be Some. let surface = self.surface.as_ref().unwrap(); // Get the default configuration, let mut config = surface .get_default_config(&context.adapter, width, height) .expect("Surface isn't supported by the adapter."); if srgb { // Not all platforms (WebGPU) support sRGB swapchains, so we need to use view formats let view_format = config.format.add_srgb_suffix(); config.view_formats.push(view_format); } else { // All platforms support non-sRGB swapchains, so we can just use the format directly. let format = config.format.remove_srgb_suffix(); config.format = format; config.view_formats.push(format); }; surface.configure(&context.device, &config); self.config = Some(config); } /// Resize the surface, making sure to not resize to zero. fn resize(&mut self, context: &ExampleContext, size: PhysicalSize) { log::info!("Surface resize {size:?}"); let config = self.config.as_mut().unwrap(); config.width = size.width.max(1); config.height = size.height.max(1); let surface = self.surface.as_ref().unwrap(); surface.configure(&context.device, config); } /// Acquire the next surface texture. fn acquire(&mut self, context: &ExampleContext) -> wgpu::SurfaceTexture { let surface = self.surface.as_ref().unwrap(); match surface.get_current_texture() { Ok(frame) => frame, // If we timed out, just try again Err(wgpu::SurfaceError::Timeout) => surface .get_current_texture() .expect("Failed to acquire next surface texture!"), Err( // If the surface is outdated, or was lost, reconfigure it. wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost // If OutOfMemory happens, reconfiguring may not help, but we might as well try | wgpu::SurfaceError::OutOfMemory, ) => { surface.configure(&context.device, self.config()); surface .get_current_texture() .expect("Failed to acquire next surface texture!") } } } /// On suspend on android, we drop the surface, as it's no longer valid. /// /// A suspend event is always followed by at least one resume event. fn suspend(&mut self) { if cfg!(target_os = "android") { self.surface = None; } } fn get(&self) -> Option<&Surface> { self.surface.as_ref() } fn config(&self) -> &wgpu::SurfaceConfiguration { self.config.as_ref().unwrap() } } /// Context containing global wgpu resources. struct ExampleContext { instance: wgpu::Instance, adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, } impl ExampleContext { /// Initializes the example context. async fn init_async(surface: &mut SurfaceWrapper, window: Arc) -> Self { log::info!("Initializing wgpu..."); let backends = wgpu::util::backend_bits_from_env().unwrap_or_default(); let dx12_shader_compiler = wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends, flags: wgpu::InstanceFlags::from_build_config().with_env(), dx12_shader_compiler, gles_minor_version, }); surface.pre_adapter(&instance, window); let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, surface.get()) .await .expect("No suitable GPU adapters found on the system!"); let adapter_info = adapter.get_info(); log::info!("Using {} ({:?})", adapter_info.name, adapter_info.backend); let optional_features = E::optional_features(); let required_features = E::required_features(); let adapter_features = adapter.features(); assert!( adapter_features.contains(required_features), "Adapter does not support required features for this example: {:?}", required_features - adapter_features ); let required_downlevel_capabilities = E::required_downlevel_capabilities(); let downlevel_capabilities = adapter.get_downlevel_capabilities(); assert!( downlevel_capabilities.shader_model >= required_downlevel_capabilities.shader_model, "Adapter does not support the minimum shader model required to run this example: {:?}", required_downlevel_capabilities.shader_model ); assert!( downlevel_capabilities .flags .contains(required_downlevel_capabilities.flags), "Adapter does not support the downlevel capabilities required to run this example: {:?}", required_downlevel_capabilities.flags - downlevel_capabilities.flags ); // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface. let needed_limits = E::required_limits().using_resolution(adapter.limits()); let trace_dir = std::env::var("WGPU_TRACE"); let (device, queue) = adapter .request_device( &wgpu::DeviceDescriptor { label: None, required_features: (optional_features & adapter_features) | required_features, required_limits: needed_limits, memory_hints: wgpu::MemoryHints::MemoryUsage, }, trace_dir.ok().as_ref().map(std::path::Path::new), ) .await .expect("Unable to find a suitable GPU adapter!"); Self { instance, adapter, device, queue, } } } struct FrameCounter { // Instant of the last time we printed the frame time. last_printed_instant: web_time::Instant, // Number of frames since the last time we printed the frame time. frame_count: u32, } impl FrameCounter { fn new() -> Self { Self { last_printed_instant: web_time::Instant::now(), frame_count: 0, } } fn update(&mut self) { self.frame_count += 1; let new_instant = web_time::Instant::now(); let elapsed_secs = (new_instant - self.last_printed_instant).as_secs_f32(); if elapsed_secs > 1.0 { let elapsed_ms = elapsed_secs * 1000.0; let frame_time = elapsed_ms / self.frame_count as f32; let fps = self.frame_count as f32 / elapsed_secs; log::info!("Frame time {:.2}ms ({:.1} FPS)", frame_time, fps); self.last_printed_instant = new_instant; self.frame_count = 0; } } } async fn start(title: &str) { init_logger(); log::debug!( "Enabled backends: {:?}", wgpu::Instance::enabled_backend_features() ); let window_loop = EventLoopWrapper::new(title); let mut surface = SurfaceWrapper::new(); let context = ExampleContext::init_async::(&mut surface, window_loop.window.clone()).await; let mut frame_counter = FrameCounter::new(); // We wait to create the example until we have a valid surface. let mut example = None; cfg_if::cfg_if! { if #[cfg(target_arch = "wasm32")] { use winit::platform::web::EventLoopExtWebSys; let event_loop_function = EventLoop::spawn; } else { let event_loop_function = EventLoop::run; } } log::info!("Entering event loop..."); // On native this is a result, but on wasm it's a unit type. #[allow(clippy::let_unit_value)] let _ = (event_loop_function)( window_loop.event_loop, move |event: Event<()>, target: &EventLoopWindowTarget<()>| { match event { ref e if SurfaceWrapper::start_condition(e) => { surface.resume(&context, window_loop.window.clone(), E::SRGB); // If we haven't created the example yet, do so now. if example.is_none() { example = Some(E::init( surface.config(), &context.adapter, &context.device, &context.queue, )); } } Event::Suspended => { surface.suspend(); } Event::WindowEvent { event, .. } => match event { WindowEvent::Resized(size) => { surface.resize(&context, size); example.as_mut().unwrap().resize( surface.config(), &context.device, &context.queue, ); window_loop.window.request_redraw(); } WindowEvent::KeyboardInput { event: KeyEvent { logical_key: Key::Named(NamedKey::Escape), .. }, .. } | WindowEvent::CloseRequested => { target.exit(); } #[cfg(not(target_arch = "wasm32"))] WindowEvent::KeyboardInput { event: KeyEvent { logical_key: Key::Character(s), .. }, .. } if s == "r" => { println!("{:#?}", context.instance.generate_report()); } WindowEvent::RedrawRequested => { // On MacOS, currently redraw requested comes in _before_ Init does. // If this happens, just drop the requested redraw on the floor. // // See https://github.com/rust-windowing/winit/issues/3235 for some discussion if example.is_none() { return; } frame_counter.update(); let frame = surface.acquire(&context); let view = frame.texture.create_view(&wgpu::TextureViewDescriptor { format: Some(surface.config().view_formats[0]), ..wgpu::TextureViewDescriptor::default() }); example .as_mut() .unwrap() .render(&view, &context.device, &context.queue); frame.present(); window_loop.window.request_redraw(); } _ => example.as_mut().unwrap().update(event), }, _ => {} } }, ); } pub fn run(title: &'static str) { cfg_if::cfg_if! { if #[cfg(target_arch = "wasm32")] { wasm_bindgen_futures::spawn_local(async move { start::(title).await }) } else { pollster::block_on(start::(title)); } } } #[cfg(target_arch = "wasm32")] /// Parse the query string as returned by `web_sys::window()?.location().search()?` and get a /// specific key out of it. pub fn parse_url_query_string<'a>(query: &'a str, search_key: &str) -> Option<&'a str> { let query_string = query.strip_prefix('?')?; for pair in query_string.split('&') { let mut pair = pair.split('='); let key = pair.next()?; let value = pair.next()?; if key == search_key { return Some(value); } } None } #[cfg(test)] pub use wgpu_test::image::ComparisonType; #[cfg(test)] #[derive(Clone)] pub struct ExampleTestParams { pub name: &'static str, // Path to the reference image, relative to the root of the repo. pub image_path: &'static str, pub width: u32, pub height: u32, pub optional_features: wgpu::Features, pub base_test_parameters: wgpu_test::TestParameters, /// Comparisons against FLIP statistics that determine if the test passes or fails. pub comparisons: &'static [ComparisonType], pub _phantom: std::marker::PhantomData, } #[cfg(test)] impl From> for wgpu_test::GpuTestConfiguration { fn from(params: ExampleTestParams) -> Self { wgpu_test::GpuTestConfiguration::new() .name(params.name) .parameters({ assert_eq!(params.width % 64, 0, "width needs to be aligned 64"); let features = E::required_features() | params.optional_features; params.base_test_parameters.clone().features(features) }) .run_async(move |ctx| async move { let format = if E::SRGB { wgpu::TextureFormat::Rgba8UnormSrgb } else { wgpu::TextureFormat::Rgba8Unorm }; let dst_texture = ctx.device.create_texture(&wgpu::TextureDescriptor { label: Some("destination"), size: wgpu::Extent3d { width: params.width, height: params.height, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); let dst_view = dst_texture.create_view(&wgpu::TextureViewDescriptor::default()); let dst_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { label: Some("image map buffer"), size: params.width as u64 * params.height as u64 * 4, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); let mut example = E::init( &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format, width: params.width, height: params.height, desired_maximum_frame_latency: 2, present_mode: wgpu::PresentMode::Fifo, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![format], }, &ctx.adapter, &ctx.device, &ctx.queue, ); example.render(&dst_view, &ctx.device, &ctx.queue); let mut cmd_buf = ctx .device .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); cmd_buf.copy_texture_to_buffer( wgpu::ImageCopyTexture { texture: &dst_texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::ImageCopyBuffer { buffer: &dst_buffer, layout: wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(params.width * 4), rows_per_image: None, }, }, wgpu::Extent3d { width: params.width, height: params.height, depth_or_array_layers: 1, }, ); ctx.queue.submit(Some(cmd_buf.finish())); let dst_buffer_slice = dst_buffer.slice(..); dst_buffer_slice.map_async(wgpu::MapMode::Read, |_| ()); ctx.async_poll(wgpu::Maintain::wait()) .await .panic_on_timeout(); let bytes = dst_buffer_slice.get_mapped_range().to_vec(); wgpu_test::image::compare_image_output( dbg!(env!("CARGO_MANIFEST_DIR").to_string() + "/../" + params.image_path), &ctx.adapter_info, params.width, params.height, &bytes, params.comparisons, ) .await; }) } }