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