From 4c2ed6ca802edd974abe66e644046d32f8605463 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 28 Apr 2024 17:51:35 -0400 Subject: [PATCH] render: create foundation of render graph, also add super simple topological sort for pass execution path --- lyra-game/src/render/graph/execution_path.rs | 86 ++++++++++++ lyra-game/src/render/graph/mod.rs | 114 ++++++++++++++++ lyra-game/src/render/graph/pass.rs | 125 ++++++++++++++++++ lyra-game/src/render/graph/passes/base.rs | 37 ++++++ .../render/graph/passes/light_cull_compute.rs | 66 +++++++++ lyra-game/src/render/graph/passes/mod.rs | 5 + lyra-game/src/render/mod.rs | 3 +- 7 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 lyra-game/src/render/graph/execution_path.rs create mode 100644 lyra-game/src/render/graph/mod.rs create mode 100644 lyra-game/src/render/graph/pass.rs create mode 100644 lyra-game/src/render/graph/passes/base.rs create mode 100644 lyra-game/src/render/graph/passes/light_cull_compute.rs create mode 100644 lyra-game/src/render/graph/passes/mod.rs diff --git a/lyra-game/src/render/graph/execution_path.rs b/lyra-game/src/render/graph/execution_path.rs new file mode 100644 index 0000000..cdbbc6e --- /dev/null +++ b/lyra-game/src/render/graph/execution_path.rs @@ -0,0 +1,86 @@ +use std::collections::{HashMap, VecDeque}; + +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::RenderGraphPassDesc; + +pub struct GraphExecutionPath { + /// Queue of the path, top is the first to be executed. + /// Each element is the handle of a pass. + pub queue: VecDeque, +} + +impl GraphExecutionPath { + pub fn new(pass_descriptions: Vec<&RenderGraphPassDesc>) -> Self { + // collect all the output slots + let mut total_outputs = HashMap::new(); + for desc in pass_descriptions.iter() { + for slot in desc.output_slots() { + total_outputs.insert(slot.name.clone(), SlotOwnerPair { + pass: desc.id, + slot: slot.id, + }); + } + } + + let mut nodes = FxHashMap::::default(); + for desc in pass_descriptions.iter() { + // find the node inputs + let mut inputs = vec![]; + for slot in desc.input_slots() { + let inp = total_outputs.get(&slot.name) + .expect(&format!("failed to find slot: '{}', ensure that there is a pass outputting it", slot.name)); + inputs.push(*inp); + } + + let node = Node { + id: desc.id, + desc: (*desc).clone(), + slot_inputs: inputs + }; + nodes.insert(node.id, node); + } + + // sort the graph + let mut stack = VecDeque::new(); + let mut visited = FxHashSet::default(); + for (_, no) in nodes.iter() { + Self::topological_sort(&nodes, &mut stack, &mut visited, no); + } + + Self { + queue: stack, + } + } + + fn topological_sort(graph: &FxHashMap, stack: &mut VecDeque, visited: &mut FxHashSet, node: &Node) { + if !visited.contains(&node.id) { + visited.insert(node.id); + + for depend in &node.slot_inputs { + let depend_node = graph.get(&depend.pass) + .expect("could not find dependent node"); + + if !visited.contains(&depend.pass) { + Self::topological_sort(graph, stack, visited, depend_node); + } + } + + stack.push_back(node.id); + } + + } +} + +#[derive(Debug, Clone, Copy)] +struct SlotOwnerPair { + pass: u64, + slot: u64, +} + +#[derive(Clone)] +struct Node { + id: u64, + desc: RenderGraphPassDesc, + slot_inputs: Vec, +} diff --git a/lyra-game/src/render/graph/mod.rs b/lyra-game/src/render/graph/mod.rs new file mode 100644 index 0000000..c6b824c --- /dev/null +++ b/lyra-game/src/render/graph/mod.rs @@ -0,0 +1,114 @@ +mod pass; +use std::collections::HashMap; + +pub use pass::*; + +mod passes; +pub use passes::*; + +mod execution_path; + +use rustc_hash::FxHashMap; +use wgpu::RenderPass; + +use super::renderer::{BasicRenderer, Renderer}; + +struct PassEntry { + inner: Box, + desc: RenderGraphPassDesc, +} + +struct ResourcedSlot { + slot: RenderPassSlot, + value: SlotValue, + bindgroup: wgpu::BindGroup, +} + +#[derive(Default)] +pub struct RenderGraph { + slots: FxHashMap, + slot_names: HashMap, + passes: FxHashMap, + current_id: u64, +} + +impl RenderGraph { + pub fn new() -> Self { + Self::default() + } + + pub fn slot_id(&self, name: &str) -> Option { + self.slot_names.get(name).cloned() + } + + pub fn pass(&self, id: u64) -> Option<&RenderGraphPassDesc> { + self.passes.get(&id) + .map(|s| &s.desc) + } + + pub fn bindgroup(&self, id: u64) -> Option<&wgpu::BindGroup> { + self.slots.get(&id) + .map(|s| &s.bindgroup) + } + + pub fn add_pass(&mut self, pass: P) { + let desc = pass.desc(&mut self.current_id); + self.passes.insert(desc.id, PassEntry { + inner: Box::new(pass), + desc, + }); + } + + /// Creates all buffers required for the passes, also creates an internal execution path. + pub fn setup(&mut self) { + todo!() + } + + pub fn prepare(&mut self) { + todo!() + } + + pub fn render(&mut self, renderer: &mut BasicRenderer) { + todo!() + } +} + +pub struct RenderGraphContext { + +} + +impl RenderGraphContext { + pub fn begin_render_pass(&mut self, desc: &wgpu::RenderPassDescriptor) -> wgpu::RenderPass { + todo!() + } + + pub fn begin_compute_pass(&mut self, desc: &wgpu::ComputePassDescriptor) -> wgpu::ComputePass { + todo!() + } +} + +#[cfg(test)] +mod tests { + use winit::dpi::PhysicalSize; + + use super::{execution_path::GraphExecutionPath, BasePass, LightCullComputePass, RenderGraph}; + + #[test] + fn full_test() { + let mut graph = RenderGraph::new(); + + graph.add_pass(BasePass::new()); + graph.add_pass(LightCullComputePass::new(PhysicalSize::new(800, 600))); + + let descs = graph.passes.values().map(|pass| &pass.desc).collect(); + let mut path = GraphExecutionPath::new(descs); + + println!("Pass execution order:"); + let mut num = 1; + while let Some(pass_id) = path.queue.pop_front() { + let pass = graph.pass(pass_id).unwrap(); + println!(" {}: {}", num, pass.name); + num += 1; + } + } +} \ No newline at end of file diff --git a/lyra-game/src/render/graph/pass.rs b/lyra-game/src/render/graph/pass.rs new file mode 100644 index 0000000..7809d11 --- /dev/null +++ b/lyra-game/src/render/graph/pass.rs @@ -0,0 +1,125 @@ +use std::collections::HashMap; + +use lyra_ecs::World; + +use super::{RenderGraph, RenderGraphContext}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] +pub enum RenderPassType { + Compute, + #[default] + Render +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SlotType { + TextureView, + Sampler, + Buffer, +} + +#[derive(Debug)] +pub enum SlotValue { + TextureView(wgpu::TextureView), + Sampler(wgpu::Sampler), + Buffer(wgpu::Buffer), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SlotAttribute { + Input, + Output, +} + +#[derive(Clone)] +pub struct RenderPassSlot { + pub ty: SlotType, + pub attribute: SlotAttribute, + pub id: u64, + pub name: String, + + // buffer desc, texture desc +} + +#[derive(Clone)] +pub struct RenderGraphPassDesc { + pub id: u64, + pub name: String, + pub pass_type: RenderPassType, + pub slots: Vec, + slot_names: HashMap, +} + +impl RenderGraphPassDesc { + pub fn new(id: u64, name: &str, pass_type: RenderPassType) -> Self { + Self { + id, + name: name.to_string(), + pass_type, + slots: vec![], + slot_names: HashMap::default(), + } + } + + pub fn add_slot(&mut self, slot: RenderPassSlot) { + self.slot_names.insert(slot.name.clone(), slot.id); + self.slots.push(slot); + } + + #[inline(always)] + pub fn add_buffer_slot(&mut self, id: u64, name: &str, attribute: SlotAttribute) { + let slot = RenderPassSlot { + id, + name: name.to_string(), + ty: SlotType::Buffer, + attribute, + }; + self.add_slot(slot); + } + + #[inline(always)] + pub fn add_texture_view_slot(&mut self, id: u64, name: &str, attribute: SlotAttribute) { + let slot = RenderPassSlot { + id, + name: name.to_string(), + ty: SlotType::TextureView, + attribute, + }; + self.add_slot(slot); + } + + #[inline(always)] + pub fn add_texture_sampler_slot(&mut self, id: u64, name: &str, attribute: SlotAttribute) { + let slot = RenderPassSlot { + id, + name: name.to_string(), + ty: SlotType::Sampler, + attribute, + }; + self.add_slot(slot); + } + + pub fn input_slots(&self) -> Vec<&RenderPassSlot> { + self.slots + .iter() + .filter(|s| s.attribute == SlotAttribute::Input) + .collect() + } + + pub fn output_slots(&self) -> Vec<&RenderPassSlot> { + self.slots + .iter() + .filter(|s| s.attribute == SlotAttribute::Output) + .collect() + } +} + +pub trait RenderGraphPass: 'static { + /// Create a render pass describer. + /// + /// The `id` argument is passed as mutable so you can increment it as you use it for new slots. + fn desc(&self, id: &mut u64) -> RenderGraphPassDesc; + + fn prepare(&mut self, world: &mut World); + fn execute(&mut self, graph: &mut RenderGraph, desc: &RenderGraphPassDesc, context: &mut RenderGraphContext); +} \ No newline at end of file diff --git a/lyra-game/src/render/graph/passes/base.rs b/lyra-game/src/render/graph/passes/base.rs new file mode 100644 index 0000000..a51be89 --- /dev/null +++ b/lyra-game/src/render/graph/passes/base.rs @@ -0,0 +1,37 @@ +use crate::render::graph::{RenderGraphPass, RenderGraphPassDesc, RenderPassType, SlotAttribute}; + +/// Supplies some basic things other passes needs. +/// +/// screen size buffer, camera buffer, +#[derive(Default)] +pub struct BasePass; + +impl BasePass { + pub fn new() -> Self { + Self::default() + } +} + +impl RenderGraphPass for BasePass { + fn desc(&self, id: &mut u64) -> crate::render::graph::RenderGraphPassDesc { + let mut desc = RenderGraphPassDesc::new(*id, "BasePass", RenderPassType::Compute); + *id += 1; + + desc.add_buffer_slot(*id, "screen_size_buffer", SlotAttribute::Output); + *id += 1; + desc.add_buffer_slot(*id, "camera_buffer", SlotAttribute::Output); + *id += 1; + + desc + } + + fn prepare(&mut self, world: &mut lyra_ecs::World) { + let _ = world; + todo!() + } + + fn execute(&mut self, graph: &mut crate::render::graph::RenderGraph, desc: &crate::render::graph::RenderGraphPassDesc, context: &mut crate::render::graph::RenderGraphContext) { + let _ = (graph, desc, context); + todo!() + } +} \ No newline at end of file diff --git a/lyra-game/src/render/graph/passes/light_cull_compute.rs b/lyra-game/src/render/graph/passes/light_cull_compute.rs new file mode 100644 index 0000000..43318ff --- /dev/null +++ b/lyra-game/src/render/graph/passes/light_cull_compute.rs @@ -0,0 +1,66 @@ +use lyra_ecs::World; + +use crate::render::graph::{RenderGraphContext, RenderGraphPass, RenderGraphPassDesc, RenderPassType, SlotAttribute}; + +pub struct LightCullComputePass { + workgroup_size: glam::UVec2, +} + +impl LightCullComputePass { + pub fn new(screen_size: winit::dpi::PhysicalSize) -> Self { + Self { + workgroup_size: glam::UVec2::new(screen_size.width, screen_size.height) + } + } +} + +impl RenderGraphPass for LightCullComputePass { + fn desc(&self, id: &mut u64) -> crate::render::graph::RenderGraphPassDesc { + let mut desc = RenderGraphPassDesc::new(*id, "LightCullCompute", RenderPassType::Compute); + *id += 1; + + desc.add_buffer_slot(*id, "screen_size_buffer", SlotAttribute::Input); + *id += 1; + desc.add_buffer_slot(*id, "camera_buffer", SlotAttribute::Input); + *id += 1; + + desc.add_buffer_slot(*id, "light_indices", SlotAttribute::Output); + *id += 1; + /* desc.add_buffer_slot(*id, "indices_buffer", SlotAttribute::Output); + *id += 1; */ + desc.add_texture_view_slot(*id, "grid_texture", SlotAttribute::Output); + *id += 1; + + + desc + } + + fn prepare(&mut self, world: &mut World) { + let _ = world; + todo!() + } + + fn execute(&mut self, graph: &mut crate::render::graph::RenderGraph, _: &crate::render::graph::RenderGraphPassDesc, context: &mut RenderGraphContext) { + let mut pass = context.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("Pass_lightCull") + }); + + let depth_tex = graph.bindgroup(graph.slot_id("depth_texture").expect("Could not find depth texture slot")).unwrap(); + let camera_bg = graph.bindgroup(graph.slot_id("camera_buffer").expect("Could not find camera buffers")).unwrap(); + let screen_size_bg = graph.bindgroup(graph.slot_id("screen_size_buffer").expect("Could not find screen size buffer slot")).unwrap(); + let indices_bg = graph.bindgroup(graph.slot_id("light_indices").expect("Could not find light index buffer slot")).unwrap(); + let light_grid_bg = graph.bindgroup(graph.slot_id("grid_texture").expect("Could not find light grid buffer slot")).unwrap(); + + //pass.set_pipeline(pipeline) + + pass.set_bind_group(0, depth_tex, &[]); + pass.set_bind_group(1, camera_bg, &[]); + pass.set_bind_group(2, indices_bg, &[]); + pass.set_bind_group(3, light_grid_bg, &[]); + pass.set_bind_group(4, screen_size_bg, &[]); + + pass.dispatch_workgroups(self.workgroup_size.x, self.workgroup_size.y, 1); + + + } +} \ No newline at end of file diff --git a/lyra-game/src/render/graph/passes/mod.rs b/lyra-game/src/render/graph/passes/mod.rs new file mode 100644 index 0000000..0871d73 --- /dev/null +++ b/lyra-game/src/render/graph/passes/mod.rs @@ -0,0 +1,5 @@ +mod light_cull_compute; +pub use light_cull_compute::*; + +mod base; +pub use base::*; \ No newline at end of file diff --git a/lyra-game/src/render/mod.rs b/lyra-game/src/render/mod.rs index d7985d5..2d4c650 100755 --- a/lyra-game/src/render/mod.rs +++ b/lyra-game/src/render/mod.rs @@ -13,4 +13,5 @@ pub mod window; pub mod transform_buffer_storage; pub mod light; pub mod light_cull_compute; -pub mod avec; \ No newline at end of file +pub mod avec; +pub mod graph; \ No newline at end of file