extract graph compilation logic
This commit is contained in:
parent
5f7bbba49d
commit
195cf3e08a
|
|
@ -111,52 +111,12 @@ impl RenderGraph {
|
|||
|
||||
pub fn compile(self) -> Result<CompiledGraph, GraphError> {
|
||||
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<ResourceId, ResourceDesc>,
|
||||
accesses: &'a [Vec<crate::recorder::ResourceAccess>],
|
||||
command_count: usize,
|
||||
}
|
||||
|
||||
impl<'a> GraphCompiler<'a> {
|
||||
fn new(
|
||||
resources: &'a BTreeMap<ResourceId, ResourceDesc>,
|
||||
accesses: &'a [Vec<crate::recorder::ResourceAccess>],
|
||||
command_count: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
resources,
|
||||
accesses,
|
||||
command_count,
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
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<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 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<PipelineBarrier> {
|
||||
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<vk::ImageLayout>, Option<vk::ImageLayout>) {
|
||||
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<vk::ImageLayout>, Option<vk::ImageLayout>) {
|
||||
let current_layout = match current.subresource {
|
||||
SubresourceScopeInner::Texture { layout, .. } => Some(layout),
|
||||
SubresourceScopeInner::Buffer(_) => None,
|
||||
};
|
||||
(initial.layout, current_layout)
|
||||
}
|
||||
|
||||
fn bind_lifetimes(&self) -> HashMap<ResourceId, ResourceLifetime> {
|
||||
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<crate::recorder::ResourceAccess>],
|
||||
resource_descs: &BTreeMap<ResourceId, ResourceDesc>,
|
||||
) -> Vec<PipelineBarrier> {
|
||||
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<vk::ImageLayout>, Option<vk::ImageLayout>) {
|
||||
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<vk::ImageLayout>, Option<vk::ImageLayout>) {
|
||||
let current_layout = match current.subresource {
|
||||
SubresourceScopeInner::Texture { layout, .. } => Some(layout),
|
||||
SubresourceScopeInner::Buffer(_) => None,
|
||||
};
|
||||
(initial.layout, current_layout)
|
||||
}
|
||||
|
||||
fn bind_resources(
|
||||
resources: &BTreeMap<ResourceId, ResourceDesc>,
|
||||
accesses: &[Vec<crate::recorder::ResourceAccess>],
|
||||
) -> HashMap<ResourceId, ResourceLifetime> {
|
||||
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::*;
|
||||
|
|
|
|||
Loading…
Reference in a new issue