rend3_framework/
lib.rs

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/// User event which the framework uses to resize on wasm.
29#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub enum UserResizeEvent<T: 'static> {
31    /// Used to fire off resizing on wasm
32    Resize {
33        window_id: WindowId,
34        size: PhysicalSize<u32>,
35    },
36    /// Custom user event type
37    Other(T),
38}
39
40pub trait App<T: 'static = ()> {
41    /// The handedness of the coordinate system of the renderer.
42    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    /// Determines the sample count used, this may change dynamically. This
91    /// function is what the framework actually calls, so overriding this
92    /// will always use the right values.
93    ///
94    /// It is called on main events cleared and things are remade if this
95    /// changes.
96    fn sample_count(&self) -> SampleCount;
97
98    /// Determines the scale factor used
99    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    /// RedrawRequested/RedrawEventsCleared will only be fired if the window
114    /// size is non-zero. As such you should always render
115    /// in RedrawRequested and use MainEventsCleared for things that need to
116    /// keep running when minimized.
117    #[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    // make sure to handle JS exceptions thrown inside start.
177    // Otherwise wasm_bindgen_futures Queue would break and never handle any tasks
178    // again. This is required, because winit uses JS exception for control flow
179    // to escape from `run`.
180    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    // Create the window invisible until we are rendering
202    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    // The one line of unsafe needed. We just need to guarentee that the window
208    // outlives the use of the surface.
209    //
210    // Android has to defer the surface until `Resumed` is fired. This doesn't fire
211    // on other platforms though :|
212    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    // Make us a renderer.
219    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    // Get the preferred format for the surface.
227    //
228    // Assume android supports Rgba8Srgb, as it has 100% device coverage
229    let format = surface.as_ref().map_or(TextureFormat::Rgba8UnormSrgb, |s| {
230        let format = s.get_preferred_format(&iad.adapter).unwrap();
231
232        // Configure the surface to be ready for rendering.
233        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    // We're ready, so lets make things visible
270    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        // We move to Wait when we get suspended so we don't spin at 50k FPS.
303        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        // We need to block all updates
314        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            // Reconfigure the surface for the new size.
378            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            // Tell the renderer about the new aspect ratio.
386            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}