lumen_core/
render_backend.rs1use 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#[derive(Clone, Copy, Debug)]
22pub struct RenderBackend {
23 pub name: &'static str,
25 pub priority: i32,
27 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
45pub type RenderBackends = Backends<RenderBackend>;
49
50pub 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
64fn 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
73fn 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 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 #[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 #[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 #[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 #[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}