extract kahn algo and build interference graph
This commit is contained in:
parent
195cf3e08a
commit
6a41cccf8b
|
|
@ -53,6 +53,100 @@ pub struct ResourceLifetime {
|
|||
pub memory_offset: u64,
|
||||
}
|
||||
|
||||
impl ResourceLifetime {
|
||||
fn overlaps(&self, other: &Self) -> bool {
|
||||
self.first_command <= other.last_command && other.first_command <= self.last_command
|
||||
}
|
||||
}
|
||||
|
||||
/// The resource-level interference graph used by transient-resource aliasing.
|
||||
/// An edge means the two resources are live at the same command index.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResourceInterference {
|
||||
adj: BTreeMap<ResourceId, BTreeSet<ResourceId>>,
|
||||
}
|
||||
|
||||
impl ResourceInterference {
|
||||
/// Build a symmetric interference graph from inclusive resource lifetimes.
|
||||
pub fn from_lifetimes(lifetimes: &HashMap<ResourceId, ResourceLifetime>) -> Self {
|
||||
let mut graph = Self::default();
|
||||
|
||||
let resources: Vec<_> = lifetimes
|
||||
.iter()
|
||||
.map(|(&id, lifetime)| (id, *lifetime))
|
||||
.collect();
|
||||
|
||||
for (index, &(id, lifetime)) in resources.iter().enumerate() {
|
||||
graph.adj.entry(id).or_default();
|
||||
for &(other_id, other) in &resources[index + 1..] {
|
||||
if lifetime.overlaps(&other) {
|
||||
graph.adj.entry(id).or_default().insert(other_id);
|
||||
graph.adj.entry(other_id).or_default().insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
graph
|
||||
}
|
||||
|
||||
/// Returns whether both resources are live at any common command index.
|
||||
pub fn interferes(&self, a: ResourceId, b: ResourceId) -> bool {
|
||||
self.adj
|
||||
.get(&a)
|
||||
.is_some_and(|neighbors| neighbors.contains(&b))
|
||||
}
|
||||
|
||||
/// Returns the resources that overlap `id`'s inclusive lifetime interval.
|
||||
pub fn neighbors(&self, id: ResourceId) -> Option<&BTreeSet<ResourceId>> {
|
||||
self.adj.get(&id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministic Kahn topological-sort state for command dependencies.
|
||||
struct Kahn {
|
||||
edges: Vec<BTreeSet<usize>>,
|
||||
indegree: Vec<usize>,
|
||||
ready: BTreeSet<usize>,
|
||||
emitted: usize,
|
||||
total: usize,
|
||||
}
|
||||
|
||||
impl Kahn {
|
||||
fn new(node_count: usize) -> Self {
|
||||
Self {
|
||||
edges: vec![BTreeSet::new(); node_count],
|
||||
indegree: vec![0; node_count],
|
||||
ready: (0..node_count).collect(),
|
||||
emitted: 0,
|
||||
total: node_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_edge(&mut self, from: usize, to: usize) {
|
||||
if self.edges[from].insert(to) {
|
||||
self.indegree[to] += 1;
|
||||
self.ready.remove(&to);
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_ready(&mut self) -> Option<usize> {
|
||||
self.ready.pop_first()
|
||||
}
|
||||
|
||||
fn complete(&mut self, node: usize) {
|
||||
self.emitted += 1;
|
||||
for &dependent in &self.edges[node] {
|
||||
self.indegree[dependent] -= 1;
|
||||
if self.indegree[dependent] == 0 {
|
||||
self.ready.insert(dependent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool {
|
||||
self.emitted == self.total
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderGraph {
|
||||
next_id: u64,
|
||||
resources: BTreeMap<ResourceId, ResourceDesc>,
|
||||
|
|
@ -116,7 +210,7 @@ impl RenderGraph {
|
|||
let order = compiler.topo_sort()?;
|
||||
compiler.validate_resources()?;
|
||||
let barriers = compiler.build_barriers(&order);
|
||||
let lifetimes = compiler.bind_lifetimes();
|
||||
let (lifetimes, interference) = compiler.bind_lifetimes();
|
||||
|
||||
Ok(CompiledGraph {
|
||||
resources: self.resources,
|
||||
|
|
@ -125,6 +219,7 @@ impl RenderGraph {
|
|||
order,
|
||||
barriers,
|
||||
lifetimes,
|
||||
interference,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -148,9 +243,8 @@ impl<'a> GraphCompiler<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> (Vec<BTreeSet<usize>>, Vec<usize>) {
|
||||
let mut edges = vec![BTreeSet::new(); self.command_count];
|
||||
let mut indegree = vec![0usize; self.command_count];
|
||||
fn dependencies(&self) -> Kahn {
|
||||
let mut kahn = Kahn::new(self.command_count);
|
||||
|
||||
for current in 0..self.command_count {
|
||||
for prior in 0..current {
|
||||
|
|
@ -159,32 +253,22 @@ impl<'a> GraphCompiler<'a> {
|
|||
earlier.resource == later.resource && earlier.scope.conflicts(&later.scope)
|
||||
})
|
||||
});
|
||||
if dependency && edges[prior].insert(current) {
|
||||
indegree[current] += 1;
|
||||
if dependency {
|
||||
kahn.add_edge(prior, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
(edges, indegree)
|
||||
kahn
|
||||
}
|
||||
|
||||
fn topo_sort(&self) -> Result<Vec<CommandIndex>, 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 kahn = self.dependencies();
|
||||
let mut order = Vec::with_capacity(self.command_count);
|
||||
while let Some(index) = ready.pop_first() {
|
||||
while let Some(index) = kahn.pop_ready() {
|
||||
order.push(index.into());
|
||||
for &dependent in &edges[index] {
|
||||
indegree[dependent] -= 1;
|
||||
if indegree[dependent] == 0 {
|
||||
ready.insert(dependent);
|
||||
}
|
||||
}
|
||||
kahn.complete(index);
|
||||
}
|
||||
if order.len() != self.command_count {
|
||||
if !kahn.is_complete() {
|
||||
return Err(GraphError::DependencyCycle);
|
||||
}
|
||||
Ok(order)
|
||||
|
|
@ -284,7 +368,7 @@ impl<'a> GraphCompiler<'a> {
|
|||
(initial.layout, current_layout)
|
||||
}
|
||||
|
||||
fn bind_lifetimes(&self) -> HashMap<ResourceId, ResourceLifetime> {
|
||||
fn bind_lifetimes(&self) -> (HashMap<ResourceId, ResourceLifetime>, ResourceInterference) {
|
||||
let mut lifetimes = HashMap::new();
|
||||
for (command, command_accesses) in self.accesses.iter().enumerate() {
|
||||
for access in command_accesses {
|
||||
|
|
@ -301,9 +385,13 @@ impl<'a> GraphCompiler<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
let interference = ResourceInterference::from_lifetimes(&lifetimes);
|
||||
|
||||
// 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)
|
||||
// share a heap offset. The graph test is equivalent to the previous
|
||||
// strict `last_use < first_command` check (intervals are inclusive).
|
||||
// Actual size/alignment-aware Vulkan allocation is TODO.
|
||||
let mut slots: Vec<(u64, ResourceId, u64)> = Vec::new(); // (offset, last occupant, capacity)
|
||||
let mut next_offset = 0;
|
||||
let mut ids: Vec<_> = lifetimes.keys().copied().collect();
|
||||
ids.sort_by_key(|id| lifetimes[id].first_command);
|
||||
|
|
@ -317,20 +405,20 @@ impl<'a> GraphCompiler<'a> {
|
|||
if self.resources[&id].is_created()
|
||||
&& let Some((offset, last_use, capacity)) = slots
|
||||
.iter_mut()
|
||||
.find(|(_, last, cap)| *last < lifetime.first_command && *cap >= size)
|
||||
.find(|(_, last, cap)| !interference.interferes(*last, id) && *cap >= size)
|
||||
{
|
||||
lifetime.memory_offset = *offset;
|
||||
*last_use = lifetime.last_command;
|
||||
*last_use = id;
|
||||
let _ = capacity;
|
||||
} else {
|
||||
lifetime.memory_offset = next_offset;
|
||||
if self.resources[&id].is_created() {
|
||||
slots.push((next_offset, lifetime.last_command, size));
|
||||
slots.push((next_offset, id, size));
|
||||
next_offset = next_offset.saturating_add(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
lifetimes
|
||||
(lifetimes, interference)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,6 +435,7 @@ pub struct CompiledGraph {
|
|||
order: Vec<CommandIndex>,
|
||||
barriers: Vec<PipelineBarrier>,
|
||||
lifetimes: HashMap<ResourceId, ResourceLifetime>,
|
||||
interference: ResourceInterference,
|
||||
}
|
||||
|
||||
enum ResolvedResource {
|
||||
|
|
@ -367,6 +456,10 @@ impl CompiledGraph {
|
|||
&self.lifetimes
|
||||
}
|
||||
|
||||
pub fn interference(&self) -> &ResourceInterference {
|
||||
&self.interference
|
||||
}
|
||||
|
||||
/// Create the graph's transient resources. Compilation remains independent
|
||||
/// of a Vulkan device, while command recording requires this resolution step
|
||||
/// for any created resources used by commands.
|
||||
|
|
@ -701,9 +794,124 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::{
|
||||
commands::{ClearTexture, CopyBuffer},
|
||||
resources::{BufferRange, GraphBufferDesc, GraphTextureDesc},
|
||||
resources::{BufferRange, GraphBufferDesc, GraphTextureDesc, ResourceDesc},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn kahn_emits_dag_in_deterministic_order() {
|
||||
let mut kahn = Kahn::new(4);
|
||||
kahn.add_edge(0, 2);
|
||||
kahn.add_edge(1, 2);
|
||||
kahn.add_edge(2, 3);
|
||||
|
||||
let mut order = Vec::new();
|
||||
while let Some(node) = kahn.pop_ready() {
|
||||
order.push(node);
|
||||
kahn.complete(node);
|
||||
}
|
||||
|
||||
assert_eq!(order, [0, 1, 2, 3]);
|
||||
assert!(kahn.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kahn_detects_cycles() {
|
||||
let mut kahn = Kahn::new(3);
|
||||
kahn.add_edge(0, 1);
|
||||
kahn.add_edge(1, 2);
|
||||
kahn.add_edge(2, 1);
|
||||
|
||||
while let Some(node) = kahn.pop_ready() {
|
||||
kahn.complete(node);
|
||||
}
|
||||
|
||||
assert_eq!(kahn.emitted, 1);
|
||||
assert!(!kahn.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interference_tracks_overlapping_inclusive_lifetimes() {
|
||||
let lifetimes = HashMap::from([
|
||||
(
|
||||
ResourceId(0),
|
||||
ResourceLifetime {
|
||||
first_command: CommandIndex(0),
|
||||
last_command: CommandIndex(2),
|
||||
memory_offset: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
ResourceId(1),
|
||||
ResourceLifetime {
|
||||
first_command: CommandIndex(2),
|
||||
last_command: CommandIndex(3),
|
||||
memory_offset: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
ResourceId(2),
|
||||
ResourceLifetime {
|
||||
first_command: CommandIndex(4),
|
||||
last_command: CommandIndex(5),
|
||||
memory_offset: 0,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let interference = ResourceInterference::from_lifetimes(&lifetimes);
|
||||
|
||||
assert!(interference.interferes(ResourceId(0), ResourceId(1)));
|
||||
assert!(interference.interferes(ResourceId(1), ResourceId(0)));
|
||||
assert!(!interference.interferes(ResourceId(0), ResourceId(2)));
|
||||
assert!(!interference.interferes(ResourceId(1), ResourceId(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliasing_reuses_non_interfering_slot_and_separates_interfering_resources() {
|
||||
let resources = BTreeMap::from([
|
||||
(
|
||||
ResourceId(0),
|
||||
ResourceDesc::CreatedBuffer(GraphBufferDesc {
|
||||
size: 8,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
(
|
||||
ResourceId(1),
|
||||
ResourceDesc::CreatedBuffer(GraphBufferDesc {
|
||||
size: 8,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
(
|
||||
ResourceId(2),
|
||||
ResourceDesc::CreatedBuffer(GraphBufferDesc {
|
||||
size: 8,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
]);
|
||||
let accesses = vec![
|
||||
vec![resource_access_for(ResourceId(0))],
|
||||
vec![
|
||||
resource_access_for(ResourceId(0)),
|
||||
resource_access_for(ResourceId(1)),
|
||||
],
|
||||
vec![resource_access_for(ResourceId(0))],
|
||||
vec![resource_access_for(ResourceId(2))],
|
||||
];
|
||||
let compiler = GraphCompiler::new(&resources, &accesses, accesses.len());
|
||||
let (lifetimes, interference) = compiler.bind_lifetimes();
|
||||
|
||||
let long_lived_offset = lifetimes[&ResourceId(0)].memory_offset;
|
||||
let overlapping_offset = lifetimes[&ResourceId(1)].memory_offset;
|
||||
let later_offset = lifetimes[&ResourceId(2)].memory_offset;
|
||||
assert!(interference.interferes(ResourceId(0), ResourceId(1)));
|
||||
assert_ne!(long_lived_offset, overlapping_offset);
|
||||
assert!(!interference.interferes(ResourceId(0), ResourceId(2)));
|
||||
assert!(!interference.interferes(ResourceId(1), ResourceId(2)));
|
||||
assert!(later_offset == long_lived_offset || later_offset == overlapping_offset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_buffer_compiles_with_dependency_and_barrier() {
|
||||
let mut graph = RenderGraph::new();
|
||||
|
|
@ -732,6 +940,7 @@ mod tests {
|
|||
assert_eq!(compiled.command_order(), &[CommandIndex(0)]);
|
||||
assert!(compiled.barriers().is_empty());
|
||||
assert_eq!(compiled.resource_lifetimes().len(), 2);
|
||||
assert!(compiled.interference().interferes(src.id(), dst.id()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -977,4 +1186,15 @@ mod tests {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_access_for(resource: ResourceId) -> crate::recorder::ResourceAccess {
|
||||
crate::recorder::ResourceAccess {
|
||||
resource,
|
||||
scope: SubresourceScope {
|
||||
stage: vk::PipelineStageFlags2::COPY,
|
||||
access: vk::AccessFlags2::TRANSFER_WRITE,
|
||||
subresource: SubresourceScopeInner::Buffer(BufferRange::full()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ pub mod graph;
|
|||
pub mod recorder;
|
||||
pub mod resources;
|
||||
|
||||
pub use graph::{CompiledGraph, GraphError, RenderGraph};
|
||||
pub use graph::{CompiledGraph, GraphError, RenderGraph, ResourceInterference};
|
||||
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
||||
pub use resources::{Buffer, GraphBufferDesc, GraphTextureDesc, ResourceId, Texture};
|
||||
|
|
|
|||
Loading…
Reference in a new issue