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