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,
|
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 {
|
pub struct RenderGraph {
|
||||||
next_id: u64,
|
next_id: u64,
|
||||||
resources: BTreeMap<ResourceId, ResourceDesc>,
|
resources: BTreeMap<ResourceId, ResourceDesc>,
|
||||||
|
|
@ -116,7 +210,7 @@ impl RenderGraph {
|
||||||
let order = compiler.topo_sort()?;
|
let order = compiler.topo_sort()?;
|
||||||
compiler.validate_resources()?;
|
compiler.validate_resources()?;
|
||||||
let barriers = compiler.build_barriers(&order);
|
let barriers = compiler.build_barriers(&order);
|
||||||
let lifetimes = compiler.bind_lifetimes();
|
let (lifetimes, interference) = compiler.bind_lifetimes();
|
||||||
|
|
||||||
Ok(CompiledGraph {
|
Ok(CompiledGraph {
|
||||||
resources: self.resources,
|
resources: self.resources,
|
||||||
|
|
@ -125,6 +219,7 @@ impl RenderGraph {
|
||||||
order,
|
order,
|
||||||
barriers,
|
barriers,
|
||||||
lifetimes,
|
lifetimes,
|
||||||
|
interference,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -148,9 +243,8 @@ impl<'a> GraphCompiler<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dependencies(&self) -> (Vec<BTreeSet<usize>>, Vec<usize>) {
|
fn dependencies(&self) -> Kahn {
|
||||||
let mut edges = vec![BTreeSet::new(); self.command_count];
|
let mut kahn = Kahn::new(self.command_count);
|
||||||
let mut indegree = vec![0usize; self.command_count];
|
|
||||||
|
|
||||||
for current in 0..self.command_count {
|
for current in 0..self.command_count {
|
||||||
for prior in 0..current {
|
for prior in 0..current {
|
||||||
|
|
@ -159,32 +253,22 @@ impl<'a> GraphCompiler<'a> {
|
||||||
earlier.resource == later.resource && earlier.scope.conflicts(&later.scope)
|
earlier.resource == later.resource && earlier.scope.conflicts(&later.scope)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if dependency && edges[prior].insert(current) {
|
if dependency {
|
||||||
indegree[current] += 1;
|
kahn.add_edge(prior, current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(edges, indegree)
|
kahn
|
||||||
}
|
}
|
||||||
|
|
||||||
fn topo_sort(&self) -> Result<Vec<CommandIndex>, GraphError> {
|
fn topo_sort(&self) -> Result<Vec<CommandIndex>, GraphError> {
|
||||||
let (edges, mut indegree) = self.dependencies();
|
let mut kahn = 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);
|
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());
|
order.push(index.into());
|
||||||
for &dependent in &edges[index] {
|
kahn.complete(index);
|
||||||
indegree[dependent] -= 1;
|
|
||||||
if indegree[dependent] == 0 {
|
|
||||||
ready.insert(dependent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if order.len() != self.command_count {
|
if !kahn.is_complete() {
|
||||||
return Err(GraphError::DependencyCycle);
|
return Err(GraphError::DependencyCycle);
|
||||||
}
|
}
|
||||||
Ok(order)
|
Ok(order)
|
||||||
|
|
@ -284,7 +368,7 @@ impl<'a> GraphCompiler<'a> {
|
||||||
(initial.layout, current_layout)
|
(initial.layout, current_layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bind_lifetimes(&self) -> HashMap<ResourceId, ResourceLifetime> {
|
fn bind_lifetimes(&self) -> (HashMap<ResourceId, ResourceLifetime>, ResourceInterference) {
|
||||||
let mut lifetimes = HashMap::new();
|
let mut lifetimes = HashMap::new();
|
||||||
for (command, command_accesses) in self.accesses.iter().enumerate() {
|
for (command, command_accesses) in self.accesses.iter().enumerate() {
|
||||||
for access in command_accesses {
|
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
|
// Skeleton aliasing policy: resources with non-overlapping lifetimes may
|
||||||
// share a heap offset. Actual size/alignment-aware Vulkan allocation is TODO.
|
// share a heap offset. The graph test is equivalent to the previous
|
||||||
let mut slots: Vec<(u64, CommandIndex, u64)> = Vec::new(); // (offset, last use, capacity)
|
// 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 next_offset = 0;
|
||||||
let mut ids: Vec<_> = lifetimes.keys().copied().collect();
|
let mut ids: Vec<_> = lifetimes.keys().copied().collect();
|
||||||
ids.sort_by_key(|id| lifetimes[id].first_command);
|
ids.sort_by_key(|id| lifetimes[id].first_command);
|
||||||
|
|
@ -317,20 +405,20 @@ impl<'a> GraphCompiler<'a> {
|
||||||
if self.resources[&id].is_created()
|
if self.resources[&id].is_created()
|
||||||
&& let Some((offset, last_use, capacity)) = slots
|
&& let Some((offset, last_use, capacity)) = slots
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|(_, last, cap)| *last < lifetime.first_command && *cap >= size)
|
.find(|(_, last, cap)| !interference.interferes(*last, id) && *cap >= size)
|
||||||
{
|
{
|
||||||
lifetime.memory_offset = *offset;
|
lifetime.memory_offset = *offset;
|
||||||
*last_use = lifetime.last_command;
|
*last_use = id;
|
||||||
let _ = capacity;
|
let _ = capacity;
|
||||||
} else {
|
} else {
|
||||||
lifetime.memory_offset = next_offset;
|
lifetime.memory_offset = next_offset;
|
||||||
if self.resources[&id].is_created() {
|
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);
|
next_offset = next_offset.saturating_add(size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lifetimes
|
(lifetimes, interference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,6 +435,7 @@ pub struct CompiledGraph {
|
||||||
order: Vec<CommandIndex>,
|
order: Vec<CommandIndex>,
|
||||||
barriers: Vec<PipelineBarrier>,
|
barriers: Vec<PipelineBarrier>,
|
||||||
lifetimes: HashMap<ResourceId, ResourceLifetime>,
|
lifetimes: HashMap<ResourceId, ResourceLifetime>,
|
||||||
|
interference: ResourceInterference,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ResolvedResource {
|
enum ResolvedResource {
|
||||||
|
|
@ -367,6 +456,10 @@ impl CompiledGraph {
|
||||||
&self.lifetimes
|
&self.lifetimes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn interference(&self) -> &ResourceInterference {
|
||||||
|
&self.interference
|
||||||
|
}
|
||||||
|
|
||||||
/// Create the graph's transient resources. Compilation remains independent
|
/// Create the graph's transient resources. Compilation remains independent
|
||||||
/// of a Vulkan device, while command recording requires this resolution step
|
/// of a Vulkan device, while command recording requires this resolution step
|
||||||
/// for any created resources used by commands.
|
/// for any created resources used by commands.
|
||||||
|
|
@ -701,9 +794,124 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{ClearTexture, CopyBuffer},
|
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]
|
#[test]
|
||||||
fn copy_buffer_compiles_with_dependency_and_barrier() {
|
fn copy_buffer_compiles_with_dependency_and_barrier() {
|
||||||
let mut graph = RenderGraph::new();
|
let mut graph = RenderGraph::new();
|
||||||
|
|
@ -732,6 +940,7 @@ mod tests {
|
||||||
assert_eq!(compiled.command_order(), &[CommandIndex(0)]);
|
assert_eq!(compiled.command_order(), &[CommandIndex(0)]);
|
||||||
assert!(compiled.barriers().is_empty());
|
assert!(compiled.barriers().is_empty());
|
||||||
assert_eq!(compiled.resource_lifetimes().len(), 2);
|
assert_eq!(compiled.resource_lifetimes().len(), 2);
|
||||||
|
assert!(compiled.interference().interferes(src.id(), dst.id()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 recorder;
|
||||||
pub mod resources;
|
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 recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
||||||
pub use resources::{Buffer, GraphBufferDesc, GraphTextureDesc, ResourceId, Texture};
|
pub use resources::{Buffer, GraphBufferDesc, GraphTextureDesc, ResourceId, Texture};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue