Skip to main content

wde_renderer/passes/
render_graph.rs

1use crate::prelude::*;
2use wde_logger::prelude::*;
3use wde_wgpu::passes::{
4    CommandBuffer, RenderPassBuilder, RenderPassColorAttachment, RenderPassDepth
5};
6
7use bevy::ecs::system::{ReadOnlySystemParam, SystemParamItem, SystemState};
8use bevy::prelude::*;
9use std::collections::HashMap;
10use std::ops::Range;
11use wde_wgpu::pipelines::BindGroup;
12
13use crate::{
14    assets::{Mesh, Texture},
15    core::SwapchainFrame
16};
17
18pub use wde_wgpu::command_buffer::{LoadOp, Operations, StoreOp};
19pub use wde_wgpu::render_pass::RenderPassInstance;
20
21/// Color attachment description for a render pass.
22/// If `texture` is None, the pass will render to the swapchain texture.
23pub struct RenderPassDescColorAttachment {
24    /// The texture to render to.
25    pub texture: Option<AssetId<Texture>>,
26    /// How to load the texture at the start of the pass (default: Load).
27    pub load: LoadOp<crate::prelude::Color>,
28    /// How to store the texture at the end of the pass (default: Store).
29    pub store: StoreOp,
30    /// Optional resolve target for multisampled textures. If set, the multisampled texture will be resolved into this texture at the end of the pass.
31    pub resolve_target: Option<AssetId<Texture>>
32}
33impl Default for RenderPassDescColorAttachment {
34    fn default() -> Self {
35        Self {
36            texture: None,
37            load: LoadOp::Load,
38            store: StoreOp::Store,
39            resolve_target: None
40        }
41    }
42}
43/// Depth attachment description for a render pass.
44pub struct RenderPassDescDepthAttachment {
45    /// The texture to use as depth buffer.
46    pub texture: Option<AssetId<Texture>>,
47    /// How to load the texture at the start of the pass (default: Load).
48    pub load: LoadOp<f32>,
49    /// How to store the texture at the end of the pass (default: Store).
50    pub store: StoreOp,
51    /// How to load the stencil at the start of the pass (default: Load).
52    pub stencil_load: LoadOp<u32>,
53    /// How to store the stencil at the end of the pass (default: Store).
54    pub stencil_store: StoreOp
55}
56impl Default for RenderPassDescDepthAttachment {
57    fn default() -> Self {
58        Self {
59            texture: None,
60            load: LoadOp::Load,
61            store: StoreOp::Store,
62            stencil_load: LoadOp::Load,
63            stencil_store: StoreOp::Store
64        }
65    }
66}
67/// Descibes a render pass and its execution logic.
68#[derive(Default)]
69pub struct RenderPassDesc {
70    /// Optional depth attachment. If None, no depth buffer is used.
71    pub attachments_depth: Option<RenderPassDescDepthAttachment>,
72    /// Optional color attachment. If None, the pass renders to the swapchain texture.
73    pub attachments_colors: Option<Vec<RenderPassDescColorAttachment>>
74}
75
76/// A batch of draw commands that can be issued together with the same pipeline and bind groups.
77pub struct DrawCommandsBatch {
78    pub bind_group: Option<(u32, BindGroup)>, // index, bind group
79    pub index_range: Range<u32>,
80    pub instance_range: Range<u32>
81}
82impl Default for DrawCommandsBatch {
83    fn default() -> Self {
84        Self {
85            bind_group: None,
86            index_range: 0..0,
87            instance_range: 0..1
88        }
89    }
90}
91/// Commands to execute in a render pass, in order. For example: set pipeline, set bind groups, draw calls, etc.
92pub enum SubPassCommand {
93    /// Set the pipeline for subsequent draw calls.
94    Pipeline(Option<CachedPipelineIndex>),
95    /// Set a bind group at the given index for subsequent draw calls.
96    BindGroup(u32, Option<BindGroup>),
97    /// Set the vertex and index buffers for subsequent draw calls.
98    Mesh(Option<AssetId<Mesh>>),
99    /// Set the stencil reference value used by stencil compare and write operations.
100    StencilReference(u32),
101    /// Issue draw calls with the given index and instance ranges, using the currently set pipeline and bind groups.
102    DrawBatches(Vec<DrawCommandsBatch>),
103    /// Execute a custom function that has access to the render world and the render pass instance. This can be used for custom rendering logic that doesn't fit into the other command types. This function should be written as `fn _name_(world: &World, render_pass: &mut RenderPassInstance) { ... }`.
104    Custom(for<'pass> fn(&'pass World, &mut RenderPassInstance<'pass>))
105}
106/// A sub-pass is a sequence of commands executed within a render pass.
107/// For example, a GBuffer pass might have one sub-pass for rendering opaque objects and another for transparent objects.
108#[derive(Default)]
109pub struct RenderSubPassDesc(pub Vec<SubPassCommand>);
110
111/// Type alias for render pass IDs. These are numeric identifiers that control the execution order of passes in the render graph; lower IDs execute first.
112pub type RenderPassId = i32;
113/// Describes a render pass in the render graph, with its attachments, load ops, etc.
114/// This is returned by the `describe` method of the `RenderPass` trait, which is called in the render world before rendering to get the pass description and attachments.
115/// See [crate::passes] for more details and examples.
116pub trait RenderPass {
117    type Params: ReadOnlySystemParam;
118
119    /// Describe the render pass (attachments, load ops, etc).
120    fn describe(params: &SystemParamItem<Self::Params>) -> RenderPassDesc;
121    /// Return the unique numeric ID of this render pass. Passes execute in order of their IDs (lower IDs execute first).
122    fn id() -> RenderPassId;
123    /// Return the (non-unique) label of the render pass, used for logging and debugging.
124    fn label() -> &'static str;
125
126    /// Custom rendering logic for this pass.
127    /// This can be used for passes that require custom rendering logic that doesn't fit into the default render graph execution flow.
128    ///
129    /// Notes:
130    /// - Returns None by default, meaning the pass will be rendered with the default render graph execution flow.
131    /// - If it returns Some(true), it means the pass executed custom rendering logic and should be considered successfully rendered for this frame.
132    /// - The provided command buffer is already begun and will be submitted at the end of the render graph execution, so the custom rendering logic can simply record commands to it.
133    fn custom_render(_world: &mut World, _command_buffer: &mut CommandBuffer) -> Option<bool> {
134        None
135    }
136}
137// Utility traits for the render graph implementation. Not meant to be used by users of the render graph.
138trait RenderPassNode: Send + Sync + 'static {
139    fn describe(&mut self, world: &mut World) -> RenderPassDesc;
140    fn label(&mut self) -> &'static str;
141    fn custom_render(
142        &mut self,
143        world: &mut World,
144        command_buffer: &mut CommandBuffer
145    ) -> Option<bool>;
146}
147struct RenderPassHolder<P: RenderPass> {
148    _phantom: std::marker::PhantomData<P>
149}
150impl<P> RenderPassNode for RenderPassHolder<P>
151where
152    P: RenderPass + Send + Sync + 'static
153{
154    fn describe(&mut self, world: &mut World) -> RenderPassDesc {
155        let mut state: SystemState<P::Params> = SystemState::new(world);
156        let params = state.get(world);
157        P::describe(&params)
158    }
159    fn label(&mut self) -> &'static str {
160        P::label()
161    }
162    fn custom_render(
163        &mut self,
164        world: &mut World,
165        command_buffer: &mut CommandBuffer
166    ) -> Option<bool> {
167        P::custom_render(world, command_buffer)
168    }
169}
170
171/// Core trait for all render sub-passes in the render graph. A sub-pass is a sequence of commands executed within a render pass.
172/// For example, a GBuffer pass might have one sub-pass for rendering opaque objects and another for transparent objects.
173/// The `describe` method defines the commands to execute in this sub-pass, which can be setting pipelines, bind groups, vertex buffers, draw calls, or even custom rendering logic.
174/// See [crate::passes] for more details and examples.
175pub trait RenderSubPass {
176    type Params: ReadOnlySystemParam;
177
178    /// Describe the sub-pass commands to execute within the render pass.
179    fn describe(params: &SystemParamItem<Self::Params>) -> RenderSubPassDesc;
180    /// Return the (non-unique) label of this sub-pass, used for logging and debugging.
181    fn label() -> &'static str;
182}
183// Utility traits for the render graph implementation. Not meant to be used by users of the render graph.
184trait RenderSubPassNode: Send + Sync + 'static {
185    fn describe(&mut self, world: &mut World) -> RenderSubPassDesc;
186    fn label(&mut self) -> &'static str;
187}
188struct RenderSubPassHolder<SP: RenderSubPass> {
189    _phantom: std::marker::PhantomData<SP>
190}
191impl<SP> RenderSubPassNode for RenderSubPassHolder<SP>
192where
193    SP: RenderSubPass + Send + Sync + 'static
194{
195    fn describe(&mut self, world: &mut World) -> RenderSubPassDesc {
196        let mut state: SystemState<SP::Params> = SystemState::new(world);
197        let params = state.get(world);
198        SP::describe(&params)
199    }
200    fn label(&mut self) -> &'static str {
201        SP::label()
202    }
203}
204
205/// The render graph resource, which stores all render passes and sub-passes, their descriptions, and their execution order.
206/// This is the core of the render graph system, and is responsible for executing the render passes in the correct order with the correct attachments and commands.
207///
208/// To register a new render pass, use the `add_pass` method with a type that implements the `RenderPass` trait. To register a sub-pass to a render pass, use the `add_sub_pass` method with a type that implements the `RenderSubPass` trait and the parent render pass type.
209#[derive(Resource, Default)]
210pub struct RenderGraph {
211    // Raw storage of passes and subpasses
212    passes: Vec<Box<dyn RenderPassNode>>,
213    sub_passes: Vec<Box<dyn RenderSubPassNode>>,
214
215    // Ordering logic for the passes
216    sorted_ids: Vec<RenderPassId>, // pass indices in `passes`, sorted by their numeric IDs
217
218    // Indexing for execution
219    render_passes_by_id: HashMap<RenderPassId, usize>, // pass id -> pass index in `passes`
220    sub_passes_by_renderpass: HashMap<usize, Vec<usize>>  // render pass index -> sub-pass indices
221}
222impl RenderGraph {
223    /// Add a render pass to the graph. Passes execute in order of their IDs (lower IDs execute first).
224    /// The pass type must implement the `RenderPass` trait, which defines the pass's description and execution logic.
225    pub fn add_pass<P: RenderPass + Send + Sync + 'static>(&mut self) -> &mut Self {
226        let id = P::id();
227
228        // Assert that the pass ID is unique
229        debug_assert!(
230            !self.render_passes_by_id.contains_key(&id),
231            "A render pass with ID {} already exists in the render graph.",
232            id
233        );
234
235        // Insert the new pass
236        self.passes.push(Box::new(RenderPassHolder {
237            _phantom: std::marker::PhantomData::<P>
238        }));
239        self.render_passes_by_id.insert(id, self.passes.len() - 1);
240        self.sorted_ids.push(id);
241
242        // Sort the passes by ID
243        self.sorted_ids.sort();
244        self
245    }
246
247    /// Add a sub-pass to a render pass in the graph. The sub-pass will be executed in the order it was added within its parent render pass.
248    /// The sub-pass type must implement the `RenderSubPass` trait, which defines the sub-pass's commands and the render pass it belongs to.
249    pub fn add_sub_pass<SP: RenderSubPass + Send + Sync + 'static, P: RenderPass>(
250        &mut self
251    ) -> &mut Self {
252        self.sub_passes.push(Box::new(RenderSubPassHolder {
253            _phantom: std::marker::PhantomData::<SP>
254        }));
255        let sub_pass_index = self.render_passes_by_id.get(&P::id()).copied();
256        debug_assert!(
257            sub_pass_index.is_some(),
258            "Cannot add sub-pass '{}' to render pass '{}': no render pass with ID {} found in the render graph.",
259            SP::label(),
260            P::label(),
261            P::id()
262        );
263        self.sub_passes_by_renderpass
264            .entry(sub_pass_index.unwrap())
265            .or_default()
266            .push(self.sub_passes.len() - 1);
267        self
268    }
269
270    pub(crate) fn render(world: &mut World) {
271        world.resource_scope(|world, mut graph: Mut<RenderGraph>| {
272            render(&mut graph, world);
273        });
274    }
275}
276
277fn render(graph: &mut RenderGraph, world: &mut World) {
278    let _span = debug_span!("render_graph").entered();
279
280    // Create command buffer
281    let mut command_buffer = {
282        let render_instance = world.get_resource::<RenderInstance>().unwrap();
283        CommandBuffer::new(&render_instance.0.read().unwrap(), "render-graph")
284    };
285
286    // Execute passes in order of their IDs
287    for pass_id in &graph.sorted_ids {
288        // Get pass description
289        let pass_index = *graph.render_passes_by_id.get(pass_id).unwrap();
290        let pass = &mut graph.passes[pass_index];
291        let pass_label = pass.label();
292        let pass_desc = pass.describe(world);
293
294        // Try custom rendering logic for this pass
295        if let Some(custom_render_result) = pass.custom_render(world, &mut command_buffer) {
296            // If it was Some(true or false), it means the pass used custom rendering logic.
297            if !custom_render_result {
298                debug!(
299                    "Custom rendering for pass '{}' could not be completed. Skipping this pass for this frame.",
300                    pass_label
301                );
302            }
303            continue;
304        }
305
306        // Precompute sub-pass descriptions before creating the render pass to avoid borrow conflicts with the world
307        let mut sub_passes = Vec::new();
308        if let Some(sub_pass_indices) = graph.sub_passes_by_renderpass.get(&pass_index) {
309            for sub_pass_index in sub_pass_indices {
310                let sub_pass = &mut graph.sub_passes[*sub_pass_index];
311                let sub_pass_label = sub_pass.label();
312                let sub_pass_desc = sub_pass.describe(world);
313                sub_passes.push((sub_pass_label, sub_pass_desc));
314            }
315        }
316
317        // Create the render pass from its description
318        let _pass_span = debug_span!("render_pass", pass_id = *pass_id, pass_label).entered();
319        let mut render_pass =
320            match create_render_pass(world, &mut command_buffer, pass_label, &pass_desc) {
321                Ok(pass) => pass,
322                Err(e) => {
323                    debug!(
324                        "Failed to create render pass: '{}'. Skipping this frame.",
325                        e
326                    );
327                    continue;
328                }
329            };
330
331        // Execute sub-passes in order of addition
332        for (sub_pass_label, sub_pass_desc) in &sub_passes {
333            let _sub_pass_span =
334                debug_span!("render_sub_pass", sub_pass_label = *sub_pass_label).entered();
335            render_sub_pass(world, &mut render_pass, sub_pass_desc);
336        }
337    }
338
339    // Submit commands
340    let render_instance = world.get_resource::<RenderInstance>().unwrap();
341    command_buffer.submit(&render_instance.0.read().unwrap());
342}
343
344fn create_render_pass<'p>(
345    world: &'p World,
346    command_buffer: &'p mut CommandBuffer,
347    label: &str,
348    pass_desc: &RenderPassDesc
349) -> Result<RenderPassInstance<'p>, String> {
350    // Get generic handlers
351    let render_instance = world.get_resource::<RenderInstance>().unwrap();
352    let textures = world.get_resource::<RenderAssets<GpuTexture>>().unwrap();
353    let swapchain_frame = world.get_resource::<SwapchainFrame>().unwrap();
354    let (surface_width, surface_height) = {
355        let render_instance = render_instance.0.read().unwrap();
356        let config = render_instance.surface_config.as_ref().unwrap();
357        (config.width, config.height)
358    };
359
360    // Create the render pass
361    command_buffer.create_render_pass(label, |builder: &mut RenderPassBuilder| -> Result<(), String> {
362        // Set color attachments
363        if let Some(attachments_color) = &pass_desc.attachments_colors {
364            // Set specified color attachments
365            for color_attachment_desc in attachments_color.iter() {
366                if let Some(texture_id) = color_attachment_desc.texture
367                    && let Some(texture) = textures.get(texture_id) {
368                    if !(surface_width == texture.texture.size.0 && surface_height == texture.texture.size.1) {
369                        return Err(format!("Color attachment texture has invalid size: expected ({}, {}), got ({}, {})", surface_width, surface_height, texture.texture.size.0, texture.texture.size.1));
370                    }
371                    let resolve_target = if let Some(resolve_target) = color_attachment_desc.resolve_target {
372                        if let Some(resolve_target) = textures.get(resolve_target)
373                            && surface_width == resolve_target.texture.size.0 && surface_height == resolve_target.texture.size.1 {
374                            Some(resolve_target)
375                        } else {
376                            return Err("Invalid resolve target for color attachment".to_string());
377                        }
378                    } else { None };
379                    builder.add_color_attachment(RenderPassColorAttachment {
380                        texture: Some(&texture.texture.view),
381                        load: match color_attachment_desc.load {
382                            LoadOp::Load => LoadOp::Load,
383                            LoadOp::Clear(color) => LoadOp::Clear(color.into())
384                        },
385                        store: color_attachment_desc.store,
386                        resolve_target: resolve_target.map(|tex| &tex.texture.view)
387                    });
388                } else {
389                    return Err("Invalid texture for color attachment".to_string());
390                }
391            }
392        } else {
393            // Set swapchain as color attachment if no other color attachments are specified
394            let swapchain_frame = match swapchain_frame.data.as_ref() {
395                Some(frame) => frame,
396                None => return Err("No swapchain frame available".to_string())
397            };
398            builder.add_color_attachment(RenderPassColorAttachment {
399                texture: Some(&swapchain_frame.view),
400                ..default()
401            });
402        }
403
404        // Set depth attachments
405        if let Some(depth_attachment) = pass_desc.attachments_depth.as_ref() {
406            if let Some(depth_texture) = depth_attachment.texture
407               && let Some(depth_texture) = textures.get(depth_texture) {
408                // Depth-only passes (empty color list) use off-screen textures at arbitrary resolutions;
409                // skip the surface-size check for them.
410                let depth_only = pass_desc.attachments_colors.as_ref().is_some_and(|c| c.is_empty());
411                if !(depth_only || surface_width == depth_texture.texture.size.0 && surface_height == depth_texture.texture.size.1) {
412                    return Err(format!("Depth attachment texture has invalid size: expected ({}, {}), got ({}, {}).", surface_width, surface_height, depth_texture.texture.size.0, depth_texture.texture.size.1));
413                }
414                builder.set_depth_texture(RenderPassDepth {
415                    texture: Some(&depth_texture.texture.view),
416                    load: depth_attachment.load,
417                    store: depth_attachment.store,
418                    stencil_load: depth_attachment.stencil_load,
419                    stencil_store: depth_attachment.stencil_store
420                });
421            } else {
422                return Err("Invalid texture for depth attachment".to_string());
423            }
424        }
425        Ok(())
426    })
427}
428
429fn render_sub_pass<'p>(
430    world: &'p World,
431    render_pass: &mut RenderPassInstance<'p>,
432    sub_pass_desc: &'p RenderSubPassDesc
433) {
434    // Get generic handlers
435    let pipeline_manager = world.get_resource::<PipelineManager>().unwrap();
436    let meshes = world.get_resource::<RenderAssets<GpuMesh>>().unwrap();
437
438    // Issue global commands
439    for stage_command in &sub_pass_desc.0 {
440        match stage_command {
441            // Set pipeline
442            SubPassCommand::Pipeline(pipeline) => {
443                if let Some(pipeline) = pipeline
444                    && let CachedPipelineStatus::OkRender(pipeline) =
445                        pipeline_manager.get_pipeline(*pipeline)
446                {
447                    if let Err(e) = render_pass.set_pipeline(pipeline) {
448                        error!("Failed to set pipeline: {:?}.", e);
449                        return;
450                    }
451                } else {
452                    return;
453                }
454            }
455
456            // Set bind groups at given index
457            SubPassCommand::BindGroup(index, bind_group) => {
458                if let Some(bind_group) = bind_group {
459                    render_pass.set_bind_group(*index, bind_group);
460                } else {
461                    return;
462                }
463            }
464
465            // Bind mesh vertex and index buffers
466            SubPassCommand::Mesh(mesh) => {
467                if let Some(mesh) = mesh
468                    && let Some(mesh) = meshes.get(*mesh)
469                {
470                    render_pass.set_vertex_buffer(0, mesh.vertex_buffer.as_ref().unwrap());
471                    render_pass.set_index_buffer(mesh.index_buffer.as_ref().unwrap());
472                } else {
473                    return;
474                }
475            }
476
477            // Set stencil reference value
478            SubPassCommand::StencilReference(reference) => {
479                render_pass.set_stencil_reference(*reference);
480            }
481
482            // Issue draw calls
483            SubPassCommand::DrawBatches(batches) => {
484                for batch in batches {
485                    // Set bind group
486                    if let Some((index, bind_group)) = &batch.bind_group {
487                        render_pass.set_bind_group(*index, bind_group);
488                    }
489
490                    // Draw the mesh
491                    match render_pass
492                        .draw_indexed(batch.index_range.clone(), batch.instance_range.clone())
493                    {
494                        Ok(_) => {}
495                        Err(e) => {
496                            error!("Failed to draw: {:?}.", e);
497                        }
498                    }
499                }
500            }
501
502            // Custom commands
503            SubPassCommand::Custom(custom_fn) => {
504                custom_fn(world, render_pass);
505            }
506        }
507    }
508}