1use bevy_app::{First, SubApp};
2use bevy_ecs::{prelude::*, schedule::ScheduleLabel, system::SystemParam};
3
4use crate::{
5 pipelines::oit_resolve,
6 resource::{RenderTextureState, RenderTextures},
7 utils::WgpuContext,
8 world::resolve_scene_order,
9};
10
11#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]
13struct RenderUpdate;
14
15#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]
17struct RenderGraph;
18
19#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]
21pub enum RenderSchedule {
22 Extract,
25 Prepare,
27 View,
29}
30
31#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
33pub enum ViewPhase {
34 Clear,
36 Compute,
38 Depth,
40 Color,
42 Resolve,
44}
45
46#[derive(Resource, Clone, Copy)]
47pub struct RenderDimensions {
48 pub width: u32,
49 pub height: u32,
50}
51
52#[derive(Resource)]
53pub struct FrameTarget {
54 pub(crate) render_view: wgpu::TextureView,
55 pub(crate) depth_stencil_view: wgpu::TextureView,
56 pub(crate) depth_bind_group: wgpu::BindGroup,
57 clear_color: wgpu::Color,
58 texture_state: RenderTextureState,
59}
60
61impl FrameTarget {
62 pub fn render_view(&self) -> &wgpu::TextureView {
64 &self.render_view
65 }
66
67 pub fn depth_stencil_view(&self) -> &wgpu::TextureView {
69 &self.depth_stencil_view
70 }
71
72 pub(crate) fn new(textures: &RenderTextures, clear_color: wgpu::Color) -> Self {
73 Self {
74 render_view: textures.render_view.clone(),
75 depth_stencil_view: textures.depth_stencil_view.clone(),
76 depth_bind_group: textures.depth_bind_group.clone(),
77 clear_color,
78 texture_state: textures.state(),
79 }
80 }
81}
82
83#[derive(Resource, Default)]
84struct FrameEncoder(Option<wgpu::CommandEncoder>);
85
86#[derive(SystemParam)]
87pub struct RenderContext<'w> {
88 encoder: ResMut<'w, FrameEncoder>,
89}
90
91impl RenderContext<'_> {
92 pub fn encoder(&mut self) -> &mut wgpu::CommandEncoder {
93 self.encoder
94 .0
95 .as_mut()
96 .expect("frame encoder was not initialized")
97 }
98}
99
100#[derive(Resource)]
108pub struct RenderProfiler {
109 pub(crate) inner: Option<wgpu_profiler::GpuProfiler>,
110 enabled: std::sync::atomic::AtomicBool,
111 pub(crate) last_frame_scopes: Option<Vec<wgpu_profiler::GpuTimerQueryResult>>,
113}
114
115impl RenderProfiler {
116 pub(crate) fn new(_ctx: &WgpuContext) -> Self {
117 Self {
118 inner: wgpu_profiler::GpuProfiler::new(
119 &_ctx.device,
120 wgpu_profiler::GpuProfilerSettings::default(),
121 )
122 .ok(),
123 enabled: std::sync::atomic::AtomicBool::new(
124 std::env::var("RANIM_PROFILE_GPU").is_ok_and(|v| v != "0" && !v.is_empty()),
125 ),
126 last_frame_scopes: None,
127 }
128 }
129
130 pub(crate) fn is_enabled(&self) -> bool {
131 self.enabled.load(std::sync::atomic::Ordering::Relaxed)
132 }
133
134 pub(crate) fn set_enabled(&self, on: bool) {
135 self.enabled.store(on, std::sync::atomic::Ordering::Relaxed);
136 }
137
138 pub fn scope_pass<R, T>(&self, label: &str, pass: &mut R, f: impl FnOnce(&mut R) -> T) -> T
141 where
142 R: wgpu_profiler::ProfilerCommandRecorder,
143 {
144 if !self.is_enabled() {
145 return f(pass);
146 }
147 let Some(profiler) = self.inner.as_ref() else {
148 return f(pass);
149 };
150 let mut scope = profiler.scope(label.to_string(), pass);
151 f(&mut *scope)
152 }
153}
154
155pub(crate) fn install_schedules(app: &mut SubApp) {
156 app.update_schedule = Some(RenderUpdate.intern());
157 app.init_resource::<FrameEncoder>()
158 .init_schedule(RenderSchedule::Extract)
159 .init_schedule(RenderSchedule::Prepare)
160 .add_systems(RenderUpdate, render_update);
161 app.configure_sets(
162 RenderSchedule::View,
163 (
164 ViewPhase::Clear,
165 ViewPhase::Compute,
166 ViewPhase::Depth,
167 ViewPhase::Color,
168 ViewPhase::Resolve,
169 )
170 .chain(),
171 );
172 app.add_systems(RenderSchedule::View, clear.in_set(ViewPhase::Clear));
173 app.add_systems(
174 RenderSchedule::View,
175 oit_resolve::resolve.in_set(ViewPhase::Resolve),
176 );
177
178 app.add_systems(
179 RenderGraph,
180 (begin_frame, view_driver, submit, finish_frame).chain(),
181 );
182}
183
184fn render_update(world: &mut World) {
185 world.run_schedule(First);
186 world.run_schedule(RenderSchedule::Extract);
187 resolve_scene_order(world);
188 world.run_schedule(RenderSchedule::Prepare);
189 let _span = crate::cpu_probe::span("render_graph");
190 world.run_schedule(RenderGraph);
191}
192
193fn begin_frame(ctx: Res<WgpuContext>, mut encoder: ResMut<FrameEncoder>) {
194 encoder.0 = Some(
195 ctx.device
196 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()),
197 );
198}
199
200fn clear(mut render: RenderContext, target: Res<FrameTarget>, profiler: Res<RenderProfiler>) {
201 let pass_desc = wgpu::RenderPassDescriptor {
202 label: Some("Clear Pass"),
203 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
204 depth_slice: None,
205 view: &target.render_view,
206 resolve_target: None,
207 ops: wgpu::Operations {
208 load: wgpu::LoadOp::Clear(target.clear_color),
209 store: wgpu::StoreOp::Store,
210 },
211 })],
212 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
213 view: &target.depth_stencil_view,
214 depth_ops: Some(wgpu::Operations {
215 load: wgpu::LoadOp::Clear(1.0),
216 store: wgpu::StoreOp::Store,
217 }),
218 stencil_ops: None,
219 }),
220 timestamp_writes: None,
221 occlusion_query_set: None,
222 multiview_mask: None,
223 };
224 let encoder = render.encoder();
225 let mut pass = encoder.begin_render_pass(&pass_desc);
226 profiler.scope_pass("clear", &mut pass, |_| ());
227}
228
229fn view_driver(world: &mut World) {
230 world.run_schedule(RenderSchedule::View);
231}
232
233fn submit(
234 ctx: Res<WgpuContext>,
235 mut encoder: ResMut<FrameEncoder>,
236 mut profiler: ResMut<RenderProfiler>,
237) {
238 let mut encoder = encoder.0.take().expect("frame encoder was not initialized");
239 if profiler.is_enabled()
240 && let Some(inner) = profiler.inner.as_mut()
241 {
242 inner.resolve_queries(&mut encoder);
243 }
244 ctx.queue.submit(Some(encoder.finish()));
245}
246
247fn finish_frame(
248 target: Res<FrameTarget>,
249 ctx: Res<WgpuContext>,
250 mut profiler: ResMut<RenderProfiler>,
251) {
252 if profiler.is_enabled()
255 && let Some(inner) = profiler.inner.as_mut()
256 && inner.end_frame().is_ok()
257 {
258 ctx.device
259 .poll(wgpu::PollType::wait_indefinitely())
260 .unwrap();
261 if let Some(results) = inner.process_finished_frame(ctx.queue.get_timestamp_period()) {
262 profiler.last_frame_scopes = Some(results);
263 }
264 }
265 target.texture_state.mark_dirty();
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn registered_phases_enforce_the_shared_view_order() {
274 #[derive(Resource, Default)]
275 struct ExecutionLog(Vec<&'static str>);
276
277 let mut app = SubApp::new();
278 app.init_resource::<ExecutionLog>();
279 app.update_schedule = Some(RenderSchedule::View.intern());
280 app.configure_sets(
281 RenderSchedule::View,
282 (
283 ViewPhase::Clear,
284 ViewPhase::Compute,
285 ViewPhase::Depth,
286 ViewPhase::Color,
287 ViewPhase::Resolve,
288 )
289 .chain(),
290 );
291 app.add_systems(
293 RenderSchedule::View,
294 (
295 (|mut log: ResMut<ExecutionLog>| log.0.push("resolve")).in_set(ViewPhase::Resolve),
296 (|mut log: ResMut<ExecutionLog>| log.0.push("color")).in_set(ViewPhase::Color),
297 (|mut log: ResMut<ExecutionLog>| log.0.push("depth")).in_set(ViewPhase::Depth),
298 (|mut log: ResMut<ExecutionLog>| log.0.push("compute")).in_set(ViewPhase::Compute),
299 (|mut log: ResMut<ExecutionLog>| log.0.push("clear")).in_set(ViewPhase::Clear),
300 ),
301 );
302 app.update();
303 assert_eq!(
304 app.world().resource::<ExecutionLog>().0,
305 ["clear", "compute", "depth", "color", "resolve"]
306 );
307 }
308
309 #[test]
310 fn render_update_runs_extraction_before_preparation_and_graph() {
311 #[derive(Resource, Default)]
312 struct ExecutionLog(Vec<&'static str>);
313
314 let mut app = SubApp::new();
315 install_schedules(&mut app);
316 app.init_resource::<ExecutionLog>();
317 app.add_systems(First, |mut log: ResMut<ExecutionLog>| {
318 log.0.push("first");
319 });
320 app.add_systems(RenderSchedule::Extract, |mut log: ResMut<ExecutionLog>| {
321 log.0.push("extract");
322 });
323 app.add_systems(RenderSchedule::Prepare, |mut log: ResMut<ExecutionLog>| {
324 log.0.push("prepare");
325 });
326 app.add_schedule(Schedule::new(RenderGraph));
329 app.add_systems(RenderGraph, |mut log: ResMut<ExecutionLog>| {
330 log.0.push("graph")
331 });
332
333 app.update();
334 app.update();
335 assert_eq!(
336 app.world().resource::<ExecutionLog>().0,
337 ["first", "extract", "prepare", "graph"].repeat(2)
338 );
339 }
340}