diff --git a/crates/renderer_rendergraph/Cargo.toml b/crates/renderer_rendergraph/Cargo.toml index f33a9c7..8471f90 100644 --- a/crates/renderer_rendergraph/Cargo.toml +++ b/crates/renderer_rendergraph/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +ash.workspace = true diff --git a/crates/renderer_rendergraph/src/commands.rs b/crates/renderer_rendergraph/src/commands.rs new file mode 100644 index 0000000..af0e367 --- /dev/null +++ b/crates/renderer_rendergraph/src/commands.rs @@ -0,0 +1,151 @@ +use ash::vk; + +use crate::{ + recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap}, + resources::{ + Buffer, BufferRange, ResourceId, SubresourceScope, SubresourceScopeInner, Texture, + }, +}; + +/// A recorded operation declares its accesses before compilation and applies +/// itself to a Vulkan command buffer after resource binding. +pub trait Command: RecordedCommand { + fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap); +} + +fn access( + stage: vk::PipelineStageFlags2, + access: vk::AccessFlags2, + subresource: SubresourceScopeInner, +) -> SubresourceScope { + SubresourceScope { + stage, + access, + subresource, + } +} + +#[derive(Debug, Clone, Copy)] +pub struct CopyBuffer { + pub src: Buffer, + pub src_range: BufferRange, + pub dst: Buffer, + pub dst_range: BufferRange, +} + +impl Command for CopyBuffer { + fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) { + map.insert( + command_index, + self.src.id(), + access( + vk::PipelineStageFlags2::COPY, + vk::AccessFlags2::TRANSFER_READ, + SubresourceScopeInner::Buffer(self.src_range), + ), + ); + map.insert( + command_index, + self.dst.id(), + access( + vk::PipelineStageFlags2::COPY, + vk::AccessFlags2::TRANSFER_WRITE, + SubresourceScopeInner::Buffer(self.dst_range), + ), + ); + } +} + +impl RecordedCommand for CopyBuffer { + fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> { + let region = vk::BufferCopy2::default() + .src_offset(self.src_range.offset) + .dst_offset(self.dst_range.offset) + .size(self.src_range.size.min(self.dst_range.size)); + let regions = [region]; + let info = vk::CopyBufferInfo2::default() + .src_buffer(recorder.buffer(self.src.id())?) + .dst_buffer(recorder.buffer(self.dst.id())?) + .regions(®ions); + unsafe { + recorder + .device + .cmd_copy_buffer2(recorder.command_buffer, &info) + }; + Ok(()) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ClearTexture { + pub texture: Texture, + pub range: vk::ImageSubresourceRange, + pub layout: vk::ImageLayout, + pub color: [f32; 4], +} + +impl Command for ClearTexture { + fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) { + map.insert( + command_index, + self.texture.id(), + access( + vk::PipelineStageFlags2::CLEAR, + vk::AccessFlags2::TRANSFER_WRITE, + SubresourceScopeInner::Texture { + range: self.range, + layout: self.layout, + }, + ), + ); + } +} + +impl RecordedCommand for ClearTexture { + fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> { + let value = vk::ClearColorValue { + float32: self.color, + }; + unsafe { + recorder.device.cmd_clear_color_image( + recorder.command_buffer, + recorder.image(self.texture.id())?, + self.layout, + &value, + &[self.range], + ); + } + Ok(()) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Draw { + pub vertex_count: u32, + pub instance_count: u32, + pub first_vertex: u32, + pub first_instance: u32, +} + +impl Command for Draw { + fn side_effects(&self, _command_index: CommandIndex, _map: &mut SideEffectMap) {} +} + +impl RecordedCommand for Draw { + fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> { + unsafe { + recorder.device.cmd_draw( + recorder.command_buffer, + self.vertex_count, + self.instance_count, + self.first_vertex, + self.first_instance, + ); + } + Ok(()) + } +} + +pub fn resource_id(buffer: Buffer) -> ResourceId { + buffer.id() +} diff --git a/crates/renderer_rendergraph/src/graph.rs b/crates/renderer_rendergraph/src/graph.rs new file mode 100644 index 0000000..608d6f3 --- /dev/null +++ b/crates/renderer_rendergraph/src/graph.rs @@ -0,0 +1,442 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use ash::vk; + +use crate::{ + recorder::{CommandIndex, CommandList, CommandRecorder, RecordError, scope_range}, + resources::{ + Buffer, ResourceDesc, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner, + Texture, access_flag_is_write, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GraphError { + UnknownResource(ResourceId), + DependencyCycle, +} + +#[derive(Debug, Clone, Copy)] +pub struct BarrierResource { + pub id: ResourceId, + pub range: Subresource, + pub src_stage: vk::PipelineStageFlags2, + pub src_access: vk::AccessFlags2, + pub dst_stage: vk::PipelineStageFlags2, + pub dst_access: vk::AccessFlags2, + pub old_layout: Option, + pub new_layout: Option, +} + +#[derive(Debug, Clone)] +pub struct PipelineBarrier { + pub before_command: CommandIndex, + pub resources: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResourceLifetime { + pub first_command: CommandIndex, + pub last_command: CommandIndex, + /// Placeholder allocator binding; resources with disjoint lifetimes can + /// share an offset when their descriptor sizes permit it. + pub memory_offset: u64, +} + +pub struct RenderGraph { + next_id: u64, + resources: BTreeMap, + pub commands: CommandList, +} + +impl RenderGraph { + pub fn new() -> Self { + Self { + next_id: 0, + resources: BTreeMap::new(), + commands: CommandList::new(), + } + } + + fn add_resource(&mut self, desc: ResourceDesc) -> ResourceId { + let id = ResourceId(self.next_id); + self.next_id = self.next_id.checked_add(1).expect("resource id overflow"); + self.resources.insert(id, desc); + id + } + + pub fn create_buffer(&mut self, desc: crate::resources::BufferDesc) -> Buffer { + Buffer(self.add_resource(ResourceDesc::CreatedBuffer(desc))) + } + + pub fn create_texture(&mut self, desc: crate::resources::TextureDesc) -> Texture { + Texture(self.add_resource(ResourceDesc::CreatedTexture(desc))) + } + + pub fn import_buffer( + &mut self, + handle: vk::Buffer, + initial: crate::resources::ResourceState, + ) -> Buffer { + Buffer(self.add_resource(ResourceDesc::ImportedBuffer { handle, initial })) + } + + pub fn import_texture( + &mut self, + handle: vk::Image, + initial: crate::resources::ResourceState, + ) -> Texture { + Texture(self.add_resource(ResourceDesc::ImportedTexture { handle, initial })) + } + + pub fn push(&mut self, command: C) -> CommandIndex { + self.commands.push(command) + } + + pub fn compile(self) -> Result { + let command_count = self.commands.len(); + let mut edges = vec![BTreeSet::new(); command_count]; + let mut indegree = vec![0usize; command_count]; + let accesses = self.commands.side_effects.command_accesses(); + + for current in 0..command_count { + for prior in 0..current { + let dependency = accesses[prior].iter().any(|earlier| { + accesses[current].iter().any(|later| { + earlier.resource == later.resource && earlier.scope.conflicts(&later.scope) + }) + }); + if dependency && edges[prior].insert(current) { + indegree[current] += 1; + } + } + } + + let mut ready: BTreeSet<_> = indegree + .iter() + .enumerate() + .filter_map(|(index, °ree)| (degree == 0).then_some(index)) + .collect(); + let mut order = Vec::with_capacity(command_count); + while let Some(index) = ready.pop_first() { + order.push(index.into()); + for &dependent in &edges[index] { + indegree[dependent] -= 1; + if indegree[dependent] == 0 { + ready.insert(dependent); + } + } + } + if order.len() != command_count { + return Err(GraphError::DependencyCycle); + } + + for command_accesses in accesses { + for access in command_accesses { + if !self.resources.contains_key(&access.resource) { + return Err(GraphError::UnknownResource(access.resource)); + } + } + } + + let barriers = build_barriers(&order, accesses, &self.resources); + let lifetimes = bind_resources(&self.resources, accesses); + + Ok(CompiledGraph { + resources: self.resources, + commands: self.commands, + order, + barriers, + lifetimes, + }) + } +} + +impl Default for RenderGraph { + fn default() -> Self { + Self::new() + } +} + +pub struct CompiledGraph { + resources: BTreeMap, + commands: CommandList, + order: Vec, + barriers: Vec, + lifetimes: BTreeMap, +} + +impl CompiledGraph { + pub fn command_order(&self) -> &[CommandIndex] { + &self.order + } + + pub fn barriers(&self) -> &[PipelineBarrier] { + &self.barriers + } + + pub fn resource_lifetimes(&self) -> &BTreeMap { + &self.lifetimes + } + + /// Record the compiled order into a real Vulkan command buffer. Imported + /// handles are automatically bound. Created resources require a future + /// allocator integration and currently return a missing-handle error when + /// used by a command. + pub fn record_commands( + &mut self, + recorder: &mut CommandRecorder<'_>, + ) -> Result<(), RecordError> { + for (&id, &desc) in &self.resources { + match desc { + ResourceDesc::ImportedBuffer { handle, .. } => { + recorder.buffers.insert(id, handle); + } + ResourceDesc::ImportedTexture { handle, .. } => { + recorder.images.insert(id, handle); + } + ResourceDesc::CreatedBuffer(_) | ResourceDesc::CreatedTexture(_) => {} + } + } + + let mut barriers = self.barriers.iter().peekable(); + for (ordered_index, &command_index) in self.order.iter().enumerate() { + while barriers + .peek() + .is_some_and(|barrier| barrier.before_command == ordered_index.into()) + { + recorder.pipeline_barrier(barriers.next().expect("peeked barrier")); + } + self.commands.commands[command_index.index()].apply(recorder)?; + } + Ok(()) + } +} + +fn build_barriers( + order: &[CommandIndex], + accesses: &[Vec], + resource_descs: &BTreeMap, +) -> Vec { + let mut result = Vec::new(); + for (ordered_index, &command_index) in order.iter().enumerate() { + let mut barrier_resources = Vec::new(); + for current in &accesses[command_index.index()] { + let previous = order[..ordered_index] + .iter() + .rev() + .flat_map(|&prior_index| accesses[prior_index.index()].iter().rev()) + .find(|previous| { + previous.resource == current.resource + && previous.scope.conflicts(¤t.scope) + }); + if let Some(previous) = previous { + let (old_layout, new_layout) = layouts(previous.scope, current.scope); + barrier_resources.push(BarrierResource { + id: current.resource, + range: scope_range(current.scope), + src_stage: previous.scope.stage, + src_access: previous.scope.access, + dst_stage: current.scope.stage, + dst_access: current.scope.access, + old_layout, + new_layout, + }); + continue; + } + + let desc = resource_descs[¤t.resource]; + let initial = desc.initial_state(); + let (old_layout, new_layout) = layouts_from_state(initial, current.scope); + let layout_transition = old_layout != new_layout && new_layout.is_some(); + let initial_hazard = matches!( + desc, + ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } + ) && !initial.access.is_empty() + && (access_flag_is_write(initial.access) + || access_flag_is_write(current.scope.access)); + if layout_transition || initial_hazard { + barrier_resources.push(BarrierResource { + id: current.resource, + range: scope_range(current.scope), + src_stage: initial.stage, + src_access: initial.access, + dst_stage: current.scope.stage, + dst_access: current.scope.access, + old_layout, + new_layout, + }); + } + } + if !barrier_resources.is_empty() { + result.push(PipelineBarrier { + before_command: ordered_index.into(), + resources: barrier_resources, + }); + } + } + result +} + +fn layouts( + previous: SubresourceScope, + current: SubresourceScope, +) -> (Option, Option) { + let layout = |scope: SubresourceScope| match scope.subresource { + SubresourceScopeInner::Texture { layout, .. } => Some(layout), + SubresourceScopeInner::Buffer(_) => None, + }; + (layout(previous), layout(current)) +} + +fn layouts_from_state( + initial: crate::resources::ResourceState, + current: SubresourceScope, +) -> (Option, Option) { + let current_layout = match current.subresource { + SubresourceScopeInner::Texture { layout, .. } => Some(layout), + SubresourceScopeInner::Buffer(_) => None, + }; + (initial.layout, current_layout) +} + +fn bind_resources( + resources: &BTreeMap, + accesses: &[Vec], +) -> BTreeMap { + let mut lifetimes = BTreeMap::new(); + for (command, command_accesses) in accesses.iter().enumerate() { + for access in command_accesses { + lifetimes + .entry(access.resource) + .and_modify(|lifetime: &mut ResourceLifetime| { + lifetime.last_command = command.into() + }) + .or_insert(ResourceLifetime { + first_command: command.into(), + last_command: command.into(), + memory_offset: 0, + }); + } + } + + // Skeleton aliasing policy: resources with non-overlapping lifetimes may + // share a heap offset. Actual size/alignment-aware Vulkan allocation is TODO. + let mut slots: Vec<(u64, CommandIndex, u64)> = Vec::new(); // (offset, last use, capacity) + let mut next_offset = 0; + let mut ids: Vec<_> = lifetimes.keys().copied().collect(); + ids.sort_by_key(|id| lifetimes[id].first_command); + for id in ids { + let lifetime = lifetimes.get_mut(&id).expect("lifetime key exists"); + let size = match resources[&id] { + ResourceDesc::CreatedBuffer(desc) => desc.size, + ResourceDesc::CreatedTexture(desc) => u64::from(desc.extent.width) + .saturating_mul(u64::from(desc.extent.height)) + .saturating_mul(u64::from(desc.extent.depth)) + .saturating_mul(4), + ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => 0, + }; + if resources[&id].is_created() + && let Some((offset, last_use, capacity)) = slots + .iter_mut() + .find(|(_, last, cap)| *last < lifetime.first_command && *cap >= size) + { + lifetime.memory_offset = *offset; + *last_use = lifetime.last_command; + let _ = capacity; + } else { + lifetime.memory_offset = next_offset; + if resources[&id].is_created() { + slots.push((next_offset, lifetime.last_command, size)); + next_offset = next_offset.saturating_add(size); + } + } + } + lifetimes +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{commands::CopyBuffer, resources::BufferRange}; + + #[test] + fn copy_buffer_compiles_with_dependency_and_barrier() { + let mut graph = RenderGraph::new(); + let src = graph.create_buffer(crate::resources::BufferDesc { + size: 64, + usage: vk::BufferUsageFlags::TRANSFER_SRC, + }); + let dst = graph.create_buffer(crate::resources::BufferDesc { + size: 64, + usage: vk::BufferUsageFlags::TRANSFER_DST, + }); + graph.push(CopyBuffer { + src, + src_range: BufferRange { + offset: 0, + size: 64, + }, + dst, + dst_range: BufferRange { + offset: 0, + size: 64, + }, + }); + + let compiled = graph.compile().expect("graph compiles"); + assert_eq!(compiled.command_order(), &[CommandIndex(0)]); + assert!(compiled.barriers().is_empty()); + assert_eq!(compiled.resource_lifetimes().len(), 2); + } + + #[test] + fn write_then_read_creates_order_and_barrier() { + let mut graph = RenderGraph::new(); + let a = graph.create_buffer(crate::resources::BufferDesc { + size: 32, + usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST, + }); + let b = graph.create_buffer(crate::resources::BufferDesc { + size: 32, + usage: vk::BufferUsageFlags::TRANSFER_DST, + }); + let c = graph.create_buffer(crate::resources::BufferDesc { + size: 32, + usage: vk::BufferUsageFlags::TRANSFER_DST, + }); + graph.push(CopyBuffer { + src: a, + src_range: BufferRange { + offset: 0, + size: 32, + }, + dst: b, + dst_range: BufferRange { + offset: 0, + size: 32, + }, + }); + graph.push(CopyBuffer { + src: b, + src_range: BufferRange { + offset: 0, + size: 32, + }, + dst: c, + dst_range: BufferRange { + offset: 0, + size: 32, + }, + }); + + let compiled = graph.compile().expect("graph compiles"); + assert_eq!( + compiled.command_order(), + &[CommandIndex(0), CommandIndex(1)] + ); + assert_eq!(compiled.barriers().len(), 1); + assert_eq!(compiled.barriers()[0].before_command, CommandIndex(1)); + assert_eq!(compiled.barriers()[0].resources[0].id, b.id()); + } +} diff --git a/crates/renderer_rendergraph/src/lib.rs b/crates/renderer_rendergraph/src/lib.rs new file mode 100644 index 0000000..6cd3c14 --- /dev/null +++ b/crates/renderer_rendergraph/src/lib.rs @@ -0,0 +1,11 @@ +//! A small render-graph skeleton following the design in `rendergraph.org`. +#![allow(dead_code)] + +pub mod commands; +pub mod graph; +pub mod recorder; +pub mod resources; + +pub use graph::{CompiledGraph, GraphError, RenderGraph}; +pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap}; +pub use resources::{Buffer, ResourceId, Texture}; diff --git a/crates/renderer_rendergraph/src/main.rs b/crates/renderer_rendergraph/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/crates/renderer_rendergraph/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/crates/renderer_rendergraph/src/recorder.rs b/crates/renderer_rendergraph/src/recorder.rs new file mode 100644 index 0000000..a3dfeb5 --- /dev/null +++ b/crates/renderer_rendergraph/src/recorder.rs @@ -0,0 +1,214 @@ +use std::collections::BTreeMap; + +use ash::vk; + +use crate::resources::{ + BufferRange, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner, +}; + +/// The position of a command in its command list. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CommandIndex(pub usize); + +impl CommandIndex { + pub fn index(self) -> usize { + self.0 + } +} + +impl From for CommandIndex { + fn from(index: usize) -> Self { + Self(index) + } +} + +impl From for usize { + fn from(index: CommandIndex) -> Self { + index.0 + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ResourceAccess { + pub resource: ResourceId, + pub scope: SubresourceScope, +} + +/// Stores every declared access together with the command that declared it. +#[derive(Debug, Default, Clone)] +pub struct SideEffectMap { + accesses: Vec>, +} + +impl SideEffectMap { + pub fn begin_command(&mut self) -> CommandIndex { + let index = self.accesses.len(); + self.accesses.push(Vec::new()); + index.into() + } + + pub fn insert( + &mut self, + command_index: CommandIndex, + resource: ResourceId, + scope: SubresourceScope, + ) { + if self.accesses.len() <= command_index.0 { + self.accesses.resize_with(command_index.0 + 1, Vec::new); + } + let accesses = &mut self.accesses[command_index.0]; + for existing in accesses.iter_mut() { + if existing.resource == resource && existing.scope.try_merge(&scope) { + return; + } + } + accesses.push(ResourceAccess { resource, scope }); + } + + pub fn accesses(&self, command_index: CommandIndex) -> &[ResourceAccess] { + self.accesses + .get(command_index.0) + .map_or(&[], Vec::as_slice) + } + + pub fn command_accesses(&self) -> &[Vec] { + &self.accesses + } +} + +/// A safe, type-erased temporary command buffer. Each command is owned until +/// compilation/recording, while its access declarations are stored separately. +pub struct CommandList { + pub(crate) commands: Vec>, + pub side_effects: SideEffectMap, +} + +pub trait RecordedCommand { + fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError>; +} + +impl CommandList { + pub fn new() -> Self { + Self { + commands: Vec::new(), + side_effects: SideEffectMap::default(), + } + } + + pub fn push(&mut self, command: C) -> CommandIndex { + let index = self.side_effects.begin_command(); + command.side_effects(index, &mut self.side_effects); + self.commands.push(Box::new(command)); + index + } + + pub fn len(&self) -> usize { + self.commands.len() + } + + pub fn is_empty(&self) -> bool { + self.commands.is_empty() + } +} + +impl Default for CommandList { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RecordError { + MissingBuffer(ResourceId), + MissingImage(ResourceId), +} + +/// Vulkan recording context. Handle maps are populated for imported resources; +/// transient allocation is intentionally left to a later allocator integration. +pub struct CommandRecorder<'a> { + pub device: &'a ash::Device, + pub command_buffer: vk::CommandBuffer, + pub buffers: BTreeMap, + pub images: BTreeMap, +} + +impl<'a> CommandRecorder<'a> { + pub fn new(device: &'a ash::Device, command_buffer: vk::CommandBuffer) -> Self { + Self { + device, + command_buffer, + buffers: BTreeMap::new(), + images: BTreeMap::new(), + } + } + + pub fn buffer(&self, id: ResourceId) -> Result { + self.buffers + .get(&id) + .copied() + .ok_or(RecordError::MissingBuffer(id)) + } + + pub fn image(&self, id: ResourceId) -> Result { + self.images + .get(&id) + .copied() + .ok_or(RecordError::MissingImage(id)) + } + + pub fn pipeline_barrier(&self, barrier: &super::graph::PipelineBarrier) { + let buffer_barriers: Vec<_> = barrier + .resources + .iter() + .filter_map(|resource| match resource.range { + Subresource::Buffer(BufferRange { offset, size }) => { + self.buffers.get(&resource.id).map(|&buffer| { + vk::BufferMemoryBarrier2::default() + .src_stage_mask(resource.src_stage) + .src_access_mask(resource.src_access) + .dst_stage_mask(resource.dst_stage) + .dst_access_mask(resource.dst_access) + .buffer(buffer) + .offset(offset) + .size(size) + }) + } + Subresource::Texture(_) => None, + }) + .collect(); + + let image_barriers: Vec<_> = barrier + .resources + .iter() + .filter_map(|resource| match resource.range { + Subresource::Texture(range) => self.images.get(&resource.id).map(|&image| { + vk::ImageMemoryBarrier2::default() + .src_stage_mask(resource.src_stage) + .src_access_mask(resource.src_access) + .dst_stage_mask(resource.dst_stage) + .dst_access_mask(resource.dst_access) + .old_layout(resource.old_layout.unwrap_or(vk::ImageLayout::UNDEFINED)) + .new_layout(resource.new_layout.unwrap_or(vk::ImageLayout::GENERAL)) + .image(image) + .subresource_range(range) + }), + Subresource::Buffer(_) => None, + }) + .collect(); + + let dependency = vk::DependencyInfo::default() + .buffer_memory_barriers(&buffer_barriers) + .image_memory_barriers(&image_barriers); + unsafe { + self.device + .cmd_pipeline_barrier2(self.command_buffer, &dependency); + } + } +} + +pub(crate) fn scope_range(scope: SubresourceScope) -> Subresource { + match scope.subresource { + SubresourceScopeInner::Buffer(range) => Subresource::Buffer(range), + SubresourceScopeInner::Texture { range, .. } => Subresource::Texture(range), + } +} diff --git a/crates/renderer_rendergraph/src/resources.rs b/crates/renderer_rendergraph/src/resources.rs new file mode 100644 index 0000000..478af4f --- /dev/null +++ b/crates/renderer_rendergraph/src/resources.rs @@ -0,0 +1,248 @@ +use ash::vk; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct ResourceId(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Buffer(pub ResourceId); + +impl Buffer { + pub fn id(self) -> ResourceId { + self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Texture(pub ResourceId); + +impl Texture { + pub fn id(self) -> ResourceId { + self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BufferRange { + pub offset: u64, + pub size: u64, +} + +impl BufferRange { + pub const fn full() -> Self { + Self { + offset: 0, + size: vk::WHOLE_SIZE, + } + } + + pub fn end(self) -> u64 { + if self.size == vk::WHOLE_SIZE { + u64::MAX + } else { + self.offset.saturating_add(self.size) + } + } + + pub fn overlaps(self, other: Self) -> bool { + self.offset < other.end() && other.offset < self.end() + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Subresource { + Buffer(BufferRange), + Texture(vk::ImageSubresourceRange), +} + +#[derive(Debug, Clone, Copy)] +pub enum SubresourceScopeInner { + Buffer(BufferRange), + Texture { + range: vk::ImageSubresourceRange, + layout: vk::ImageLayout, + }, +} + +#[derive(Debug, Clone, Copy)] +pub struct SubresourceScope { + pub access: vk::AccessFlags2, + pub stage: vk::PipelineStageFlags2, + pub subresource: SubresourceScopeInner, +} + +impl SubresourceScope { + /// Fold identical ranges with compatible access polarity into one scope. + pub fn try_merge(&mut self, other: &Self) -> bool { + if access_flag_is_read(self.access) != access_flag_is_read(other.access) + || access_flag_is_write(self.access) != access_flag_is_write(other.access) + { + return false; + } + + let same_range = match (self.subresource, other.subresource) { + (SubresourceScopeInner::Buffer(a), SubresourceScopeInner::Buffer(b)) => a == b, + ( + SubresourceScopeInner::Texture { + range: a, + layout: layout_a, + }, + SubresourceScopeInner::Texture { + range: b, + layout: layout_b, + }, + ) => texture_ranges_equal(a, b) && layout_a == layout_b, + _ => false, + }; + + if same_range { + self.stage |= other.stage; + self.access |= other.access; + } + same_range + } + + /// Whether accesses overlap and require ordering (a write or layout change). + pub fn conflicts(&self, other: &Self) -> bool { + let overlaps = match (self.subresource, other.subresource) { + (SubresourceScopeInner::Buffer(a), SubresourceScopeInner::Buffer(b)) => a.overlaps(b), + ( + SubresourceScopeInner::Texture { range: a, .. }, + SubresourceScopeInner::Texture { range: b, .. }, + ) => texture_ranges_overlap(a, b), + _ => false, + }; + + if !overlaps { + return false; + } + + match (self.subresource, other.subresource) { + ( + SubresourceScopeInner::Texture { layout: a, .. }, + SubresourceScopeInner::Texture { layout: b, .. }, + ) if a != b => true, + _ => { + !(access_flag_is_read(self.access) + && !access_flag_is_write(self.access) + && access_flag_is_read(other.access) + && !access_flag_is_write(other.access)) + } + } + } +} + +fn texture_ranges_equal(a: vk::ImageSubresourceRange, b: vk::ImageSubresourceRange) -> bool { + a.aspect_mask == b.aspect_mask + && a.base_mip_level == b.base_mip_level + && a.level_count == b.level_count + && a.base_array_layer == b.base_array_layer + && a.layer_count == b.layer_count +} + +fn range_end(base: u32, count: u32) -> u64 { + if count == vk::REMAINING_MIP_LEVELS || count == vk::REMAINING_ARRAY_LAYERS { + u64::MAX + } else { + u64::from(base) + u64::from(count) + } +} + +fn texture_ranges_overlap(a: vk::ImageSubresourceRange, b: vk::ImageSubresourceRange) -> bool { + a.aspect_mask.intersects(b.aspect_mask) + && u64::from(a.base_mip_level) < range_end(b.base_mip_level, b.level_count) + && u64::from(b.base_mip_level) < range_end(a.base_mip_level, a.level_count) + && u64::from(a.base_array_layer) < range_end(b.base_array_layer, b.layer_count) + && u64::from(b.base_array_layer) < range_end(a.base_array_layer, a.layer_count) +} + +/// True when an access mask contains a Vulkan read access. +pub fn access_flag_is_read(access: vk::AccessFlags2) -> bool { + access.intersects( + vk::AccessFlags2::INDIRECT_COMMAND_READ + | vk::AccessFlags2::INDEX_READ + | vk::AccessFlags2::VERTEX_ATTRIBUTE_READ + | vk::AccessFlags2::UNIFORM_READ + | vk::AccessFlags2::INPUT_ATTACHMENT_READ + | vk::AccessFlags2::SHADER_READ + | vk::AccessFlags2::COLOR_ATTACHMENT_READ + | vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ + | vk::AccessFlags2::TRANSFER_READ + | vk::AccessFlags2::HOST_READ + | vk::AccessFlags2::MEMORY_READ + | vk::AccessFlags2::SHADER_SAMPLED_READ + | vk::AccessFlags2::SHADER_STORAGE_READ + | vk::AccessFlags2::ACCELERATION_STRUCTURE_READ_KHR, + ) +} + +/// True when an access mask contains a Vulkan write access. +pub fn access_flag_is_write(access: vk::AccessFlags2) -> bool { + access.intersects( + vk::AccessFlags2::SHADER_WRITE + | vk::AccessFlags2::COLOR_ATTACHMENT_WRITE + | vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE + | vk::AccessFlags2::TRANSFER_WRITE + | vk::AccessFlags2::HOST_WRITE + | vk::AccessFlags2::MEMORY_WRITE + | vk::AccessFlags2::SHADER_STORAGE_WRITE + | vk::AccessFlags2::ACCELERATION_STRUCTURE_WRITE_KHR, + ) +} + +#[derive(Debug, Clone, Copy)] +pub struct BufferDesc { + pub size: u64, + pub usage: vk::BufferUsageFlags, +} + +#[derive(Debug, Clone, Copy)] +pub struct TextureDesc { + pub format: vk::Format, + pub extent: vk::Extent3D, + pub usage: vk::ImageUsageFlags, +} + +#[derive(Debug, Clone, Copy)] +pub struct ResourceState { + pub stage: vk::PipelineStageFlags2, + pub access: vk::AccessFlags2, + pub layout: Option, +} + +impl Default for ResourceState { + fn default() -> Self { + Self { + stage: vk::PipelineStageFlags2::NONE, + access: vk::AccessFlags2::NONE, + layout: None, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ResourceDesc { + CreatedBuffer(BufferDesc), + CreatedTexture(TextureDesc), + ImportedBuffer { + handle: vk::Buffer, + initial: ResourceState, + }, + ImportedTexture { + handle: vk::Image, + initial: ResourceState, + }, +} + +impl ResourceDesc { + pub fn initial_state(self) -> ResourceState { + match self { + Self::ImportedBuffer { initial, .. } | Self::ImportedTexture { initial, .. } => initial, + Self::CreatedBuffer(_) | Self::CreatedTexture(_) => ResourceState::default(), + } + } + + pub fn is_created(self) -> bool { + matches!(self, Self::CreatedBuffer(_) | Self::CreatedTexture(_)) + } +}