#![allow(dead_code)] use std::{ collections::{BTreeMap, BTreeSet}, fmt::Debug, sync::Arc, }; use crate::{ buffers::{Buffer, BufferDesc}, commands, def_monotonic_id, device::{self, DeviceOwned}, images::{self, Image, ImageDesc}, sync, util::{self, Rgba, WithLifetime}, SwapchainFrame, }; use ash::vk; use itertools::Itertools; use petgraph::{ graph::NodeIndex, visit::{EdgeRef, IntoNodeReferences, NodeRef}, }; def_monotonic_id!(pub GraphResourceId); #[derive(Debug, Clone)] pub enum GraphResourceDesc { Image(ImageDesc), Buffer(BufferDesc), } #[derive(Debug, PartialEq, Eq)] pub enum GraphResource { Framebuffer(Arc), ImportedImage(Arc), ImportedBuffer(Arc), Image(Arc), Buffer(Buffer), } #[derive(Debug, Clone, Copy)] pub enum LoadOp { Clear(Rgba), Load, DontCare, } #[derive(Debug, Clone, Copy)] pub enum StoreOp { DontCare, Store, } pub struct RenderContext<'a> { pub device: device::Device, pub cmd: commands::SingleUseCommand, pub resources: &'a BTreeMap, } impl RenderContext<'_> { pub fn get_image(&self, id: GraphResourceId) -> Option<&Arc> { self.resources.get(&id).and_then(|res| match res { GraphResource::ImportedImage(arc) => Some(arc), GraphResource::Image(image) => Some(image), GraphResource::Framebuffer(fb) => Some(&fb.image), _ => None, }) } pub fn get_buffer(&self, id: GraphResourceId) -> Option<&Buffer> { self.resources.get(&id).and_then(|res| match res { GraphResource::ImportedBuffer(arc) => Some(arc.as_ref()), GraphResource::Buffer(buffer) => Some(buffer), _ => None, }) } } #[derive(Debug, Clone, Copy, Default)] pub struct Access { pub stage: vk::PipelineStageFlags2, pub mask: vk::AccessFlags2, pub layout: Option, } impl core::ops::BitOr for Access { type Output = Self; fn bitor(self, rhs: Self) -> Self::Output { assert_eq!(self.layout, rhs.layout); Self { stage: self.stage | rhs.stage, mask: self.mask | rhs.mask, layout: self.layout, } } } impl Access { pub fn undefined() -> Self { Self { stage: vk::PipelineStageFlags2::NONE, mask: vk::AccessFlags2::empty(), layout: Some(vk::ImageLayout::UNDEFINED), } } pub fn general() -> Self { Self { stage: vk::PipelineStageFlags2::NONE, mask: vk::AccessFlags2::empty(), layout: Some(vk::ImageLayout::GENERAL), } } pub fn transfer_read() -> Self { Self { stage: vk::PipelineStageFlags2::TRANSFER, mask: vk::AccessFlags2::TRANSFER_READ, layout: Some(vk::ImageLayout::TRANSFER_SRC_OPTIMAL), } } pub fn transfer_write() -> Self { Self { stage: vk::PipelineStageFlags2::TRANSFER, mask: vk::AccessFlags2::TRANSFER_WRITE, layout: Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL), } } pub fn vertex_read() -> Self { Self { stage: vk::PipelineStageFlags2::VERTEX_ATTRIBUTE_INPUT, mask: vk::AccessFlags2::VERTEX_ATTRIBUTE_READ, layout: None, } } pub fn index_read() -> Self { Self { stage: vk::PipelineStageFlags2::INDEX_INPUT, mask: vk::AccessFlags2::INDEX_READ, layout: None, } } pub fn indirect_read() -> Self { Self { stage: vk::PipelineStageFlags2::DRAW_INDIRECT, mask: vk::AccessFlags2::INDIRECT_COMMAND_READ, layout: None, } } pub fn color_attachment_read_only() -> Self { Self { stage: vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, mask: vk::AccessFlags2::COLOR_ATTACHMENT_READ, layout: Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL), } } pub fn color_attachment_write_only() -> Self { Self { stage: vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, mask: vk::AccessFlags2::COLOR_ATTACHMENT_WRITE, layout: Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL), } } pub fn color_attachment_read_write() -> Self { Self { stage: vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, mask: vk::AccessFlags2::COLOR_ATTACHMENT_WRITE | vk::AccessFlags2::COLOR_ATTACHMENT_READ, layout: Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL), } } pub fn present() -> Self { Self { stage: vk::PipelineStageFlags2::NONE, mask: vk::AccessFlags2::empty(), layout: Some(vk::ImageLayout::PRESENT_SRC_KHR), } } } pub type RecordFn = dyn FnOnce(&RenderContext) -> crate::Result<()> + Send; pub struct PassDesc { // this pass performs `Access` read on `GraphResourceId`. // some `GraphResourceId` may occur multiple times. pub reads: Vec<(GraphResourceId, Access)>, // this pass performs `Access` write on `GraphResourceId`. // some `GraphResourceId` may occur multiple times. pub writes: Vec<(GraphResourceId, Access)>, pub record: Box, } impl Default for PassDesc { fn default() -> Self { Self { reads: Default::default(), writes: Default::default(), record: Box::new(|_| Ok(())), } } } impl Debug for PassDesc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PassDesc") .field("reads", &self.reads) .field("write", &self.writes) .finish_non_exhaustive() } } def_monotonic_id!(pub RenderGraphPassId); // Non-imported resources remain `RenderGraphResourceDesc`s because they may be // able to be aliased. // This should be dual to liveness/register allocation in a compiler. // Dummy-impl is just allocating every resource_desc itself. 5head-impl is trying // to find resource_descs which are eq, but whose liveness doesn't overlap. #[derive(Debug)] pub struct RenderGraph { resource_descs: BTreeMap, resources: BTreeMap, accesses: BTreeMap, pass_descs: Vec, /// the rendergraph produces these resources. Any passes on which these /// outputs do not depend are pruned. outputs: Vec, } impl RenderGraph { pub fn new() -> Self { Self { resource_descs: BTreeMap::new(), resources: BTreeMap::new(), pass_descs: Vec::new(), accesses: BTreeMap::new(), outputs: Vec::new(), } } pub fn add_resource(&mut self, desc: GraphResourceDesc) -> GraphResourceId { let id = GraphResourceId::new(); self.resource_descs.insert(id, desc); self.accesses.insert(id, Access::undefined()); id } pub fn mark_as_output(&mut self, id: GraphResourceId) { // TODO: dedup self.outputs.push(id); } pub fn import_resource(&mut self, res: GraphResource, access: Access) -> GraphResourceId { if let Some((&id, _)) = self .resources .iter() .find(|(_, resident)| &&res == resident) { id } else { let id = GraphResourceId::new(); self.resources.insert(id, res); self.accesses.insert(id, access); id } } pub fn import_image(&mut self, image: Arc, access: Access) -> GraphResourceId { let res = GraphResource::ImportedImage(image); self.import_resource(res, access) } pub fn import_buffer(&mut self, buffer: Arc, access: Access) -> GraphResourceId { let res = GraphResource::ImportedBuffer(buffer); self.import_resource(res, access) } pub fn import_framebuffer(&mut self, frame: Arc) -> GraphResourceId { let id = GraphResourceId::new(); self.resources.insert(id, GraphResource::Framebuffer(frame)); self.mark_as_output(id); id } pub fn add_pass(&mut self, pass: PassDesc) { self.pass_descs.push(pass); } // https://blog.traverseresearch.nl/render-graph-101-f42646255636 // https://github.com/EmbarkStudios/kajiya/blob/main/crates/lib/kajiya-rg/src/graph.rs // https://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/ pub fn resolve( &mut self, device: device::Device, ) -> crate::Result>> { // create internal resources: for (&id, desc) in self.resource_descs.iter() { tracing::trace!("creating resource {id:?} with {desc:?}"); match desc.clone() { GraphResourceDesc::Image(image_desc) => { self.resources.insert( id, GraphResource::Image(Arc::new(Image::new(device.clone(), image_desc)?)), ); } GraphResourceDesc::Buffer(buffer_desc) => { self.resources.insert( id, GraphResource::Buffer(Buffer::new(device.clone(), buffer_desc)?), ); } } } #[derive(Debug, Clone, Copy)] enum PassNode { First, Pass(usize), Last, } let mut dag = petgraph::stable_graph::StableDiGraph::new(); let root = dag.add_node(PassNode::First); let mut last_write: BTreeMap = self .resources .keys() .filter_map(|id| self.accesses.get(id).map(|access| (*id, (root, *access)))) .collect::>(); let mut last_read: BTreeMap = BTreeMap::new(); // TODO: rewrite finding edges properly. // finding out if this graph is cyclical is actually non-trivial // some pass might require both a read of a resource 1, and a read of a resource 2, where 2 is the product of another pass writing to resource 1. // this could be resolved by copying resource 1 before the write pass. // tl;dr: write-after-read makes this all more complicated // insert edges between write->read edges of 2 passes for (i, pass) in self.pass_descs.iter().enumerate() { let node = dag.add_node(PassNode::Pass(i)); let mut read_accesses = BTreeMap::new(); for (rid, access) in &pass.reads { read_accesses .entry(*rid) .and_modify(|a| { // a single pass must not read one image twice with different layouts *a = *a | *access; }) .or_insert(*access); } for (rid, after) in read_accesses { if let Some(&(other, before)) = last_write.get(&rid) { tracing::trace!("adding edge between {other:?} and {node:?} for {rid:?} with ({before:?} -> {after:?})"); dag.add_edge(other, node, (rid, (before, after))); } last_read.insert(rid, (node, after)); } let mut write_accesses = BTreeMap::new(); for (rid, access) in &pass.writes { write_accesses .entry(*rid) .and_modify(|a| { // a single pass must not write one image twice with different layouts *a = *a | *access; }) .or_insert(*access); } for (rid, after) in write_accesses { last_write.insert(rid, (node, after)); if let Some(&(other, read)) = last_read.get(&rid) && other != node { tracing::trace!("adding edge between {other:?} and {node:?} for {rid:?} with ({read:?} -> {after:?}) (WaR)"); dag.add_edge(other, node, (rid, (read, after))); } } } // pseudo pass for tracking outputs let output = dag.add_node(PassNode::Last); for (id, (node, access)) in self .outputs .iter() .filter_map(|&id| last_write.get(&id).cloned().map(|node| (id, node))) { dag.add_edge( node, output, ( id, ( access, // make output writes available Access { stage: vk::PipelineStageFlags2::NONE, mask: vk::AccessFlags2::empty(), ..access }, ), ), ); } // prune dead nodes loop { let sinks = dag .externals(petgraph::Direction::Outgoing) .filter(|idx| idx != &output) .collect::>(); if sinks.is_empty() { break; } for sink in sinks { dag.remove_node(sink); } } // handle layout additional transitions let edges = dag .node_references() .map(|(source, _)| { let mut per_resourcelayout_multimap: BTreeMap< (GraphResourceId, Option), Vec<(Access, NodeIndex)>, > = BTreeMap::new(); let mut resources = BTreeSet::new(); dag.edges_directed(source, petgraph::Direction::Outgoing) .for_each(|edge| { let (rid, (_, after)) = edge.weight(); let target = edge.target(); let key = (*rid, after.layout); let item = (*after, target); resources.insert(*rid); per_resourcelayout_multimap .entry(key) .and_modify(|list| list.push(item)) .or_insert(vec![item]); }); let mut edges = vec![]; for resource in resources { for (a, b) in per_resourcelayout_multimap .range( (resource, None) ..=(resource, Some(vk::ImageLayout::from_raw(i32::MAX))), ) .tuple_windows() { let a = a.1; let b = b.1; // create new edge between all members of (a) and (b). // topological mapping will fold all transitions into one. for i in 0..a.len().max(b.len()) { let from = a.get(i).unwrap_or(a.last().unwrap()); let to = b.get(i).unwrap_or(b.last().unwrap()); let edge = ((from.1, to.1), (resource, (from.0, to.0))); edges.push(edge); } } } edges }) .flatten() .collect::>(); for ((from, to), edge) in edges { tracing::trace!( "adding additional edge between {from:?} and {to:?} for {:?} with ({:?} -> {:?})", edge.0, edge.1 .0, edge.1 .1 ); dag.add_edge(from, to, edge); } // #[cfg(any(debug_assertions, test))] // std::fs::write( // "render_graph.dot", // &format!( // "{:?}", // petgraph::dot::Dot::with_attr_getters( // &dag, // &[], // &|_graph, edgeref| { format!("label = \"{:?}\"", edgeref.weight()) }, // &|_graph, noderef| { format!("label = \"Pass({:?})\"", noderef.weight()) } // ) // ), // ) // .expect("writing render_graph repr"); let mut topological_map = Vec::new(); let mut top_dag = dag.clone(); // create topological map of DAG from sink to source loop { let (sinks, passes): (Vec<_>, Vec<_>) = top_dag .externals(petgraph::Direction::Outgoing) .filter(|&id| id != root) .filter_map(|id| top_dag.node_weight(id).cloned().map(|idx| (id, idx))) .unzip(); if sinks.is_empty() { break; } let mut barriers = BTreeMap::new(); for &sink in &sinks { top_dag .edges_directed(sink, petgraph::Direction::Incoming) .for_each(|edge| { let (rid, (before, after)) = edge.weight(); barriers .entry(*rid) .and_modify(|(from, to)| { *from = *from | *before; *to = *to | *after; }) .or_insert((*before, *after)); }); top_dag.remove_node(sink); } topological_map.push((passes, barriers)); } // I don't think this can currently happen with the way passes are added. top_dag.remove_node(root); if top_dag.node_count() > 0 { eprintln!("dag: {top_dag:?}"); panic!("dag is cyclic!"); } let pool = commands::SingleUseCommandPool::new(device.clone(), device.graphics_queue().clone())?; let resources = &self.resources; let cmds = topological_map .iter() .rev() .map(|(set, accesses)| { let pool = pool.clone(); let device = device.clone(); let passes = set .into_iter() .filter_map(|pass| { if let &PassNode::Pass(i) = pass { Some(i) } else { None } }) .map(|i| core::mem::take(&mut self.pass_descs[i])) .collect::>(); let cmd = pool.alloc()?; // transitions for (&id, &(from, to)) in accesses.iter() { Self::transition_resource( resources.get(&id).unwrap(), device.dev(), unsafe { &cmd.buffer() }, from, to, ); } let ctx = RenderContext { device, cmd, resources, }; for pass in passes { (pass.record)(&ctx)?; } ctx.cmd.end()?; crate::Result::Ok(ctx.cmd) }) .collect::>>()?; let cmd_list = commands::CommandList(cmds); // let future = cmd_list.submit(None, None, Arc::new(sync::Fence::create(device.clone())?))?; // future.block()?; // let outputs = self // .outputs // .iter() // .filter_map(|id| self.resources.remove(id).map(|res| (*id, res))) // .collect::>(); Ok(WithLifetime::new(cmd_list)) } pub fn get_outputs(&mut self) -> BTreeMap { let outputs = self .outputs .iter() .filter_map(|id| self.resources.remove(id).map(|res| (*id, res))) .collect::>(); outputs } pub fn transition_resource( res: &GraphResource, dev: &ash::Device, cmd: &vk::CommandBuffer, from: Access, to: Access, ) { let barrier: Barrier = match res { GraphResource::Framebuffer(arc) => { image_barrier(arc.image.handle(), arc.image.format(), from, to, None).into() } GraphResource::ImportedImage(arc) => { image_barrier(arc.handle(), arc.format(), from, to, None).into() } GraphResource::ImportedBuffer(arc) => { buffer_barrier(arc.handle(), 0, arc.len(), from, to, None).into() } GraphResource::Image(image) => { image_barrier(image.handle(), image.format(), from, to, None).into() } GraphResource::Buffer(buffer) => { buffer_barrier(buffer.handle(), 0, buffer.len(), from, to, None).into() } }; unsafe { dev.cmd_pipeline_barrier2(*cmd, &((&barrier).into())); } } fn transition_resource_to( accesses: &mut BTreeMap, resources: &BTreeMap, dev: &ash::Device, cmd: &vk::CommandBuffer, id: GraphResourceId, to: Access, ) { let old_access = accesses.get(&id); let res = resources.get(&id); if let (Some(&old_access), Some(res)) = (old_access, res) { Self::transition_resource(res, dev, cmd, old_access, to); accesses.insert(id, to); } } } pub enum Barrier { Image(vk::ImageMemoryBarrier2<'static>), Buffer(vk::BufferMemoryBarrier2<'static>), } impl<'a> From<&'a Barrier> for vk::DependencyInfo<'a> { fn from(value: &'a Barrier) -> Self { let info = vk::DependencyInfo::default(); let info = match value { Barrier::Image(barrier) => info.image_memory_barriers(core::slice::from_ref(barrier)), Barrier::Buffer(barrier) => info.buffer_memory_barriers(core::slice::from_ref(barrier)), }; info } } impl From> for Barrier { fn from(value: vk::ImageMemoryBarrier2<'static>) -> Self { Self::Image(value) } } impl From> for Barrier { fn from(value: vk::BufferMemoryBarrier2<'static>) -> Self { Self::Buffer(value) } } pub fn buffer_barrier( buffer: vk::Buffer, offset: u64, size: u64, before: Access, after: Access, queue_families: Option<(u32, u32)>, ) -> vk::BufferMemoryBarrier2<'static> { vk::BufferMemoryBarrier2::default() .buffer(buffer) .offset(offset) .size(size) .src_access_mask(before.mask) .src_stage_mask(before.stage) .dst_access_mask(after.mask) .dst_stage_mask(after.stage) .src_queue_family_index( queue_families .map(|(src, _)| src) .unwrap_or(vk::QUEUE_FAMILY_IGNORED), ) .dst_queue_family_index( queue_families .map(|(_, dst)| dst) .unwrap_or(vk::QUEUE_FAMILY_IGNORED), ) } pub fn image_barrier( image: vk::Image, format: vk::Format, before_access: Access, after_access: Access, queue_families: Option<(u32, u32)>, ) -> vk::ImageMemoryBarrier2<'static> { vk::ImageMemoryBarrier2::default() .src_access_mask(before_access.mask) .src_stage_mask(before_access.stage) .dst_access_mask(after_access.mask) .dst_stage_mask(after_access.stage) .image(image) .old_layout(before_access.layout.unwrap_or_default()) .new_layout(after_access.layout.unwrap_or_default()) .subresource_range(vk::ImageSubresourceRange { aspect_mask: util::image_aspect_from_format(format), ..images::SUBRESOURCERANGE_ALL }) .src_queue_family_index( queue_families .map(|(src, _)| src) .unwrap_or(vk::QUEUE_FAMILY_IGNORED), ) .dst_queue_family_index( queue_families .map(|(_, dst)| dst) .unwrap_or(vk::QUEUE_FAMILY_IGNORED), ) } // #[cfg(test)] // mod tests { // use super::*; // macro_rules! def_dummy_pass { // ($name:ident: {$queue:path, $layout_in:path, $layout_out:path}) => { // #[derive(Debug, Clone)] // struct $name(Vec, Vec); // impl Pass for $name { // fn get_read_resource_access(&self, _id: RenderGraphResourceId) -> ResourceAccess { // ResourceAccess::default() // } // fn get_write_resource_access(&self, _id: RenderGraphResourceId) -> ResourceAccess { // ResourceAccess::default() // } // fn get_queue_capability_requirements(&self) -> device::QueueFlags { // $queue // } // fn get_read_dependencies<'a>( // &'a self, // ) -> Box + 'a> { // Box::new(self.0.iter().cloned()) // } // fn get_write_dependencies<'a>( // &'a self, // ) -> Box + 'a> { // Box::new(self.1.iter().cloned()) // } // fn record(&self, _ctx: &RenderContext) -> crate::Result<()> { // Ok(()) // } // } // }; // } // def_dummy_pass!(DepthPass: { // device::QueueFlags::GRAPHICS, // vk::ImageLayout::DEPTH_ATTACHMENT_OPTIMAL, // vk::ImageLayout::DEPTH_ATTACHMENT_OPTIMAL}); // def_dummy_pass!(RenderPass: { // device::QueueFlags::GRAPHICS, // vk::ImageLayout::COLORiATTACHMENT_OPTIMAL, // vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL}); // def_dummy_pass!(AsyncPass: { // device::QueueFlags::ASYNC_COMPUTE, // vk::ImageLayout::UNDEFINED, // vk::ImageLayout::GENERAL}); // def_dummy_pass!(PostProcessPass: { // device::QueueFlags::ASYNC_COMPUTE, // vk::ImageLayout::GENERAL, // vk::ImageLayout::GENERAL}); // def_dummy_pass!(PresentPass: { // device::QueueFlags::PRESENT, // vk::ImageLayout::PRESENT_SRC_KHR, // vk::ImageLayout::UNDEFINED}); // def_dummy_pass!(DepthVisualisationPass: { // device::QueueFlags::ASYNC_COMPUTE, // vk::ImageLayout::GENERAL, // vk::ImageLayout::UNDEFINED}); // #[test] // fn resolve_graph() { // let mut graph = RenderGraph::new(); // let gbuffer = graph.add_resource(RenderGraphResourceDesc::Image(ImageDesc { // ..Default::default() // })); // let depth_image = graph.add_resource(RenderGraphResourceDesc::Image(ImageDesc { // ..Default::default() // })); // let depth_visualisation = graph.add_resource(RenderGraphResourceDesc::Image(ImageDesc { // ..Default::default() // })); // let compute_buffer = graph.add_resource(RenderGraphResourceDesc::Buffer(BufferDesc { // ..Default::default() // })); // graph.add_pass(DepthPass(vec![depth_image], vec![depth_image])); // graph.add_pass(DepthVisualisationPass( // vec![depth_image, depth_visualisation], // vec![depth_visualisation], // )); // graph.add_pass(AsyncPass(vec![compute_buffer], vec![compute_buffer])); // graph.add_pass(RenderPass( // vec![depth_image, compute_buffer, gbuffer], // vec![gbuffer], // )); // graph.add_pass(PostProcessPass(vec![gbuffer], vec![gbuffer])); // graph.mark_as_output(gbuffer); // graph.mark_as_output(depth_image); // // graph.resolve(); // } // } pub fn clear_pass(rg: &mut RenderGraph, color: Rgba, target: GraphResourceId) { let reads = [(target, Access::transfer_write())].to_vec(); let writes = [(target, Access::transfer_write())].to_vec(); let record: Box = Box::new({ move |ctx| { let target = ctx.get_image(target).unwrap(); let cmd = &ctx.cmd; cmd.clear_color_image( target.handle(), target.format(), vk::ImageLayout::TRANSFER_DST_OPTIMAL, color, &[images::SUBRESOURCERANGE_COLOR_ALL], ); Ok(()) } }); rg.add_pass(PassDesc { reads, writes, record, }); } pub fn present_pass(rg: &mut RenderGraph, target: GraphResourceId) { let record: Box = Box::new(|_| Ok(())); let reads = vec![(target, Access::present())]; let writes = vec![(target, Access::present())]; rg.add_pass(PassDesc { reads, writes, record, }); }