wgpu_playground/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::future::Future;
4#[cfg(not(target_arch = "wasm32"))]
5use std::time::{Duration, Instant};
6use winit::{
7    event::{DeviceEvent, DeviceId, Event, WindowEvent},
8    event_loop::{ControlFlow, EventLoop},
9    window::Window,
10};
11
12pub use wgpu;
13pub use winit;
14
15pub mod projection;
16
17pub trait Playground: 'static + Sized {
18    fn optional_features() -> wgpu::Features {
19        wgpu::Features::empty()
20    }
21    fn required_features() -> wgpu::Features {
22        wgpu::Features::empty()
23    }
24    fn required_downlevel_capabilities() -> wgpu::DownlevelCapabilities {
25        wgpu::DownlevelCapabilities {
26            flags: wgpu::DownlevelFlags::empty(),
27            shader_model: wgpu::ShaderModel::Sm5,
28            ..wgpu::DownlevelCapabilities::default()
29        }
30    }
31    fn required_limits() -> wgpu::Limits {
32        #[cfg(target_arch = "wasm32")]
33        {
34            wgpu::Limits::downlevel_webgl2_defaults() // These downlevel limits will allow the code to run on all possible hardware
35        }
36        #[cfg(not(target_arch = "wasm32"))]
37        {
38            wgpu::Limits::downlevel_defaults()
39        }
40    }
41    fn init(
42        config: &wgpu::SurfaceConfiguration,
43        adapter: &wgpu::Adapter,
44        device: &wgpu::Device,
45        queue: &wgpu::Queue,
46    ) -> Self;
47
48    fn event(&mut self, _device_id: DeviceId, _event: DeviceEvent) {}
49    fn resize(
50        &mut self,
51        config: &wgpu::SurfaceConfiguration,
52        device: &wgpu::Device,
53        queue: &wgpu::Queue,
54    );
55    fn update(&mut self, event: WindowEvent, control_flow: &mut ControlFlow) {
56        if let WindowEvent::CloseRequested = event {
57            *control_flow = ControlFlow::Exit;
58        }
59    }
60    fn render(
61        &mut self,
62        view: &wgpu::TextureView,
63        device: &wgpu::Device,
64        queue: &wgpu::Queue,
65        spawner: &Spawner,
66    );
67}
68
69struct Setup {
70    window: winit::window::Window,
71    event_loop: EventLoop<()>,
72    instance: wgpu::Instance,
73    size: winit::dpi::PhysicalSize<u32>,
74    surface: wgpu::Surface,
75    adapter: wgpu::Adapter,
76    device: wgpu::Device,
77    queue: wgpu::Queue,
78}
79
80fn setup_logging(_window: &Window) {
81    #[cfg(not(target_arch = "wasm32"))]
82    {
83        env_logger::init();
84    }
85
86    #[cfg(target_arch = "wasm32")]
87    {
88        use winit::platform::web::WindowExtWebSys;
89        let query_string = web_sys::window().unwrap().location().search().unwrap();
90        let level: log::Level = parse_url_query_string(&query_string, "RUST_LOG")
91            .map(|x| x.parse().ok())
92            .flatten()
93            .unwrap_or(log::Level::Error);
94        console_log::init_with_level(level).expect("could not initialize logger");
95        std::panic::set_hook(Box::new(console_error_panic_hook::hook));
96        // On wasm, append the canvas to the document body
97        web_sys::window()
98            .and_then(|win| win.document())
99            .and_then(|doc| doc.body())
100            .and_then(|body| {
101                body.append_child(&web_sys::Element::from(_window.canvas()))
102                    .ok()
103            })
104            .expect("couldn't append canvas to document body");
105    }
106}
107
108async fn setup<P: Playground>(window: Window, event_loop: EventLoop<()>) -> Setup {
109    setup_logging(&window);
110
111    log::info!("Initializing the surface...");
112
113    let backend = wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all);
114
115    let instance = wgpu::Instance::new(backend);
116    let (size, surface) = unsafe {
117        let size = window.inner_size();
118        let surface = instance.create_surface(&window);
119        (size, surface)
120    };
121    let adapter =
122        wgpu::util::initialize_adapter_from_env_or_default(&instance, backend, Some(&surface))
123            .await
124            .expect("No suitable GPU adapters found on the system!");
125
126    #[cfg(not(target_arch = "wasm32"))]
127    {
128        let adapter_info = adapter.get_info();
129        println!("Using {} ({:?})", adapter_info.name, adapter_info.backend);
130    }
131
132    let optional_features = P::optional_features();
133    let required_features = P::required_features();
134    let adapter_features = adapter.features();
135    assert!(
136        adapter_features.contains(required_features),
137        "Adapter does not support required features for this example: {:?}",
138        required_features - adapter_features
139    );
140
141    let required_downlevel_capabilities = P::required_downlevel_capabilities();
142    let downlevel_capabilities = adapter.get_downlevel_properties();
143    assert!(
144        downlevel_capabilities.shader_model >= required_downlevel_capabilities.shader_model,
145        "Adapter does not support the minimum shader model required to run this example: {:?}",
146        required_downlevel_capabilities.shader_model
147    );
148    assert!(
149        downlevel_capabilities
150            .flags
151            .contains(required_downlevel_capabilities.flags),
152        "Adapter does not support the downlevel capabilities required to run this example: {:?}",
153        required_downlevel_capabilities.flags - downlevel_capabilities.flags
154    );
155
156    // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface.
157    let needed_limits = P::required_limits().using_resolution(adapter.limits());
158
159    let trace_dir = std::env::var("WGPU_TRACE");
160    let (device, queue) = adapter
161        .request_device(
162            &wgpu::DeviceDescriptor {
163                label: None,
164                features: (optional_features & adapter_features) | required_features,
165                limits: needed_limits,
166            },
167            trace_dir.ok().as_ref().map(std::path::Path::new),
168        )
169        .await
170        .expect("Unable to find a suitable GPU adapter!");
171
172    Setup {
173        window,
174        event_loop,
175        instance,
176        size,
177        surface,
178        adapter,
179        device,
180        queue,
181    }
182}
183
184fn start<E: Playground>(
185    Setup {
186        window,
187        event_loop,
188        instance,
189        size,
190        surface,
191        adapter,
192        device,
193        queue,
194    }: Setup,
195) {
196    let spawner = Spawner::new();
197    let mut config = wgpu::SurfaceConfiguration {
198        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
199        format: surface.get_preferred_format(&adapter).unwrap(),
200        width: size.width,
201        height: size.height,
202        present_mode: wgpu::PresentMode::Mailbox,
203    };
204    surface.configure(&device, &config);
205
206    log::info!("Initializing the example...");
207    let mut playground = E::init(&config, &adapter, &device, &queue);
208
209    #[cfg(not(target_arch = "wasm32"))]
210    let mut last_update_inst = Instant::now();
211    #[cfg(not(target_arch = "wasm32"))]
212    let mut last_frame_inst = Instant::now();
213    #[cfg(not(target_arch = "wasm32"))]
214    let (mut frame_count, mut accum_time) = (0, 0.0);
215
216    log::info!("Entering render loop...");
217    event_loop.run(move |event, _, control_flow| {
218        let _ = (&instance, &adapter); // force ownership by the closure
219        *control_flow = if cfg!(feature = "metal-auto-capture") {
220            ControlFlow::Exit
221        } else {
222            ControlFlow::Poll
223        };
224        match event {
225            Event::NewEvents(_start_cause) => (),
226            Event::WindowEvent { event, .. } => {
227                match event {
228                    WindowEvent::Resized(size)
229                    | WindowEvent::ScaleFactorChanged {
230                        new_inner_size: &mut size,
231                        ..
232                    } => {
233                        log::info!("Resizing to {:?}", size);
234                        config.width = size.width.max(1);
235                        config.height = size.height.max(1);
236                        playground.resize(&config, &device, &queue);
237                        surface.configure(&device, &config);
238                    }
239                    _ => (),
240                }
241                playground.update(event, control_flow);
242            }
243            Event::DeviceEvent { device_id, event } => playground.event(device_id, event),
244            Event::UserEvent(_event) => (),
245            Event::Suspended => (),
246            Event::Resumed => (),
247            Event::MainEventsCleared => (),
248            Event::RedrawRequested(_) => {
249                #[cfg(not(target_arch = "wasm32"))]
250                {
251                    accum_time += last_frame_inst.elapsed().as_secs_f32();
252                    last_frame_inst = Instant::now();
253                    frame_count += 1;
254                    if frame_count == 100 {
255                        println!(
256                            "Avg frame time {}ms",
257                            accum_time * 1000.0 / frame_count as f32
258                        );
259                        accum_time = 0.0;
260                        frame_count = 0;
261                    }
262                }
263
264                let frame = match surface.get_current_texture() {
265                    Ok(frame) => frame,
266                    Err(_) => {
267                        surface.configure(&device, &config);
268                        surface
269                            .get_current_texture()
270                            .expect("Failed to acquire next surface texture!")
271                    }
272                };
273                let view = frame
274                    .texture
275                    .create_view(&wgpu::TextureViewDescriptor::default());
276
277                playground.render(&view, &device, &queue, &spawner);
278
279                frame.present();
280            }
281            Event::RedrawEventsCleared => {
282                #[cfg(not(target_arch = "wasm32"))]
283                {
284                    // Clamp to some max framerate to avoid busy-looping too much
285                    // (we might be in wgpu::PresentMode::Mailbox, thus discarding superfluous frames)
286                    //
287                    // winit has window.current_monitor().video_modes() but that is a list of all full screen video modes.
288                    // So without extra dependencies it's a bit tricky to get the max refresh rate we can run the window on.
289                    // Therefore we just go with 60fps - sorry 120hz+ folks!
290                    let target_frametime = Duration::from_secs_f64(1.0 / 60.0);
291                    let now = Instant::now();
292                    let time_since_last_frame = now.duration_since(last_update_inst);
293                    if time_since_last_frame >= target_frametime {
294                        window.request_redraw();
295                        last_update_inst = now;
296                    } else {
297                        *control_flow =
298                            ControlFlow::WaitUntil(now + target_frametime - time_since_last_frame);
299                    }
300
301                    spawner.run_until_stalled();
302                }
303
304                #[cfg(target_arch = "wasm32")]
305                window.request_redraw();
306            }
307
308            Event::LoopDestroyed => (),
309        }
310    });
311}
312
313#[cfg(not(target_arch = "wasm32"))]
314pub struct Spawner<'a> {
315    executor: async_executor::LocalExecutor<'a>,
316}
317
318#[cfg(not(target_arch = "wasm32"))]
319impl<'a> Spawner<'a> {
320    fn new() -> Self {
321        Self {
322            executor: async_executor::LocalExecutor::new(),
323        }
324    }
325
326    #[allow(dead_code)]
327    pub fn spawn_local(&self, future: impl Future<Output = ()> + 'a) {
328        self.executor.spawn(future).detach();
329    }
330
331    fn run_until_stalled(&self) {
332        while self.executor.try_tick() {}
333    }
334}
335
336#[cfg(target_arch = "wasm32")]
337pub struct Spawner {}
338
339#[cfg(target_arch = "wasm32")]
340impl Spawner {
341    fn new() -> Self {
342        Self {}
343    }
344
345    #[allow(dead_code)]
346    pub fn spawn_local(&self, future: impl Future<Output = ()> + 'static) {
347        wasm_bindgen_futures::spawn_local(future);
348    }
349}
350
351#[cfg(not(target_arch = "wasm32"))]
352pub fn run<E: Playground>(window: Window, event_loop: EventLoop<()>) {
353    let setup = pollster::block_on(setup::<E>(window, event_loop));
354    start::<E>(setup);
355}
356
357#[cfg(target_arch = "wasm32")]
358pub fn run<E: Playground>(window: Window, event_loop: EventLoop<()>) {
359    use wasm_bindgen::{prelude::*, JsCast};
360
361    wasm_bindgen_futures::spawn_local(async move {
362        let setup = setup::<E>(window, event_loop).await;
363        let start_closure = Closure::once_into_js(move || start::<E>(setup));
364
365        // make sure to handle JS exceptions thrown inside start.
366        // Otherwise wasm_bindgen_futures Queue would break and never handle any tasks again.
367        // This is required, because winit uses JS exception for control flow to escape from `run`.
368        if let Err(error) = call_catch(&start_closure) {
369            let is_control_flow_exception = error.dyn_ref::<js_sys::Error>().map_or(false, |e| {
370                e.message().includes("Using exceptions for control flow", 0)
371            });
372
373            if !is_control_flow_exception {
374                web_sys::console::error_1(&error);
375            }
376        }
377
378        #[wasm_bindgen]
379        extern "C" {
380            #[wasm_bindgen(catch, js_namespace = Function, js_name = "prototype.call.call")]
381            fn call_catch(this: &JsValue) -> Result<(), JsValue>;
382        }
383    });
384}
385
386#[cfg(target_arch = "wasm32")]
387/// Parse the query string as returned by `web_sys::window()?.location().search()?` and get a
388/// specific key out of it.
389pub fn parse_url_query_string<'a>(query: &'a str, search_key: &str) -> Option<&'a str> {
390    let query_string = query.strip_prefix('?')?;
391
392    for pair in query_string.split('&') {
393        let mut pair = pair.split('=');
394        let key = pair.next()?;
395        let value = pair.next()?;
396
397        if key == search_key {
398            return Some(value);
399        }
400    }
401
402    None
403}