diff --git a/crates/renderer_rendergraph/src/graph.rs b/crates/renderer_rendergraph/src/graph.rs index b5102be..6017e0a 100644 --- a/crates/renderer_rendergraph/src/graph.rs +++ b/crates/renderer_rendergraph/src/graph.rs @@ -111,52 +111,12 @@ impl RenderGraph { 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); + let compiler = GraphCompiler::new(&self.resources, accesses, command_count); + let order = compiler.topo_sort()?; + compiler.validate_resources()?; + let barriers = compiler.build_barriers(&order); + let lifetimes = compiler.bind_lifetimes(); Ok(CompiledGraph { resources: self.resources, @@ -169,6 +129,211 @@ impl RenderGraph { } } +struct GraphCompiler<'a> { + resources: &'a BTreeMap, + accesses: &'a [Vec], + command_count: usize, +} + +impl<'a> GraphCompiler<'a> { + fn new( + resources: &'a BTreeMap, + accesses: &'a [Vec], + command_count: usize, + ) -> Self { + Self { + resources, + accesses, + command_count, + } + } + + fn dependencies(&self) -> (Vec>, Vec) { + let mut edges = vec![BTreeSet::new(); self.command_count]; + let mut indegree = vec![0usize; self.command_count]; + + for current in 0..self.command_count { + for prior in 0..current { + let dependency = self.accesses[prior].iter().any(|earlier| { + self.accesses[current].iter().any(|later| { + earlier.resource == later.resource && earlier.scope.conflicts(&later.scope) + }) + }); + if dependency && edges[prior].insert(current) { + indegree[current] += 1; + } + } + } + (edges, indegree) + } + + fn topo_sort(&self) -> Result, GraphError> { + let (edges, mut indegree) = self.dependencies(); + let mut ready: BTreeSet<_> = indegree + .iter() + .enumerate() + .filter_map(|(index, °ree)| (degree == 0).then_some(index)) + .collect(); + let mut order = Vec::with_capacity(self.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() != self.command_count { + return Err(GraphError::DependencyCycle); + } + Ok(order) + } + + fn validate_resources(&self) -> Result<(), GraphError> { + for command_accesses in self.accesses { + for access in command_accesses { + if !self.resources.contains_key(&access.resource) { + return Err(GraphError::UnknownResource(access.resource)); + } + } + } + Ok(()) + } + + fn build_barriers(&self, order: &[CommandIndex]) -> Vec { + let mut result = Vec::new(); + for (ordered_index, &command_index) in order.iter().enumerate() { + let mut barrier_resources = Vec::new(); + for current in &self.accesses[command_index.index()] { + let previous = order[..ordered_index] + .iter() + .rev() + .flat_map(|&prior_index| self.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) = Self::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 = &self.resources[¤t.resource]; + let initial = desc.initial_state(); + let (old_layout, new_layout) = Self::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_lifetimes(&self) -> HashMap { + let mut lifetimes = HashMap::new(); + for (command, command_accesses) in self.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 &self.resources[&id] { + ResourceDesc::CreatedBuffer(desc) => desc.size, + ResourceDesc::CreatedTexture(desc) => texture_size_estimate(desc), + ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => 0, + }; + if self.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 self.resources[&id].is_created() { + slots.push((next_offset, lifetime.last_command, size)); + next_offset = next_offset.saturating_add(size); + } + } + } + lifetimes + } +} + impl Default for RenderGraph { fn default() -> Self { Self::new() @@ -531,145 +696,6 @@ fn texture_size_estimate(desc: &GraphTextureDesc) -> u64 { .saturating_mul(u64::from(desc.samples.as_raw().max(1))) } -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], -) -> HashMap { - let mut lifetimes = HashMap::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) => texture_size_estimate(desc), - 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::*;