Skip to main content

bevy_core_pipeline/
schedule.rs

1//! The core rendering pipelines schedules. These schedules define the "default" render graph
2//! for 2D and 3D rendering in Bevy.
3//!
4//! Rendering in Bevy is "camera driven", meaning that for each camera in the world, its
5//! associated rendering schedule is executed. This allows different cameras to have different
6//! rendering pipelines, for example a 3D camera with post-processing effects and a 2D camera
7//! with a simple clear and sprite rendering.
8//!
9//! The [`camera_driver`] system is responsible for iterating over all cameras in the world
10//! and executing their associated schedules. In this way, the schedule for each camera is a
11//! sub-schedule or sub-graph of the root render graph schedule.
12use core::fmt::{self, Display, Formatter};
13
14use bevy_camera::{ClearColor, NormalizedRenderTarget};
15use bevy_ecs::{
16    entity::EntityHashSet,
17    prelude::*,
18    schedule::{InternedScheduleLabel, IntoScheduleConfigs, Schedule, ScheduleLabel, SystemSet},
19    system::SystemState,
20};
21#[cfg(feature = "trace")]
22use bevy_log::info_span;
23use bevy_reflect::Reflect;
24use bevy_render::{
25    camera::{ExtractedCamera, SortedCameras},
26    diagnostic::{DiagnosticsRecorder, RecordDiagnostics},
27    render_resource::{
28        CommandEncoderDescriptor, LoadOp, Operations, RenderPassColorAttachment,
29        RenderPassDescriptor, StoreOp,
30    },
31    renderer::{CurrentView, PendingCommandBuffers, RenderDevice, RenderQueue},
32    sync_world::MainEntity,
33    view::ExtractedWindow,
34};
35
36/// Schedule label for the Core 3D rendering pipeline.
37#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash, Default)]
38pub struct Core3d;
39
40/// System sets for the Core 3D rendering pipeline, defining the main stages of rendering.
41/// These stages include and run in the following order:
42/// - `Prepass`: Initial rendering operations, such as depth pre-pass.
43/// - `MainPass`: The primary rendering operations, including drawing opaque and transparent objects.
44/// - `EarlyPostProcess`: Initial post processing effects.
45/// - `PostProcess`: Final rendering operations, such as post-processing effects.
46///
47/// Additional systems can be added to these sets to customize the rendering pipeline, or additional
48/// sets can be created relative to these core sets.
49#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
50pub enum Core3dSystems {
51    Prepass,
52    MainPass,
53    EarlyPostProcess,
54    PostProcess,
55}
56
57impl Core3d {
58    pub fn base_schedule() -> Schedule {
59        use bevy_ecs::schedule::ScheduleBuildSettings;
60        use Core3dSystems::*;
61
62        let mut schedule = Schedule::new(Self);
63
64        schedule.set_build_settings(ScheduleBuildSettings {
65            auto_insert_apply_deferred: false,
66            ..Default::default()
67        });
68
69        schedule.configure_sets((Prepass, MainPass, EarlyPostProcess, PostProcess).chain_weak());
70
71        schedule
72    }
73}
74
75/// Schedule label for the Core 2D rendering pipeline.
76#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash, Default)]
77pub struct Core2d;
78
79/// System sets for the Core 2D rendering pipeline, defining the main stages of rendering.
80/// These stages include and run in the following order:
81/// - `Prepass`: Initial rendering operations, such as depth pre-pass.
82/// - `MainPass`: The primary rendering operations, including drawing 2D sprites and meshes.
83/// - `EarlyPostProcess`: Initial post processing effects.
84/// - `PostProcess`: Final rendering operations, such as post-processing effects.
85///
86/// Additional systems can be added to these sets to customize the rendering pipeline, or additional
87/// sets can be created relative to these core sets.
88#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
89pub enum Core2dSystems {
90    Prepass,
91    MainPass,
92    EarlyPostProcess,
93    PostProcess,
94}
95
96impl Core2d {
97    pub fn base_schedule() -> Schedule {
98        use bevy_ecs::schedule::ScheduleBuildSettings;
99        use Core2dSystems::*;
100
101        let mut schedule = Schedule::new(Self);
102
103        schedule.set_build_settings(ScheduleBuildSettings {
104            auto_insert_apply_deferred: false,
105            ..Default::default()
106        });
107
108        schedule.configure_sets((Prepass, MainPass, EarlyPostProcess, PostProcess).chain_weak());
109
110        schedule
111    }
112}
113
114/// Holds the entity of windows that are a render target for a camera
115#[derive(Resource)]
116struct CameraWindows(EntityHashSet);
117
118/// A render-world marker component for a view that corresponds to neither a
119/// camera nor a camera-associated shadow map.
120///
121/// This is used for point light and spot light shadow maps, since these aren't
122/// associated with views.
123#[derive(Clone, Copy, Component, Debug, Reflect)]
124#[reflect(Clone, Component)]
125#[reflect(from_reflect = false)]
126pub struct RootNonCameraView(#[reflect(ignore)] pub InternedScheduleLabel);
127
128/// The default entry point for camera driven rendering added to the root [`bevy_render::renderer::RenderGraph`]
129/// schedule. This system iterates over all cameras in the world, executing their associated
130/// rendering schedules defined by the [`bevy_render::camera::CameraRenderGraph`] component.
131///
132/// After executing all camera schedules, it submits any pending command buffers to the GPU
133/// and clears any swap chains that were not covered by a camera. Users can order any additional
134/// operations (e.g. one-off compute passes) before or after this system in the root render
135/// graph schedule.
136pub fn camera_driver(
137    world: &mut World,
138    state: &mut SystemState<Query<(MainEntity, &ExtractedWindow)>>,
139) {
140    // Gather up all cameras and auxiliary views not associated with a camera.
141    let root_views: Vec<_> = {
142        let mut auxiliary_views = world.query_filtered::<Entity, With<RootNonCameraView>>();
143        let sorted = world.resource::<SortedCameras>();
144        auxiliary_views
145            .iter(world)
146            .map(RootView::Auxiliary)
147            .chain(sorted.0.iter().map(|c| RootView::Camera {
148                entity: c.entity,
149                order: c.order,
150            }))
151            .collect()
152    };
153
154    let mut camera_windows = EntityHashSet::default();
155
156    let diagnostics_enabled = world.contains_resource::<DiagnosticsRecorder>();
157    let mut reached_root_camera = false;
158    if diagnostics_enabled {
159        let device = world.resource::<RenderDevice>().clone();
160        let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
161            label: Some("camera_driver_diagnostic_span"),
162        });
163        world
164            .resource::<DiagnosticsRecorder>()
165            .begin_time_span(&mut encoder, "auxiliary_views".into());
166        world
167            .resource_mut::<PendingCommandBuffers>()
168            .push_encoder(encoder, "camera_driver_span_begin");
169    }
170    let end_time_span = |world: &mut World| {
171        let device = world.resource::<RenderDevice>().clone();
172        let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
173            label: Some("camera_driver_diagnostic_span"),
174        });
175        world
176            .resource::<DiagnosticsRecorder>()
177            .end_time_span(&mut encoder);
178        world
179            .resource_mut::<PendingCommandBuffers>()
180            .push_encoder(encoder, "camera_driver_span_end");
181    };
182
183    for root_view in root_views {
184        let mut run_schedule = true;
185        let (schedule, view_entity);
186
187        match root_view {
188            RootView::Camera {
189                entity: camera_entity,
190                ..
191            } => {
192                if diagnostics_enabled && !reached_root_camera {
193                    reached_root_camera = true;
194                    end_time_span(world);
195                }
196                let Some(camera) = world.get::<ExtractedCamera>(camera_entity) else {
197                    continue;
198                };
199
200                schedule = camera.schedule;
201                let target = camera.target.clone();
202
203                if let Some(NormalizedRenderTarget::Window(window_ref)) = &target {
204                    let window_entity = window_ref.entity();
205
206                    if let Ok(windows) = state.get(world)
207                        && windows
208                            .iter()
209                            .find(|(e, _)| *e == window_entity)
210                            .is_some_and(|(_, w)| w.physical_width > 0 && w.physical_height > 0)
211                    {
212                        camera_windows.insert(window_entity);
213                    } else {
214                        run_schedule = false;
215                    }
216                }
217
218                view_entity = camera_entity;
219            }
220
221            RootView::Auxiliary(auxiliary_view_entity) => {
222                let Some(root_view) = world.get::<RootNonCameraView>(auxiliary_view_entity) else {
223                    continue;
224                };
225
226                view_entity = auxiliary_view_entity;
227                schedule = root_view.0;
228            }
229        }
230
231        if run_schedule {
232            world.insert_resource(CurrentView(view_entity));
233
234            #[cfg(feature = "trace")]
235            let _span =
236                bevy_log::info_span!("camera_schedule", camera = root_view.to_string()).entered();
237
238            world.run_schedule(schedule);
239        }
240    }
241    if diagnostics_enabled && !reached_root_camera {
242        end_time_span(world);
243    }
244
245    world.remove_resource::<CurrentView>();
246
247    world.insert_resource(CameraWindows(camera_windows));
248}
249
250/// A view not associated with any other camera.
251enum RootView {
252    /// A camera.
253    Camera { entity: Entity, order: isize },
254
255    /// An auxiliary view not associated with a camera.
256    ///
257    /// This is currently used for point and spot light shadow maps.
258    Auxiliary(Entity),
259}
260
261impl Display for RootView {
262    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
263        match *self {
264            RootView::Camera { entity, order } => write!(f, "Camera {} ({:?})", order, entity),
265            RootView::Auxiliary(entity) => write!(f, "Auxiliary View {:?}", entity),
266        }
267    }
268}
269
270pub(crate) fn submit_pending_command_buffers(
271    world: &mut World,
272    state: &mut SystemState<(ResMut<PendingCommandBuffers>, Res<RenderQueue>)>,
273) {
274    let (mut pending, queue) = state.get_mut(world).unwrap();
275    #[cfg(feature = "trace")]
276    let buffer_count = pending.len();
277    let mut buffers = pending.finish().peekable();
278
279    if buffers.peek().is_some() {
280        #[cfg(feature = "trace")]
281        let _span = info_span!("queue_submit", count = buffer_count).entered();
282        queue.submit(buffers);
283    }
284}
285
286pub(crate) fn handle_uncovered_swap_chains(world: &mut World) {
287    let windows_to_clear: Vec<_> = {
288        let clear_color = world.resource::<ClearColor>().0.to_linear();
289        let Some(camera_windows) = world.remove_resource::<CameraWindows>() else {
290            return;
291        };
292
293        world
294            .query::<(MainEntity, &ExtractedWindow)>()
295            .iter(world)
296            .filter_map(|(window_entity, window)| {
297                if camera_windows.0.contains(&window_entity.entity()) {
298                    return None;
299                }
300                let swap_chain_texture = window.swap_chain_texture_view.as_ref()?;
301                Some((swap_chain_texture.clone(), clear_color))
302            })
303            .collect()
304    };
305
306    if windows_to_clear.is_empty() {
307        return;
308    }
309
310    let render_device = world.resource::<RenderDevice>();
311    let render_queue = world.resource::<RenderQueue>();
312
313    let mut encoder = render_device.create_command_encoder(&CommandEncoderDescriptor::default());
314
315    for (swap_chain_texture, clear_color) in &windows_to_clear {
316        #[cfg(feature = "trace")]
317        let _span = bevy_log::info_span!("no_camera_clear_pass").entered();
318
319        let pass_descriptor = RenderPassDescriptor {
320            label: Some("no_camera_clear_pass"),
321            color_attachments: &[Some(RenderPassColorAttachment {
322                view: swap_chain_texture,
323                depth_slice: None,
324                resolve_target: None,
325                ops: Operations {
326                    load: LoadOp::Clear((*clear_color).into()),
327                    store: StoreOp::Store,
328                },
329            })],
330            depth_stencil_attachment: None,
331            timestamp_writes: None,
332            occlusion_query_set: None,
333            multiview_mask: None,
334        };
335
336        encoder.begin_render_pass(&pass_descriptor);
337    }
338
339    render_queue.submit([encoder.finish()]);
340}