1use std::sync::Arc;
2
3use wgpu::{Instance, Surface};
4use winit::{
5 dpi::PhysicalSize,
6 event::{Event, KeyEvent, StartCause, WindowEvent},
7 event_loop::{EventLoop, EventLoopWindowTarget},
8 keyboard::{Key, NamedKey},
9 window::Window,
10};
11
12pub trait Example: 'static + Sized {
13 const SRGB: bool = true;
14
15 fn optional_features() -> wgpu::Features {
16 wgpu::Features::empty()
17 }
18
19 fn required_features() -> wgpu::Features {
20 wgpu::Features::empty()
21 }
22
23 fn required_downlevel_capabilities() -> wgpu::DownlevelCapabilities {
24 wgpu::DownlevelCapabilities {
25 flags: wgpu::DownlevelFlags::empty(),
26 shader_model: wgpu::ShaderModel::Sm5,
27 ..wgpu::DownlevelCapabilities::default()
28 }
29 }
30
31 fn required_limits() -> wgpu::Limits {
32 wgpu::Limits::downlevel_webgl2_defaults() }
34
35 fn init(
36 config: &wgpu::SurfaceConfiguration,
37 adapter: &wgpu::Adapter,
38 device: &wgpu::Device,
39 queue: &wgpu::Queue,
40 ) -> Self;
41
42 fn resize(
43 &mut self,
44 config: &wgpu::SurfaceConfiguration,
45 device: &wgpu::Device,
46 queue: &wgpu::Queue,
47 );
48
49 fn update(&mut self, event: WindowEvent);
50
51 fn render(&mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue);
52}
53
54fn init_logger() {
56 cfg_if::cfg_if! {
57 if #[cfg(target_arch = "wasm32")] {
58 let query_string = web_sys::window().unwrap().location().search().unwrap();
60 let query_level: Option<log::LevelFilter> = parse_url_query_string(&query_string, "RUST_LOG")
61 .and_then(|x| x.parse().ok());
62
63 let base_level = query_level.unwrap_or(log::LevelFilter::Info);
65 let wgpu_level = query_level.unwrap_or(log::LevelFilter::Error);
66
67 fern::Dispatch::new()
69 .level(base_level)
70 .level_for("wgpu_core", wgpu_level)
71 .level_for("wgpu_hal", wgpu_level)
72 .level_for("naga", wgpu_level)
73 .chain(fern::Output::call(console_log::log))
74 .apply()
75 .unwrap();
76 std::panic::set_hook(Box::new(console_error_panic_hook::hook));
77 } else {
78 env_logger::builder()
81 .filter_level(log::LevelFilter::Info)
82 .filter_module("wgpu_core", log::LevelFilter::Info)
84 .filter_module("wgpu_hal", log::LevelFilter::Error)
85 .filter_module("naga", log::LevelFilter::Error)
86 .parse_default_env()
87 .init();
88 }
89 }
90}
91
92struct EventLoopWrapper {
93 event_loop: EventLoop<()>,
94 window: Arc<Window>,
95}
96
97impl EventLoopWrapper {
98 pub fn new(title: &str) -> Self {
99 let event_loop = EventLoop::new().unwrap();
100 let mut builder = winit::window::WindowBuilder::new();
101 #[cfg(target_arch = "wasm32")]
102 {
103 use wasm_bindgen::JsCast;
104 use winit::platform::web::WindowBuilderExtWebSys;
105 let canvas = web_sys::window()
106 .unwrap()
107 .document()
108 .unwrap()
109 .get_element_by_id("canvas")
110 .unwrap()
111 .dyn_into::<web_sys::HtmlCanvasElement>()
112 .unwrap();
113 builder = builder.with_canvas(Some(canvas));
114 }
115 builder = builder.with_title(title);
116 let window = Arc::new(builder.build(&event_loop).unwrap());
117
118 Self { event_loop, window }
119 }
120}
121
122struct SurfaceWrapper {
126 surface: Option<wgpu::Surface<'static>>,
127 config: Option<wgpu::SurfaceConfiguration>,
128}
129
130impl SurfaceWrapper {
131 fn new() -> Self {
133 Self {
134 surface: None,
135 config: None,
136 }
137 }
138
139 fn pre_adapter(&mut self, instance: &Instance, window: Arc<Window>) {
147 if cfg!(target_arch = "wasm32") {
148 self.surface = Some(instance.create_surface(window).unwrap());
149 }
150 }
151
152 fn start_condition(e: &Event<()>) -> bool {
154 match e {
155 Event::NewEvents(StartCause::Init) => !cfg!(target_os = "android"),
157 Event::Resumed => cfg!(target_os = "android"),
159 _ => false,
160 }
161 }
162
163 fn resume(&mut self, context: &ExampleContext, window: Arc<Window>, srgb: bool) {
169 let window_size = window.inner_size();
171 let width = window_size.width.max(1);
172 let height = window_size.height.max(1);
173
174 log::info!("Surface resume {window_size:?}");
175
176 if !cfg!(target_arch = "wasm32") {
178 self.surface = Some(context.instance.create_surface(window).unwrap());
179 }
180
181 let surface = self.surface.as_ref().unwrap();
184
185 let mut config = surface
187 .get_default_config(&context.adapter, width, height)
188 .expect("Surface isn't supported by the adapter.");
189 if srgb {
190 let view_format = config.format.add_srgb_suffix();
192 config.view_formats.push(view_format);
193 } else {
194 let format = config.format.remove_srgb_suffix();
196 config.format = format;
197 config.view_formats.push(format);
198 };
199
200 surface.configure(&context.device, &config);
201 self.config = Some(config);
202 }
203
204 fn resize(&mut self, context: &ExampleContext, size: PhysicalSize<u32>) {
206 log::info!("Surface resize {size:?}");
207
208 let config = self.config.as_mut().unwrap();
209 config.width = size.width.max(1);
210 config.height = size.height.max(1);
211 let surface = self.surface.as_ref().unwrap();
212 surface.configure(&context.device, config);
213 }
214
215 fn acquire(&mut self, context: &ExampleContext) -> wgpu::SurfaceTexture {
217 let surface = self.surface.as_ref().unwrap();
218
219 match surface.get_current_texture() {
220 Ok(frame) => frame,
221 Err(wgpu::SurfaceError::Timeout) => surface
223 .get_current_texture()
224 .expect("Failed to acquire next surface texture!"),
225 Err(
226 wgpu::SurfaceError::Outdated
228 | wgpu::SurfaceError::Lost
229 | wgpu::SurfaceError::Other
230 | wgpu::SurfaceError::OutOfMemory,
232 ) => {
233 surface.configure(&context.device, self.config());
234 surface
235 .get_current_texture()
236 .expect("Failed to acquire next surface texture!")
237 }
238 }
239 }
240
241 fn suspend(&mut self) {
245 if cfg!(target_os = "android") {
246 self.surface = None;
247 }
248 }
249
250 fn get(&self) -> Option<&'_ Surface<'static>> {
251 self.surface.as_ref()
252 }
253
254 fn config(&self) -> &wgpu::SurfaceConfiguration {
255 self.config.as_ref().unwrap()
256 }
257}
258
259struct ExampleContext {
261 instance: wgpu::Instance,
262 adapter: wgpu::Adapter,
263 device: wgpu::Device,
264 queue: wgpu::Queue,
265}
266impl ExampleContext {
267 async fn init_async<E: Example>(surface: &mut SurfaceWrapper, window: Arc<Window>) -> Self {
269 log::info!("Initializing wgpu...");
270
271 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::from_env_or_default());
272 surface.pre_adapter(&instance, window);
273
274 let adapter = get_adapter_with_capabilities_or_from_env(
275 &instance,
276 &E::required_features(),
277 &E::required_downlevel_capabilities(),
278 &surface.get(),
279 )
280 .await;
281 let needed_limits = E::required_limits().using_resolution(adapter.limits());
283
284 let (device, queue) = adapter
285 .request_device(&wgpu::DeviceDescriptor {
286 label: None,
287 required_features: (E::optional_features() & adapter.features())
288 | E::required_features(),
289 required_limits: needed_limits,
290 experimental_features: unsafe { wgpu::ExperimentalFeatures::enabled() },
291 memory_hints: wgpu::MemoryHints::MemoryUsage,
292 trace: match std::env::var_os("WGPU_TRACE") {
293 Some(path) => wgpu::Trace::Directory(path.into()),
294 None => wgpu::Trace::Off,
295 },
296 })
297 .await
298 .expect("Unable to find a suitable GPU adapter!");
299
300 Self {
301 instance,
302 adapter,
303 device,
304 queue,
305 }
306 }
307}
308
309struct FrameCounter {
310 last_printed_instant: web_time::Instant,
312 frame_count: u32,
314}
315
316impl FrameCounter {
317 fn new() -> Self {
318 Self {
319 last_printed_instant: web_time::Instant::now(),
320 frame_count: 0,
321 }
322 }
323
324 fn update(&mut self) {
325 self.frame_count += 1;
326 let new_instant = web_time::Instant::now();
327 let elapsed_secs = (new_instant - self.last_printed_instant).as_secs_f32();
328 if elapsed_secs > 1.0 {
329 let elapsed_ms = elapsed_secs * 1000.0;
330 let frame_time = elapsed_ms / self.frame_count as f32;
331 let fps = self.frame_count as f32 / elapsed_secs;
332 log::info!("Frame time {frame_time:.2}ms ({fps:.1} FPS)");
333
334 self.last_printed_instant = new_instant;
335 self.frame_count = 0;
336 }
337 }
338}
339
340async fn start<E: Example>(title: &str) {
341 init_logger();
342
343 log::debug!(
344 "Enabled backends: {:?}",
345 wgpu::Instance::enabled_backend_features()
346 );
347
348 let window_loop = EventLoopWrapper::new(title);
349 let mut surface = SurfaceWrapper::new();
350 let context = ExampleContext::init_async::<E>(&mut surface, window_loop.window.clone()).await;
351 let mut frame_counter = FrameCounter::new();
352
353 let mut example = None;
355
356 cfg_if::cfg_if! {
357 if #[cfg(target_arch = "wasm32")] {
358 use winit::platform::web::EventLoopExtWebSys;
359 let event_loop_function = EventLoop::spawn;
360 } else {
361 let event_loop_function = EventLoop::run;
362 }
363 }
364
365 log::info!("Entering event loop...");
366 #[cfg_attr(target_arch = "wasm32", expect(clippy::let_unit_value))]
367 let _ = (event_loop_function)(
368 window_loop.event_loop,
369 move |event: Event<()>, target: &EventLoopWindowTarget<()>| {
370 match event {
371 ref e if SurfaceWrapper::start_condition(e) => {
372 surface.resume(&context, window_loop.window.clone(), E::SRGB);
373
374 if example.is_none() {
376 example = Some(E::init(
377 surface.config(),
378 &context.adapter,
379 &context.device,
380 &context.queue,
381 ));
382 }
383 }
384 Event::Suspended => {
385 surface.suspend();
386 }
387 Event::WindowEvent { event, .. } => match event {
388 WindowEvent::Resized(size) => {
389 surface.resize(&context, size);
390 example.as_mut().unwrap().resize(
391 surface.config(),
392 &context.device,
393 &context.queue,
394 );
395
396 window_loop.window.request_redraw();
397 }
398 WindowEvent::KeyboardInput {
399 event:
400 KeyEvent {
401 logical_key: Key::Named(NamedKey::Escape),
402 ..
403 },
404 ..
405 }
406 | WindowEvent::CloseRequested => {
407 target.exit();
408 }
409 #[cfg(not(target_arch = "wasm32"))]
410 WindowEvent::KeyboardInput {
411 event:
412 KeyEvent {
413 logical_key: Key::Character(s),
414 ..
415 },
416 ..
417 } if s == "r" => {
418 println!("{:#?}", context.instance.generate_report());
419 }
420 WindowEvent::RedrawRequested => {
421 if example.is_none() {
426 return;
427 }
428
429 frame_counter.update();
430
431 let frame = surface.acquire(&context);
432 let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
433 format: Some(surface.config().view_formats[0]),
434 ..wgpu::TextureViewDescriptor::default()
435 });
436
437 example
438 .as_mut()
439 .unwrap()
440 .render(&view, &context.device, &context.queue);
441
442 window_loop.window.pre_present_notify();
443 frame.present();
444
445 window_loop.window.request_redraw();
446 }
447 _ => example.as_mut().unwrap().update(event),
448 },
449 _ => {}
450 }
451 },
452 );
453}
454
455pub fn run<E: Example>(title: &'static str) {
456 cfg_if::cfg_if! {
457 if #[cfg(target_arch = "wasm32")] {
458 wasm_bindgen_futures::spawn_local(async move { start::<E>(title).await })
459 } else {
460 pollster::block_on(start::<E>(title));
461 }
462 }
463}
464
465#[cfg(target_arch = "wasm32")]
466pub fn parse_url_query_string<'a>(query: &'a str, search_key: &str) -> Option<&'a str> {
469 let query_string = query.strip_prefix('?')?;
470
471 for pair in query_string.split('&') {
472 let mut pair = pair.split('=');
473 let key = pair.next()?;
474 let value = pair.next()?;
475
476 if key == search_key {
477 return Some(value);
478 }
479 }
480
481 None
482}
483
484#[cfg(test)]
485pub use wgpu_test::image::ComparisonType;
486
487use crate::utils::get_adapter_with_capabilities_or_from_env;
488
489#[cfg(test)]
490#[derive(Clone)]
491pub struct ExampleTestParams<E> {
492 pub name: &'static str,
493 pub image_path: &'static str,
495 pub width: u32,
496 pub height: u32,
497 pub optional_features: wgpu::Features,
498 pub base_test_parameters: wgpu_test::TestParameters,
499 pub comparisons: &'static [ComparisonType],
501 pub _phantom: std::marker::PhantomData<E>,
502}
503
504#[cfg(test)]
505impl<E: Example + wgpu::WasmNotSendSync> From<ExampleTestParams<E>>
506 for wgpu_test::GpuTestConfiguration
507{
508 fn from(params: ExampleTestParams<E>) -> Self {
509 wgpu_test::GpuTestConfiguration::new()
510 .name(params.name)
511 .parameters({
512 assert_eq!(params.width % 64, 0, "width needs to be aligned 64");
513
514 let features = E::required_features() | params.optional_features;
515
516 params
517 .base_test_parameters
518 .clone()
519 .features(features)
520 .limits(E::required_limits())
521 })
522 .run_async(move |ctx| async move {
523 let format = if E::SRGB {
524 wgpu::TextureFormat::Rgba8UnormSrgb
525 } else {
526 wgpu::TextureFormat::Rgba8Unorm
527 };
528 let dst_texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
529 label: Some("destination"),
530 size: wgpu::Extent3d {
531 width: params.width,
532 height: params.height,
533 depth_or_array_layers: 1,
534 },
535 mip_level_count: 1,
536 sample_count: 1,
537 dimension: wgpu::TextureDimension::D2,
538 format,
539 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
540 view_formats: &[],
541 });
542
543 let dst_view = dst_texture.create_view(&wgpu::TextureViewDescriptor::default());
544
545 let dst_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
546 label: Some("image map buffer"),
547 size: params.width as u64 * params.height as u64 * 4,
548 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
549 mapped_at_creation: false,
550 });
551
552 let mut example = E::init(
553 &wgpu::SurfaceConfiguration {
554 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
555 format,
556 width: params.width,
557 height: params.height,
558 desired_maximum_frame_latency: 2,
559 present_mode: wgpu::PresentMode::Fifo,
560 alpha_mode: wgpu::CompositeAlphaMode::Auto,
561 view_formats: vec![format],
562 },
563 &ctx.adapter,
564 &ctx.device,
565 &ctx.queue,
566 );
567
568 example.render(&dst_view, &ctx.device, &ctx.queue);
569
570 let mut cmd_buf = ctx
571 .device
572 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
573
574 cmd_buf.copy_texture_to_buffer(
575 wgpu::TexelCopyTextureInfo {
576 texture: &dst_texture,
577 mip_level: 0,
578 origin: wgpu::Origin3d::ZERO,
579 aspect: wgpu::TextureAspect::All,
580 },
581 wgpu::TexelCopyBufferInfo {
582 buffer: &dst_buffer,
583 layout: wgpu::TexelCopyBufferLayout {
584 offset: 0,
585 bytes_per_row: Some(params.width * 4),
586 rows_per_image: None,
587 },
588 },
589 wgpu::Extent3d {
590 width: params.width,
591 height: params.height,
592 depth_or_array_layers: 1,
593 },
594 );
595
596 ctx.queue.submit(Some(cmd_buf.finish()));
597
598 let dst_buffer_slice = dst_buffer.slice(..);
599 dst_buffer_slice.map_async(wgpu::MapMode::Read, |_| ());
600 ctx.async_poll(wgpu::PollType::wait()).await.unwrap();
601 let bytes = dst_buffer_slice.get_mapped_range().to_vec();
602
603 wgpu_test::image::compare_image_output(
604 dbg!(env!("CARGO_MANIFEST_DIR").to_string() + "/../../" + params.image_path),
605 &ctx.adapter_info,
606 params.width,
607 params.height,
608 &bytes,
609 params.comparisons,
610 )
611 .await;
612 })
613 }
614}