Skip to main content

unlit3dx/
present.rs

1//! The multi-world presentation path.
2//!
3//! The machinery a windowed scene presents with -- the [`Presenter`]
4//! component, the window surface, the offscreen target and the behaviours
5//! that pump, resize and present -- lives in
6//! [`unlit3d::winit::presenter`]; this module re-exports it and adds the
7//! multi-world pacing. The host world owns the window and the presenter;
8//! the renderer and the passes live in a render sub-world. Frames are
9//! queued as a command rather than drawn inline, so the host's own async
10//! tasks are polled between the event callbacks, and the draw behaviour
11//! drains the sub-world's command queue before dispatching the frame into
12//! it -- which is what makes the sub-world a server: clients' requests
13//! land as jobs here, on the main thread, between one frame's render and
14//! the next.
15
16use unlit_ecs::{Entity, World};
17
18pub use unlit3d::winit::presenter::{
19    BLIT_PASS_ORDER, Presenter, PresenterError, SCENE_PASS_ORDER, resize_presented,
20};
21
22use unlit3d::frame::{FrameBehaviourOrder, OnFrame, dispatch_frame};
23use unlit3d::offscreen::{OFFSCREEN_SAMPLES, TargetConfig};
24use unlit3d::winit::builtin::{Resumed, window};
25use unlit3d::winit::event::{OnAboutToWait, OnWindowEvent};
26use unlit3d::winit::presenter::{PRESENT_ORDER, PresentationPath, pump_presented_with};
27
28use crate::executor::LocalTasks;
29use crate::gpu::Gpu;
30use crate::host::drain_sub_world;
31
32/// Build the presentation path and spawn its [`Presenter`] in the host
33/// world.
34///
35/// `render`, `renderer` and `scene_pass` name the render sub-world, its
36/// renderer and the pass the scene's own sources draw into, all already
37/// filled in. The window comes from the host world; when it has not
38/// reported a size yet, `fallback_size` stands in.
39///
40/// `offscreen` chooses the path. With it the scene's frames land in a
41/// persistent target that a screenshot reads back, and a pass added after
42/// the scene's copies that target over the swap chain; without it the
43/// scene's pass draws straight into the swap chain -- one pass and one
44/// copy fewer, which is what a plain window wants.
45///
46/// # Errors
47///
48/// Returns an error when the host has no window, the surface cannot be
49/// built, or the render sub-world, its renderer or its targets are gone.
50pub fn setup_presenter(
51    world: &mut World,
52    render: Entity,
53    renderer: Entity,
54    scene_pass: Entity,
55    gpu: &Gpu,
56    fallback_size: (u32, u32),
57    offscreen: bool,
58) -> Result<(), PresenterError> {
59    let surface = gpu
60        .instance
61        .create_surface(window(world).ok_or(PresenterError::NoWindow)?)?;
62    let path = if offscreen {
63        PresentationPath::Intermediate(TargetConfig::default())
64    } else {
65        PresentationPath::Surface {
66            samples: OFFSCREEN_SAMPLES,
67            with_depth: true,
68        }
69    };
70    unlit3d::winit::presenter::setup_presenter(
71        world,
72        Some(render),
73        renderer,
74        scene_pass,
75        surface,
76        &gpu.instance,
77        &gpu.adapter,
78        fallback_size,
79        path,
80    )
81    .map(|_| ())
82}
83
84/// The draw behaviour: drain the render sub-world's command queue, then
85/// dispatch the frame into it and present the result.
86///
87/// `render` names the render sub-world the presenter presents from.
88/// Draining is what makes the sub-world a server: clients' requests land
89/// as jobs here, on the main thread, between one frame's render and the
90/// next.
91///
92/// The renderer walks every pass of the sub-world here, so the scene's own
93/// pass -- and the offscreen path's copying pass, when there is one -- both
94/// record on this one call.
95///
96/// The behaviour holds while the winit host reports the app suspended; a
97/// host with no winit loop counts as resumed. When the host carries more
98/// than one presenter, the first one the query yields presents.
99pub fn draw_presented(render: Entity) -> OnFrame {
100    OnFrame::with_order(
101        FrameBehaviourOrder(PRESENT_ORDER),
102        move |world, _entity, frame| {
103            // A suspended host has no display worth drawing into.
104            let resumed = world
105                .root_scope()
106                .query::<&Resumed>()
107                .iter()
108                .next()
109                .is_none_or(|state| state.0);
110            if !resumed {
111                return;
112            }
113            let mut presenter_rows = world.root_scope().query::<(Entity, &mut Presenter)>();
114
115            let Some(presenter) = presenter_rows.iter().next().map(|(_, p)| p) else {
116                return;
117            };
118            let Ok(mut nested) = world.root_scope().query_one::<&mut World>(render) else {
119                return;
120            };
121            drain_sub_world(&mut *nested.get());
122            // The render world's tasks run between the jobs that queued them
123            // and the dispatch that draws what they landed.
124            LocalTasks::pump(&mut *nested.get());
125            dispatch_frame(&mut *nested.get(), frame);
126            drop(nested);
127            if let Err(error) = presenter.present(world) {
128                log::error!("failed to present a frame: {error}");
129            }
130        },
131    )
132}
133
134/// The behaviours that keep frames coming: one per turn of the event loop,
135/// plus one per compositor redraw request.
136///
137/// The host world's own tasks are polled before each dispatch, so a task one
138/// behaviour spawns is drained by the next frame. Redraw requests alone go
139/// quiet whenever the window stops being visible to the compositor, and a
140/// simulation left waiting on them freezes with it; the pump keeps the scene
141/// advancing on its own.
142pub fn pump_presented() -> (OnAboutToWait, OnWindowEvent) {
143    pump_presented_with(Some(Box::new(LocalTasks::pump)))
144}