wgpu_examples/
framework.rs

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() // These downlevel limits will allow the code to run on all possible hardware
35    }
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
56// Initialize logging in platform dependent ways.
57fn init_logger() {
58    cfg_if::cfg_if! {
59        if #[cfg(target_arch = "wasm32")] {
60            // As we don't have an environment to pull logging level from, we use the query string.
61            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            // 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                .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            // parse_default_env will read the RUST_LOG environment variable and apply it on top
76            // of these default filters.
77            env_logger::builder()
78                .filter_level(log::LevelFilter::Info)
79                .parse_default_env()
80                .init();
81        }
82    }
83}
84
85/// Runs a future to completion. On native this blocks via pollster, on wasm this spawns
86/// a local task. This allows the same async wgpu initialization code to work on both platforms.
87#[cfg(not(target_arch = "wasm32"))]
88fn spawn(f: impl Future<Output = ()> + 'static) {
89    pollster::block_on(f);
90}
91
92/// Runs a future to completion. On native this blocks via pollster, on wasm this spawns
93/// a local task. This allows the same async wgpu initialization code to work on both platforms.
94#[cfg(target_arch = "wasm32")]
95fn spawn(f: impl Future<Output = ()> + 'static) {
96    wasm_bindgen_futures::spawn_local(f);
97}
98
99/// Wrapper type which manages the surface and surface configuration.
100///
101/// As surface usage varies per platform, wrapping this up cleans up the event loop code.
102struct SurfaceWrapper {
103    surface: Option<wgpu::Surface<'static>>,
104    config: Option<wgpu::SurfaceConfiguration>,
105}
106
107impl SurfaceWrapper {
108    /// Create a new surface wrapper with no surface or configuration.
109    fn new() -> Self {
110        Self {
111            surface: None,
112            config: None,
113        }
114    }
115
116    /// Called after the instance is created, but before we request an adapter.
117    ///
118    /// On wasm, we need to create the surface here, as the WebGL backend needs
119    /// a surface (and hence a canvas) to be present to create the adapter.
120    ///
121    /// We cannot unconditionally create a surface here, as Android requires
122    /// us to wait until we receive the `Resumed` event to do so.
123    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    /// Called on resume to create (on native) and configure the surface.
130    ///
131    /// On all native platforms, this is where we create the surface.
132    /// On wasm, the surface was already created in [`Self::pre_adapter`].
133    ///
134    /// Additionally, we configure the surface based on the (now valid) window size.
135    fn resume(&mut self, context: &ExampleContext, window: Arc<Window>, srgb: bool) {
136        // Window size is only actually valid after we enter the event loop.
137        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        // We didn't create the surface in pre_adapter, so we need to do so now.
144        if !cfg!(target_arch = "wasm32") {
145            self.surface = Some(context.instance.create_surface(window).unwrap());
146        }
147
148        // From here on, self.surface should be Some.
149
150        let surface = self.surface.as_ref().unwrap();
151
152        // Get the default configuration,
153        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            // Not all platforms (WebGPU) support sRGB swapchains, so we need to use view formats
158            let view_format = config.format.add_srgb_suffix();
159            config.view_formats.push(view_format);
160        } else {
161            // All platforms support non-sRGB swapchains, so we can just use the format directly.
162            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    /// Resize the surface, making sure to not resize to zero.
173    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    /// Acquire the next surface texture.
184    ///
185    /// Returns `None` on failure.
186    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            // If we timed out or the window is occluded, skip this frame:
198            CurrentSurfaceTexture::Timeout | CurrentSurfaceTexture::Occluded => None,
199            // If the surface is outdated or suboptimal, reconfigure and retry.
200            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            // If the surface is lost, recreate and reconfigure it.
221            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    /// On suspend on android, we drop the surface, as it's no longer valid.
237    ///
238    /// A suspend event is always followed by at least one resume event.
239    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
254/// Context containing global wgpu resources.
255struct ExampleContext {
256    instance: wgpu::Instance,
257    adapter: wgpu::Adapter,
258    device: wgpu::Device,
259    queue: wgpu::Queue,
260}
261impl ExampleContext {
262    /// Initializes the example context.
263    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        // 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 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    // Instant of the last time we printed the frame time.
314    last_printed_instant: web_time::Instant,
315    // Number of frames since the last time we printed the frame time.
316    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
343/// User event sent via [`EventLoopProxy`] to deliver async initialization results
344/// back to the main event loop.
345enum AppAction {
346    /// The async wgpu initialization has completed.
347    WgpuInitialized {
348        context: ExampleContext,
349        surface: SurfaceWrapper,
350    },
351}
352
353#[expect(clippy::large_enum_variant)]
354enum AppState<E> {
355    /// Waiting for the first `resumed()` call.
356    Uninitialized,
357    /// Window created, async wgpu initialization in progress.
358    Loading,
359    /// Fully initialized and rendering.
360    Running {
361        context: ExampleContext,
362        surface: SurfaceWrapper,
363        example: E,
364    },
365}
366
367/// The main application struct, implementing winit's [`ApplicationHandler`].
368///
369/// Winit 0.30 requires that windows are not created until the `resumed()` callback,
370/// and that all wgpu resources (instance, adapter, device) are initialized after the
371/// window exists. On native, this init happens synchronously via `pollster::block_on`.
372/// On wasm, it happens asynchronously via `wasm_bindgen_futures::spawn_local`, with
373/// the results delivered back through an [`EventLoopProxy`] user event.
374struct 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    /// Called when the application is (re)started. On the first call, the window and wgpu
398    /// resources are created. On Android, this may be called again after each suspend —
399    /// in that case we only need to re-create the surface.
400    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
401        // On Android, re-create the surface after a suspend/resume cycle.
402        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 the async wgpu initialization. On native, `spawn` uses `pollster::block_on`
452        // so this completes synchronously before `resumed()` returns. On wasm, `spawn` uses
453        // `wasm_bindgen_futures::spawn_local` so the result arrives later via `user_event()`.
454        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    /// Receives the result of the async wgpu initialization. Creates the [`Example`] and
464    /// transitions to the running state.
465    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                // Don't render while occluded, this may leak on apple platforms.
542                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                // Resume rendering when un-occluded.
570                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")]
610/// Parse the query string as returned by `web_sys::window()?.location().search()?` and get a
611/// specific key out of it.
612pub 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    // Path to the reference image, relative to the root of the repo.
638    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    /// Comparisons against FLIP statistics that determine if the test passes or fails.
644    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}