From 146ffa654f99bb2380554914c4b2fb7f59352e9b Mon Sep 17 00:00:00 2001 From: Janis Date: Mon, 6 Jan 2025 02:15:14 +0100 Subject: [PATCH] quick save --- crates/renderer/src/render_graph.rs | 488 +++++++++++++++++++++++++++- 1 file changed, 485 insertions(+), 3 deletions(-) diff --git a/crates/renderer/src/render_graph.rs b/crates/renderer/src/render_graph.rs index 6a09145..1584227 100644 --- a/crates/renderer/src/render_graph.rs +++ b/crates/renderer/src/render_graph.rs @@ -2,7 +2,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, - fmt::Debug, + fmt::{Debug, Display}, sync::Arc, }; @@ -75,7 +75,7 @@ impl RenderContext<'_> { } } -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Access { pub stage: vk::PipelineStageFlags2, pub mask: vk::AccessFlags2, @@ -95,7 +95,72 @@ impl core::ops::BitOr for Access { } } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct AccessMask { + pub stage: vk::PipelineStageFlags2, + pub mask: vk::AccessFlags2, +} +impl AccessMask { + fn undefined() -> Self { + Self { + stage: vk::PipelineStageFlags2::NONE, + mask: vk::AccessFlags2::empty(), + } + } + fn is_empty(&self) -> bool { + self.stage.is_empty() && self.mask.is_empty() + } +} +impl core::ops::BitOr for AccessMask { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self { + stage: self.stage | rhs.stage, + mask: self.mask | rhs.mask, + } + } +} +impl core::ops::Not for AccessMask { + type Output = Self; + + fn not(self) -> Self::Output { + Self { + stage: !self.stage, + mask: !self.mask, + } + } +} + +impl core::ops::BitAnd for AccessMask { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self { + stage: self.stage & rhs.stage, + mask: self.mask & rhs.mask, + } + } +} + +impl core::ops::BitXor for AccessMask { + type Output = Self; + + fn bitxor(self, rhs: Self) -> Self::Output { + Self { + stage: self.stage ^ rhs.stage, + mask: self.mask ^ rhs.mask, + } + } +} + impl Access { + pub fn into_access_mask(&self) -> AccessMask { + AccessMask { + stage: self.stage, + mask: self.mask, + } + } pub fn undefined() -> Self { Self { stage: vk::PipelineStageFlags2::NONE, @@ -103,6 +168,22 @@ impl Access { layout: Some(vk::ImageLayout::UNDEFINED), } } + /// Only use this for `Ord`! + pub fn min() -> Self { + Self { + stage: vk::PipelineStageFlags2::from_raw(u64::MAX), + mask: vk::AccessFlags2::from_raw(u64::MAX), + layout: None, + } + } + /// Only use this for `Ord`! + pub fn max() -> Self { + Self { + stage: vk::PipelineStageFlags2::from_raw(u64::MAX), + mask: vk::AccessFlags2::from_raw(u64::MAX), + layout: Some(vk::ImageLayout::from_raw(i32::MAX)), + } + } pub fn general() -> Self { Self { stage: vk::PipelineStageFlags2::NONE, @@ -304,13 +385,414 @@ impl RenderGraph { } } - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] enum PassNode { First, Pass(usize), Last, } + impl PassNode { + fn into_u32(&self) -> u32 { + match self { + PassNode::First => 0, + PassNode::Last => 1, + PassNode::Pass(i) => 2 + *i as u32, + } + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + enum RefAccess { + __Min, + None, + Read(Access), + Write(Access), + __Max, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + struct GraphRef { + pass: PassNode, + resource: GraphResourceId, + access: RefAccess, + } + + // gather references to resources. + let (references, intervals) = { + let mut references = BTreeSet::::new(); + + // interval for each resource from (first pass referencing, last pass referencing) + let mut intervals = BTreeMap::::new(); + + // the root node creates and transitions resources added to the + // graph. don't want to create any resources which are never used + // after `First`. a newly created resource has no layout and no + // writes to make-available. + for rid in self.resource_descs.keys() { + references.insert(GraphRef { + pass: PassNode::First, + resource: *rid, + access: RefAccess::Write(Access::undefined()), + }); + } + + for (i, pass) in self.pass_descs.iter().enumerate() { + let mut reads = BTreeMap::new(); + for (rid, access) in &pass.reads { + reads + .entry(*rid) + .and_modify(|entry| { + *entry = *entry | *access; + }) + .or_insert(*access); + + intervals + .entry(*rid) + .and_modify(|entry| { + entry.1 = PassNode::Pass(i); + }) + .or_insert((PassNode::Pass(i), PassNode::Pass(i))); + } + references.extend(reads.into_iter().map(|(resource, access)| GraphRef { + pass: PassNode::Pass(i), + resource, + access: RefAccess::Read(access), + })); + + let mut writes = BTreeMap::new(); + for (rid, access) in &pass.writes { + writes + .entry(*rid) + .and_modify(|entry| { + *entry = *entry | *access; + }) + .or_insert(*access); + + intervals + .entry(*rid) + .and_modify(|entry| { + entry.1 = PassNode::Pass(i); + }) + .or_insert((PassNode::Pass(i), PassNode::Pass(i))); + } + references.extend(writes.into_iter().map(|(resource, access)| GraphRef { + pass: PassNode::Pass(i), + resource, + access: RefAccess::Write(access), + })); + } + + // any resource marked as output should be created and returned even + // if it isn't referenced by any pass. + for rid in &self.outputs { + references.insert(GraphRef { + pass: PassNode::Last, + resource: *rid, + access: RefAccess::None, + }); + + intervals + .entry(*rid) + .and_modify(|entry| { + entry.1 = PassNode::Last; + }) + .or_insert((PassNode::Last, PassNode::Last)); + } + + (references, intervals) + }; + + #[derive(Debug)] + enum Barrier { + Logical, + Execution { + src: vk::PipelineStageFlags2, + dst: vk::PipelineStageFlags2, + }, + LayoutTransition { + src: (vk::PipelineStageFlags2, vk::ImageLayout), + dst: (vk::PipelineStageFlags2, vk::ImageLayout), + }, + + MakeAvailable { + src: (vk::PipelineStageFlags2, vk::AccessFlags2), + dst: vk::PipelineStageFlags2, + }, + MakeVisible { + src: vk::PipelineStageFlags2, + dst: (vk::PipelineStageFlags2, vk::AccessFlags2), + }, + MemoryBarrier { + src: (vk::PipelineStageFlags2, vk::AccessFlags2), + dst: (vk::PipelineStageFlags2, vk::AccessFlags2), + }, + } + + impl Display for Barrier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Barrier::Logical => write!(f, "Logical"), + Barrier::Execution { .. } => write!(f, "Execution"), + Barrier::LayoutTransition { .. } => write!(f, "Layout"), + Barrier::MakeAvailable { .. } => write!(f, "MakeAvailable"), + Barrier::MakeVisible { .. } => write!(f, "MakeVisible"), + Barrier::MemoryBarrier { .. } => write!(f, "MemoryBarrier"), + } + } + } + + // build graph from references. + { + let mut edges = Vec::new(); + + intervals.iter().for_each(|(&rid, &(from, to))| { + #[derive(Clone, Copy, Debug)] + enum PreviousRef { + Write(PassNode, Access), + Read(PassNode, Access), + } + impl PreviousRef { + fn node(&self) -> PassNode { + match self { + PreviousRef::Write(pass_node, _) => *pass_node, + PreviousRef::Read(pass_node, _) => *pass_node, + } + } + fn access(&self) -> Access { + match self { + PreviousRef::Write(_, access) => *access, + PreviousRef::Read(_, access) => *access, + } + } + } + // writes not yet made available + let mut to_make_available = AccessMask::undefined(); + // writes already made-visible + let mut made_visible = AccessMask::undefined(); + let mut last_read = PreviousRef::Write(PassNode::First, Access::undefined()); + let mut last_write = PreviousRef::Write(PassNode::First, Access::undefined()); + let mut last_ref = PreviousRef::Write(PassNode::First, Access::undefined()); + + references + .range( + GraphRef { + pass: from, + resource: rid, + access: RefAccess::__Min, + }..GraphRef { + pass: to, + resource: rid, + access: RefAccess::__Max, + }, + ) + .for_each(|a| { + // TODO: VkEvents can make this more + // fine-grained but also probably have zero + // real-world benefit :< + + last_ref = match a.access { + RefAccess::None => { + // make-available previous writes + // no-op edge between previous reference and a.pass + + edges.push(((last_ref.node(), a.pass), (rid, Barrier::Logical))); + // because this is the last node, setting last_ref isn't required. + PreviousRef::Read(a.pass, Access::undefined()) + } + RefAccess::Read(access) => { + // - if read: no writes pending, check for + // layout transition, otherwise an edge to + // previous write. make sure it is only executed + // once. + // - if write: make-available writes + make-visible for reads + let make_visible_mask = access.into_access_mask() & !made_visible; + made_visible = made_visible | make_visible_mask; + + match last_ref { + PreviousRef::Read(pass_node, before) => { + if !make_visible_mask.is_empty() { + // make-visible reads. + edges.push(( + (last_write.node(), a.pass), + ( + rid, + Barrier::MakeVisible { + src: last_write.access().stage, + dst: ( + make_visible_mask.stage, + make_visible_mask.mask, + ), + }, + ), + )); + } else { + // still require a after b + edges.push(( + (last_write.node(), a.pass), + ( + rid, + Barrier::Execution { + src: last_write.access().stage, + dst: access.stage, + }, + ), + )); + } + + if before.layout != access.layout { + edges.push(( + (pass_node, a.pass), + ( + rid, + Barrier::LayoutTransition { + src: (before.stage, before.layout.unwrap()), + dst: (access.stage, access.layout.unwrap()), + }, + ), + )); + } + } + PreviousRef::Write(..) => { + // make writes visible + if make_visible_mask.is_empty() { + edges.push(( + (last_write.node(), a.pass), + ( + rid, + Barrier::MakeVisible { + src: last_write.access().stage, + dst: ( + make_visible_mask.stage, + make_visible_mask.mask, + ), + }, + ), + )); + } + // make all writes available + if !to_make_available.is_empty() { + edges.push(( + (last_write.node(), a.pass), + ( + rid, + Barrier::MakeAvailable { + src: ( + to_make_available.stage, + to_make_available.mask, + ), + dst: access.stage, + }, + ), + )); + // mark that we've made all pending writes available + to_make_available = AccessMask::undefined(); + } + + if make_visible_mask.is_empty() + && to_make_available.is_empty() + { + // still require a after b + edges.push(( + (last_write.node(), a.pass), + ( + rid, + Barrier::Execution { + src: last_write.access().stage, + dst: access.stage, + }, + ), + )); + } + } + } + last_read = PreviousRef::Read(a.pass, access); + last_write + } + RefAccess::Write(access) => { + // - if read: execution barrier against write-after-read + // - if write: check for layout transition, otherwise a no-op edge. + to_make_available = to_make_available | access.into_access_mask(); + + match last_ref { + PreviousRef::Read(pass_node, before) => { + // execution barrier to ward against write-after-read + edges.push(( + (pass_node, a.pass), + ( + rid, + Barrier::Execution { + src: before.stage, + dst: access.stage, + }, + ), + )); + } + PreviousRef::Write(pass_node, before) => { + if before.layout != access.layout { + // as far as I understand the spec, + // this already makes-available + edges.push(( + (pass_node, a.pass), + ( + rid, + Barrier::LayoutTransition { + src: (before.stage, before.layout.unwrap()), + dst: (access.stage, access.layout.unwrap()), + }, + ), + )); + } else { + // write_no_sync: pass tells us that + // writes do not interleave. + edges.push(( + (last_read.node(), a.pass), + (rid, Barrier::Logical), + )); + } + } + } + last_write = PreviousRef::Write(a.pass, access); + last_write + } + _ => unreachable!(), + }; + }); + }); + + let mut dag = petgraph::graph::DiGraph::new(); + dag.add_node(PassNode::First); + for i in 0..self.pass_descs.len() { + dag.add_node(PassNode::Pass(i)); + } + dag.add_node(PassNode::Last); + + for ((from, to), weight) in edges { + dag.add_edge(from.into_u32().into(), to.into_u32().into(), weight); + } + + #[cfg(any(debug_assertions, test))] + std::fs::write( + "render_graph2.dot", + &format!( + "{:?}", + petgraph::dot::Dot::with_attr_getters( + &dag, + &[], + &|_graph, edgeref| { + format!( + "label = \"{},{}\"", + edgeref.weight().0.as_u32(), + edgeref.weight().1, + ) + }, + &|_graph, noderef| { format!("label = \"Pass({:?})\"", noderef.weight()) } + ) + ), + ) + .expect("writing render_graph repr"); + } + let now = std::time::Instant::now(); let mut dag = petgraph::stable_graph::StableDiGraph::new();