rendergraph dummy impl

This commit is contained in:
Janis 2025-01-03 00:21:22 +01:00
parent da0befbeaf
commit 1ef4a667c7
2 changed files with 306 additions and 36 deletions

View file

@ -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::<Vec<_>>();
// for window in window_ids {
// self.handle_draw_request(window);
// }
self.last_resize_events.clear();
if self.windows2.is_empty() {

View file

@ -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<SwapchainFrame>),
ImportedImage(Arc<Image>),
ImportedBuffer(Arc<Buffer>),
Image(Image),
Buffer(Buffer),
}
#[derive(Debug, Clone, Copy)]
pub struct Rgba(pub [f32; 4]);
impl std::hash::Hash for Rgba {
fn hash<H: std::hash::Hasher>(&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<dyn Iterator<Item = RenderGraphResourceId> + 'a>;
fn get_out_dependencies<'a>(&'a self) -> Box<dyn Iterator<Item = RenderGraphResourceId> + '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<RenderGraphResourceId, RenderGraphResourceDesc>,
resources: BTreeMap<RenderGraphResourceId, RenderGraphResource>,
passes: Vec<Box<dyn Pass>>,
/// the rendergraph produces these resources. Any passes on which these
/// outputs do not depend are pruned.
outputs: Vec<RenderGraphResourceId>,
}
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<Image>) -> RenderGraphResourceId {
let id = RenderGraphResourceId::new();
self.resources
.insert(id, RenderGraphResource::ImportedImage(image));
id
}
pub fn import_buffer(&mut self, buffer: Arc<Buffer>) -> RenderGraphResourceId {
let id = RenderGraphResourceId::new();
self.resources
.insert(id, RenderGraphResource::ImportedBuffer(buffer));
id
}
pub fn import_framebuffer(&mut self, frame: Arc<SwapchainFrame>) -> RenderGraphResourceId {
let id = RenderGraphResourceId::new();
self.resources
.insert(id, RenderGraphResource::Framebuffer(frame));
id
}
pub fn add_pass<P: Pass + 'static>(&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::<Vec<_>>();
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<RenderGraphResourceId>, Vec<RenderGraphResourceId>);
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<dyn Iterator<Item = RenderGraphResourceId> + 'a> {
Box::new(self.0.iter().cloned())
}
fn get_out_dependencies<'a>(
&'a self,
) -> Box<dyn Iterator<Item = RenderGraphResourceId> + '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();
}
}