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