1use std::{future::Future, pin::Pin, sync::Arc};
2
3use glam::UVec2;
4use rend3::{
5 types::{Handedness, SampleCount, Surface, TextureFormat},
6 InstanceAdapterDevice, Renderer,
7};
8use rend3_routine::base::BaseRenderGraph;
9use wgpu::Instance;
10use winit::{
11 dpi::PhysicalSize,
12 event::WindowEvent,
13 event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
14 window::{Window, WindowBuilder, WindowId},
15};
16
17mod assets;
18mod grab;
19#[cfg(target_arch = "wasm32")]
20mod resize_observer;
21
22pub use assets::*;
23pub use grab::*;
24
25pub use parking_lot::{Mutex, MutexGuard};
26pub type Event<'a, T> = winit::event::Event<'a, UserResizeEvent<T>>;
27
28#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub enum UserResizeEvent<T: 'static> {
31 Resize {
33 window_id: WindowId,
34 size: PhysicalSize<u32>,
35 },
36 Other(T),
38}
39
40pub trait App<T: 'static = ()> {
41 const HANDEDNESS: Handedness;
43
44 fn register_logger(&mut self) {
45 #[cfg(target_arch = "wasm32")]
46 console_log::init().unwrap();
47
48 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
49 env_logger::init();
50 }
51
52 fn register_panic_hook(&mut self) {
53 #[cfg(target_arch = "wasm32")]
54 std::panic::set_hook(Box::new(console_error_panic_hook::hook));
55 }
56
57 fn create_window(&mut self, builder: WindowBuilder) -> (EventLoop<UserResizeEvent<T>>, Window) {
58 profiling::scope!("creating window");
59
60 let event_loop = EventLoop::with_user_event();
61 let window = builder.build(&event_loop).expect("Could not build window");
62
63 #[cfg(target_arch = "wasm32")]
64 {
65 use winit::platform::web::WindowExtWebSys;
66
67 let canvas = window.canvas();
68 let style = canvas.style();
69 style.set_property("width", "100%").unwrap();
70 style.set_property("height", "100%").unwrap();
71
72 web_sys::window()
73 .and_then(|win| win.document())
74 .and_then(|doc| doc.body())
75 .and_then(|body| body.append_child(&canvas).ok())
76 .expect("couldn't append canvas to document body");
77 }
78
79 (event_loop, window)
80 }
81
82 fn create_iad<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<InstanceAdapterDevice>> + 'a>> {
83 Box::pin(async move { Ok(rend3::create_iad(None, None, None, None).await?) })
84 }
85
86 fn create_base_rendergraph(&mut self, renderer: &Renderer) -> BaseRenderGraph {
87 BaseRenderGraph::new(renderer)
88 }
89
90 fn sample_count(&self) -> SampleCount;
97
98 fn scale_factor(&self) -> f32 {
100 1.0
101 }
102
103 fn setup(
104 &mut self,
105 window: &Window,
106 renderer: &Arc<Renderer>,
107 routines: &Arc<DefaultRoutines>,
108 surface_format: rend3::types::TextureFormat,
109 ) {
110 let _ = (window, renderer, routines, surface_format);
111 }
112
113 #[allow(clippy::too_many_arguments)]
118 fn handle_event(
119 &mut self,
120 window: &Window,
121 renderer: &Arc<rend3::Renderer>,
122 routines: &Arc<DefaultRoutines>,
123 base_rendergraph: &BaseRenderGraph,
124 surface: Option<&Arc<Surface>>,
125 resolution: UVec2,
126 event: Event<'_, T>,
127 control_flow: impl FnOnce(winit::event_loop::ControlFlow),
128 ) {
129 let _ = (
130 window,
131 renderer,
132 routines,
133 base_rendergraph,
134 resolution,
135 surface,
136 event,
137 control_flow,
138 );
139 }
140}
141
142pub fn lock<T>(lock: &parking_lot::Mutex<T>) -> parking_lot::MutexGuard<'_, T> {
143 #[cfg(target_arch = "wasm32")]
144 let guard = lock.try_lock().expect("Could not lock mutex on single-threaded wasm. Do not hold locks open while an .await causes you to yield execution.");
145 #[cfg(not(target_arch = "wasm32"))]
146 let guard = lock.lock();
147
148 guard
149}
150
151pub struct DefaultRoutines {
152 pub pbr: Mutex<rend3_routine::pbr::PbrRoutine>,
153 pub skybox: Mutex<rend3_routine::skybox::SkyboxRoutine>,
154 pub tonemapping: Mutex<rend3_routine::tonemapping::TonemappingRoutine>,
155}
156
157#[cfg(not(target_arch = "wasm32"))]
158fn winit_run<F, T>(event_loop: winit::event_loop::EventLoop<T>, event_handler: F) -> !
159where
160 F: FnMut(winit::event::Event<'_, T>, &EventLoopWindowTarget<T>, &mut ControlFlow) + 'static,
161 T: 'static,
162{
163 event_loop.run(event_handler)
164}
165
166#[cfg(target_arch = "wasm32")]
167fn winit_run<F, T>(event_loop: EventLoop<T>, event_handler: F)
168where
169 F: FnMut(winit::event::Event<'_, T>, &EventLoopWindowTarget<T>, &mut ControlFlow) + 'static,
170 T: 'static,
171{
172 use wasm_bindgen::{prelude::*, JsCast};
173
174 let winit_closure = Closure::once_into_js(move || event_loop.run(event_handler));
175
176 if let Err(error) = call_catch(&winit_closure) {
181 let is_control_flow_exception = error
182 .dyn_ref::<js_sys::Error>()
183 .map_or(false, |e| e.message().includes("Using exceptions for control flow", 0));
184
185 if !is_control_flow_exception {
186 web_sys::console::error_1(&error);
187 }
188 }
189
190 #[wasm_bindgen]
191 extern "C" {
192 #[wasm_bindgen(catch, js_namespace = Function, js_name = "prototype.call.call")]
193 fn call_catch(this: &JsValue) -> Result<(), JsValue>;
194 }
195}
196
197pub async fn async_start<A: App + 'static>(mut app: A, window_builder: WindowBuilder) {
198 app.register_logger();
199 app.register_panic_hook();
200
201 let (event_loop, window) = app.create_window(window_builder.with_visible(false));
203 let window_size = window.inner_size();
204
205 let iad = app.create_iad().await.unwrap();
206
207 let mut surface = if cfg!(target_os = "android") {
213 None
214 } else {
215 Some(Arc::new(unsafe { iad.instance.create_surface(&window) }))
216 };
217
218 let renderer = rend3::Renderer::new(
220 iad.clone(),
221 A::HANDEDNESS,
222 Some(window_size.width as f32 / window_size.height as f32),
223 )
224 .unwrap();
225
226 let format = surface.as_ref().map_or(TextureFormat::Rgba8UnormSrgb, |s| {
230 let format = s.get_preferred_format(&iad.adapter).unwrap();
231
232 rend3::configure_surface(
234 s,
235 &iad.device,
236 format,
237 glam::UVec2::new(window_size.width, window_size.height),
238 rend3::types::PresentMode::Mailbox,
239 );
240
241 format
242 });
243
244 let base_rendergraph = app.create_base_rendergraph(&renderer);
245 let mut data_core = renderer.data_core.lock();
246 let routines = Arc::new(DefaultRoutines {
247 pbr: Mutex::new(rend3_routine::pbr::PbrRoutine::new(
248 &renderer,
249 &mut data_core,
250 &base_rendergraph.interfaces,
251 )),
252 skybox: Mutex::new(rend3_routine::skybox::SkyboxRoutine::new(
253 &renderer,
254 &base_rendergraph.interfaces,
255 )),
256 tonemapping: Mutex::new(rend3_routine::tonemapping::TonemappingRoutine::new(
257 &renderer,
258 &base_rendergraph.interfaces,
259 format,
260 )),
261 });
262 drop(data_core);
263
264 app.setup(&window, &renderer, &routines, format);
265
266 #[cfg(target_arch = "wasm32")]
267 let _observer = resize_observer::ResizeObserver::new(&window, event_loop.create_proxy());
268
269 window.set_visible(true);
271
272 let mut suspended = cfg!(target_os = "android");
273 let mut last_user_control_mode = ControlFlow::Poll;
274 let mut stored_surface_info = StoredSurfaceInfo {
275 size: glam::UVec2::new(window_size.width, window_size.height),
276 scale_factor: app.scale_factor(),
277 sample_count: app.sample_count(),
278 };
279
280 winit_run(event_loop, move |event, _event_loop, control_flow| {
281 let event = match event {
282 Event::UserEvent(UserResizeEvent::Resize { size, window_id }) => Event::WindowEvent {
283 window_id,
284 event: WindowEvent::Resized(size),
285 },
286 e => e,
287 };
288
289 if let Some(suspend) = handle_surface(
290 &app,
291 &window,
292 &event,
293 &iad.instance,
294 &mut surface,
295 &renderer,
296 format,
297 &mut stored_surface_info,
298 ) {
299 suspended = suspend;
300 }
301
302 match event {
304 Event::Suspended => {
305 *control_flow = ControlFlow::Wait;
306 }
307 Event::Resumed => {
308 *control_flow = last_user_control_mode;
309 }
310 _ => {}
311 }
312
313 if let Event::RedrawRequested(_) | Event::RedrawEventsCleared | Event::MainEventsCleared = event {
315 if suspended {
316 return;
317 }
318 }
319
320 app.handle_event(
321 &window,
322 &renderer,
323 &routines,
324 &base_rendergraph,
325 surface.as_ref(),
326 stored_surface_info.size,
327 event,
328 |c: ControlFlow| {
329 *control_flow = c;
330 last_user_control_mode = c;
331 },
332 )
333 });
334}
335
336struct StoredSurfaceInfo {
337 size: UVec2,
338 scale_factor: f32,
339 sample_count: SampleCount,
340}
341
342#[allow(clippy::too_many_arguments)]
343fn handle_surface<A: App, T: 'static>(
344 app: &A,
345 window: &Window,
346 event: &Event<T>,
347 instance: &Instance,
348 surface: &mut Option<Arc<Surface>>,
349 renderer: &Arc<Renderer>,
350 format: rend3::types::TextureFormat,
351 surface_info: &mut StoredSurfaceInfo,
352) -> Option<bool> {
353 match *event {
354 Event::Resumed => {
355 *surface = Some(Arc::new(unsafe { instance.create_surface(window) }));
356 Some(false)
357 }
358 Event::Suspended => {
359 *surface = None;
360 Some(true)
361 }
362 Event::WindowEvent {
363 event: winit::event::WindowEvent::Resized(size),
364 ..
365 } => {
366 log::debug!("resize {:?}", size);
367 let size = UVec2::new(size.width, size.height);
368
369 if size.x == 0 || size.y == 0 {
370 return Some(false);
371 }
372
373 surface_info.size = size;
374 surface_info.scale_factor = app.scale_factor();
375 surface_info.sample_count = app.sample_count();
376
377 rend3::configure_surface(
379 surface.as_ref().unwrap(),
380 &renderer.device,
381 format,
382 glam::UVec2::new(size.x, size.y),
383 rend3::types::PresentMode::Mailbox,
384 );
385 renderer.set_aspect_ratio(size.x as f32 / size.y as f32);
387 Some(false)
388 }
389 _ => None,
390 }
391}
392
393pub fn start<A: App + 'static>(app: A, window_builder: WindowBuilder) {
394 #[cfg(target_arch = "wasm32")]
395 {
396 wasm_bindgen_futures::spawn_local(async_start(app, window_builder));
397 }
398
399 #[cfg(not(target_arch = "wasm32"))]
400 {
401 pollster::block_on(async_start(app, window_builder));
402 }
403}