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