Skip to main content

ranim_render/
schedule.rs

1use bevy_ecs::{prelude::*, schedule::ScheduleLabel, system::SystemParam};
2use ranim_core::core_item::{camera_frame::CameraFrame, mesh_item::MeshItem, vitem::VItem};
3
4use crate::{
5    pipelines::{mesh_item, oit_resolve, vitem},
6    primitives::{
7        mesh_items::MeshItemsBuffer,
8        viewport::{ViewportGpuPacket, ViewportUniform},
9        vitems::VItemsBuffer,
10    },
11    resource::{RenderTextureState, RenderTextures},
12    utils::WgpuContext,
13    world::SceneOrder,
14};
15
16#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]
17pub(crate) struct RenderPrepare;
18
19#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]
20pub(crate) struct RenderGraph;
21
22#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]
23struct ViewRender;
24
25#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
26enum PrepareSystems {
27    Collect,
28    PrepareResources,
29    Upload,
30    PrepareBindGroups,
31}
32
33#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
34enum RenderGraphSystems {
35    Begin,
36    Render,
37    Submit,
38    Finish,
39}
40
41#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
42enum ViewSystems {
43    Clear,
44    Compute,
45    Depth,
46    Color,
47    Resolve,
48}
49
50#[derive(Resource, Clone, Copy)]
51pub(crate) struct RenderDimensions {
52    pub width: u32,
53    pub height: u32,
54}
55
56#[derive(Resource)]
57pub(crate) struct FrameTarget {
58    pub(crate) render_view: wgpu::TextureView,
59    pub(crate) depth_stencil_view: wgpu::TextureView,
60    pub(crate) depth_bind_group: wgpu::BindGroup,
61    clear_color: wgpu::Color,
62    texture_state: RenderTextureState,
63}
64
65impl FrameTarget {
66    pub(crate) fn new(textures: &RenderTextures, clear_color: wgpu::Color) -> Self {
67        Self {
68            render_view: textures.render_view.clone(),
69            depth_stencil_view: textures.depth_stencil_view.clone(),
70            depth_bind_group: textures.depth_bind_group.clone(),
71            clear_color,
72            texture_state: textures.state(),
73        }
74    }
75}
76
77#[derive(Resource, Default)]
78struct FrameEncoder(Option<wgpu::CommandEncoder>);
79
80#[derive(SystemParam)]
81pub(crate) struct RenderContext<'w> {
82    encoder: ResMut<'w, FrameEncoder>,
83}
84
85impl RenderContext<'_> {
86    pub(crate) fn encoder(&mut self) -> &mut wgpu::CommandEncoder {
87        self.encoder
88            .0
89            .as_mut()
90            .expect("frame encoder was not initialized")
91    }
92}
93
94/// Always-present render profiler resource. GPU timer scopes are
95/// **runtime-toggled** (`RANIM_PROFILE_GPU=1` at startup or
96/// [`RenderProfiler::set_enabled`]): when off (the default) scope wrapping
97/// is a pass-through and no queries are resolved or polled, so profiling
98/// costs nothing; when on, each processed frame's scope tree is stored in
99/// `last_frame_scopes`. `inner` is `None` when the device lacks timestamp
100/// query support.
101#[derive(Resource)]
102pub(crate) struct RenderProfiler {
103    pub(crate) inner: Option<wgpu_profiler::GpuProfiler>,
104    enabled: std::sync::atomic::AtomicBool,
105    /// GPU timer scopes of the most recent processed frame.
106    pub(crate) last_frame_scopes: Option<Vec<wgpu_profiler::GpuTimerQueryResult>>,
107}
108
109impl RenderProfiler {
110    pub(crate) fn new(_ctx: &WgpuContext) -> Self {
111        Self {
112            inner: wgpu_profiler::GpuProfiler::new(
113                &_ctx.device,
114                wgpu_profiler::GpuProfilerSettings::default(),
115            )
116            .ok(),
117            enabled: std::sync::atomic::AtomicBool::new(
118                std::env::var("RANIM_PROFILE_GPU").is_ok_and(|v| v != "0" && !v.is_empty()),
119            ),
120            last_frame_scopes: None,
121        }
122    }
123
124    pub(crate) fn is_enabled(&self) -> bool {
125        self.enabled.load(std::sync::atomic::Ordering::Relaxed)
126    }
127
128    pub(crate) fn set_enabled(&self, on: bool) {
129        self.enabled.store(on, std::sync::atomic::Ordering::Relaxed);
130    }
131
132    /// Run `f` with `pass` wrapped in a GPU timer scope labeled `label`.
133    /// Pass-through while GPU timers are disabled or unsupported.
134    pub(crate) fn scope_pass<R, T>(
135        &self,
136        label: &str,
137        pass: &mut R,
138        f: impl FnOnce(&mut R) -> T,
139    ) -> T
140    where
141        R: wgpu_profiler::ProfilerCommandRecorder,
142    {
143        if !self.is_enabled() {
144            return f(pass);
145        }
146        let Some(profiler) = self.inner.as_ref() else {
147            return f(pass);
148        };
149        let mut scope = profiler.scope(label.to_string(), pass);
150        f(&mut *scope)
151    }
152}
153
154pub(crate) fn install_schedules(world: &mut World) {
155    world.init_resource::<FrameEncoder>();
156    let mut prepare = Schedule::new(RenderPrepare);
157    prepare.configure_sets(
158        (
159            PrepareSystems::Collect,
160            PrepareSystems::PrepareResources,
161            PrepareSystems::Upload,
162            PrepareSystems::PrepareBindGroups,
163        )
164            .chain(),
165    );
166    prepare.add_systems(
167        (prepare_vitems, prepare_mesh_items)
168            .in_set(PrepareSystems::PrepareResources)
169            .ambiguous_with_all(),
170    );
171    world.add_schedule(prepare);
172
173    let mut view = Schedule::new(ViewRender);
174    view.configure_sets(
175        (
176            ViewSystems::Clear,
177            ViewSystems::Compute,
178            ViewSystems::Depth,
179            ViewSystems::Color,
180            ViewSystems::Resolve,
181        )
182            .chain(),
183    );
184    view.add_systems(clear.in_set(ViewSystems::Clear));
185    view.add_systems(vitem::compute.in_set(ViewSystems::Compute));
186    view.add_systems(
187        (vitem::depth, mesh_item::depth)
188            .chain()
189            .in_set(ViewSystems::Depth),
190    );
191    view.add_systems(
192        (vitem::color, mesh_item::color)
193            .chain()
194            .in_set(ViewSystems::Color),
195    );
196    view.add_systems(oit_resolve::resolve.in_set(ViewSystems::Resolve));
197    world.add_schedule(view);
198
199    let mut graph = Schedule::new(RenderGraph);
200    graph.configure_sets(
201        (
202            RenderGraphSystems::Begin,
203            RenderGraphSystems::Render,
204            RenderGraphSystems::Submit,
205            RenderGraphSystems::Finish,
206        )
207            .chain(),
208    );
209    graph.add_systems(begin_frame.in_set(RenderGraphSystems::Begin));
210    graph.add_systems(view_driver.in_set(RenderGraphSystems::Render));
211    graph.add_systems(submit.in_set(RenderGraphSystems::Submit));
212    graph.add_systems(finish_frame.in_set(RenderGraphSystems::Finish));
213    world.add_schedule(graph);
214}
215
216fn prepare_vitems(
217    ctx: Res<WgpuContext>,
218    mut buffer: ResMut<VItemsBuffer>,
219    items: Query<(&SceneOrder, &VItem)>,
220) {
221    let _span = crate::cpu_probe::span("prepare_vitems");
222    let mut items = items.iter().collect::<Vec<_>>();
223    items.sort_by_key(|(order, _)| order.0);
224    buffer.update(
225        &ctx,
226        items.iter().map(|&(order, item)| (order.0 as f32, item)),
227    );
228}
229
230fn prepare_mesh_items(
231    ctx: Res<WgpuContext>,
232    mut buffer: ResMut<MeshItemsBuffer>,
233    items: Query<(&SceneOrder, &MeshItem)>,
234) {
235    let _span = crate::cpu_probe::span("prepare_mesh_items");
236    let mut items = items.iter().collect::<Vec<_>>();
237    items.sort_by_key(|(order, _)| order.0);
238    buffer.update(
239        &ctx,
240        items.iter().map(|&(order, item)| (order.0 as f32, item)),
241    );
242}
243
244fn begin_frame(ctx: Res<WgpuContext>, mut encoder: ResMut<FrameEncoder>) {
245    encoder.0 = Some(
246        ctx.device
247            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()),
248    );
249}
250
251fn clear(mut render: RenderContext, target: Res<FrameTarget>, profiler: Res<RenderProfiler>) {
252    let pass_desc = wgpu::RenderPassDescriptor {
253        label: Some("Clear Pass"),
254        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
255            depth_slice: None,
256            view: &target.render_view,
257            resolve_target: None,
258            ops: wgpu::Operations {
259                load: wgpu::LoadOp::Clear(target.clear_color),
260                store: wgpu::StoreOp::Store,
261            },
262        })],
263        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
264            view: &target.depth_stencil_view,
265            depth_ops: Some(wgpu::Operations {
266                load: wgpu::LoadOp::Clear(1.0),
267                store: wgpu::StoreOp::Store,
268            }),
269            stencil_ops: None,
270        }),
271        timestamp_writes: None,
272        occlusion_query_set: None,
273        multiview_mask: None,
274    };
275    let encoder = render.encoder();
276    let mut pass = encoder.begin_render_pass(&pass_desc);
277    profiler.scope_pass("clear", &mut pass, |_| ());
278}
279
280fn view_driver(world: &mut World) {
281    let mut cameras = world
282        .query::<(&SceneOrder, &CameraFrame)>()
283        .iter(world)
284        .map(|(order, camera)| (order.0, camera.clone()))
285        .collect::<Vec<_>>();
286    cameras.sort_by_key(|(order, _)| *order);
287
288    let camera = take_single_camera(cameras);
289    let dimensions = *world.resource::<RenderDimensions>();
290    // The per-order depth bias epsilon: the fixed span is split evenly across
291    // all items of the frame so the total offset stays bounded.
292    let item_count = world.resource::<VItemsBuffer>().item_count()
293        + world.resource::<MeshItemsBuffer>().item_count();
294    let bias_epsilon = crate::primitives::viewport::DEPTH_ORDER_SPAN / item_count.max(1) as f32;
295    let uniform = ViewportUniform::from_camera_frame(
296        &camera,
297        dimensions.width,
298        dimensions.height,
299        bias_epsilon,
300    );
301    world.resource_scope(|world, mut viewport: Mut<ViewportGpuPacket>| {
302        viewport.update(world.resource::<WgpuContext>(), &uniform);
303    });
304    world.run_schedule(ViewRender);
305}
306
307fn take_single_camera(mut cameras: Vec<(usize, CameraFrame)>) -> CameraFrame {
308    assert_eq!(
309        cameras.len(),
310        1,
311        "D0002 requires exactly one active CameraFrame per rendered frame"
312    );
313    cameras.pop().unwrap().1
314}
315
316fn submit(
317    ctx: Res<WgpuContext>,
318    mut encoder: ResMut<FrameEncoder>,
319    mut profiler: ResMut<RenderProfiler>,
320) {
321    let mut encoder = encoder.0.take().expect("frame encoder was not initialized");
322    if profiler.is_enabled()
323        && let Some(inner) = profiler.inner.as_mut()
324    {
325        inner.resolve_queries(&mut encoder);
326    }
327    ctx.queue.submit(Some(encoder.finish()));
328}
329
330fn finish_frame(
331    target: Res<FrameTarget>,
332    ctx: Res<WgpuContext>,
333    mut profiler: ResMut<RenderProfiler>,
334) {
335    // Processing timer queries forces a device poll (a sync point), so it
336    // only happens while GPU timers are explicitly enabled.
337    if profiler.is_enabled()
338        && let Some(inner) = profiler.inner.as_mut()
339        && inner.end_frame().is_ok()
340    {
341        ctx.device
342            .poll(wgpu::PollType::wait_indefinitely())
343            .unwrap();
344        if let Some(results) = inner.process_finished_frame(ctx.queue.get_timestamp_period()) {
345            profiler.last_frame_scopes = Some(results);
346        }
347    }
348    target.texture_state.mark_dirty();
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn single_camera_is_accepted() {
357        let camera = CameraFrame::default();
358
359        assert_eq!(take_single_camera(vec![(7, camera.clone())]), camera);
360    }
361
362    #[test]
363    #[should_panic(expected = "D0002 requires exactly one active CameraFrame")]
364    fn missing_camera_is_rejected() {
365        take_single_camera(Vec::new());
366    }
367
368    #[test]
369    #[should_panic(expected = "D0002 requires exactly one active CameraFrame")]
370    fn multiple_cameras_are_rejected() {
371        take_single_camera(vec![
372            (0, CameraFrame::default()),
373            (1, CameraFrame::default()),
374        ]);
375    }
376}