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(texture) => {
201 drop(texture);
202 surface.configure(&context.device, self.config());
203 match surface.get_current_texture() {
204 CurrentSurfaceTexture::Success(frame)
205 | CurrentSurfaceTexture::Suboptimal(frame) => Some(frame),
206 other => panic!("Failed to acquire next surface texture: {other:?}"),
207 }
208 }
209 CurrentSurfaceTexture::Outdated => {
210 surface.configure(&context.device, self.config());
211 match surface.get_current_texture() {
212 CurrentSurfaceTexture::Success(frame)
213 | CurrentSurfaceTexture::Suboptimal(frame) => Some(frame),
214 other => panic!("Failed to acquire next surface texture: {other:?}"),
215 }
216 }
217 CurrentSurfaceTexture::Validation => {
218 unreachable!("No error scope registered, so validation errors will panic")
219 }
220 CurrentSurfaceTexture::Lost => {
222 self.surface = Some(context.instance.create_surface(window).unwrap());
223 self.surface
224 .as_ref()
225 .unwrap()
226 .configure(&context.device, self.config());
227 match self.surface.as_ref().unwrap().get_current_texture() {
228 CurrentSurfaceTexture::Success(frame)
229 | CurrentSurfaceTexture::Suboptimal(frame) => Some(frame),
230 other => panic!("Failed to acquire next surface texture: {other:?}"),
231 }
232 }
233 }
234 }
235
236 fn suspend(&mut self) {
240 if cfg!(target_os = "android") {
241 self.surface = None;
242 }
243 }
244
245 fn get(&self) -> Option<&'_ Surface<'static>> {
246 self.surface.as_ref()
247 }
248
249 fn config(&self) -> &wgpu::SurfaceConfiguration {
250 self.config.as_ref().unwrap()
251 }
252}
253
254struct ExampleContext {
256 instance: wgpu::Instance,
257 adapter: wgpu::Adapter,
258 device: wgpu::Device,
259 queue: wgpu::Queue,
260}
261impl ExampleContext {
262 async fn init_async<E: Example>(
264 surface: &mut SurfaceWrapper,
265 window: Arc<Window>,
266 display_handle: winit::event_loop::OwnedDisplayHandle,
267 ) -> Self {
268 log::info!("Initializing wgpu...");
269
270 let instance_descriptor =
271 wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(display_handle));
272 let instance = wgpu::Instance::new(instance_descriptor);
273 surface.pre_adapter(&instance, window);
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 info = adapter.get_info();
285 log::info!("Selected adapter: {} ({:?})", info.name, info.backend);
286
287 let (device, queue) = adapter
288 .request_device(&wgpu::DeviceDescriptor {
289 label: None,
290 required_features: (E::optional_features() & adapter.features())
291 | E::required_features(),
292 required_limits: needed_limits,
293 experimental_features: unsafe { wgpu::ExperimentalFeatures::enabled() },
294 memory_hints: wgpu::MemoryHints::MemoryUsage,
295 trace: match std::env::var_os("WGPU_TRACE") {
296 Some(path) => wgpu::Trace::Directory(path.into()),
297 None => wgpu::Trace::Off,
298 },
299 })
300 .await
301 .expect("Unable to find a suitable GPU adapter!");
302
303 Self {
304 instance,
305 adapter,
306 device,
307 queue,
308 }
309 }
310}
311
312struct FrameCounter {
313 last_printed_instant: web_time::Instant,
315 frame_count: u32,
317}
318
319impl FrameCounter {
320 fn new() -> Self {
321 Self {
322 last_printed_instant: web_time::Instant::now(),
323 frame_count: 0,
324 }
325 }
326
327 fn update(&mut self) {
328 self.frame_count += 1;
329 let new_instant = web_time::Instant::now();
330 let elapsed_secs = (new_instant - self.last_printed_instant).as_secs_f32();
331 if elapsed_secs > 1.0 {
332 let elapsed_ms = elapsed_secs * 1000.0;
333 let frame_time = elapsed_ms / self.frame_count as f32;
334 let fps = self.frame_count as f32 / elapsed_secs;
335 log::info!("Frame time {frame_time:.2}ms ({fps:.1} FPS)");
336
337 self.last_printed_instant = new_instant;
338 self.frame_count = 0;
339 }
340 }
341}
342
343enum AppAction {
346 WgpuInitialized {
348 context: ExampleContext,
349 surface: SurfaceWrapper,
350 },
351}
352
353#[expect(clippy::large_enum_variant)]
354enum AppState<E> {
355 Uninitialized,
357 Loading,
359 Running {
361 context: ExampleContext,
362 surface: SurfaceWrapper,
363 example: E,
364 },
365}
366
367struct App<E: Example> {
375 title: &'static str,
376 proxy: EventLoopProxy<AppAction>,
377 window: Option<Arc<Window>>,
378 frame_counter: FrameCounter,
379 occluded: bool,
380 state: AppState<E>,
381}
382
383impl<E: Example> App<E> {
384 fn new(title: &'static str, event_loop: &EventLoop<AppAction>) -> Self {
385 Self {
386 title,
387 proxy: event_loop.create_proxy(),
388 window: None,
389 frame_counter: FrameCounter::new(),
390 occluded: false,
391 state: AppState::Uninitialized,
392 }
393 }
394}
395
396impl<E: Example> ApplicationHandler<AppAction> for App<E> {
397 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
401 if let AppState::Running {
403 ref context,
404 ref mut surface,
405 ..
406 } = self.state
407 {
408 if let Some(window) = &self.window {
409 surface.resume(context, window.clone(), E::SRGB);
410 window.request_redraw();
411 }
412 return;
413 }
414
415 if !matches!(self.state, AppState::Uninitialized) {
416 return;
417 }
418 self.state = AppState::Loading;
419
420 #[cfg_attr(
421 not(target_arch = "wasm32"),
422 expect(unused_mut, reason = "wasm32 re-assigns to specify canvas")
423 )]
424 let mut attributes = Window::default_attributes().with_title(self.title);
425
426 #[cfg(target_arch = "wasm32")]
427 {
428 use wasm_bindgen::JsCast;
429 use winit::platform::web::WindowAttributesExtWebSys;
430 let canvas = web_sys::window()
431 .unwrap()
432 .document()
433 .unwrap()
434 .get_element_by_id("canvas")
435 .unwrap()
436 .dyn_into::<web_sys::HtmlCanvasElement>()
437 .unwrap();
438 attributes = attributes.with_canvas(Some(canvas));
439 }
440
441 let window = Arc::new(
442 event_loop
443 .create_window(attributes)
444 .expect("Failed to create window"),
445 );
446 self.window = Some(window.clone());
447
448 let display_handle = event_loop.owned_display_handle();
449 let proxy = self.proxy.clone();
450
451 spawn(async move {
455 let mut surface = SurfaceWrapper::new();
456 let context =
457 ExampleContext::init_async::<E>(&mut surface, window.clone(), display_handle).await;
458 surface.resume(&context, window, E::SRGB);
459 let _ = proxy.send_event(AppAction::WgpuInitialized { context, surface });
460 });
461 }
462
463 fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppAction) {
466 match event {
467 AppAction::WgpuInitialized { context, surface } => {
468 let example = E::init(
469 surface.config(),
470 &context.adapter,
471 &context.device,
472 &context.queue,
473 );
474
475 self.state = AppState::Running {
476 context,
477 surface,
478 example,
479 };
480
481 if let Some(window) = &self.window {
482 window.request_redraw();
483 }
484 }
485 }
486 }
487
488 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
489 if let AppState::Running { surface, .. } = &mut self.state {
490 surface.suspend();
491 }
492 }
493
494 fn window_event(
495 &mut self,
496 event_loop: &ActiveEventLoop,
497 _window_id: winit::window::WindowId,
498 event: WindowEvent,
499 ) {
500 let AppState::Running {
501 ref mut context,
502 ref mut surface,
503 ref mut example,
504 } = self.state
505 else {
506 return;
507 };
508
509 match event {
510 WindowEvent::Resized(size) => {
511 surface.resize(context, size);
512 example.resize(surface.config(), &context.device, &context.queue);
513
514 if let Some(window) = &self.window {
515 window.request_redraw();
516 }
517 }
518 WindowEvent::KeyboardInput {
519 event:
520 KeyEvent {
521 logical_key: Key::Named(NamedKey::Escape),
522 ..
523 },
524 ..
525 }
526 | WindowEvent::CloseRequested => {
527 event_loop.exit();
528 }
529 #[cfg(not(target_arch = "wasm32"))]
530 WindowEvent::KeyboardInput {
531 event:
532 KeyEvent {
533 logical_key: Key::Character(s),
534 ..
535 },
536 ..
537 } if s == "r" => {
538 println!("{:#?}", context.instance.generate_report());
539 }
540 WindowEvent::RedrawRequested => {
541 if self.occluded {
543 return;
544 }
545
546 self.frame_counter.update();
547
548 let window_arc = self.window.clone().unwrap();
549 if let Some(frame) = surface.acquire(context, window_arc) {
550 let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
551 format: Some(surface.config().view_formats[0]),
552 ..wgpu::TextureViewDescriptor::default()
553 });
554
555 example.render(&view, &context.device, &context.queue);
556
557 if let Some(window) = &self.window {
558 window.pre_present_notify();
559 }
560 context.queue.present(frame);
561 }
562
563 if let Some(window) = &self.window {
564 window.request_redraw();
565 }
566 }
567 WindowEvent::Occluded(is_occluded) => {
568 self.occluded = is_occluded;
569 if !is_occluded {
571 if let Some(window) = &self.window {
572 window.request_redraw();
573 }
574 }
575 }
576 _ => example.update(event),
577 }
578 }
579}
580
581fn start<E: Example>(title: &'static str) {
582 init_logger();
583
584 log::debug!(
585 "Enabled backends: {:?}",
586 wgpu::Instance::enabled_backend_features()
587 );
588
589 let event_loop = EventLoop::with_user_event().build().unwrap();
590
591 #[cfg_attr(target_arch = "wasm32", expect(unused_mut))]
592 let mut app = App::<E>::new(title, &event_loop);
593
594 log::info!("Entering event loop...");
595 cfg_if::cfg_if! {
596 if #[cfg(target_arch = "wasm32")] {
597 use winit::platform::web::EventLoopExtWebSys;
598 event_loop.spawn_app(app);
599 } else {
600 event_loop.run_app(&mut app).unwrap();
601 }
602 }
603}
604
605pub fn run<E: Example>(title: &'static str) {
606 start::<E>(title);
607}
608
609#[cfg(target_arch = "wasm32")]
610pub fn parse_url_query_string<'a>(query: &'a str, search_key: &str) -> Option<&'a str> {
613 let query_string = query.strip_prefix('?')?;
614
615 for pair in query_string.split('&') {
616 let mut pair = pair.split('=');
617 let key = pair.next()?;
618 let value = pair.next()?;
619
620 if key == search_key {
621 return Some(value);
622 }
623 }
624
625 None
626}
627
628#[cfg(test)]
629pub use wgpu_test::image::ComparisonType;
630
631use crate::utils::get_adapter_with_capabilities_or_from_env;
632
633#[cfg(test)]
634#[derive(Clone)]
635pub struct ExampleTestParams<E> {
636 pub name: &'static str,
637 pub image_path: &'static str,
639 pub width: u32,
640 pub height: u32,
641 pub optional_features: wgpu::Features,
642 pub base_test_parameters: wgpu_test::TestParameters,
643 pub comparisons: &'static [ComparisonType],
645 pub _phantom: std::marker::PhantomData<E>,
646}
647
648#[cfg(test)]
649impl<E: Example + wgpu::WasmNotSendSync> From<ExampleTestParams<E>>
650 for wgpu_test::GpuTestConfiguration
651{
652 fn from(params: ExampleTestParams<E>) -> Self {
653 wgpu_test::GpuTestConfiguration::new()
654 .name(params.name)
655 .parameters({
656 assert_eq!(params.width % 64, 0, "width needs to be aligned 64");
657
658 let features = E::required_features() | params.optional_features;
659
660 params
661 .base_test_parameters
662 .clone()
663 .features(features)
664 .limits(E::required_limits())
665 })
666 .run_async(move |ctx| async move {
667 let format = if E::SRGB {
668 wgpu::TextureFormat::Rgba8UnormSrgb
669 } else {
670 wgpu::TextureFormat::Rgba8Unorm
671 };
672 let dst_texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
673 label: Some("destination"),
674 size: wgpu::Extent3d {
675 width: params.width,
676 height: params.height,
677 depth_or_array_layers: 1,
678 },
679 mip_level_count: 1,
680 sample_count: 1,
681 dimension: wgpu::TextureDimension::D2,
682 format,
683 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
684 view_formats: &[],
685 });
686
687 let dst_view = dst_texture.create_view(&wgpu::TextureViewDescriptor::default());
688
689 let dst_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
690 label: Some("image map buffer"),
691 size: params.width as u64 * params.height as u64 * 4,
692 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
693 mapped_at_creation: false,
694 });
695
696 let mut example = E::init(
697 &wgpu::SurfaceConfiguration {
698 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
699 format,
700 width: params.width,
701 height: params.height,
702 desired_maximum_frame_latency: 2,
703 present_mode: wgpu::PresentMode::Fifo,
704 alpha_mode: wgpu::CompositeAlphaMode::Auto,
705 view_formats: vec![format],
706 },
707 &ctx.adapter,
708 &ctx.device,
709 &ctx.queue,
710 );
711
712 example.render(&dst_view, &ctx.device, &ctx.queue);
713
714 let mut cmd_buf = ctx
715 .device
716 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
717
718 cmd_buf.copy_texture_to_buffer(
719 wgpu::TexelCopyTextureInfo {
720 texture: &dst_texture,
721 mip_level: 0,
722 origin: wgpu::Origin3d::ZERO,
723 aspect: wgpu::TextureAspect::All,
724 },
725 wgpu::TexelCopyBufferInfo {
726 buffer: &dst_buffer,
727 layout: wgpu::TexelCopyBufferLayout {
728 offset: 0,
729 bytes_per_row: Some(params.width * 4),
730 rows_per_image: None,
731 },
732 },
733 wgpu::Extent3d {
734 width: params.width,
735 height: params.height,
736 depth_or_array_layers: 1,
737 },
738 );
739
740 ctx.queue.submit(Some(cmd_buf.finish()));
741
742 let dst_buffer_slice = dst_buffer.slice(..);
743 dst_buffer_slice.map_async(wgpu::MapMode::Read, |_| ());
744 ctx.async_poll(wgpu::PollType::wait_indefinitely())
745 .await
746 .unwrap();
747 let bytes = dst_buffer_slice.get_mapped_range().unwrap().to_vec();
748
749 wgpu_test::image::compare_image_output(
750 dbg!(env!("CARGO_MANIFEST_DIR").to_string() + "/../../" + params.image_path),
751 &ctx.adapter_info,
752 params.width,
753 params.height,
754 &bytes,
755 params.comparisons,
756 )
757 .await;
758 })
759 }
760}