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() }
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 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 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); *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 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 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")]
387pub 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}