Skip to main content

lumen_core/
render_backend.rs

1//! The render backends a binary carries, how a launch picks one, and the
2//! render-world system that drives a renderer drawing offscreen.
3//!
4//! The core names no backend. Each one registers a [`RenderBackend`] into the
5//! app's [`RenderBackends`] while the app is built, from its own crate, and a
6//! launch path asks the registry for what the app's configuration names: one
7//! backend by name, or every backend in priority order so a launch can fall
8//! through to the next one when the first cannot start. A binary that does
9//! not link a backend has no entry for it, which is how an app pays only for
10//! the renderer it uses. The registry is the one every backend kind shares;
11//! see [`crate::backends`].
12
13use crate::app::App;
14use crate::backends::{BackendEntry, Backends};
15use crate::render_world::{RenderStage, Viewport, install_extract_pipeline};
16use crate::traits::{FrameRequest, Renderer};
17use bevy_ecs::world::World;
18
19/// One render backend: its name, its rank under `auto`, and how to make its
20/// renderer.
21#[derive(Clone, Copy, Debug)]
22pub struct RenderBackend {
23    /// The name an app's configuration selects it by.
24    pub name: &'static str,
25    /// Trial order when the configuration names no backend: higher first.
26    pub priority: i32,
27    /// A renderer bound to nothing yet. Construction does no work; binding
28    /// to a window or an offscreen image happens in [`Renderer::attach`],
29    /// and a failure there is what lets a launch try the next backend.
30    pub renderer: fn() -> Box<dyn Renderer>,
31}
32
33impl BackendEntry for RenderBackend {
34    const KIND: &'static str = "render";
35
36    fn name(&self) -> &'static str {
37        self.name
38    }
39
40    fn priority(&self) -> i32 {
41        self.priority
42    }
43}
44
45/// Main-world registry of the render backends this binary carries. A
46/// backend's capability install adds itself with
47/// [`crate::backends::register_backend`].
48pub type RenderBackends = Backends<RenderBackend>;
49
50/// Put `renderer`, already attached to a
51/// [`crate::traits::FrameTarget::Offscreen`] image, into `app`.
52///
53/// The renderer becomes a non-send render-world resource of type `R`, and a
54/// system in [`RenderStage::Render`] drives it through [`Renderer`] each
55/// frame the tick renders: it resizes the image to the viewport's physical
56/// size and presents when the renderer says the frame changed. Also installs
57/// the extract pipeline the renderer reads the retained scene from.
58pub fn install_offscreen<R: Renderer>(app: &mut App, renderer: R) {
59    install_extract_pipeline(app);
60    app.render_world.insert_non_send(renderer);
61    app.add_render_systems(RenderStage::Render, present_offscreen::<R>);
62}
63
64/// The viewport's size in physical pixels, at least one each way.
65fn physical_size(viewport: &Viewport) -> (u32, u32) {
66    let dpr = viewport.scale_factor.max(0.01);
67    (
68        (viewport.size.x * dpr).max(1.0) as u32,
69        (viewport.size.y * dpr).max(1.0) as u32,
70    )
71}
72
73/// Render-world system driving the offscreen renderer installed as `R`.
74///
75/// The renderer is lifted out of the world for the call, because
76/// [`Renderer::present`] reads the same world it lives in.
77fn present_offscreen<R: Renderer>(world: &mut World) {
78    let Some(mut renderer) = world.remove_non_send::<R>() else {
79        return;
80    };
81    let (width, height) = world
82        .get_resource::<Viewport>()
83        .map(physical_size)
84        .unwrap_or((1, 1));
85    let resized = renderer.resize(width, height);
86    // The tick runs the render schedule only when its frame is dirty, so the
87    // request always is; a fresh image holds no frame and repaints in full.
88    let request = FrameRequest {
89        dirty: true,
90        force_full: resized,
91    };
92    if renderer.wants_present(world, request)
93        && let Err(e) = renderer.present(world)
94    {
95        eprintln!("lumen: offscreen render failed: {e}");
96    }
97    world.insert_non_send(renderer);
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::traits::{FrameTarget, RenderError};
104    use bevy_ecs::world::World;
105
106    /// A renderer that records what it was asked and answers `true` to
107    /// every present gate, so the offscreen driver can be watched.
108    #[derive(Default)]
109    struct Recorder {
110        attached: Option<(u32, u32)>,
111        requests: Vec<FrameRequest>,
112        presents: usize,
113    }
114
115    impl Renderer for Recorder {
116        fn attach(&mut self, target: FrameTarget) -> Result<(), RenderError> {
117            match target {
118                FrameTarget::Offscreen { width, height } => {
119                    self.attached = Some((width, height));
120                    Ok(())
121                }
122                FrameTarget::Window(_) => Err(RenderError::Init("no windows here".into())),
123            }
124        }
125        fn resize(&mut self, width: u32, height: u32) -> bool {
126            let changed = self.attached != Some((width, height));
127            self.attached = Some((width, height));
128            changed
129        }
130        fn wants_present(&mut self, _: &mut World, request: FrameRequest) -> bool {
131            self.requests.push(request);
132            true
133        }
134        fn present(&mut self, _: &mut World) -> Result<(), RenderError> {
135            self.presents += 1;
136            Ok(())
137        }
138        fn detach(&mut self) {
139            self.attached = None;
140        }
141    }
142
143    fn renderer() -> Box<dyn Renderer> {
144        Box::new(Recorder::default())
145    }
146
147    fn backend(name: &'static str, priority: i32) -> RenderBackend {
148        RenderBackend {
149            name,
150            priority,
151            renderer,
152        }
153    }
154
155    /// What a launch gets back from the registry is the backend's own
156    /// constructor: the renderer it builds binds to the targets it supports
157    /// and refuses the ones it does not, with the reason intact.
158    #[test]
159    fn a_selected_backend_builds_the_renderer_it_registered() {
160        let mut backends = RenderBackends::default();
161        backends.register(backend("test", 1));
162        let [test] = backends.select(None).expect("one").try_into().expect("one");
163
164        let mut renderer = (test.renderer)();
165        renderer
166            .attach(FrameTarget::Offscreen {
167                width: 8,
168                height: 8,
169            })
170            .expect("offscreen binds");
171        assert!(!renderer.resize(8, 8));
172        assert!(renderer.resize(9, 8));
173        renderer.detach();
174    }
175
176    /// A render backend the app names and the build lacks is reported as a
177    /// render backend, by the name the configuration used.
178    #[test]
179    fn a_missing_render_backend_says_render() {
180        let mut backends = RenderBackends::default();
181        backends.register(backend("gpu", 1));
182        let err = backends.select(Some("cpu")).expect_err("missing");
183        assert_eq!(
184            err,
185            "the app asks for the 'cpu' render backend, and this build carries only 'gpu'"
186        );
187    }
188
189    /// The offscreen driver sizes the image from the viewport in physical
190    /// pixels, asks for a frame on every rendered tick, and forces a full
191    /// one when the size changed.
192    #[test]
193    fn the_offscreen_driver_follows_the_viewport() {
194        let mut app = App::new();
195        let mut recorder = Recorder::default();
196        recorder
197            .attach(FrameTarget::Offscreen {
198                width: 20,
199                height: 10,
200            })
201            .expect("binds");
202        install_offscreen(&mut app, recorder);
203        for world in [&mut app.world, &mut app.render_world] {
204            let mut viewport = world.resource_mut::<Viewport>();
205            viewport.size = glam::Vec2::new(10.0, 5.0);
206            viewport.scale_factor = 2.0;
207        }
208
209        app.tick();
210        let recorder = app.render_world.non_send::<Recorder>();
211        assert_eq!(recorder.attached, Some((20, 10)));
212        assert_eq!(recorder.presents, 1);
213        assert!(recorder.requests[0].dirty);
214        assert!(!recorder.requests[0].force_full, "the size did not change");
215
216        for world in [&mut app.world, &mut app.render_world] {
217            world.resource_mut::<Viewport>().size = glam::Vec2::new(12.0, 5.0);
218        }
219        app.tick();
220        let recorder = app.render_world.non_send::<Recorder>();
221        assert_eq!(recorder.attached, Some((24, 10)));
222        assert!(recorder.requests[1].force_full, "a resize repaints in full");
223    }
224}