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 config.desired_maximum_frame_latency = 3;
200
201 surface.configure(&context.device, &config);
202 self.config = Some(config);
203 }
204
205 fn resize(&mut self, context: &ExampleContext, size: PhysicalSize<u32>) {
207 log::info!("Surface resize {size:?}");
208
209 let config = self.config.as_mut().unwrap();
210 config.width = size.width.max(1);
211 config.height = size.height.max(1);
212 let surface = self.surface.as_ref().unwrap();
213 surface.configure(&context.device, config);
214 }
215
216 fn acquire(&mut self, context: &ExampleContext) -> wgpu::SurfaceTexture {
218 let surface = self.surface.as_ref().unwrap();
219
220 match surface.get_current_texture() {
221 Ok(frame) => frame,
222 Err(wgpu::SurfaceError::Timeout) => surface
224 .get_current_texture()
225 .expect("Failed to acquire next surface texture!"),
226 Err(
227 wgpu::SurfaceError::Outdated
229 | wgpu::SurfaceError::Lost
230 | wgpu::SurfaceError::Other
231 | wgpu::SurfaceError::OutOfMemory,
233 ) => {
234 surface.configure(&context.device, self.config());
235 surface
236 .get_current_texture()
237 .expect("Failed to acquire next surface texture!")
238 }
239 }
240 }
241
242 fn suspend(&mut self) {
246 if cfg!(target_os = "android") {
247 self.surface = None;
248 }
249 }
250
251 fn get(&self) -> Option<&'_ Surface<'static>> {
252 self.surface.as_ref()
253 }
254
255 fn config(&self) -> &wgpu::SurfaceConfiguration {
256 self.config.as_ref().unwrap()
257 }
258}
259
260struct ExampleContext {
262 instance: wgpu::Instance,
263 adapter: wgpu::Adapter,
264 device: wgpu::Device,
265 queue: wgpu::Queue,
266}
267impl ExampleContext {
268 async fn init_async<E: Example>(surface: &mut SurfaceWrapper, window: Arc<Window>) -> Self {
270 log::info!("Initializing wgpu...");
271
272 let instance_descriptor = wgpu::InstanceDescriptor::from_env_or_default();
273 let instance = wgpu::Instance::new(&instance_descriptor);
274 surface.pre_adapter(&instance, window);
275 let adapter = get_adapter_with_capabilities_or_from_env(
276 &instance,
277 &E::required_features(),
278 &E::required_downlevel_capabilities(),
279 &surface.get(),
280 )
281 .await;
282 let needed_limits = E::required_limits().using_resolution(adapter.limits());
284
285 let info = adapter.get_info();
286 log::info!("Selected adapter: {} ({:?})", info.name, info.backend);
287
288 let (device, queue) = adapter
289 .request_device(&wgpu::DeviceDescriptor {
290 label: None,
291 required_features: (E::optional_features() & adapter.features())
292 | E::required_features(),
293 required_limits: needed_limits,
294 experimental_features: unsafe { wgpu::ExperimentalFeatures::enabled() },
295 memory_hints: wgpu::MemoryHints::MemoryUsage,
296 trace: match std::env::var_os("WGPU_TRACE") {
297 Some(path) => wgpu::Trace::Directory(path.into()),
298 None => wgpu::Trace::Off,
299 },
300 })
301 .await
302 .expect("Unable to find a suitable GPU adapter!");
303
304 Self {
305 instance,
306 adapter,
307 device,
308 queue,
309 }
310 }
311}
312
313struct FrameCounter {
314 last_printed_instant: web_time::Instant,
316 frame_count: u32,
318}
319
320impl FrameCounter {
321 fn new() -> Self {
322 Self {
323 last_printed_instant: web_time::Instant::now(),
324 frame_count: 0,
325 }
326 }
327
328 fn update(&mut self) {
329 self.frame_count += 1;
330 let new_instant = web_time::Instant::now();
331 let elapsed_secs = (new_instant - self.last_printed_instant).as_secs_f32();
332 if elapsed_secs > 1.0 {
333 let elapsed_ms = elapsed_secs * 1000.0;
334 let frame_time = elapsed_ms / self.frame_count as f32;
335 let fps = self.frame_count as f32 / elapsed_secs;
336 log::info!("Frame time {frame_time:.2}ms ({fps:.1} FPS)");
337
338 self.last_printed_instant = new_instant;
339 self.frame_count = 0;
340 }
341 }
342}
343
344async fn start<E: Example>(title: &str) {
345 init_logger();
346
347 log::debug!(
348 "Enabled backends: {:?}",
349 wgpu::Instance::enabled_backend_features()
350 );
351
352 let window_loop = EventLoopWrapper::new(title);
353 let mut surface = SurfaceWrapper::new();
354 let context = ExampleContext::init_async::<E>(&mut surface, window_loop.window.clone()).await;
355 let mut frame_counter = FrameCounter::new();
356
357 let mut example = None;
359
360 cfg_if::cfg_if! {
361 if #[cfg(target_arch = "wasm32")] {
362 use winit::platform::web::EventLoopExtWebSys;
363 let event_loop_function = EventLoop::spawn;
364 } else {
365 let event_loop_function = EventLoop::run;
366 }
367 }
368
369 log::info!("Entering event loop...");
370 #[cfg_attr(target_arch = "wasm32", expect(clippy::let_unit_value))]
371 let _ = (event_loop_function)(
372 window_loop.event_loop,
373 move |event: Event<()>, target: &EventLoopWindowTarget<()>| {
374 match event {
375 ref e if SurfaceWrapper::start_condition(e) => {
376 surface.resume(&context, window_loop.window.clone(), E::SRGB);
377
378 if example.is_none() {
380 example = Some(E::init(
381 surface.config(),
382 &context.adapter,
383 &context.device,
384 &context.queue,
385 ));
386 }
387 }
388 Event::Suspended => {
389 surface.suspend();
390 }
391 Event::WindowEvent { event, .. } => match event {
392 WindowEvent::Resized(size) => {
393 surface.resize(&context, size);
394 example.as_mut().unwrap().resize(
395 surface.config(),
396 &context.device,
397 &context.queue,
398 );
399
400 window_loop.window.request_redraw();
401 }
402 WindowEvent::KeyboardInput {
403 event:
404 KeyEvent {
405 logical_key: Key::Named(NamedKey::Escape),
406 ..
407 },
408 ..
409 }
410 | WindowEvent::CloseRequested => {
411 target.exit();
412 }
413 #[cfg(not(target_arch = "wasm32"))]
414 WindowEvent::KeyboardInput {
415 event:
416 KeyEvent {
417 logical_key: Key::Character(s),
418 ..
419 },
420 ..
421 } if s == "r" => {
422 println!("{:#?}", context.instance.generate_report());
423 }
424 WindowEvent::RedrawRequested => {
425 if example.is_none() {
430 return;
431 }
432
433 frame_counter.update();
434
435 let frame = surface.acquire(&context);
436 let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
437 format: Some(surface.config().view_formats[0]),
438 ..wgpu::TextureViewDescriptor::default()
439 });
440
441 example
442 .as_mut()
443 .unwrap()
444 .render(&view, &context.device, &context.queue);
445
446 window_loop.window.pre_present_notify();
447 frame.present();
448
449 window_loop.window.request_redraw();
450 }
451 _ => example.as_mut().unwrap().update(event),
452 },
453 _ => {}
454 }
455 },
456 );
457}
458
459pub fn run<E: Example>(title: &'static str) {
460 cfg_if::cfg_if! {
461 if #[cfg(target_arch = "wasm32")] {
462 wasm_bindgen_futures::spawn_local(async move { start::<E>(title).await })
463 } else {
464 pollster::block_on(start::<E>(title));
465 }
466 }
467}
468
469#[cfg(target_arch = "wasm32")]
470pub fn parse_url_query_string<'a>(query: &'a str, search_key: &str) -> Option<&'a str> {
473 let query_string = query.strip_prefix('?')?;
474
475 for pair in query_string.split('&') {
476 let mut pair = pair.split('=');
477 let key = pair.next()?;
478 let value = pair.next()?;
479
480 if key == search_key {
481 return Some(value);
482 }
483 }
484
485 None
486}
487
488#[cfg(test)]
489pub use wgpu_test::image::ComparisonType;
490
491use crate::utils::get_adapter_with_capabilities_or_from_env;
492
493#[cfg(test)]
494#[derive(Clone)]
495pub struct ExampleTestParams<E> {
496 pub name: &'static str,
497 pub image_path: &'static str,
499 pub width: u32,
500 pub height: u32,
501 pub optional_features: wgpu::Features,
502 pub base_test_parameters: wgpu_test::TestParameters,
503 pub comparisons: &'static [ComparisonType],
505 pub _phantom: std::marker::PhantomData<E>,
506}
507
508#[cfg(test)]
509impl<E: Example + wgpu::WasmNotSendSync> From<ExampleTestParams<E>>
510 for wgpu_test::GpuTestConfiguration
511{
512 fn from(params: ExampleTestParams<E>) -> Self {
513 wgpu_test::GpuTestConfiguration::new()
514 .name(params.name)
515 .parameters({
516 assert_eq!(params.width % 64, 0, "width needs to be aligned 64");
517
518 let features = E::required_features() | params.optional_features;
519
520 params
521 .base_test_parameters
522 .clone()
523 .features(features)
524 .limits(E::required_limits())
525 })
526 .run_async(move |ctx| async move {
527 let format = if E::SRGB {
528 wgpu::TextureFormat::Rgba8UnormSrgb
529 } else {
530 wgpu::TextureFormat::Rgba8Unorm
531 };
532 let dst_texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
533 label: Some("destination"),
534 size: wgpu::Extent3d {
535 width: params.width,
536 height: params.height,
537 depth_or_array_layers: 1,
538 },
539 mip_level_count: 1,
540 sample_count: 1,
541 dimension: wgpu::TextureDimension::D2,
542 format,
543 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
544 view_formats: &[],
545 });
546
547 let dst_view = dst_texture.create_view(&wgpu::TextureViewDescriptor::default());
548
549 let dst_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
550 label: Some("image map buffer"),
551 size: params.width as u64 * params.height as u64 * 4,
552 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
553 mapped_at_creation: false,
554 });
555
556 let mut example = E::init(
557 &wgpu::SurfaceConfiguration {
558 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
559 format,
560 width: params.width,
561 height: params.height,
562 desired_maximum_frame_latency: 2,
563 present_mode: wgpu::PresentMode::Fifo,
564 alpha_mode: wgpu::CompositeAlphaMode::Auto,
565 view_formats: vec![format],
566 },
567 &ctx.adapter,
568 &ctx.device,
569 &ctx.queue,
570 );
571
572 example.render(&dst_view, &ctx.device, &ctx.queue);
573
574 let mut cmd_buf = ctx
575 .device
576 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
577
578 cmd_buf.copy_texture_to_buffer(
579 wgpu::TexelCopyTextureInfo {
580 texture: &dst_texture,
581 mip_level: 0,
582 origin: wgpu::Origin3d::ZERO,
583 aspect: wgpu::TextureAspect::All,
584 },
585 wgpu::TexelCopyBufferInfo {
586 buffer: &dst_buffer,
587 layout: wgpu::TexelCopyBufferLayout {
588 offset: 0,
589 bytes_per_row: Some(params.width * 4),
590 rows_per_image: None,
591 },
592 },
593 wgpu::Extent3d {
594 width: params.width,
595 height: params.height,
596 depth_or_array_layers: 1,
597 },
598 );
599
600 ctx.queue.submit(Some(cmd_buf.finish()));
601
602 let dst_buffer_slice = dst_buffer.slice(..);
603 dst_buffer_slice.map_async(wgpu::MapMode::Read, |_| ());
604 ctx.async_poll(wgpu::PollType::wait_indefinitely())
605 .await
606 .unwrap();
607 let bytes = dst_buffer_slice.get_mapped_range().to_vec();
608
609 wgpu_test::image::compare_image_output(
610 dbg!(env!("CARGO_MANIFEST_DIR").to_string() + "/../../" + params.image_path),
611 &ctx.adapter_info,
612 params.width,
613 params.height,
614 &bytes,
615 params.comparisons,
616 )
617 .await;
618 })
619 }
620}