diff --git a/crates/game/src/main.rs b/crates/game/src/main.rs index 03c7395..355dfae 100644 --- a/crates/game/src/main.rs +++ b/crates/game/src/main.rs @@ -148,10 +148,7 @@ impl ApplicationHandler for WinitState { for (&window, &resize) in self.last_resize_events.clone().iter() { self.handle_final_resize(window, resize); } - // let window_ids = self.windows2.keys().cloned().collect::>(); - // for window in window_ids { - // self.handle_draw_request(window); - // } + self.last_resize_events.clear(); if self.windows2.is_empty() { diff --git a/crates/renderer/src/render_graph.rs b/crates/renderer/src/render_graph.rs index 925b5ae..75efc76 100644 --- a/crates/renderer/src/render_graph.rs +++ b/crates/renderer/src/render_graph.rs @@ -1,42 +1,44 @@ #![allow(dead_code)] -use crate::util::hash_f32; +use std::{collections::BTreeMap, fmt::Debug, sync::Arc}; + +use crate::{ + buffers::{Buffer, BufferDesc}, + commands, def_monotonic_id, + device::{self, Device}, + images::{Image, ImageDesc}, + util::Rgba, + SwapchainFrame, +}; use ash::vk; +use petgraph::visit::NodeRef; + +def_monotonic_id!(pub RenderGraphResourceId); + +#[derive(Debug)] +pub enum RenderGraphResourceDesc { + Image(ImageDesc), + Buffer(BufferDesc), +} + +#[derive(Debug)] +pub enum RenderGraphResource { + Framebuffer(Arc), + ImportedImage(Arc), + ImportedBuffer(Arc), + Image(Image), + Buffer(Buffer), +} #[derive(Debug, Clone, Copy)] -pub struct Rgba(pub [f32; 4]); - -impl std::hash::Hash for Rgba { - fn hash(&self, state: &mut H) { - self.0.map(|f| hash_f32(state, f)); - } -} - -impl Rgba { - pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { - Self([r, g, b, a]) - } - pub fn into_u32(&self) -> [u32; 4] { - self.0.map(|f| (f.clamp(0.0, 1.0) * 255.0) as u32) - } - pub fn into_f32(&self) -> [f32; 4] { - self.0 - } - pub fn into_snorm(&self) -> [f32; 4] { - self.0.map(|f| (f - 0.5) * 2.0) - } - pub fn into_i32(&self) -> [i32; 4] { - self.0.map(|f| (f.clamp(0.0, 1.0) * 255.0) as i32 - 128) - } -} - -enum LoadOp { +pub enum LoadOp { Clear(Rgba), Load, DontCare, } -enum StoreOp { +#[derive(Debug, Clone, Copy)] +pub enum StoreOp { DontCare, Store, } @@ -48,8 +50,279 @@ struct AttachmentInfo { store: StoreOp, } -struct Texture { - texture: vk::Image, +pub struct RenderContext<'a> { + device: device::Device, + cmd: &'a commands::SingleUseCommand, } -pub struct RenderGraph {} +pub trait Pass: Debug { + /// returns the layout the pass requires an image dependency to be in prior + /// to the pass. + fn get_layout_of_in_image_dependency(&self, id: RenderGraphResourceId) -> vk::ImageLayout; + /// returns the layout the pass will leave an image dependency in after the + /// pass. + fn get_layout_of_out_image_dependency(&self, id: RenderGraphResourceId) -> vk::ImageLayout; + /// mask of the queue capability requirements of this pass. + fn get_queue_capability_requirements(&self) -> device::QueueFlags; + /// returns an iterator over all (in) dependencies. + fn get_in_dependencies<'a>(&'a self) -> Box + 'a>; + fn get_out_dependencies<'a>(&'a self) -> Box + 'a>; + + fn record(self, ctx: &RenderContext) -> crate::Result<()>; +} + +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, + passes: 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(), + passes: Vec::new(), + outputs: Vec::new(), + } + } + + pub fn add_resource(&mut self, desc: RenderGraphResourceDesc) -> RenderGraphResourceId { + let id = RenderGraphResourceId::new(); + self.resource_descs.insert(id, desc); + id + } + pub fn mark_as_output(&mut self, id: RenderGraphResourceId) { + // TODO: dedup + self.outputs.push(id); + } + pub fn import_image(&mut self, image: Arc) -> RenderGraphResourceId { + let id = RenderGraphResourceId::new(); + self.resources + .insert(id, RenderGraphResource::ImportedImage(image)); + id + } + pub fn import_buffer(&mut self, buffer: Arc) -> RenderGraphResourceId { + let id = RenderGraphResourceId::new(); + self.resources + .insert(id, RenderGraphResource::ImportedBuffer(buffer)); + id + } + pub fn import_framebuffer(&mut self, frame: Arc) -> RenderGraphResourceId { + let id = RenderGraphResourceId::new(); + self.resources + .insert(id, RenderGraphResource::Framebuffer(frame)); + id + } + pub fn add_pass(&mut self, pass: P) { + self.passes.push(Box::new(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) { + eprintln!("{:#?}", &self); + let mut last_write = BTreeMap::new(); + + let mut dag = petgraph::stable_graph::StableDiGraph::new(); + + // insert edges between write->read edges of 2 passes + for (i, pass) in self.passes.iter().enumerate() { + let node = dag.add_node(i); + for dep in pass.get_in_dependencies() { + if let Some(&other) = last_write.get(&dep) { + dag.add_edge(other, node, dep); + } + } + + for dep in pass.get_out_dependencies() { + // keep track of which pass last wrote to a resource + // this is the node to build an edge from next time this resource is read + last_write.insert(dep, node); + } + } + + // pseudo pass for trackingoutputs + let output = dag.add_node(!0); + for (id, node) in self + .outputs + .into_iter() + .filter_map(|id| last_write.get(&id).cloned().map(|node| (id, node))) + { + dag.add_edge(node, output, id); + } + + // 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); + } + } + + #[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(); + loop { + let (sources, indices): (Vec<_>, Vec<_>) = dag + .externals(petgraph::Direction::Incoming) + .filter(|&id| id != output) + .filter_map(|id| dag.node_weight(id).cloned().map(|idx| (id, idx))) + .unzip(); + + if sources.is_empty() { + break; + } + + for &source in &sources { + dag.remove_node(source); + } + + topological_map.push(indices); + } + + // I don't think this can currently happen with the way passes are added. + dag.remove_node(output); + if dag.node_count() > 0 { + panic!("dag is cyclic!"); + } + + eprintln!("topology: {:?}", topological_map); + } +} + +#[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_layout_of_in_image_dependency( + &self, + _id: RenderGraphResourceId, + ) -> vk::ImageLayout { + $layout_in + } + + fn get_layout_of_out_image_dependency( + &self, + _id: RenderGraphResourceId, + ) -> vk::ImageLayout { + $layout_out + } + + fn get_queue_capability_requirements(&self) -> device::QueueFlags { + $queue + } + + fn get_in_dependencies<'a>( + &'a self, + ) -> Box + 'a> { + Box::new(self.0.iter().cloned()) + } + + fn get_out_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::COLOR_ATTACHMENT_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(); + } +}