wgpu_examples/
framework.rs

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() // These downlevel limits will allow the code to run on all possible hardware
33    }
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
54// Initialize logging in platform dependant ways.
55fn init_logger() {
56    cfg_if::cfg_if! {
57        if #[cfg(target_arch = "wasm32")] {
58            // As we don't have an environment to pull logging level from, we use the query string.
59            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            // We keep wgpu at Error level, as it's very noisy.
64            let base_level = query_level.unwrap_or(log::LevelFilter::Info);
65            let wgpu_level = query_level.unwrap_or(log::LevelFilter::Error);
66
67            // On web, we use fern, as console_log doesn't have filtering on a per-module level.
68            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            // parse_default_env will read the RUST_LOG environment variable and apply it on top
79            // of these default filters.
80            env_logger::builder()
81                .filter_level(log::LevelFilter::Info)
82                // We keep wgpu at Error level, as it's very noisy.
83                .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
122/// Wrapper type which manages the surface and surface configuration.
123///
124/// As surface usage varies per platform, wrapping this up cleans up the event loop code.
125struct SurfaceWrapper {
126    surface: Option<wgpu::Surface<'static>>,
127    config: Option<wgpu::SurfaceConfiguration>,
128}
129
130impl SurfaceWrapper {
131    /// Create a new surface wrapper with no surface or configuration.
132    fn new() -> Self {
133        Self {
134            surface: None,
135            config: None,
136        }
137    }
138
139    /// Called after the instance is created, but before we request an adapter.
140    ///
141    /// On wasm, we need to create the surface here, as the WebGL backend needs
142    /// a surface (and hence a canvas) to be present to create the adapter.
143    ///
144    /// We cannot unconditionally create a surface here, as Android requires
145    /// us to wait until we receive the `Resumed` event to do so.
146    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    /// Check if the event is the start condition for the surface.
153    fn start_condition(e: &Event<()>) -> bool {
154        match e {
155            // On all other platforms, we can create the surface immediately.
156            Event::NewEvents(StartCause::Init) => !cfg!(target_os = "android"),
157            // On android we need to wait for a resumed event to create the surface.
158            Event::Resumed => cfg!(target_os = "android"),
159            _ => false,
160        }
161    }
162
163    /// Called when an event which matches [`Self::start_condition`] is received.
164    ///
165    /// On all native platforms, this is where we create the surface.
166    ///
167    /// Additionally, we configure the surface based on the (now valid) window size.
168    fn resume(&mut self, context: &ExampleContext, window: Arc<Window>, srgb: bool) {
169        // Window size is only actually valid after we enter the event loop.
170        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        // We didn't create the surface in pre_adapter, so we need to do so now.
177        if !cfg!(target_arch = "wasm32") {
178            self.surface = Some(context.instance.create_surface(window).unwrap());
179        }
180
181        // From here on, self.surface should be Some.
182
183        let surface = self.surface.as_ref().unwrap();
184
185        // Get the default configuration,
186        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            // Not all platforms (WebGPU) support sRGB swapchains, so we need to use view formats
191            let view_format = config.format.add_srgb_suffix();
192            config.view_formats.push(view_format);
193        } else {
194            // All platforms support non-sRGB swapchains, so we can just use the format directly.
195            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    /// Resize the surface, making sure to not resize to zero.
205    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    /// Acquire the next surface texture.
216    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            // If we timed out, just try again
222            Err(wgpu::SurfaceError::Timeout) => surface
223                .get_current_texture()
224                .expect("Failed to acquire next surface texture!"),
225            Err(
226                // If the surface is outdated, or was lost, reconfigure it.
227                wgpu::SurfaceError::Outdated
228                | wgpu::SurfaceError::Lost
229                | wgpu::SurfaceError::Other
230                // If OutOfMemory happens, reconfiguring may not help, but we might as well try
231                | 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    /// On suspend on android, we drop the surface, as it's no longer valid.
242    ///
243    /// A suspend event is always followed by at least one resume event.
244    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
259/// Context containing global wgpu resources.
260struct ExampleContext {
261    instance: wgpu::Instance,
262    adapter: wgpu::Adapter,
263    device: wgpu::Device,
264    queue: wgpu::Queue,
265}
266impl ExampleContext {
267    /// Initializes the example context.
268    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        // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface.
282        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    // Instant of the last time we printed the frame time.
310    last_printed_instant: web_time::Instant,
311    // Number of frames since the last time we printed the frame time.
312    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    // We wait to create the example until we have a valid surface.
353    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 we haven't created the example yet, do so now.
374                    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                        // On MacOS, currently redraw requested comes in _before_ Init does.
421                        // If this happens, just drop the requested redraw on the floor.
422                        //
423                        // See https://github.com/rust-windowing/winit/issues/3235 for some discussion
424                        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")]
465/// Parse the query string as returned by `web_sys::window()?.location().search()?` and get a
466/// specific key out of it.
467pub 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    // Path to the reference image, relative to the root of the repo.
493    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    /// Comparisons against FLIP statistics that determine if the test passes or fails.
499    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}