1use std::future::Future;
2use std::sync::Arc;
3
4use wgpu::{Instance, Surface};
5use winit::{
6 application::ApplicationHandler,
7 dpi::PhysicalSize,
8 event::{KeyEvent, WindowEvent},
9 event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
10 keyboard::{Key, NamedKey},
11 window::Window,
12};
13
14pub trait Example: 'static + Sized {
15 const SRGB: bool = true;
16
17 fn optional_features() -> wgpu::Features {
18 wgpu::Features::empty()
19 }
20
21 fn required_features() -> wgpu::Features {
22 wgpu::Features::empty()
23 }
24
25 fn required_downlevel_capabilities() -> wgpu::DownlevelCapabilities {
26 wgpu::DownlevelCapabilities {
27 flags: wgpu::DownlevelFlags::empty(),
28 shader_model: wgpu::ShaderModel::Sm5,
29 ..wgpu::DownlevelCapabilities::default()
30 }
31 }
32
33 fn required_limits() -> wgpu::Limits {
34 wgpu::Limits::downlevel_webgl2_defaults() }
36
37 fn init(
38 config: &wgpu::SurfaceConfiguration,
39 adapter: &wgpu::Adapter,
40 device: &wgpu::Device,
41 queue: &wgpu::Queue,
42 ) -> Self;
43
44 fn resize(
45 &mut self,
46 config: &wgpu::SurfaceConfiguration,
47 device: &wgpu::Device,
48 queue: &wgpu::Queue,
49 );
50
51 fn update(&mut self, event: WindowEvent);
52
53 fn render(&mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue);
54}
55
56fn init_logger() {
58 cfg_if::cfg_if! {
59 if #[cfg(target_arch = "wasm32")] {
60 let query_string = web_sys::window().unwrap().location().search().unwrap();
62 let query_level: Option<log::LevelFilter> = parse_url_query_string(&query_string, "RUST_LOG")
63 .and_then(|x| x.parse().ok());
64
65 let base_level = query_level.unwrap_or(log::LevelFilter::Info);
66
67 fern::Dispatch::new()
69 .level(base_level)
70 .chain(fern::Output::call(console_log::log))
71 .apply()
72 .unwrap();
73 std::panic::set_hook(Box::new(console_error_panic_hook::hook));
74 } else {
75 env_logger::builder()
78 .filter_level(log::LevelFilter::Info)
79 .parse_default_env()
80 .init();
81 }
82 }
83}
84
85#[cfg(not(target_arch = "wasm32"))]
88fn spawn(f: impl Future<Output = ()> + 'static) {
89 pollster::block_on(f);
90}
91
92#[cfg(target_arch = "wasm32")]
95fn spawn(f: impl Future<Output = ()> + 'static) {
96 wasm_bindgen_futures::spawn_local(f);
97}
98
99struct SurfaceWrapper {
103 surface: Option<wgpu::Surface<'static>>,
104 config: Option<wgpu::SurfaceConfiguration>,
105}
106
107impl SurfaceWrapper {
108 fn new() -> Self {
110 Self {
111 surface: None,
112 config: None,
113 }
114 }
115
116 fn pre_adapter(&mut self, instance: &Instance, window: Arc<Window>) {
124 if cfg!(target_arch = "wasm32") {
125 self.surface = Some(instance.create_surface(window).unwrap());
126 }
127 }
128
129 fn resume(&mut self, context: &ExampleContext, window: Arc<Window>, srgb: bool) {
136 let window_size = window.inner_size();
138 let width = window_size.width.max(1);
139 let height = window_size.height.max(1);
140
141 log::info!("Surface resume {window_size:?}");
142
143 if !cfg!(target_arch = "wasm32") {
145 self.surface = Some(context.instance.create_surface(window).unwrap());
146 }
147
148 let surface = self.surface.as_ref().unwrap();
151
152 let mut config = surface
154 .get_default_config(&context.adapter, width, height)
155 .expect("Surface isn't supported by the adapter.");
156 if srgb {
157 let view_format = config.format.add_srgb_suffix();
159 config.view_formats.push(view_format);
160 } else {
161 let format = config.format.remove_srgb_suffix();
163 config.format = format;
164 config.view_formats.push(format);
165 };
166 config.desired_maximum_frame_latency = 3;
167
168 surface.configure(&context.device, &config);
169 self.config = Some(config);
170 }
171
172 fn resize(&mut self, context: &ExampleContext, size: PhysicalSize<u32>) {
174 log::info!("Surface resize {size:?}");
175
176 let config = self.config.as_mut().unwrap();
177 config.width = size.width.max(1);
178 config.height = size.height.max(1);
179 let surface = self.surface.as_ref().unwrap();
180 surface.configure(&context.device, config);
181 }
182
183 fn acquire(
187 &mut self,
188 context: &ExampleContext,
189 window: Arc<Window>,
190 ) -> Option<wgpu::SurfaceTexture> {
191 use wgpu::CurrentSurfaceTexture;
192
193 let surface = self.surface.as_ref().unwrap();
194
195 match surface.get_current_texture() {
196 CurrentSurfaceTexture::Success(frame) => Some(frame),
197 CurrentSurfaceTexture::Timeout | CurrentSurfaceTexture::Occluded => None,
199 CurrentSurfaceTexture::Suboptimal(_) | CurrentSurfaceTexture::Outdated => {
201 surface.configure(&context.device, self.config());
202 match surface.get_current_texture() {
203 CurrentSurfaceTexture::Success(frame)
204 | CurrentSurfaceTexture::Suboptimal(frame) => Some(frame),
205 other => panic!("Failed to acquire next surface texture: {other:?}"),
206 }
207 }
208 CurrentSurfaceTexture::Validation => {
209 unreachable!("No error scope registered, so validation errors will panic")
210 }
211 CurrentSurfaceTexture::Lost => {
213 self.surface = Some(context.instance.create_surface(window).unwrap());
214 self.surface
215 .as_ref()
216 .unwrap()
217 .configure(&context.device, self.config());
218 match self.surface.as_ref().unwrap().get_current_texture() {
219 CurrentSurfaceTexture::Success(frame)
220 | CurrentSurfaceTexture::Suboptimal(frame) => Some(frame),
221 other => panic!("Failed to acquire next surface texture: {other:?}"),
222 }
223 }
224 }
225 }
226
227 fn suspend(&mut self) {
231 if cfg!(target_os = "android") {
232 self.surface = None;
233 }
234 }
235
236 fn get(&self) -> Option<&'_ Surface<'static>> {
237 self.surface.as_ref()
238 }
239
240 fn config(&self) -> &wgpu::SurfaceConfiguration {
241 self.config.as_ref().unwrap()
242 }
243}
244
245struct ExampleContext {
247 instance: wgpu::Instance,
248 adapter: wgpu::Adapter,
249 device: wgpu::Device,
250 queue: wgpu::Queue,
251}
252impl ExampleContext {
253 async fn init_async<E: Example>(
255 surface: &mut SurfaceWrapper,
256 window: Arc<Window>,
257 display_handle: winit::event_loop::OwnedDisplayHandle,
258 ) -> Self {
259 log::info!("Initializing wgpu...");
260
261 let instance_descriptor =
262 wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(display_handle));
263 let instance = wgpu::Instance::new(instance_descriptor);
264 surface.pre_adapter(&instance, window);
265 let adapter = get_adapter_with_capabilities_or_from_env(
266 &instance,
267 &E::required_features(),
268 &E::required_downlevel_capabilities(),
269 &surface.get(),
270 )
271 .await;
272 let needed_limits = E::required_limits().using_resolution(adapter.limits());
274
275 let info = adapter.get_info();
276 log::info!("Selected adapter: {} ({:?})", info.name, info.backend);
277
278 let (device, queue) = adapter
279 .request_device(&wgpu::DeviceDescriptor {
280 label: None,
281 required_features: (E::optional_features() & adapter.features())
282 | E::required_features(),
283 required_limits: needed_limits,
284 experimental_features: unsafe { wgpu::ExperimentalFeatures::enabled() },
285 memory_hints: wgpu::MemoryHints::MemoryUsage,
286 trace: match std::env::var_os("WGPU_TRACE") {
287 Some(path) => wgpu::Trace::Directory(path.into()),
288 None => wgpu::Trace::Off,
289 },
290 })
291 .await
292 .expect("Unable to find a suitable GPU adapter!");
293
294 Self {
295 instance,
296 adapter,
297 device,
298 queue,
299 }
300 }
301}
302
303struct FrameCounter {
304 last_printed_instant: web_time::Instant,
306 frame_count: u32,
308}
309
310impl FrameCounter {
311 fn new() -> Self {
312 Self {
313 last_printed_instant: web_time::Instant::now(),
314 frame_count: 0,
315 }
316 }
317
318 fn update(&mut self) {
319 self.frame_count += 1;
320 let new_instant = web_time::Instant::now();
321 let elapsed_secs = (new_instant - self.last_printed_instant).as_secs_f32();
322 if elapsed_secs > 1.0 {
323 let elapsed_ms = elapsed_secs * 1000.0;
324 let frame_time = elapsed_ms / self.frame_count as f32;
325 let fps = self.frame_count as f32 / elapsed_secs;
326 log::info!("Frame time {frame_time:.2}ms ({fps:.1} FPS)");
327
328 self.last_printed_instant = new_instant;
329 self.frame_count = 0;
330 }
331 }
332}
333
334enum AppAction {
337 WgpuInitialized {
339 context: ExampleContext,
340 surface: SurfaceWrapper,
341 },
342}
343
344#[expect(clippy::large_enum_variant)]
345enum AppState<E> {
346 Uninitialized,
348 Loading,
350 Running {
352 context: ExampleContext,
353 surface: SurfaceWrapper,
354 example: E,
355 },
356}
357
358struct App<E: Example> {
366 title: &'static str,
367 proxy: EventLoopProxy<AppAction>,
368 window: Option<Arc<Window>>,
369 frame_counter: FrameCounter,
370 occluded: bool,
371 state: AppState<E>,
372}
373
374impl<E: Example> App<E> {
375 fn new(title: &'static str, event_loop: &EventLoop<AppAction>) -> Self {
376 Self {
377 title,
378 proxy: event_loop.create_proxy(),
379 window: None,
380 frame_counter: FrameCounter::new(),
381 occluded: false,
382 state: AppState::Uninitialized,
383 }
384 }
385}
386
387impl<E: Example> ApplicationHandler<AppAction> for App<E> {
388 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
392 if let AppState::Running {
394 ref context,
395 ref mut surface,
396 ..
397 } = self.state
398 {
399 if let Some(window) = &self.window {
400 surface.resume(context, window.clone(), E::SRGB);
401 window.request_redraw();
402 }
403 return;
404 }
405
406 if !matches!(self.state, AppState::Uninitialized) {
407 return;
408 }
409 self.state = AppState::Loading;
410
411 #[cfg_attr(
412 not(target_arch = "wasm32"),
413 expect(unused_mut, reason = "wasm32 re-assigns to specify canvas")
414 )]
415 let mut attributes = Window::default_attributes().with_title(self.title);
416
417 #[cfg(target_arch = "wasm32")]
418 {
419 use wasm_bindgen::JsCast;
420 use winit::platform::web::WindowAttributesExtWebSys;
421 let canvas = web_sys::window()
422 .unwrap()
423 .document()
424 .unwrap()
425 .get_element_by_id("canvas")
426 .unwrap()
427 .dyn_into::<web_sys::HtmlCanvasElement>()
428 .unwrap();
429 attributes = attributes.with_canvas(Some(canvas));
430 }
431
432 let window = Arc::new(
433 event_loop
434 .create_window(attributes)
435 .expect("Failed to create window"),
436 );
437 self.window = Some(window.clone());
438
439 let display_handle = event_loop.owned_display_handle();
440 let proxy = self.proxy.clone();
441
442 spawn(async move {
446 let mut surface = SurfaceWrapper::new();
447 let context =
448 ExampleContext::init_async::<E>(&mut surface, window.clone(), display_handle).await;
449 surface.resume(&context, window, E::SRGB);
450 let _ = proxy.send_event(AppAction::WgpuInitialized { context, surface });
451 });
452 }
453
454 fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppAction) {
457 match event {
458 AppAction::WgpuInitialized { context, surface } => {
459 let example = E::init(
460 surface.config(),
461 &context.adapter,
462 &context.device,
463 &context.queue,
464 );
465
466 self.state = AppState::Running {
467 context,
468 surface,
469 example,
470 };
471
472 if let Some(window) = &self.window {
473 window.request_redraw();
474 }
475 }
476 }
477 }
478
479 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
480 if let AppState::Running { surface, .. } = &mut self.state {
481 surface.suspend();
482 }
483 }
484
485 fn window_event(
486 &mut self,
487 event_loop: &ActiveEventLoop,
488 _window_id: winit::window::WindowId,
489 event: WindowEvent,
490 ) {
491 let AppState::Running {
492 ref mut context,
493 ref mut surface,
494 ref mut example,
495 } = self.state
496 else {
497 return;
498 };
499
500 match event {
501 WindowEvent::Resized(size) => {
502 surface.resize(context, size);
503 example.resize(surface.config(), &context.device, &context.queue);
504
505 if let Some(window) = &self.window {
506 window.request_redraw();
507 }
508 }
509 WindowEvent::KeyboardInput {
510 event:
511 KeyEvent {
512 logical_key: Key::Named(NamedKey::Escape),
513 ..
514 },
515 ..
516 }
517 | WindowEvent::CloseRequested => {
518 event_loop.exit();
519 }
520 #[cfg(not(target_arch = "wasm32"))]
521 WindowEvent::KeyboardInput {
522 event:
523 KeyEvent {
524 logical_key: Key::Character(s),
525 ..
526 },
527 ..
528 } if s == "r" => {
529 println!("{:#?}", context.instance.generate_report());
530 }
531 WindowEvent::RedrawRequested => {
532 if self.occluded {
534 return;
535 }
536
537 self.frame_counter.update();
538
539 let window_arc = self.window.clone().unwrap();
540 if let Some(frame) = surface.acquire(context, window_arc) {
541 let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
542 format: Some(surface.config().view_formats[0]),
543 ..wgpu::TextureViewDescriptor::default()
544 });
545
546 example.render(&view, &context.device, &context.queue);
547
548 if let Some(window) = &self.window {
549 window.pre_present_notify();
550 }
551 frame.present();
552 }
553
554 if let Some(window) = &self.window {
555 window.request_redraw();
556 }
557 }
558 WindowEvent::Occluded(is_occluded) => {
559 self.occluded = is_occluded;
560 if !is_occluded {
562 if let Some(window) = &self.window {
563 window.request_redraw();
564 }
565 }
566 }
567 _ => example.update(event),
568 }
569 }
570}
571
572fn start<E: Example>(title: &'static str) {
573 init_logger();
574
575 log::debug!(
576 "Enabled backends: {:?}",
577 wgpu::Instance::enabled_backend_features()
578 );
579
580 let event_loop = EventLoop::with_user_event().build().unwrap();
581
582 #[cfg_attr(target_arch = "wasm32", expect(unused_mut))]
583 let mut app = App::<E>::new(title, &event_loop);
584
585 log::info!("Entering event loop...");
586 cfg_if::cfg_if! {
587 if #[cfg(target_arch = "wasm32")] {
588 use winit::platform::web::EventLoopExtWebSys;
589 event_loop.spawn_app(app);
590 } else {
591 event_loop.run_app(&mut app).unwrap();
592 }
593 }
594}
595
596pub fn run<E: Example>(title: &'static str) {
597 start::<E>(title);
598}
599
600#[cfg(target_arch = "wasm32")]
601pub fn parse_url_query_string<'a>(query: &'a str, search_key: &str) -> Option<&'a str> {
604 let query_string = query.strip_prefix('?')?;
605
606 for pair in query_string.split('&') {
607 let mut pair = pair.split('=');
608 let key = pair.next()?;
609 let value = pair.next()?;
610
611 if key == search_key {
612 return Some(value);
613 }
614 }
615
616 None
617}
618
619#[cfg(test)]
620pub use wgpu_test::image::ComparisonType;
621
622use crate::utils::get_adapter_with_capabilities_or_from_env;
623
624#[cfg(test)]
625#[derive(Clone)]
626pub struct ExampleTestParams<E> {
627 pub name: &'static str,
628 pub image_path: &'static str,
630 pub width: u32,
631 pub height: u32,
632 pub optional_features: wgpu::Features,
633 pub base_test_parameters: wgpu_test::TestParameters,
634 pub comparisons: &'static [ComparisonType],
636 pub _phantom: std::marker::PhantomData<E>,
637}
638
639#[cfg(test)]
640impl<E: Example + wgpu::WasmNotSendSync> From<ExampleTestParams<E>>
641 for wgpu_test::GpuTestConfiguration
642{
643 fn from(params: ExampleTestParams<E>) -> Self {
644 wgpu_test::GpuTestConfiguration::new()
645 .name(params.name)
646 .parameters({
647 assert_eq!(params.width % 64, 0, "width needs to be aligned 64");
648
649 let features = E::required_features() | params.optional_features;
650
651 params
652 .base_test_parameters
653 .clone()
654 .features(features)
655 .limits(E::required_limits())
656 })
657 .run_async(move |ctx| async move {
658 let format = if E::SRGB {
659 wgpu::TextureFormat::Rgba8UnormSrgb
660 } else {
661 wgpu::TextureFormat::Rgba8Unorm
662 };
663 let dst_texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
664 label: Some("destination"),
665 size: wgpu::Extent3d {
666 width: params.width,
667 height: params.height,
668 depth_or_array_layers: 1,
669 },
670 mip_level_count: 1,
671 sample_count: 1,
672 dimension: wgpu::TextureDimension::D2,
673 format,
674 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
675 view_formats: &[],
676 });
677
678 let dst_view = dst_texture.create_view(&wgpu::TextureViewDescriptor::default());
679
680 let dst_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
681 label: Some("image map buffer"),
682 size: params.width as u64 * params.height as u64 * 4,
683 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
684 mapped_at_creation: false,
685 });
686
687 let mut example = E::init(
688 &wgpu::SurfaceConfiguration {
689 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
690 format,
691 width: params.width,
692 height: params.height,
693 desired_maximum_frame_latency: 2,
694 present_mode: wgpu::PresentMode::Fifo,
695 alpha_mode: wgpu::CompositeAlphaMode::Auto,
696 view_formats: vec![format],
697 },
698 &ctx.adapter,
699 &ctx.device,
700 &ctx.queue,
701 );
702
703 example.render(&dst_view, &ctx.device, &ctx.queue);
704
705 let mut cmd_buf = ctx
706 .device
707 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
708
709 cmd_buf.copy_texture_to_buffer(
710 wgpu::TexelCopyTextureInfo {
711 texture: &dst_texture,
712 mip_level: 0,
713 origin: wgpu::Origin3d::ZERO,
714 aspect: wgpu::TextureAspect::All,
715 },
716 wgpu::TexelCopyBufferInfo {
717 buffer: &dst_buffer,
718 layout: wgpu::TexelCopyBufferLayout {
719 offset: 0,
720 bytes_per_row: Some(params.width * 4),
721 rows_per_image: None,
722 },
723 },
724 wgpu::Extent3d {
725 width: params.width,
726 height: params.height,
727 depth_or_array_layers: 1,
728 },
729 );
730
731 ctx.queue.submit(Some(cmd_buf.finish()));
732
733 let dst_buffer_slice = dst_buffer.slice(..);
734 dst_buffer_slice.map_async(wgpu::MapMode::Read, |_| ());
735 ctx.async_poll(wgpu::PollType::wait_indefinitely())
736 .await
737 .unwrap();
738 let bytes = dst_buffer_slice.get_mapped_range().to_vec();
739
740 wgpu_test::image::compare_image_output(
741 dbg!(env!("CARGO_MANIFEST_DIR").to_string() + "/../../" + params.image_path),
742 &ctx.adapter_info,
743 params.width,
744 params.height,
745 &bytes,
746 params.comparisons,
747 )
748 .await;
749 })
750 }
751}