vidya/crates/renderer_rendergraph/src/graph.rs
janis afb322fa93
rendergraph: dynamic-rendering render passes, single-pass compiler, perf
- Add BeginRendering/EndRendering commands with imported/created attachment
  views, view plumbing, and aspect-gated depth/stencil slots
- Derive and validate render-pass spans at compile time; contract spans into
  single DAG nodes before hazard analysis; hoist cross-boundary barriers to
  pass begins with in-pass policy and created-view resolution
- Replace the four-pass ordering pipeline with a single command walk
  (build_command_orderings): span derivation, validation, lifetime-only
  injection, dependency edges, and Kahn batching in one pass; batches are
  ranges into execution_order and RenderPassSpan keeps only begin/end
- Perf: node-collapsed frontier analysis, dense command/resource indexing
  (no SipHash on hot paths), inline barrier storage, exact preallocation,
  lazy Kahn ready heap, scoped barrier fast paths
- Scope buffer and image alias barriers to the incoming resource per the
  Vulkan resource-memory-aliasing rules (availability unscoped, visibility
  scoped to the new owner)
- Add criterion compile benchmarks (two_pass/{no_pass,pass}/per_{20,50,100})
2026-10-29 00:51:52 +01:00

8695 lines
311 KiB
Rust

//! Render-graph compilation derives command dependencies from conflicting resource
//! accesses and computes their transitive happens-before closure. Transient
//! resources may share memory only when every terminal use of one precedes every
//! initial use of the other; device-dependent placement uses separate
//! linear/optimal pools and inserts alias barriers at the new occupant's first
//! use.
//!
//! Aliasing safety assumes all graph commands are recorded into one command
//! buffer that is submitted to one queue. Cross-queue aliasing is unsupported:
//! when command-to-queue assignment is introduced, alias transitions between
//! queues must also be represented by semaphore edges.
// TODO: count image creations by hash of desc to see if some particular image is being created very frequently.
// maybe it can be pooled.
use std::{
cmp::Reverse,
collections::{BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque},
sync::Arc,
};
use ash::vk;
use fixedbitset::FixedBitSet;
use renderer_resources::{
Device,
buffers::{Buffer as BufferResource, BufferDesc},
descriptors::DescriptorResourceBinding,
device::{AllocationStrategy, MemoryLocation, QueueFlags},
images::{Image as ImageResource, ImageDesc, ImageSubresourceExt, ImageView, ImageViewDesc},
pipeline::DescriptorSet,
};
use renderer_device::QueueFamilyIndex;
use crate::graph_compiler::{Batch, CommandOrderings, build_command_orderings};
use crate::{
commands::{AttachmentView, CommandKind, RenderPassDesc},
recorder::{
CommandIndex, CommandList, CommandRecorder, RecordError, SideEffectMap, scope_range,
},
resources::{
Buffer, BufferRange, DescriptorResourceAccess, DescriptorResourceIdentity, GraphBufferDesc,
GraphDescriptorSet, GraphTextureDesc, ResourceDesc, ResourceId, ResourceState, Subresource,
SubresourceScope, SubresourceScopeInner, Texture, access_flag_is_write,
},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GraphError {
UnknownResource(ResourceId),
DependencyCycle,
DescriptorImport(String),
StaleDescriptorSet,
ConflictingInitialState(ResourceId),
ConflictingDrawLayouts(ResourceId),
ConflictingDescriptorInitialStates,
ConflictingDescriptorDrawLayouts,
UnbalancedRenderPass,
NestedRenderPass,
DrawOutsideRenderPass(CommandIndex),
CommandNotAllowedInRenderPass(CommandIndex),
InvalidRenderPassAttachment(String),
/// An image layout transition would need to occur inside dynamic rendering.
UnsupportedIntraPassDependency,
/// An interior access overlaps a render-pass attachment subresource.
RenderPassFeedbackLoop(String),
/// A created attachment view could not be constructed or did not match its image.
ViewCreationFailed(String),
PlacementFailed(ResourceId),
BindFailed(ResourceId),
InvalidPooledImageLayout(ResourceId),
}
impl From<(ResourceId, renderer_resources::memory::Error)> for GraphError {
fn from((resource, error): (ResourceId, renderer_resources::memory::Error)) -> Self {
match error {
renderer_resources::memory::Error::Bind(_)
| renderer_resources::memory::Error::MisalignedOffset { .. }
| renderer_resources::memory::Error::IncompatibleMemoryType { .. }
| renderer_resources::memory::Error::OutOfBounds { .. }
| renderer_resources::memory::Error::AlreadyBound => Self::BindFailed(resource),
_ => Self::PlacementFailed(resource),
}
}
}
impl From<(ResourceId, renderer_resources::Error)> for GraphError {
fn from((resource, _error): (ResourceId, renderer_resources::Error)) -> Self {
Self::PlacementFailed(resource)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueueTransfer {
pub src: QueueFamilyIndex,
pub dst: QueueFamilyIndex,
}
/// Identifies what orders a barrier before its destination command.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BarrierOrigin {
/// Transition from the resource's declared initial state (or first use of a created resource).
Initial,
/// Ordered after the command that previously accessed the resource.
Command(CommandIndex),
}
#[derive(Debug, Clone, Copy)]
pub struct BarrierResource {
pub id: ResourceId,
pub range: Subresource,
pub origin: BarrierOrigin,
pub src_stage: vk::PipelineStageFlags2,
pub src_access: vk::AccessFlags2,
pub dst_stage: vk::PipelineStageFlags2,
pub dst_access: vk::AccessFlags2,
pub old_layout: Option<vk::ImageLayout>,
pub new_layout: Option<vk::ImageLayout>,
pub queue_transfer: Option<QueueTransfer>,
}
// TinyVec initializes its inline backing array with defaults; slots beyond its
// logical length remain inaccessible to barrier consumers.
impl Default for BarrierResource {
fn default() -> Self {
Self {
id: ResourceId::from_u32(0),
range: Subresource::Buffer(crate::resources::BufferRange { offset: 0, size: 0 }),
origin: BarrierOrigin::Initial,
src_stage: vk::PipelineStageFlags2::NONE,
src_access: vk::AccessFlags2::NONE,
dst_stage: vk::PipelineStageFlags2::NONE,
dst_access: vk::AccessFlags2::NONE,
old_layout: None,
new_layout: None,
queue_transfer: None,
}
}
}
#[derive(Debug, Clone, Copy)]
struct LayoutRegion {
range: vk::ImageSubresourceRange,
layout: vk::ImageLayout,
}
fn image_range_intersection(
a: vk::ImageSubresourceRange,
b: vk::ImageSubresourceRange,
) -> Option<vk::ImageSubresourceRange> {
let aspect_mask = a.aspect_mask & b.aspect_mask;
if aspect_mask.is_empty() {
return None;
}
let mip_start = u64::from(a.base_mip_level.max(b.base_mip_level));
let mip_end = (u64::from(a.base_mip_level) + u64::from(a.level_count))
.min(u64::from(b.base_mip_level) + u64::from(b.level_count));
let layer_start = u64::from(a.base_array_layer.max(b.base_array_layer));
let layer_end = (u64::from(a.base_array_layer) + u64::from(a.layer_count))
.min(u64::from(b.base_array_layer) + u64::from(b.layer_count));
if mip_start >= mip_end || layer_start >= layer_end {
return None;
}
Some(
vk::ImageSubresourceRange::default()
.aspect_mask(aspect_mask)
.base_mip_level(mip_start as u32)
.level_count((mip_end - mip_start) as u32)
.base_array_layer(layer_start as u32)
.layer_count((layer_end - layer_start) as u32),
)
}
fn image_range_contains(
range: vk::ImageSubresourceRange,
other: vk::ImageSubresourceRange,
) -> bool {
range.aspect_mask.contains(other.aspect_mask)
&& range.base_mip_level <= other.base_mip_level
&& u64::from(range.base_mip_level) + u64::from(range.level_count)
>= u64::from(other.base_mip_level) + u64::from(other.level_count)
&& range.base_array_layer <= other.base_array_layer
&& u64::from(range.base_array_layer) + u64::from(range.layer_count)
>= u64::from(other.base_array_layer) + u64::from(other.layer_count)
}
struct ImageRangeRemainders {
ranges: [Option<vk::ImageSubresourceRange>; 5],
len: usize,
}
impl ImageRangeRemainders {
fn new() -> Self {
Self {
ranges: [None; 5],
len: 0,
}
}
fn push(&mut self, range: vk::ImageSubresourceRange) {
self.ranges[self.len] = Some(range);
self.len += 1;
}
fn iter(self) -> impl Iterator<Item = vk::ImageSubresourceRange> {
self.ranges.into_iter().take(self.len).flatten()
}
}
/// Subtracts the cut's aspect/mip/layer coverage, returning disjoint remainder ranges.
fn subtract_image_range(
range: vk::ImageSubresourceRange,
cut: vk::ImageSubresourceRange,
) -> ImageRangeRemainders {
let mut remainder = ImageRangeRemainders::new();
let Some(intersection) = image_range_intersection(range, cut) else {
remainder.push(range);
return remainder;
};
let other_aspects = range.aspect_mask & !intersection.aspect_mask;
if !other_aspects.is_empty() {
remainder.push(vk::ImageSubresourceRange {
aspect_mask: other_aspects,
..range
});
}
let aspects = intersection.aspect_mask;
let range_mip_start = u64::from(range.base_mip_level);
let range_mip_end = range_mip_start + u64::from(range.level_count);
let range_layer_start = u64::from(range.base_array_layer);
let range_layer_end = range_layer_start + u64::from(range.layer_count);
let cut_mip_start = u64::from(intersection.base_mip_level);
let cut_mip_end = cut_mip_start + u64::from(intersection.level_count);
let cut_layer_start = u64::from(intersection.base_array_layer);
let cut_layer_end = cut_layer_start + u64::from(intersection.layer_count);
let rect = |mip_start: u64, mip_end: u64, layer_start: u64, layer_end: u64| {
(mip_start < mip_end && layer_start < layer_end).then(|| {
vk::ImageSubresourceRange::default()
.aspect_mask(aspects)
.base_mip_level(mip_start as u32)
.level_count((mip_end - mip_start) as u32)
.base_array_layer(layer_start as u32)
.layer_count((layer_end - layer_start) as u32)
})
};
// Four non-overlapping strips cover the rectangle around the intersection.
for piece in [
rect(
range_mip_start,
cut_mip_start,
range_layer_start,
range_layer_end,
),
rect(
cut_mip_end,
range_mip_end,
range_layer_start,
range_layer_end,
),
rect(
cut_mip_start,
cut_mip_end,
range_layer_start,
cut_layer_start,
),
rect(cut_mip_start, cut_mip_end, cut_layer_end, range_layer_end),
]
.into_iter()
.flatten()
{
remainder.push(piece);
}
remainder
}
fn update_layout_regions(
regions: &mut Vec<LayoutRegion>,
range: vk::ImageSubresourceRange,
layout: vk::ImageLayout,
) {
let intersects = regions
.iter()
.filter(|region| image_range_intersection(region.range, range).is_some())
.count();
if intersects == 0 {
regions.push(LayoutRegion { range, layout });
return;
}
if regions
.iter()
.any(|region| same_image_subresource_range(region.range, range) && region.layout == layout)
{
return;
}
if let Some(region) = regions
.iter_mut()
.find(|region| image_range_contains(region.range, range))
{
if region.layout == layout {
return;
}
if same_image_subresource_range(region.range, range) {
region.layout = layout;
return;
}
}
if regions
.iter()
.any(|region| image_range_contains(region.range, range) && region.layout == layout)
{
return;
}
// Replacing fully covered regions in place avoids draining and rebuilding the
// common case where a transition supersedes existing layout coverage.
if regions
.iter()
.filter(|region| image_range_intersection(region.range, range).is_some())
.all(|region| image_range_contains(range, region.range))
{
regions.retain(|region| image_range_intersection(region.range, range).is_none());
regions.push(LayoutRegion { range, layout });
return;
}
let mut updated = Vec::with_capacity(regions.len() + 4);
for region in regions.drain(..) {
for remainder in subtract_image_range(region.range, range).iter() {
updated.push(LayoutRegion {
range: remainder,
layout: region.layout,
});
}
}
updated.push(LayoutRegion { range, layout });
*regions = updated;
}
fn layout_pieces(
range: vk::ImageSubresourceRange,
fallback: vk::ImageLayout,
regions: &[LayoutRegion],
) -> Vec<(vk::ImageSubresourceRange, vk::ImageLayout)> {
if !regions
.iter()
.any(|region| image_range_intersection(range, region.range).is_some())
{
return vec![(range, fallback)];
}
if let Some(region) = regions
.iter()
.find(|region| image_range_contains(region.range, range))
{
return vec![(range, region.layout)];
}
let mut uncovered = Vec::with_capacity(5);
uncovered.push(range);
let mut next_uncovered = Vec::with_capacity(5);
let mut pieces = Vec::with_capacity(regions.len() + 1);
for region in regions {
let Some(intersection) = image_range_intersection(range, region.range) else {
continue;
};
pieces.push((intersection, region.layout));
next_uncovered.clear();
for piece in uncovered.drain(..) {
next_uncovered.extend(subtract_image_range(piece, intersection).iter());
}
std::mem::swap(&mut uncovered, &mut next_uncovered);
}
if uncovered.is_empty()
&& let Some((_, uniform_layout)) = pieces.first().copied()
&& pieces.iter().all(|(_, layout)| *layout == uniform_layout)
{
return vec![(range, uniform_layout)];
}
pieces.extend(uncovered.into_iter().map(|piece| (piece, fallback)));
pieces
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemoryBarrier {
pub src_stage: vk::PipelineStageFlags2,
pub src_access: vk::AccessFlags2,
pub dst_stage: vk::PipelineStageFlags2,
pub dst_access: vk::AccessFlags2,
}
#[derive(Debug, Clone)]
pub struct PipelineBarrier {
pub before_command: CommandIndex,
pub memory_barriers: Vec<MemoryBarrier>,
/// Inline storage avoids heap allocation for the common single-resource barrier;
/// capacity two still lets draw-input barriers spill only when they have 3+ resources.
pub resources: tinyvec::TinyVec<[BarrierResource; 2]>,
}
/// Command ids are dense `0..command_count`, so barrier-index groups use direct vector indexing.
struct BarriersByCommand {
entries: Vec<Vec<usize>>,
}
impl BarriersByCommand {
fn new(command_count: usize) -> Self {
Self {
entries: vec![Vec::new(); command_count],
}
}
fn get(&self, command: CommandIndex) -> &[usize] {
&self.entries[command.index()]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResourceLifetime {
pub first_batch: usize,
pub last_batch: usize,
pub placement: Placement,
/// Imported resources participate in analysis, but never in transient packing.
pub imported: bool,
}
/// Placement metadata reserved for the device-dependent packing phase.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Placement {
pub offset: u64,
pub size: u64,
pub alignment: u64,
pub pool: Option<u32>,
pub aliased: bool,
}
/// One command's access scope for a resource, retained for alias barriers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResourceUse {
pub command: CommandIndex,
pub stage: vk::PipelineStageFlags2,
pub access: vk::AccessFlags2,
}
/// Resource scope collapsed to one entry per contracted DAG node.
///
/// The representative commands are both real accesses: the first is used to key
/// initial-use barriers, while the last keys terminal-use ordering checks.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CollapsedUse {
node: usize,
first_command: CommandIndex,
last_command: CommandIndex,
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
}
/// Per-resource terminal and initial uses cached for placement and alias barriers.
#[derive(Debug, Default, Clone)]
pub struct ResourceFrontiers {
uses: Vec<Option<(Vec<ResourceUse>, Vec<ResourceUse>)>>,
collapsed: Vec<Option<(Vec<CollapsedUse>, Vec<CollapsedUse>)>>,
command_level_alias_frontiers: bool,
}
#[derive(Clone, Copy)]
enum AliasUse<'a> {
Command(&'a ResourceUse),
Collapsed(&'a CollapsedUse),
}
impl AliasUse<'_> {
fn first_command(self) -> CommandIndex {
match self {
Self::Command(use_) => use_.command,
Self::Collapsed(use_) => use_.first_command,
}
}
fn last_command(self) -> CommandIndex {
match self {
Self::Command(use_) => use_.command,
Self::Collapsed(use_) => use_.last_command,
}
}
fn stage(self) -> vk::PipelineStageFlags2 {
match self {
Self::Command(use_) => use_.stage,
Self::Collapsed(use_) => use_.stage,
}
}
fn access(self) -> vk::AccessFlags2 {
match self {
Self::Command(use_) => use_.access,
Self::Collapsed(use_) => use_.access,
}
}
}
enum AliasUses<'a> {
Command(&'a [ResourceUse]),
Collapsed(&'a [CollapsedUse]),
}
enum AliasUseIter<'a> {
Command(std::slice::Iter<'a, ResourceUse>),
Collapsed(std::slice::Iter<'a, CollapsedUse>),
}
impl<'a> Iterator for AliasUseIter<'a> {
type Item = AliasUse<'a>;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Command(iter) => iter.next().map(AliasUse::Command),
Self::Collapsed(iter) => iter.next().map(AliasUse::Collapsed),
}
}
}
impl<'a> AliasUses<'a> {
fn iter(&self) -> AliasUseIter<'a> {
match self {
Self::Command(uses) => AliasUseIter::Command(uses.iter()),
Self::Collapsed(uses) => AliasUseIter::Collapsed(uses.iter()),
}
}
fn is_empty(&self) -> bool {
match self {
Self::Command(uses) => uses.is_empty(),
Self::Collapsed(uses) => uses.is_empty(),
}
}
}
impl ResourceFrontiers {
/// Computes frontiers directly from command-level uses when there are no
/// render-pass spans. With identity command-to-node mapping, each command
/// use is one collapsed use, so this produces the same frontiers as the
/// contracted-node path without constructing a copy of every use.
fn from_command_uses(
uses_by_resource: &HashMap<ResourceId, Vec<ResourceUse>>,
happens_before: &HappensBefore,
) -> Self {
let mut uses_frontiers: Vec<Option<(Vec<ResourceUse>, Vec<ResourceUse>)>> =
vec![None; resource_frontier_len(uses_by_resource)];
for (&id, uses) in uses_by_resource {
let terminal_uses = terminal_uses(uses, happens_before);
let initial_uses = initial_uses(uses, happens_before);
uses_frontiers[id.index()] = Some((
terminal_uses.iter().map(|use_| **use_).collect(),
initial_uses.iter().map(|use_| **use_).collect(),
));
}
Self {
uses: uses_frontiers,
collapsed: Vec::new(),
command_level_alias_frontiers: true,
}
}
/// Computes owned terminal/initial use sets once for each resource.
pub fn from_uses(
uses_by_resource: &HashMap<ResourceId, Vec<ResourceUse>>,
happens_before: &HappensBefore,
) -> Self {
let collapsed_uses = collapse_uses(uses_by_resource, &happens_before.node_of_command);
let collapsed = Self::frontier_map(&collapsed_uses, happens_before);
let mut uses: Vec<Option<(Vec<ResourceUse>, Vec<ResourceUse>)>> =
vec![None; resource_frontier_len(uses_by_resource)];
for (&id, resource_uses) in uses_by_resource {
let terminal = terminal_uses(resource_uses, happens_before)
.into_iter()
.copied()
.collect();
let initial = initial_uses(resource_uses, happens_before)
.into_iter()
.copied()
.collect();
uses[id.index()] = Some((terminal, initial));
}
Self {
uses,
collapsed,
command_level_alias_frontiers: false,
}
}
/// Computes frontiers from uses already collapsed by contracted DAG node.
fn from_collapsed_uses(
uses_by_resource: &HashMap<ResourceId, Vec<CollapsedUse>>,
happens_before: &HappensBefore,
) -> Self {
let collapsed = Self::frontier_map(uses_by_resource, happens_before);
let mut uses: Vec<Option<(Vec<ResourceUse>, Vec<ResourceUse>)>> =
vec![None; resource_frontier_len(uses_by_resource)];
for (index, frontier) in collapsed.iter().enumerate() {
if let Some((terminal, initial)) = frontier {
let terminal = terminal.iter().map(collapsed_use_as_resource_use).collect();
let initial = initial.iter().map(collapsed_use_as_resource_use).collect();
uses[index] = Some((terminal, initial));
}
}
Self {
uses,
collapsed,
command_level_alias_frontiers: false,
}
}
fn frontier_map(
uses_by_resource: &HashMap<ResourceId, Vec<CollapsedUse>>,
happens_before: &HappensBefore,
) -> Vec<Option<(Vec<CollapsedUse>, Vec<CollapsedUse>)>> {
let mut frontiers: Vec<Option<(Vec<CollapsedUse>, Vec<CollapsedUse>)>> =
vec![None; resource_frontier_len(uses_by_resource)];
for (&id, uses) in uses_by_resource {
frontiers[id.index()] = Some((
terminal_collapsed_uses(uses, happens_before),
initial_collapsed_uses(uses, happens_before),
));
}
frontiers
}
/// Terminal uses not ordered before another use of this resource.
pub fn terminal(&self, id: ResourceId) -> &[ResourceUse] {
self.uses
.get(id.index())
.and_then(Option::as_ref)
.map(|(terminal, _)| terminal.as_slice())
.unwrap_or_default()
}
/// Initial uses not ordered after another use of this resource.
pub fn initial(&self, id: ResourceId) -> &[ResourceUse] {
self.uses
.get(id.index())
.and_then(Option::as_ref)
.map(|(_, initial)| initial.as_slice())
.unwrap_or_default()
}
fn collapsed_terminal(&self, id: ResourceId) -> &[CollapsedUse] {
self.collapsed
.get(id.index())
.and_then(Option::as_ref)
.map(|(terminal, _)| terminal.as_slice())
.unwrap_or_default()
}
fn collapsed_initial(&self, id: ResourceId) -> &[CollapsedUse] {
self.collapsed
.get(id.index())
.and_then(Option::as_ref)
.map(|(_, initial)| initial.as_slice())
.unwrap_or_default()
}
fn alias_terminal(&self, id: ResourceId) -> AliasUses<'_> {
if self.command_level_alias_frontiers {
AliasUses::Command(self.terminal(id))
} else {
AliasUses::Collapsed(self.collapsed_terminal(id))
}
}
fn alias_initial(&self, id: ResourceId) -> AliasUses<'_> {
if self.command_level_alias_frontiers {
AliasUses::Command(self.initial(id))
} else {
AliasUses::Collapsed(self.collapsed_initial(id))
}
}
/// Whether every terminal use of `a` precedes every initial use of `b`.
/// Empty frontier sets are rejected rather than treated as vacuously safe.
pub fn alias_in_order(
&self,
a: ResourceId,
b: ResourceId,
happens_before: &HappensBefore,
) -> bool {
if self.command_level_alias_frontiers {
let terminal = self.terminal(a);
let initial = self.initial(b);
return !terminal.is_empty()
&& !initial.is_empty()
&& terminal.iter().all(|terminal| {
initial.iter().all(|initial| {
happens_before.happens_before(terminal.command, initial.command)
})
});
}
let terminal = self.alias_terminal(a);
let initial = self.alias_initial(b);
!terminal.is_empty()
&& !initial.is_empty()
&& terminal.iter().all(|terminal| {
initial.iter().all(|initial| {
happens_before.happens_before(terminal.last_command(), initial.first_command())
})
})
}
/// Whether either resource order permits safe placement reuse.
pub fn alias_in_order_either(
&self,
a: ResourceId,
b: ResourceId,
happens_before: &HappensBefore,
) -> bool {
self.alias_in_order(a, b, happens_before) || self.alias_in_order(b, a, happens_before)
}
}
/// Resource IDs are dense graph indices, so frontier slots can be queried
/// directly without hashing on the placement/interference hot path.
fn resource_frontier_len<T>(uses_by_resource: &HashMap<ResourceId, T>) -> usize {
uses_by_resource
.keys()
.map(|id| id.index())
.max()
.map_or(0, |max_index| max_index + 1)
}
fn collapsed_use_as_resource_use(use_: &CollapsedUse) -> ResourceUse {
ResourceUse {
command: use_.first_command,
stage: use_.stage,
access: use_.access,
}
}
fn collapse_uses(
uses_by_resource: &HashMap<ResourceId, Vec<ResourceUse>>,
node_of_command: &[usize],
) -> HashMap<ResourceId, Vec<CollapsedUse>> {
uses_by_resource
.iter()
.map(|(&id, uses)| {
let mut collapsed: Vec<CollapsedUse> = Vec::new();
for use_ in uses {
let node = node_of_command
.get(use_.command.index())
.copied()
.unwrap_or(use_.command.index());
if let Some(last) = collapsed.last_mut()
&& last.node == node
{
last.last_command = use_.command;
last.stage |= use_.stage;
last.access |= use_.access;
} else {
collapsed.push(CollapsedUse {
node,
first_command: use_.command,
last_command: use_.command,
stage: use_.stage,
access: use_.access,
});
}
}
(id, collapsed)
})
.collect()
}
/*
* Frontier analysis operates on the contracted DAG nodes. For uses in one node,
* same-node ordering remains deliberately false and the single collapsed entry
* therefore remains both an initial and terminal use.
*/
fn terminal_collapsed_uses(
uses: &[CollapsedUse],
happens_before: &HappensBefore,
) -> Vec<CollapsedUse> {
uses.iter()
.filter(|candidate| {
!uses.iter().any(|other| {
candidate.node != other.node
&& happens_before.happens_before(candidate.last_command, other.first_command)
})
})
.copied()
.collect()
}
fn initial_collapsed_uses(
uses: &[CollapsedUse],
happens_before: &HappensBefore,
) -> Vec<CollapsedUse> {
uses.iter()
.filter(|candidate| {
!uses.iter().any(|other| {
candidate.node != other.node
&& happens_before.happens_before(other.last_command, candidate.first_command)
})
})
.copied()
.collect()
}
/// A render-pass span in original command-recording order.
#[derive(Debug, Clone)]
pub struct RenderPassSpan {
/// Begin-rendering command index.
pub begin: CommandIndex,
/// End-rendering command index.
pub end: CommandIndex,
}
impl RenderPassSpan {
fn interior_range(&self) -> std::ops::Range<usize> {
debug_assert!(
self.end >= self.begin,
"RenderPassSpan end must not precede begin"
);
self.begin.index() + 1..self.end.index()
}
fn commands(&self) -> impl Iterator<Item = CommandIndex> + '_ {
(self.begin.index()..=self.end.index()).map(CommandIndex::from)
}
}
/// The resource-level interference graph used by transient-resource aliasing.
/// An edge means no safe ordering between the resources was proven.
#[derive(Debug, Clone, Default)]
pub struct ResourceInterference {
edges: Vec<BTreeSet<ResourceId>>,
}
impl ResourceInterference {
/// Build a conservative interference graph from inclusive batch intervals.
///
/// This interval-only constructor is useful for reporting/tests. The compiler
/// uses command reachability instead; batch separation is not an ordering proof.
pub fn from_lifetimes(
lifetimes: impl IntoIterator<Item = (ResourceId, ResourceLifetime)>,
) -> Self {
let mut max_id = ResourceId::from_u32(0);
let mut resources: Vec<_> = lifetimes
.into_iter()
.inspect(|(id, _)| {
if *id > max_id {
max_id = *id;
}
})
.collect();
let mut edges = vec![BTreeSet::new(); max_id.index() + 1];
resources.sort_unstable_by_key(|&(_, lifetime)| lifetime.first_batch);
let mut resources = VecDeque::from(resources);
let mut index = 0;
while index < resources.len() {
let (id, lifetime) = resources[index];
let mut j = 0;
while j < index {
if resources[j].1.last_batch < lifetime.first_batch {
resources.swap_remove_front(j);
index -= 1;
} else {
j += 1;
}
}
index += 1;
// Active entries are partitioned by their inclusive batch intervals.
let active =
resources.partition_point(|&(_, other)| other.first_batch <= lifetime.last_batch);
for &(other_id, _) in resources.range(..active) {
edges[id.index()].insert(other_id);
edges[other_id.index()].insert(id);
}
}
Self { edges }
}
/// Returns whether the resources interfere and therefore must not share memory.
pub fn interferes(&self, a: ResourceId, b: ResourceId) -> bool {
self.edges[a.index()].contains(&b)
}
}
/// Compact transitive-closure rows over contracted nodes, queried by command index.
/// Commands within one render-pass span map to the same node and are deliberately
/// unordered with respect to one another for aliasing and interference analysis.
#[derive(Debug, Clone, Default)]
pub struct HappensBefore {
rows: Vec<FixedBitSet>,
node_of_command: Vec<usize>,
}
impl HappensBefore {
fn from_dag(
edges: &[Vec<usize>],
execution_order: &[CommandIndex],
node_of_command: &[usize],
node_representatives: &[CommandIndex],
) -> Self {
let mut closure = Self {
rows: (0..edges.len())
.map(|_| FixedBitSet::with_capacity(edges.len()))
.collect(),
node_of_command: node_of_command.to_vec(),
};
// The execution order restricted to representatives is a valid DAG topological
// order; process it backwards so each successor row is complete.
for &representative in execution_order.iter().rev() {
let node = closure.node_of_command[representative.index()];
if node_representatives[node] != representative {
continue;
}
for &successor in &edges[node] {
closure.rows[node].insert(successor);
if node != successor {
let (node_row, successor_row) = if node < successor {
let (before_successor, from_successor) =
closure.rows.split_at_mut(successor);
(&mut before_successor[node], &from_successor[0])
} else {
let (before_node, from_node) = closure.rows.split_at_mut(node);
(&mut from_node[0], &before_node[successor])
};
node_row.union_with(successor_row);
}
}
}
closure
}
fn from_command_dag(edges: &[Vec<usize>], batches: &[Vec<CommandIndex>]) -> Self {
let node_of_command = (0..edges.len()).collect::<Vec<_>>();
let execution_order = batches.iter().flatten().copied().collect::<Vec<_>>();
let node_representatives = node_of_command
.iter()
.map(|&command| CommandIndex(command))
.collect::<Vec<_>>();
Self::from_dag(
edges,
&execution_order,
&node_of_command,
&node_representatives,
)
}
fn contains(&self, from: usize, to: usize) -> bool {
let (Some(&from_node), Some(&to_node)) =
(self.node_of_command.get(from), self.node_of_command.get(to))
else {
return false;
};
from_node != to_node
&& self
.rows
.get(from_node)
.is_some_and(|row| row.contains(to_node))
}
/// Whether command `from` transitively precedes command `to`.
pub fn happens_before(&self, from: CommandIndex, to: CommandIndex) -> bool {
self.contains(from.index(), to.index())
}
}
bitflags::bitflags! {
#[derive(Debug, Default, Copy, Clone)]
struct KahnVertex: u8 {
const READY = 0b0000_0001;
const OCCUPIED = 0b0000_0010;
}
}
impl KahnVertex {
fn is_ready(self) -> bool {
self.contains(KahnVertex::READY)
}
fn is_occupied(self) -> bool {
self.contains(KahnVertex::OCCUPIED)
}
fn make_unready(&mut self) -> bool {
let was_ready = self.contains(KahnVertex::READY);
self.remove(KahnVertex::READY);
was_ready
}
fn make_ready(&mut self) -> bool {
let was_ready = self.contains(KahnVertex::READY);
self.insert(KahnVertex::READY);
was_ready
}
fn set_occupied(&mut self) -> bool {
let was_unoccupied = !self.contains(KahnVertex::OCCUPIED);
self.insert(KahnVertex::OCCUPIED);
was_unoccupied
}
}
/// Deterministic Kahn topological-sort state for command dependencies.
/// Ready nodes are added lazily when an occupied node has no remaining dependencies.
#[derive(Debug)]
pub(crate) struct Kahn {
pub(crate) edges: Vec<Vec<usize>>,
indegree: Vec<usize>,
ready: BinaryHeap<Reverse<usize>>,
// TODO: bitvec
nodes: Vec<KahnVertex>,
emitted: usize,
total: usize,
}
/// An iterator over the next batch of ready nodes in a Kahn topological sort.
/// It drains the ready heap, skipping stale or unoccupied entries, and the caller
/// must call `complete` on each node in the batch before querying for another batch.
pub(crate) struct KahnBatchIter<'a> {
kahn: &'a mut Kahn,
}
impl Iterator for KahnBatchIter<'_> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
while let Some(Reverse(node)) = self.kahn.ready.pop() {
if let Some(vert) = self.kahn.nodes.get_mut(node)
&& vert.make_unready()
&& vert.is_occupied()
{
return Some(node);
}
}
None
}
}
impl Kahn {
pub(crate) fn new(node_count: usize) -> Self {
Self {
edges: vec![Vec::new(); node_count],
indegree: vec![0; node_count],
ready: BinaryHeap::new(),
nodes: vec![KahnVertex::default(); node_count],
emitted: 0,
total: 0,
}
}
pub(crate) fn add_node(&mut self, node: usize) {
if self.nodes[node].set_occupied() {
self.total += 1;
if self.indegree[node] == 0 {
self.nodes[node].make_ready();
self.ready.push(Reverse(node));
}
}
}
pub(crate) fn add_edge(&mut self, from: usize, to: usize) {
let edges = &mut self.edges[from];
let mut newly_occupied = [false; 2];
for (index, node) in self
.nodes
.get_disjoint_mut([from, to])
.expect("Kahn nodes must be distinct")
.into_iter()
.enumerate()
{
if node.set_occupied() {
self.total += 1;
newly_occupied[index] = true;
}
}
for (node, newly_occupied) in [from, to].into_iter().zip(newly_occupied) {
if newly_occupied && self.indegree[node] == 0 {
self.nodes[node].make_ready();
self.ready.push(Reverse(node));
}
}
if let Err(index) = edges.binary_search(&to) {
edges.insert(index, to);
self.indegree[to] += 1;
self.nodes[to].remove(KahnVertex::READY);
}
}
pub(crate) fn iter_batch(&mut self) -> KahnBatchIter<'_> {
KahnBatchIter { kahn: self }
}
pub(crate) fn pop_batch(&mut self) -> Vec<usize> {
Vec::from_iter(self.iter_batch())
}
pub(crate) fn complete(&mut self, node: usize) {
if !self.nodes[node].is_occupied() {
return;
}
self.emitted += 1;
for &dependent in &self.edges[node] {
if self.nodes[dependent].is_occupied() {
self.indegree[dependent] -= 1;
if self.indegree[dependent] == 0 {
self.nodes[dependent].make_ready();
self.ready.push(Reverse(dependent));
}
}
}
}
pub(crate) fn is_complete(&self) -> bool {
self.emitted == self.total
}
}
/// A graph of resource uses whose compiled ordering also governs transient
/// resource aliasing. Aliasing is currently safe only under the single-queue
/// submission contract documented at the module level.
pub struct RenderGraph {
next_id: u32,
resources: Vec<ResourceDesc>,
imported_buffers_by_ptr: HashMap<usize, ResourceId>,
imported_buffers_by_raw: HashMap<vk::Buffer, ResourceId>,
imported_textures_by_ptr: HashMap<usize, ResourceId>,
imported_textures_by_raw: HashMap<vk::Image, ResourceId>,
pub commands: CommandList,
}
impl RenderGraph {
pub fn new() -> Self {
Self {
next_id: 0,
resources: Vec::new(),
imported_buffers_by_ptr: HashMap::new(),
imported_buffers_by_raw: HashMap::new(),
imported_textures_by_ptr: HashMap::new(),
imported_textures_by_raw: HashMap::new(),
commands: CommandList::new(),
}
}
fn add_resource(&mut self, desc: ResourceDesc) -> ResourceId {
let id = ResourceId::from_u32(self.next_id);
self.next_id = self.next_id.checked_add(1).expect("resource id overflow");
self.resources.push(desc);
id
}
pub fn create_buffer(&mut self, desc: GraphBufferDesc) -> Buffer {
Buffer(self.add_resource(ResourceDesc::CreatedBuffer(desc)))
}
pub fn create_texture(&mut self, desc: GraphTextureDesc) -> Texture {
Texture(self.add_resource(ResourceDesc::CreatedTexture(desc)))
}
pub fn import_buffer(
&mut self,
resource: impl Into<Arc<BufferResource>>,
initial: crate::resources::ResourceState,
) -> Result<Buffer, GraphError> {
let resource = resource.into();
let ptr = Arc::as_ptr(&resource) as usize;
let raw = resource.raw();
let pointer_id = self.imported_buffers_by_ptr.get(&ptr).copied();
let found_id = pointer_id.or_else(|| self.imported_buffers_by_raw.get(&raw).copied());
if let Some(id) = found_id {
let prior = self.resources[id.index()].initial_state();
if !same_state(prior, initial) {
return Err(GraphError::ConflictingInitialState(id));
}
if pointer_id.is_some() {
self.imported_buffers_by_raw.insert(raw, id);
}
return Ok(Buffer(id));
}
let id = self.add_resource(ResourceDesc::ImportedBuffer { resource, initial });
self.imported_buffers_by_ptr.insert(ptr, id);
self.imported_buffers_by_raw.insert(raw, id);
Ok(Buffer(id))
}
pub fn import_texture(
&mut self,
resource: impl Into<Arc<ImageResource>>,
initial: crate::resources::ResourceState,
) -> Result<Texture, GraphError> {
let resource = resource.into();
let ptr = Arc::as_ptr(&resource) as usize;
let raw = resource.raw();
let pointer_id = self.imported_textures_by_ptr.get(&ptr).copied();
let found_id = pointer_id.or_else(|| self.imported_textures_by_raw.get(&raw).copied());
if let Some(id) = found_id {
let prior = self.resources[id.index()].initial_state();
if !same_state(prior, initial) {
return Err(GraphError::ConflictingInitialState(id));
}
if pointer_id.is_some() {
self.imported_textures_by_raw.insert(raw, id);
}
return Ok(Texture(id));
}
let id = self.add_resource(ResourceDesc::ImportedTexture { resource, initial });
self.imported_textures_by_ptr.insert(ptr, id);
self.imported_textures_by_raw.insert(raw, id);
Ok(Texture(id))
}
/// Transitively import resources retained by a managed descriptor set.
///
/// `initial_states` must provide one explicit initial state for every unique
/// buffer/image referenced by the set. Descriptor access and image layout are
/// draw-use states and are not used as initial state.
pub fn import_descriptor_set(
&mut self,
set: Arc<DescriptorSet>,
initial_states: &[(DescriptorResourceIdentity, ResourceState)],
dynamic_offsets: &[u32],
) -> Result<GraphDescriptorSet, GraphError> {
let fail = |message: String| GraphError::DescriptorImport(message);
let raw = set.raw().map_err(map_descriptor_set_error)?;
let layout = set.layout_bindings().map_err(map_descriptor_set_error)?;
let tracked = set
.bindings()
.map_err(map_descriptor_set_error)?
.cloned()
.collect::<Vec<_>>();
let dynamic_slots = dynamic_offset_slots(layout);
if dynamic_slots.len() != dynamic_offsets.len() {
return Err(fail(format!(
"expected {} dynamic offsets, received {}",
dynamic_slots.len(),
dynamic_offsets.len()
)));
}
for (index, &(_, _, kind)) in dynamic_slots.iter().enumerate() {
let alignment = set
.dynamic_offset_alignment(kind)
.map_err(map_descriptor_set_error)?;
if !is_dynamic_offset_aligned(dynamic_offsets[index], alignment) {
return Err(fail(format!(
"dynamic offset at index {index} is misaligned"
)));
}
}
for &(binding, element, _) in &dynamic_slots {
if !tracked
.iter()
.any(|entry| entry.binding == binding && entry.array_element == element)
{
return Err(fail(format!(
"dynamic descriptor binding {binding} element {element} has no retained buffer"
)));
}
}
let mut staged_accesses = Vec::new();
let mut staged_resources = Vec::new();
let mut access_resource_indices: Vec<usize> = Vec::new();
for entry in &tracked {
let stage = shader_stages_to_pipeline(entry.stages)
.ok_or_else(|| fail(format!("unsupported shader stage mask {:?}", entry.stages)))?;
match (&entry.resource, entry.descriptor_type) {
(
DescriptorResourceBinding::Buffer {
buffer,
offset,
range,
dynamic,
},
kind,
) if matches!(
kind,
vk::DescriptorType::UNIFORM_BUFFER
| vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC
| vk::DescriptorType::STORAGE_BUFFER
| vk::DescriptorType::STORAGE_BUFFER_DYNAMIC
) =>
{
let dynamic_offset = if *dynamic {
let index = dynamic_slots
.iter()
.position(|&(binding, element, _)| {
binding == entry.binding && element == entry.array_element
})
.ok_or_else(|| fail("dynamic descriptor ordering mismatch".into()))?;
let value = u64::from(dynamic_offsets[index]);
value
} else {
0
};
let range =
effective_buffer_range(buffer.len(), *offset, *range, dynamic_offset)
.map_err(|message| fail(message.into()))?;
let (access, _) = descriptor_access(kind)
.ok_or_else(|| fail(format!("unsupported descriptor type {kind:?}")))?;
staged_accesses.push(DescriptorResourceAccess::Buffer {
buffer: Buffer(ResourceId::from_u32(u32::MAX)),
range,
stage,
access,
});
staged_resources.push(StagedImportResource::Buffer(buffer.clone()));
access_resource_indices.push(staged_resources.len() - 1);
}
(
DescriptorResourceBinding::Image {
view,
range,
layout,
..
},
kind,
) if matches!(
kind,
vk::DescriptorType::SAMPLED_IMAGE
| vk::DescriptorType::COMBINED_IMAGE_SAMPLER
| vk::DescriptorType::STORAGE_IMAGE
) =>
{
let (access, forced_layout) = descriptor_access(kind)
.ok_or_else(|| fail(format!("unsupported descriptor type {kind:?}")))?;
let layout = forced_layout.unwrap_or(*layout);
staged_accesses.push(DescriptorResourceAccess::Texture {
texture: Texture(ResourceId::from_u32(u32::MAX)),
range: *range,
layout,
stage,
access,
});
staged_resources.push(StagedImportResource::Texture(view.image().clone()));
access_resource_indices.push(staged_resources.len() - 1);
}
(DescriptorResourceBinding::Sampler { .. }, vk::DescriptorType::SAMPLER) => {}
(DescriptorResourceBinding::TexelBuffer { view, range }, kind)
if matches!(
kind,
vk::DescriptorType::UNIFORM_TEXEL_BUFFER
| vk::DescriptorType::STORAGE_TEXEL_BUFFER
) =>
{
let buffer = view.buffer();
let start = range.offset;
let size = range.size;
if start.checked_add(size).is_none_or(|end| end > buffer.len()) {
return Err(fail("texel buffer range exceeds buffer length".into()));
}
let (access, _) = descriptor_access(kind)
.ok_or_else(|| fail(format!("unsupported descriptor type {kind:?}")))?;
staged_accesses.push(DescriptorResourceAccess::Buffer {
buffer: Buffer(ResourceId::from_u32(u32::MAX)),
range: BufferRange {
offset: start,
size,
},
stage,
access,
});
staged_resources.push(StagedImportResource::Buffer(buffer.clone()));
access_resource_indices.push(staged_resources.len() - 1);
}
_ => {
return Err(fail(format!(
"descriptor type {:?} has incompatible or unsupported owner",
entry.descriptor_type
)));
}
}
}
// Validate that every distinct referenced owner has exactly one usable state
// before mutating graph resources, then route imports through the deduping APIs.
let mut resolved_states = Vec::with_capacity(staged_resources.len());
for owner in &staged_resources {
let state = state_for_identity(initial_states, owner).ok_or_else(|| {
fail("missing or conflicting explicit initial state for descriptor resource".into())
})?;
resolved_states.push(state);
}
for (identity, state) in initial_states {
let supplied = match identity {
DescriptorResourceIdentity::Buffer(buffer) => {
StagedImportResource::Buffer(buffer.clone())
}
DescriptorResourceIdentity::Texture(image) => {
StagedImportResource::Texture(image.clone())
}
};
if !staged_resources
.iter()
.any(|resource| same_imported_identity(resource, &supplied))
{
return Err(fail(
"initial state supplied for a resource not referenced by the descriptor set"
.into(),
));
}
if let Some((id, existing)) = self.find_imported(&supplied)
&& !same_state(existing, *state)
{
return Err(GraphError::ConflictingInitialState(id));
}
}
for (owner, state) in staged_resources.iter().zip(&resolved_states) {
if let Some((id, existing_state)) = self.find_imported(owner)
&& !same_state(existing_state, *state)
{
return Err(GraphError::ConflictingInitialState(id));
}
}
for index in 0..staged_resources.len() {
for other in (index + 1)..staged_resources.len() {
if same_imported_identity(&staged_resources[index], &staged_resources[other])
&& !same_state(resolved_states[index], resolved_states[other])
{
if let Some((id, _)) = self.find_imported(&staged_resources[index]) {
return Err(GraphError::ConflictingInitialState(id));
}
return Err(GraphError::ConflictingDescriptorInitialStates);
}
}
}
let mut texture_scopes = Vec::new();
for (access, &resource_index) in staged_accesses.iter().zip(&access_resource_indices) {
if let DescriptorResourceAccess::Texture { range, layout, .. } = access {
texture_scopes.push((resource_index, *range, *layout));
}
}
for (index, &(resource_a, range_a, layout_a)) in texture_scopes.iter().enumerate() {
for &(resource_b, range_b, layout_b) in &texture_scopes[index + 1..] {
if same_imported_identity(
&staged_resources[resource_a],
&staged_resources[resource_b],
) && texture_layouts_conflict(range_a, layout_a, range_b, layout_b)
{
return Err(GraphError::ConflictingDescriptorDrawLayouts);
}
}
}
for (index, first) in staged_accesses.iter().enumerate() {
if !matches!(first, DescriptorResourceAccess::Texture { .. }) {
continue;
}
let DescriptorResourceAccess::Texture {
range: range_a,
layout: layout_a,
..
} = first
else {
continue;
};
let first_resource = access_resource_indices[index];
for (second_index, second) in staged_accesses.iter().enumerate().skip(index + 1) {
let DescriptorResourceAccess::Texture {
range: range_b,
layout: layout_b,
..
} = second
else {
continue;
};
let second_resource = access_resource_indices[second_index];
if same_imported_identity(
&staged_resources[first_resource],
&staged_resources[second_resource],
) && texture_layouts_conflict(*range_a, *layout_a, *range_b, *layout_b)
{
return Err(GraphError::ConflictingDescriptorDrawLayouts);
}
}
}
let resolved_ids = self.import_transaction(|graph| {
let mut resolved_ids = Vec::with_capacity(staged_resources.len());
for (owner, state) in staged_resources.iter().zip(resolved_states) {
let id = match owner {
StagedImportResource::Buffer(resource) => graph
.import_buffer(resource.clone(), state)
.map(|id| id.id()),
StagedImportResource::Texture(resource) => graph
.import_texture(resource.clone(), state)
.map(|id| id.id()),
}?;
resolved_ids.push(id);
}
Ok(resolved_ids)
})?;
for (access, &resource_index) in staged_accesses.iter_mut().zip(&access_resource_indices) {
let id = resolved_ids[resource_index];
match access {
DescriptorResourceAccess::Buffer { buffer, .. } => {
*buffer = Buffer(id);
}
DescriptorResourceAccess::Texture { texture, .. } => {
*texture = Texture(id);
}
}
}
Ok(GraphDescriptorSet {
raw,
accesses: staged_accesses,
managed: Some(set),
dynamic_offsets: dynamic_offsets.to_vec(),
})
}
/// Tracks a raw descriptor set without ownership or content verification.
///
/// This is the untracked escape hatch. The caller must retain every bound
/// resource and declare all accesses accurately; prefer `import_descriptor_set`
/// for managed sets. The graph cannot verify raw descriptor contents.
pub fn track_descriptor_set(
&self,
raw: vk::DescriptorSet,
accesses: Vec<DescriptorResourceAccess>,
) -> GraphDescriptorSet {
GraphDescriptorSet {
raw,
accesses,
managed: None,
dynamic_offsets: Vec::new(),
}
}
pub fn push<C: crate::commands::Command + 'static>(&mut self, command: C) -> CommandIndex {
self.commands.push(command)
}
/// Compile dependencies, their transitive happens-before closure, and
/// lifetimes for transient-alias analysis. Placement-specific alias barriers
/// are added by `CompiledGraph::resolve`. The closure is derived from
/// resource-hazard edges; batch membership alone is not proof that uses are
/// ordered.
pub fn compile(mut self) -> Result<CompiledGraph, GraphError> {
// Keep dependency construction on the recorded accesses; the walk adds
// lifetime-only attachment accesses to `side_effects` as it proceeds.
let orderings = build_command_orderings(
&self.resources,
&self.commands.commands,
&mut self.commands.side_effects,
)?;
let CommandOrderings {
execution_order,
batches,
spans: render_pass_spans,
node_of_command,
node_representatives,
dependency_edges,
} = orderings;
validate_feedback_loops(
&self.commands.commands,
&self.commands.side_effects,
&render_pass_spans,
)?;
// Check for conflicting draw layouts before building the dependency graph.
for accesses in self.commands.side_effects.command_accesses() {
for (index, first) in accesses.iter().enumerate() {
for second in &accesses[index + 1..] {
if first.resource == second.resource
&& let (
SubresourceScopeInner::Texture {
range: a,
layout: la,
},
SubresourceScopeInner::Texture {
range: b,
layout: lb,
},
) = (first.scope.subresource, second.scope.subresource)
&& texture_layouts_conflict(a, la, b, lb)
{
return Err(GraphError::ConflictingDrawLayouts(first.resource));
}
}
}
}
let command_count = self.commands.len();
let accesses = self.commands.side_effects.command_accesses();
let compiler = GraphCompiler::new_with_nodes(
&self.resources,
accesses,
command_count,
&self.commands.side_effects,
node_of_command,
node_representatives,
&render_pass_spans,
);
compiler.validate_resources()?;
let mut barriers = compiler.build_barriers(&execution_order);
hoist_span_barriers(&mut barriers, &render_pass_spans, command_count);
validate_intra_pass_barriers(&barriers, &render_pass_spans)?;
let happens_before = HappensBefore::from_dag(
&dependency_edges,
&execution_order,
&compiler.node_of_command,
&compiler.node_representatives,
);
let uses_by_resource = compiler.resource_uses();
// Span graphs need contracted uses so accesses within one pass become
// a single node. Without spans, command nodes are the identity mapping:
// command-level frontiers are therefore exactly the contracted
// frontiers, and avoid the otherwise redundant collapsed-use map.
let frontiers = if render_pass_spans.is_empty() {
let has_created_resource_uses = uses_by_resource
.keys()
.any(|id| self.resources[id.index()].is_created());
if has_created_resource_uses {
ResourceFrontiers::from_command_uses(&uses_by_resource, &happens_before)
} else {
ResourceFrontiers::default()
}
} else {
let collapsed_uses_by_resource = compiler.collapsed_resource_uses(&uses_by_resource);
// Imported resources never enter transient packing; avoid frontier
// construction when there is no created resource that could be aliased.
let has_created_resource_uses = collapsed_uses_by_resource
.keys()
.any(|id| self.resources[id.index()].is_created());
if !has_created_resource_uses {
ResourceFrontiers::default()
} else {
ResourceFrontiers::from_collapsed_uses(&collapsed_uses_by_resource, &happens_before)
}
};
let (lifetimes, interference) = compiler.bind_lifetimes(
&execution_order,
&batches,
&frontiers,
&happens_before,
&uses_by_resource,
&render_pass_spans,
);
let ordinary_barrier_count = barriers.len();
let has_created_views = render_pass_spans.iter().any(|span| {
self.commands.commands[span.begin.index()]
.render_pass_desc()
.is_some_and(|desc| {
desc.color
.iter()
.any(|attachment| matches!(attachment.view, AttachmentView::Created(_)))
|| desc.depth.is_some_and(|attachment| {
matches!(attachment.view, AttachmentView::Created(_))
})
})
});
compiler.build_alias_barriers(&mut barriers, &lifetimes, &frontiers, &happens_before);
hoist_span_barriers(&mut barriers, &render_pass_spans, command_count);
validate_intra_pass_barriers(&barriers, &render_pass_spans)?;
let barrier_indices_by_command = grouped_barrier_indices(&barriers, command_count);
let GraphCompiler {
node_of_command: compiled_node_of_command,
node_representatives: compiled_node_representatives,
..
} = compiler;
Ok(CompiledGraph {
resources: self.resources,
resolved: HashMap::new(),
views: HashMap::new(),
created_view_bindings: Vec::new(),
has_created_views,
placements_resolved: false,
commands: self.commands,
batches,
execution_order,
barriers,
barrier_indices_by_command,
ordinary_barrier_count,
lifetimes,
interference,
dependency_edges,
node_of_command: compiled_node_of_command,
node_representatives: compiled_node_representatives,
happens_before,
uses_by_resource,
frontiers,
render_pass_spans,
})
}
}
fn validate_feedback_loops(
commands: &[Box<dyn crate::recorder::RecordedCommand>],
side_effects: &SideEffectMap,
spans: &[RenderPassSpan],
) -> Result<(), GraphError> {
for span in spans {
let Some(desc) = commands[span.begin.index()].render_pass_desc() else {
continue;
};
let attachments = desc
.color
.iter()
.map(|attachment| (attachment.texture.id(), attachment.range))
.chain(
desc.depth
.into_iter()
.map(|attachment| (attachment.texture.id(), attachment.range)),
)
.collect::<Vec<_>>();
for command in span.interior_range().map(CommandIndex::from) {
for access in side_effects.accesses(command) {
let SubresourceScopeInner::Texture { range, .. } = access.scope.subresource else {
continue;
};
if attachments.iter().any(|(id, attachment_range)| {
*id == access.resource
&& image_range_intersection(range, *attachment_range).is_some()
}) {
return Err(GraphError::RenderPassFeedbackLoop(format!(
"texture {:?} is accessed by command {:?} inside its render pass",
access.resource, command
)));
}
}
}
}
Ok(())
}
/// Move outside-origin interior transitions to their span begin, retaining the
/// sequence of transitions so Vulkan observes chained layouts in order.
fn hoist_span_barriers(
barriers: &mut Vec<PipelineBarrier>,
spans: &[RenderPassSpan],
command_count: usize,
) {
if spans.is_empty() {
return;
}
let mut hoisted = (0..command_count)
.map(|_| None::<Vec<BarrierResource>>)
.collect::<Vec<_>>();
let mut hoisted_memory = (0..command_count)
.map(|_| None::<Vec<MemoryBarrier>>)
.collect::<Vec<_>>();
let mut interior_owner = vec![None; command_count];
for span in spans {
hoisted[span.begin.index()] = Some(Vec::new());
hoisted_memory[span.begin.index()] = Some(Vec::new());
for command in span.interior_range().map(CommandIndex::from) {
interior_owner[command.index()] = Some(span.begin);
}
}
for barrier in barriers.iter_mut() {
let Some(begin) = interior_owner[barrier.before_command.index()] else {
continue;
};
// Legacy global memory barriers carry no per-resource origin, so retain
// their conservative pass-boundary hoisting behavior.
hoisted_memory[begin.index()]
.as_mut()
.expect("span begin has a memory-barrier entry")
.append(&mut barrier.memory_barriers);
let mut retained = tinyvec::TinyVec::<[BarrierResource; 2]>::new();
for resource in barrier.resources.drain(..) {
let outside_origin = match resource.origin {
BarrierOrigin::Initial => true,
BarrierOrigin::Command(origin) => interior_owner[origin.index()] != Some(begin),
};
if outside_origin {
let resources = hoisted[begin.index()]
.as_mut()
.expect("span has hoisted resources");
resources.push(resource);
} else {
retained.push(resource);
}
}
barrier.resources = retained;
}
barriers.retain(|barrier| !barrier.resources.is_empty() || !barrier.memory_barriers.is_empty());
let mut barrier_index_by_command = vec![None; command_count];
for (index, barrier) in barriers.iter().enumerate() {
barrier_index_by_command[barrier.before_command.index()].get_or_insert(index);
}
for span in spans {
let begin = span.begin;
let resources = hoisted[begin.index()].take().unwrap_or_default();
let memory_barriers =
coalesce_hoisted_memory(hoisted_memory[begin.index()].take().unwrap_or_default());
let resources = coalesce_hoisted_resources(resources);
if resources.is_empty() && memory_barriers.is_empty() {
continue;
}
if let Some(index) = barrier_index_by_command[begin.index()] {
barriers[index].memory_barriers.extend(memory_barriers);
barriers[index].resources.extend(resources);
} else {
barriers.push(PipelineBarrier {
before_command: begin,
memory_barriers,
resources: resources.into_iter().collect(),
});
barrier_index_by_command[begin.index()] = Some(barriers.len() - 1);
}
}
}
fn coalesce_hoisted_resources(incoming: Vec<BarrierResource>) -> Vec<BarrierResource> {
let mut resources = Vec::new();
let mut exact_image_indices = HashMap::new();
let mut target_image_indices = HashMap::new();
let mut target_range_indices: HashMap<(ResourceId, vk::ImageLayout), Vec<usize>> =
HashMap::new();
let mut buffer_indices = HashMap::new();
for resource in incoming {
coalesce_hoisted_image_barrier(
&mut resources,
&mut exact_image_indices,
&mut target_image_indices,
&mut target_range_indices,
&mut buffer_indices,
resource,
);
}
resources
}
fn coalesce_hoisted_image_barrier(
resources: &mut Vec<BarrierResource>,
exact_image_indices: &mut HashMap<
(
ResourceId,
(u32, u32, u32, u32, u32),
vk::ImageLayout,
vk::ImageLayout,
BarrierOrigin,
),
usize,
>,
target_image_indices: &mut HashMap<
(
ResourceId,
(u32, u32, u32, u32, u32),
vk::ImageLayout,
BarrierOrigin,
),
Vec<usize>,
>,
target_range_indices: &mut HashMap<(ResourceId, vk::ImageLayout), Vec<usize>>,
buffer_indices: &mut HashMap<(ResourceId, u64, u64, BarrierOrigin, u64, u64), usize>,
mut incoming: BarrierResource,
) {
let Subresource::Texture(incoming_range) = incoming.range else {
let Subresource::Buffer(incoming_range) = incoming.range else {
unreachable!()
};
let key = (
incoming.id,
incoming_range.offset,
incoming_range.size,
incoming.origin,
incoming.src_stage.as_raw(),
incoming.src_access.as_raw(),
);
if let Some(&index) = buffer_indices.get(&key) {
let existing = &mut resources[index];
existing.dst_stage |= incoming.dst_stage;
existing.dst_access |= incoming.dst_access;
} else {
buffer_indices.insert(key, resources.len());
resources.push(incoming);
}
return;
};
let (Some(old_layout), Some(new_layout)) = (incoming.old_layout, incoming.new_layout) else {
resources.push(incoming);
return;
};
let range_key = image_range_key(incoming_range);
let exact_key = (
incoming.id,
range_key,
old_layout,
new_layout,
incoming.origin,
);
let existing_index = exact_image_indices.get(&exact_key).copied().or_else(|| {
[new_layout, old_layout].into_iter().find_map(|layout| {
target_image_indices
.get(&(incoming.id, range_key, layout, incoming.origin))
.and_then(|indices| {
indices.iter().copied().find(|&index| {
resources[index].new_layout == Some(new_layout)
&& (resources[index].old_layout == Some(old_layout)
|| resources[index].new_layout == Some(old_layout))
})
})
})
});
if let Some(index) = existing_index {
let existing = &mut resources[index];
existing.dst_stage |= incoming.dst_stage;
existing.dst_access |= incoming.dst_access;
return;
}
// Keep only subresources not already transitioned to this target layout.
// Layout-region normalization normally makes these residual pieces empty or
// disjoint, but retaining subtraction here protects partially overlapping ranges.
let mut residual = vec![incoming_range];
for &index in target_range_indices
.get(&(incoming.id, new_layout))
.into_iter()
.flatten()
{
let existing = &resources[index];
let Subresource::Texture(covered) = existing.range else {
continue;
};
if same_image_subresource_range(covered, incoming_range) {
continue;
}
residual = residual
.into_iter()
.flat_map(|piece| subtract_image_range(piece, covered).iter())
.collect();
if residual.is_empty() {
break;
}
}
for range in residual {
incoming.range = Subresource::Texture(range);
let key = (
incoming.id,
image_range_key(range),
old_layout,
new_layout,
incoming.origin,
);
exact_image_indices.insert(key, resources.len());
target_image_indices
.entry((
incoming.id,
image_range_key(range),
old_layout,
incoming.origin,
))
.or_default()
.push(resources.len());
target_image_indices
.entry((
incoming.id,
image_range_key(range),
new_layout,
incoming.origin,
))
.or_default()
.push(resources.len());
target_range_indices
.entry((incoming.id, new_layout))
.or_default()
.push(resources.len());
resources.push(incoming);
}
}
fn image_range_key(range: vk::ImageSubresourceRange) -> (u32, u32, u32, u32, u32) {
(
range.aspect_mask.as_raw(),
range.base_mip_level,
range.level_count,
range.base_array_layer,
range.layer_count,
)
}
fn coalesce_hoisted_memory(incoming: Vec<MemoryBarrier>) -> Vec<MemoryBarrier> {
let mut indices = HashMap::new();
let mut barriers = Vec::new();
for barrier in incoming {
let key = (
barrier.src_stage,
barrier.src_access,
barrier.dst_stage,
barrier.dst_access,
);
if indices.contains_key(&key) {
continue;
}
indices.insert(key, barriers.len());
barriers.push(barrier);
}
barriers
}
fn validate_intra_pass_barriers(
barriers: &[PipelineBarrier],
spans: &[RenderPassSpan],
) -> Result<(), GraphError> {
if spans.is_empty() {
return Ok(());
}
for barrier in barriers {
if is_render_pass_interior(barrier.before_command, spans)
&& barrier.resources.iter().any(|resource| {
matches!((resource.old_layout, resource.new_layout), (Some(old), Some(new)) if old != new)
})
{
return Err(GraphError::UnsupportedIntraPassDependency);
}
}
Ok(())
}
fn is_render_pass_interior(command: CommandIndex, spans: &[RenderPassSpan]) -> bool {
let span_index = spans.partition_point(|span| span.end < command);
spans
.get(span_index)
.is_some_and(|span| span.interior_range().contains(&command.index()))
}
fn flatten_batches(batches: &[Vec<CommandIndex>]) -> Vec<CommandIndex> {
batches.iter().flatten().copied().collect()
}
pub(crate) fn validate_render_pass_attachments(
resources: &[ResourceDesc],
desc: RenderPassDesc<'_>,
) -> Result<(), GraphError> {
let mut seen_subresources = HashSet::new();
let attachments = desc
.color
.iter()
.map(|attachment| {
(
attachment.texture,
&attachment.view,
attachment.range,
attachment.layout,
true,
)
})
.chain(desc.depth.into_iter().map(|attachment| {
(
attachment.texture,
&attachment.view,
attachment.range,
attachment.layout,
false,
)
}));
for (texture, view, range, layout, is_color) in attachments {
let id = texture.id();
let Some(resource) = resources.get(id.index()) else {
return Err(GraphError::UnknownResource(id));
};
let reason = |message: &str| {
GraphError::InvalidRenderPassAttachment(format!("texture {id:?}: {message}"))
};
let counts = match resource {
ResourceDesc::CreatedTexture(desc) => Some((desc.mip_levels, desc.array_layers)),
ResourceDesc::ImportedTexture { .. } => None,
ResourceDesc::CreatedBuffer(_) | ResourceDesc::ImportedBuffer { .. } => {
return Err(reason("attachment resource is not a texture"));
}
};
if let AttachmentView::Imported(view) = view
&& let ResourceDesc::ImportedTexture { resource, .. } = resource
&& view.image().as_ref() != resource.as_ref()
{
return Err(reason("imported view refers to a different image"));
}
if let AttachmentView::Created(view) = view
&& (view.mip_range.end == vk::REMAINING_MIP_LEVELS
|| view.layer_range.end == vk::REMAINING_ARRAY_LAYERS)
{
return Err(reason("created view uses a remaining-range sentinel"));
}
if let AttachmentView::Created(view) = view
&& (view.mip_range.end < view.mip_range.start
|| view.layer_range.end < view.layer_range.start)
{
return Err(reason("created view range is reversed"));
}
let view_range = match view {
AttachmentView::Imported(view) => view.subresource_range(),
AttachmentView::Created(view) => vk::ImageSubresourceRange::default()
.aspect_mask(view.aspect)
.base_mip_level(view.mip_range.start)
.level_count(view.mip_range.end - view.mip_range.start)
.base_array_layer(view.layer_range.start)
.layer_count(view.layer_range.end - view.layer_range.start),
};
if !same_image_subresource_range(range, view_range) {
return Err(reason("declared range does not match the attachment view"));
}
if !is_legal_attachment_layout(layout, is_color) {
return Err(reason("layout is not legal for this attachment type"));
}
if range.level_count == vk::REMAINING_MIP_LEVELS
|| range.layer_count == vk::REMAINING_ARRAY_LAYERS
|| range.level_count == 0
|| range.layer_count == 0
{
return Err(reason("attachment range must be concrete and non-empty"));
}
let Some(mip_end) = range.base_mip_level.checked_add(range.level_count) else {
return Err(reason("mip range overflows"));
};
let Some(layer_end) = range.base_array_layer.checked_add(range.layer_count) else {
return Err(reason("layer range overflows"));
};
if let Some((mip_count, layer_count)) = counts
&& (mip_end > mip_count || layer_end > layer_count)
{
return Err(reason("attachment range exceeds texture mip/layer counts"));
}
let aspects = [
vk::ImageAspectFlags::COLOR,
vk::ImageAspectFlags::DEPTH,
vk::ImageAspectFlags::STENCIL,
]
.into_iter()
.filter(|aspect| range.aspect_mask.intersects(*aspect))
.collect::<Vec<_>>();
let known_aspects = aspects
.iter()
.copied()
.fold(vk::ImageAspectFlags::empty(), |mask, aspect| mask | aspect);
if aspects.is_empty() || known_aspects != range.aspect_mask {
return Err(reason("attachment range has unsupported aspect bits"));
}
for aspect in aspects {
for mip in range.base_mip_level..mip_end {
for layer in range.base_array_layer..layer_end {
if !seen_subresources.insert((id, aspect.as_raw(), mip, layer)) {
return Err(reason("duplicate attachment subresource"));
}
}
}
}
}
Ok(())
}
fn same_image_subresource_range(
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 is_legal_attachment_layout(layout: vk::ImageLayout, is_color: bool) -> bool {
if is_color {
matches!(
layout,
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL | vk::ImageLayout::GENERAL
)
} else {
matches!(
layout,
vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL
| vk::ImageLayout::DEPTH_READ_ONLY_OPTIMAL
| vk::ImageLayout::DEPTH_STENCIL_READ_ONLY_OPTIMAL
| vk::ImageLayout::DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL
| vk::ImageLayout::GENERAL
)
}
}
struct GraphCompiler<'a> {
resources: &'a [ResourceDesc],
accesses: &'a [Vec<crate::recorder::ResourceAccess>],
command_count: usize,
node_of_command: Vec<usize>,
node_representatives: Vec<CommandIndex>,
node_count: usize,
lifetime_only: BTreeSet<(usize, ResourceId)>,
}
impl<'a> GraphCompiler<'a> {
fn new_with_nodes(
resources: &'a [ResourceDesc],
accesses: &'a [Vec<crate::recorder::ResourceAccess>],
command_count: usize,
side_effects: &SideEffectMap,
node_of_command: Vec<usize>,
node_representatives: Vec<CommandIndex>,
spans: &[RenderPassSpan],
) -> Self {
let node_count = node_representatives.len();
Self {
resources,
accesses,
command_count,
node_of_command,
node_representatives,
node_count,
lifetime_only: if spans.is_empty() {
BTreeSet::new()
} else {
accesses
.iter()
.enumerate()
.flat_map(|(command, accesses)| {
accesses.iter().filter_map(move |access| {
side_effects
.is_lifetime_only(command.into(), access.resource)
.then_some((command, access.resource))
})
})
.collect()
},
}
}
#[cfg(test)]
fn new(
resources: &'a [ResourceDesc],
accesses: &'a [Vec<crate::recorder::ResourceAccess>],
command_count: usize,
side_effects: &SideEffectMap,
spans: &[RenderPassSpan],
) -> Self {
let mut node_of_command = vec![usize::MAX; command_count];
let mut node_representatives = Vec::new();
let mut span_index = 0;
let mut command = 0;
while command < command_count {
let node = node_representatives.len();
if let Some(span) = spans.get(span_index)
&& span.begin.index() == command
{
node_representatives.push(span.begin);
node_of_command[command..=span.end.index()].fill(node);
command = span.end.index() + 1;
span_index += 1;
} else {
node_representatives.push(CommandIndex(command));
node_of_command[command] = node;
command += 1;
}
}
Self::new_with_nodes(
resources,
accesses,
command_count,
side_effects,
node_of_command,
node_representatives,
spans,
)
}
fn is_lifetime_only(&self, command: CommandIndex, resource: ResourceId) -> bool {
self.lifetime_only.contains(&(command.index(), resource))
}
fn validate_resources(&self) -> Result<(), GraphError> {
for command_accesses in self.accesses {
for access in command_accesses {
if access.resource.index() >= self.resources.len() {
return Err(GraphError::UnknownResource(access.resource));
}
}
}
Ok(())
}
fn build_barriers(&self, execution_order: &[CommandIndex]) -> Vec<PipelineBarrier> {
let initial_states = self
.resources
.iter()
.map(ResourceDesc::initial_state)
.collect::<Vec<_>>();
self.build_barriers_with_initial_states(execution_order, &initial_states)
}
fn build_barriers_with_initial_states(
&self,
execution_order: &[CommandIndex],
initial_states: &[crate::resources::ResourceState],
) -> Vec<PipelineBarrier> {
let mut execution_positions = vec![usize::MAX; self.command_count];
for (position, command) in execution_order.iter().enumerate() {
execution_positions[command.index()] = position;
}
let mut result: Vec<PipelineBarrier> = Vec::with_capacity(self.command_count);
let mut prior_accesses_by_resource: Vec<
Vec<(CommandIndex, crate::recorder::ResourceAccess)>,
> = (0..self.resources.len()).map(|_| Vec::new()).collect();
let mut prior_access_summaries = (0..self.resources.len())
.map(|_| None::<PriorAccessSummary>)
.collect::<Vec<_>>();
let mut current_layouts: Vec<Vec<LayoutRegion>> =
(0..self.resources.len()).map(|_| Vec::new()).collect();
for &command_index in execution_order {
let mut barrier_resources = tinyvec::TinyVec::new();
for current in &self.accesses[command_index.index()] {
if self.is_lifetime_only(command_index, current.resource) {
continue;
}
// The most recent conflicting access is the complete source scope
// for this barrier; prior barrier masks are not carried forward.
let read_only = crate::resources::access_flag_is_read(current.scope.access)
&& !access_flag_is_write(current.scope.access);
let can_skip_history_scan = read_only
&& prior_access_summaries[current.resource.index()]
.as_ref()
.is_some_and(|summary| {
!summary.has_write
&& !summary.has_non_read_only
&& match current.scope.subresource {
SubresourceScopeInner::Buffer(_) => true,
SubresourceScopeInner::Texture { layout, .. } => {
summary.uniform_layout == Some(layout)
}
}
});
let previous = (!can_skip_history_scan)
.then(|| &prior_accesses_by_resource[current.resource.index()])
.and_then(|prior_accesses| {
prior_accesses
.iter()
.rev()
.find(|(_, prior)| prior.scope.conflicts(&current.scope))
})
.map(|(command, access)| (*command, *access));
if let Some((previous_command, previous)) = previous {
debug_assert!(
execution_positions[previous_command.index()]
< execution_positions[command_index.index()]
);
let (fallback_layout, new_layout) =
Self::layouts(previous.scope, current.scope);
let pieces = match (scope_range(current.scope), fallback_layout) {
(Subresource::Texture(range), Some(fallback)) => layout_pieces(
range,
fallback,
&current_layouts[current.resource.index()],
)
.into_iter()
.map(|(range, layout)| (Subresource::Texture(range), Some(layout)))
.collect::<Vec<_>>(),
(range, old_layout) => vec![(range, old_layout)],
};
for (range, old_layout) in pieces {
barrier_resources.push(BarrierResource {
id: current.resource,
range,
origin: BarrierOrigin::Command(previous_command),
src_stage: previous.scope.stage,
src_access: previous.scope.access,
dst_stage: current.scope.stage,
dst_access: current.scope.access,
old_layout,
new_layout,
queue_transfer: None,
});
if let (Subresource::Texture(range), Some(old), Some(new)) =
(range, old_layout, new_layout)
&& old != new
{
update_layout_regions(
&mut current_layouts[current.resource.index()],
range,
new,
);
}
}
continue;
}
let desc = &self.resources[current.resource.index()];
let initial = initial_states[current.resource.index()];
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 {
let pieces = match (scope_range(current.scope), old_layout) {
(Subresource::Texture(range), Some(fallback)) => layout_pieces(
range,
fallback,
&current_layouts[current.resource.index()],
)
.into_iter()
.map(|(range, layout)| (Subresource::Texture(range), Some(layout)))
.collect::<Vec<_>>(),
(range, old_layout) => vec![(range, old_layout)],
};
for (range, old_layout) in pieces {
barrier_resources.push(BarrierResource {
id: current.resource,
range,
origin: BarrierOrigin::Initial,
src_stage: initial.stage,
src_access: initial.access,
dst_stage: current.scope.stage,
dst_access: current.scope.access,
old_layout,
new_layout,
queue_transfer: None,
});
if let (Subresource::Texture(range), Some(old), Some(new)) =
(range, old_layout, new_layout)
&& old != new
{
update_layout_regions(
&mut current_layouts[current.resource.index()],
range,
new,
);
}
}
}
}
if !barrier_resources.is_empty() {
result.push(PipelineBarrier {
before_command: command_index,
memory_barriers: Vec::new(),
resources: barrier_resources,
});
}
for access in &self.accesses[command_index.index()] {
if self.is_lifetime_only(command_index, access.resource) {
continue;
}
prior_accesses_by_resource[access.resource.index()].push((command_index, *access));
let summary = prior_access_summaries[access.resource.index()]
.get_or_insert_with(PriorAccessSummary::default);
let is_read = crate::resources::access_flag_is_read(access.scope.access);
let is_write = access_flag_is_write(access.scope.access);
summary.has_write |= is_write;
summary.has_non_read_only |= !is_read || is_write;
if let SubresourceScopeInner::Texture { layout, .. } = access.scope.subresource {
if let Some(uniform_layout) = summary.uniform_layout
&& uniform_layout != layout
{
summary.uniform_layout = None;
summary.texture_layouts_diverged = true;
} else if summary.uniform_layout.is_none() && !summary.texture_layouts_diverged
{
summary.uniform_layout = Some(layout);
}
}
}
}
result
}
fn build_alias_barriers(
&self,
barriers: &mut Vec<PipelineBarrier>,
lifetimes: &HashMap<ResourceId, ResourceLifetime>,
frontiers: &ResourceFrontiers,
happens_before: &HappensBefore,
) {
let mut barrier_index_by_command = vec![None; self.command_count];
for (index, barrier) in barriers.iter().enumerate() {
barrier_index_by_command[barrier.before_command.index()].get_or_insert(index);
}
let mut resources_by_pool: HashMap<u32, Vec<ResourceId>> = HashMap::new();
for (&id, lifetime) in lifetimes {
if let Some(pool) = lifetime.placement.pool {
resources_by_pool.entry(pool).or_default().push(id);
}
}
let mut new_resources = lifetimes
.iter()
.filter_map(|(&id, lifetime)| {
(lifetime.placement.aliased && !lifetime.imported).then_some(id)
})
.collect::<Vec<_>>();
new_resources.sort_unstable();
for new_id in new_resources {
let Some(new_lifetime) = lifetimes.get(&new_id) else {
continue;
};
let new_first_uses = frontiers.alias_initial(new_id);
if new_first_uses.is_empty() {
continue;
}
let mut old_resources = new_lifetime
.placement
.pool
.and_then(|pool| resources_by_pool.get(&pool))
.into_iter()
.flatten()
.filter_map(|&old_id| {
let old_lifetime = lifetimes.get(&old_id)?;
(old_id != new_id
&& !old_lifetime.imported
&& placement_ranges_overlap(old_lifetime.placement, new_lifetime.placement))
.then_some(old_id)
})
.collect::<Vec<_>>();
old_resources.sort_unstable();
let old_terminal_uses = old_resources
.iter()
.map(|&old_id| frontiers.alias_terminal(old_id))
.collect::<Vec<_>>();
for first_use in new_first_uses.iter() {
let first_use_command = ResourceUse {
command: first_use.first_command(),
stage: first_use.stage(),
access: first_use.access(),
};
let mut old_stage = vk::PipelineStageFlags2::NONE;
let mut old_access = vk::AccessFlags2::NONE;
let mut old_image_writes = false;
let mut has_old_image = false;
let mut merged_src_stage = vk::PipelineStageFlags2::NONE;
let mut merged_src_access = vk::AccessFlags2::NONE;
let mut has_old_buffer = false;
for old_terminal_uses in &old_terminal_uses {
if old_terminal_uses.is_empty()
|| !old_terminal_uses.iter().all(|terminal| {
happens_before
.happens_before(terminal.last_command(), first_use.first_command())
})
{
continue;
}
let (src_stage, src_access) = aggregate_alias_uses(old_terminal_uses.iter());
match &self.resources[new_id.index()] {
ResourceDesc::CreatedBuffer(_) => {
has_old_buffer = true;
merged_src_stage |= src_stage;
merged_src_access |= if old_terminal_uses
.iter()
.any(|use_| access_flag_is_write(use_.access()))
{
src_access
} else {
vk::AccessFlags2::NONE
};
}
ResourceDesc::CreatedTexture(_) => {
has_old_image = true;
old_stage |= src_stage;
old_access |= src_access;
old_image_writes |= old_terminal_uses
.iter()
.any(|use_| access_flag_is_write(use_.access()));
}
ResourceDesc::ImportedBuffer { .. }
| ResourceDesc::ImportedTexture { .. } => {}
}
}
if has_old_image {
let barrier = pipeline_barrier_at_command_indexed(
barriers,
&mut barrier_index_by_command,
first_use.first_command(),
);
let mut found_image_barrier = false;
for image_barrier in barrier
.resources
.iter_mut()
.filter(|resource| resource.id == new_id)
{
found_image_barrier = true;
image_barrier.src_stage = old_stage;
image_barrier.src_access = if old_image_writes {
old_access
} else {
vk::AccessFlags2::NONE
};
image_barrier.dst_stage = first_use.stage();
image_barrier.dst_access = first_use.access();
image_barrier.old_layout = Some(vk::ImageLayout::UNDEFINED);
image_barrier.origin = BarrierOrigin::Initial;
}
if !found_image_barrier {
let first_scope =
first_use_scope(&first_use_command, self.accesses, new_id);
barrier.resources.push(BarrierResource {
id: new_id,
range: scope_range(first_scope),
origin: BarrierOrigin::Initial,
src_stage: old_stage,
src_access: if old_image_writes {
old_access
} else {
vk::AccessFlags2::NONE
},
dst_stage: first_use.stage(),
dst_access: first_use.access(),
old_layout: Some(vk::ImageLayout::UNDEFINED),
new_layout: match first_scope.subresource {
SubresourceScopeInner::Texture { layout, .. } => Some(layout),
SubresourceScopeInner::Buffer(_) => None,
},
queue_transfer: None,
});
}
}
if has_old_buffer {
let barrier = pipeline_barrier_at_command_indexed(
barriers,
&mut barrier_index_by_command,
first_use.first_command(),
);
// Vulkan aliasing needs one dependency from all outgoing terminal uses
// to this incoming first use. Availability (src masks) is unscoped for
// a buffer barrier, while visibility (dst masks) is scoped to its range;
// targeting the new owner is therefore sufficient. A global barrier is
// valid but strictly coarser. Keep the union from every overlapping
// occupant, including execution-only src masks for read-only terminals.
let mut found_buffer_barrier = false;
for buffer_barrier in barrier
.resources
.iter_mut()
.filter(|resource| resource.id == new_id)
{
found_buffer_barrier = true;
buffer_barrier.src_stage |= merged_src_stage;
buffer_barrier.src_access |= merged_src_access;
// Preserve the entry's destination scope and origin: a command-origin
// entry inside a pass remains there, unlike a newly added Initial entry.
}
if !found_buffer_barrier {
// Initial origin gives newly added alias dependencies the same
// span-begin hoisting behavior as the former global barriers.
// Each graph buffer owns a distinct VkBuffer handle, even when its
// allocation is pooled at Placement::offset. Barrier ranges are
// relative to that handle, so cover it fully rather than using the
// backing allocation's placement offset/size.
barrier.resources.push(BarrierResource {
id: new_id,
range: Subresource::Buffer(BufferRange::full()),
origin: BarrierOrigin::Initial,
src_stage: merged_src_stage,
src_access: merged_src_access,
dst_stage: first_use.stage(),
dst_access: first_use.access(),
old_layout: None,
new_layout: None,
queue_transfer: None,
});
}
}
}
}
}
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,
};
let initial_layout = match current.subresource {
SubresourceScopeInner::Texture { .. } => {
Some(initial.layout.unwrap_or(vk::ImageLayout::UNDEFINED))
}
SubresourceScopeInner::Buffer(_) => None,
};
(initial_layout, current_layout)
}
fn bind_lifetimes(
&self,
execution_order: &[CommandIndex],
batches: &[Batch],
frontiers: &ResourceFrontiers,
happens_before: &HappensBefore,
uses_by_resource: &HashMap<ResourceId, Vec<ResourceUse>>,
spans: &[RenderPassSpan],
) -> (HashMap<ResourceId, ResourceLifetime>, ResourceInterference) {
let mut batch_for_node = vec![usize::MAX; self.node_count];
for (batch_index, batch) in batches.iter().enumerate() {
// Span interiors map back to their begin node and are always in the
// same execution batch as that begin command.
for &command in &execution_order[batch.start..batch.start + batch.count] {
let node = self.node_of_command[command.index()];
batch_for_node[node] = batch_index;
}
}
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| {
let batch = batch_for_node[self.node_of_command[command]];
lifetime.first_batch = lifetime.first_batch.min(batch);
lifetime.last_batch = lifetime.last_batch.max(batch);
})
.or_insert(ResourceLifetime {
first_batch: batch_for_node[self.node_of_command[command]],
last_batch: batch_for_node[self.node_of_command[command]],
placement: Placement {
offset: 0,
size: match &self.resources[access.resource.index()] {
ResourceDesc::CreatedBuffer(_)
| ResourceDesc::CreatedTexture(_) => 0,
ResourceDesc::ImportedBuffer { .. }
| ResourceDesc::ImportedTexture { .. } => 0,
},
alignment: 1,
pool: None,
aliased: false,
},
imported: !self.resources[access.resource.index()].is_created(),
});
}
}
let mut interference = ResourceInterference::from_frontiers(frontiers, happens_before);
let mut pass_commands = vec![false; self.command_count];
for span in spans {
pass_commands[span.begin.index()..=span.end.index()].fill(true);
}
let pass_resources = uses_by_resource
.iter()
.filter_map(|(&id, uses)| {
(self.resources[id.index()].is_created()
&& uses.iter().any(|use_| pass_commands[use_.command.index()]))
.then_some(id)
})
.collect::<Vec<_>>();
for (index, &a) in pass_resources.iter().enumerate() {
for &b in &pass_resources[index + 1..] {
// Different pool classes cannot share memory, so pass-boundary
// alias transitions only constrain resources of the same class.
if pool_class_for_resource(&self.resources[a.index()])
!= pool_class_for_resource(&self.resources[b.index()])
{
continue;
}
interference.edges[a.index()].insert(b);
interference.edges[b.index()].insert(a);
}
}
(lifetimes, interference)
}
#[cfg(test)]
fn bind_lifetimes_for_test(
&self,
batches: &[Vec<CommandIndex>],
frontiers: &ResourceFrontiers,
happens_before: &HappensBefore,
uses_by_resource: &HashMap<ResourceId, Vec<ResourceUse>>,
spans: &[RenderPassSpan],
) -> (HashMap<ResourceId, ResourceLifetime>, ResourceInterference) {
let execution_order = flatten_batches(batches);
let mut range_batches = Vec::new();
let mut start = 0;
for batch in batches {
range_batches.push(Batch {
start,
count: batch.len(),
});
start += batch.len();
}
self.bind_lifetimes(
&execution_order,
&range_batches,
frontiers,
happens_before,
uses_by_resource,
spans,
)
}
fn resource_uses(&self) -> HashMap<ResourceId, Vec<ResourceUse>> {
let mut uses_by_resource: HashMap<ResourceId, Vec<ResourceUse>> = HashMap::new();
for (command, command_accesses) in self.accesses.iter().enumerate() {
for access in command_accesses {
let resource_uses = uses_by_resource.entry(access.resource).or_default();
if let Some(resource_use) = resource_uses.last_mut()
&& resource_use.command.index() == command
{
resource_use.stage |= access.scope.stage;
resource_use.access |= access.scope.access;
} else {
resource_uses.push(ResourceUse {
command: command.into(),
stage: access.scope.stage,
access: access.scope.access,
});
}
}
}
uses_by_resource
}
/// Collapse each resource's command-level scopes to one first/last representative per DAG node.
fn collapsed_resource_uses(
&self,
uses_by_resource: &HashMap<ResourceId, Vec<ResourceUse>>,
) -> HashMap<ResourceId, Vec<CollapsedUse>> {
collapse_uses(uses_by_resource, &self.node_of_command)
}
}
#[derive(Default)]
struct PriorAccessSummary {
has_write: bool,
has_non_read_only: bool,
uniform_layout: Option<vk::ImageLayout>,
texture_layouts_diverged: bool,
}
impl ResourceInterference {
fn from_frontiers(frontiers: &ResourceFrontiers, happens_before: &HappensBefore) -> Self {
let mut result = Self {
edges: vec![
BTreeSet::new();
frontiers.uses.len().max(frontiers.collapsed.len()).max(1)
],
};
let resource_ids = if frontiers.command_level_alias_frontiers {
frontiers
.uses
.iter()
.enumerate()
.filter_map(|(index, frontier)| {
frontier
.as_ref()
.map(|_| ResourceId::from_u32(index as u32))
})
.collect::<Vec<_>>()
} else {
frontiers
.collapsed
.iter()
.enumerate()
.filter_map(|(index, frontier)| {
frontier
.as_ref()
.map(|_| ResourceId::from_u32(index as u32))
})
.collect::<Vec<_>>()
};
for (index, &a) in resource_ids.iter().enumerate() {
for &b in &resource_ids[index + 1..] {
if !frontiers.alias_in_order_either(a, b, happens_before) {
result.edges[a.index()].insert(b);
result.edges[b.index()].insert(a);
}
}
}
result
}
}
fn terminal_uses<'a>(
uses: &'a [ResourceUse],
happens_before: &HappensBefore,
) -> Vec<&'a ResourceUse> {
uses.iter()
.filter(|candidate| {
!uses.iter().any(|other| {
candidate.command != other.command
&& happens_before.contains(candidate.command.index(), other.command.index())
})
})
.collect()
}
fn initial_uses<'a>(
uses: &'a [ResourceUse],
happens_before: &HappensBefore,
) -> Vec<&'a ResourceUse> {
uses.iter()
.filter(|candidate| {
!uses.iter().any(|other| {
candidate.command != other.command
&& happens_before.contains(other.command.index(), candidate.command.index())
})
})
.collect()
}
/// Returns whether every terminal use of A precedes every initial use of B.
fn resources_alias_in_order(
uses_a: &[ResourceUse],
uses_b: &[ResourceUse],
happens_before: &HappensBefore,
) -> bool {
aliasing_permitted(uses_a, uses_b, happens_before)
}
/// Returns whether every terminal use of A precedes every initial use of B.
/// Empty use sets are rejected rather than treated as vacuously safe. This
/// ordering proves alias safety only under the current single-queue submission
/// contract; if command-to-queue assignment is added, cross-queue alias
/// transitions must also be semaphore edges.
///
/// The graph currently records all commands into one command buffer. It has no
/// command-to-queue assignment, so cross-queue aliasing is out of scope.
pub fn aliasing_permitted(
uses_a: &[ResourceUse],
uses_b: &[ResourceUse],
happens_before: &HappensBefore,
) -> bool {
let terminal_a = terminal_uses(uses_a, happens_before);
let initial_b = initial_uses(uses_b, happens_before);
resource_use_ref_slices_alias_in_order(&terminal_a, &initial_b, happens_before)
}
fn resource_use_ref_slices_alias_in_order(
terminal_a: &[&ResourceUse],
initial_b: &[&ResourceUse],
happens_before: &HappensBefore,
) -> bool {
!terminal_a.is_empty()
&& !initial_b.is_empty()
&& terminal_a.iter().all(|terminal| {
initial_b
.iter()
.all(|initial| happens_before.happens_before(terminal.command, initial.command))
})
}
fn aggregate_resource_uses(uses: &[ResourceUse]) -> (vk::PipelineStageFlags2, vk::AccessFlags2) {
uses.iter().fold(
(vk::PipelineStageFlags2::NONE, vk::AccessFlags2::NONE),
|(stage, access), resource_use| (stage | resource_use.stage, access | resource_use.access),
)
}
fn aggregate_alias_uses<'a>(
uses: impl Iterator<Item = AliasUse<'a>>,
) -> (vk::PipelineStageFlags2, vk::AccessFlags2) {
uses.fold(
(vk::PipelineStageFlags2::NONE, vk::AccessFlags2::NONE),
|(stage, access), resource_use| {
(stage | resource_use.stage(), access | resource_use.access())
},
)
}
fn placement_ranges_overlap(a: Placement, b: Placement) -> bool {
if a.pool.is_none() || a.pool != b.pool || a.size == 0 || b.size == 0 {
return false;
}
let (Some(a_end), Some(b_end)) = (a.offset.checked_add(a.size), b.offset.checked_add(b.size))
else {
return false;
};
a.offset < b_end && b.offset < a_end
}
fn is_host_visible_location(location: MemoryLocation) -> bool {
matches!(
location,
MemoryLocation::CpuToGpu | MemoryLocation::GpuToCpu
)
}
fn is_poolable(
location: MemoryLocation,
strategy: AllocationStrategy,
requirements: renderer_resources::memory::MemoryRequirements,
) -> bool {
!is_host_visible_location(location)
&& strategy != AllocationStrategy::Dedicated
&& !requirements.requires_dedicated_allocation
}
fn allocation_strategy_for(
requirements: renderer_resources::memory::MemoryRequirements,
) -> AllocationStrategy {
if requirements.requires_dedicated_allocation
|| requirements.size >= DEDICATED_ALLOCATION_THRESHOLD
{
AllocationStrategy::Dedicated
} else {
AllocationStrategy::AllocatorManaged
}
}
fn allocation_strategy_for_size(
requested_size: u64,
requirements: renderer_resources::memory::MemoryRequirements,
) -> AllocationStrategy {
if requested_size >= DEDICATED_ALLOCATION_THRESHOLD {
AllocationStrategy::Dedicated
} else {
allocation_strategy_for(requirements)
}
}
fn pool_class_for_image(tiling: vk::ImageTiling) -> PoolClass {
if tiling == vk::ImageTiling::LINEAR {
PoolClass::Linear
} else {
PoolClass::Optimal
}
}
fn pool_class_for_resource(resource: &ResourceDesc) -> PoolClass {
match resource {
ResourceDesc::CreatedBuffer(_) => PoolClass::Linear,
ResourceDesc::CreatedTexture(desc) => pool_class_for_image(desc.tiling),
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => {
unreachable!("imported resources are not transient placement candidates")
}
}
}
fn validate_pooled_image_layout(
id: ResourceId,
desc: &GraphTextureDesc,
requirements: renderer_resources::memory::MemoryRequirements,
) -> Result<(), GraphError> {
if is_poolable(
desc.mem_location,
allocation_strategy_for(requirements),
requirements,
) && desc.layout != vk::ImageLayout::UNDEFINED
{
return Err(GraphError::InvalidPooledImageLayout(id));
}
Ok(())
}
fn memory_property_flags(location: MemoryLocation) -> vk::MemoryPropertyFlags {
match location {
MemoryLocation::CpuToGpu | MemoryLocation::GpuToCpu => {
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT
}
MemoryLocation::GpuOnly => vk::MemoryPropertyFlags::DEVICE_LOCAL,
MemoryLocation::Unknown => vk::MemoryPropertyFlags::empty(),
}
}
fn pool_accepts_member(
pool: &PlacementPool,
class: PoolClass,
property_flags: vk::MemoryPropertyFlags,
requirements: renderer_resources::memory::MemoryRequirements,
) -> bool {
pool.class == class
&& pool.property_flags == property_flags
&& requirements.memory_type_bits & (1 << pool.memory_type_index) != 0
}
fn should_plan_created_resource(
id: ResourceId,
lifetimes: &HashMap<ResourceId, ResourceLifetime>,
resolved: &HashMap<ResourceId, ResolvedResource>,
) -> bool {
lifetimes
.get(&id)
.is_some_and(|lifetime| !lifetime.imported)
&& !resolved.contains_key(&id)
}
fn set_placement(
lifetimes: &mut HashMap<ResourceId, ResourceLifetime>,
id: ResourceId,
requirements: renderer_resources::memory::MemoryRequirements,
offset: u64,
pool: Option<u32>,
aliased: bool,
) {
if let Some(lifetime) = lifetimes.get_mut(&id) {
lifetime.placement = Placement {
offset,
size: requirements.size,
alignment: requirements.alignment,
pool,
aliased,
};
}
}
fn byte_ranges_overlap(a_offset: u64, a_size: u64, b_offset: u64, b_size: u64) -> bool {
let (Some(a_end), Some(b_end)) = (a_offset.checked_add(a_size), b_offset.checked_add(b_size))
else {
return true;
};
a_offset < b_end && b_offset < a_end
}
fn checked_alignment_lcm(lhs: u64, rhs: u64) -> Option<u64> {
if lhs == 0 || rhs == 0 {
return None;
}
let (mut a, mut b) = (lhs, rhs);
while b != 0 {
(a, b) = (b, a % b);
}
lhs.checked_div(a)?.checked_mul(rhs)
}
fn first_fit_placement(
id: ResourceId,
size: u64,
alignment: u64,
occupied: &[(ResourceId, u64)],
lifetimes: &HashMap<ResourceId, ResourceLifetime>,
interference: &ResourceInterference,
frontiers: &ResourceFrontiers,
happens_before: &HappensBefore,
) -> Option<u64> {
if alignment == 0 || size == 0 {
return None;
}
// Validate all lifetimes up front to preserve the missing-lifetime failure behavior.
for (other_id, _) in occupied {
lifetimes.get(other_id)?;
}
let mut candidate = 0_u64;
let mut range_index = 0;
while range_index < occupied.len() {
let aligned = candidate
.checked_add(alignment - 1)?
.checked_div(alignment)?
.checked_mul(alignment)?;
let candidate_end = aligned.checked_add(size)?;
let (other_id, other_offset) = occupied[range_index];
let other_size = lifetimes.get(&other_id)?.placement.size;
let other_end = other_offset.checked_add(other_size);
if other_end.is_some_and(|end| end <= aligned) {
range_index += 1;
continue;
}
if other_offset >= candidate_end && other_end.is_some() {
return Some(aligned);
}
if !byte_ranges_overlap(aligned, size, other_offset, other_size) {
range_index += 1;
continue;
}
let ordered = !interference.interferes(id, other_id)
&& frontiers.alias_in_order_either(id, other_id, happens_before);
if ordered {
range_index += 1;
continue;
}
let next_offset = other_end?;
if next_offset <= aligned {
return None;
}
candidate = next_offset;
}
let aligned = candidate
.checked_add(alignment - 1)?
.checked_div(alignment)?
.checked_mul(alignment)?;
aligned.checked_add(size)?;
Some(aligned)
}
fn image_desc_for_graph_texture(
desc: &GraphTextureDesc,
accesses: &[crate::recorder::ResourceAccess],
queue_families: QueueFlags,
alloc_scheme: AllocationStrategy,
flags: vk::ImageCreateFlags,
) -> ImageDesc {
ImageDesc {
flags,
name: desc.name.clone(),
format: desc.format,
kind: desc.kind,
mip_levels: desc.mip_levels,
array_layers: desc.array_layers,
samples: desc.samples,
extent: desc.extent,
tiling: desc.tiling,
usage: derive_image_usage(accesses, desc.extra_usage),
queue_families,
layout: desc.layout,
mem_location: desc.mem_location,
alloc_scheme,
}
}
fn first_use_scope<'a>(
resource_use: &ResourceUse,
accesses: &'a [Vec<crate::recorder::ResourceAccess>],
resource: ResourceId,
) -> SubresourceScope {
accesses[resource_use.command.index()]
.iter()
.find(|access| access.resource == resource)
.expect("resource use has a matching declared access")
.scope
}
fn pipeline_barrier_at_command(
barriers: &mut Vec<PipelineBarrier>,
command: CommandIndex,
) -> &mut PipelineBarrier {
let index = barriers
.iter()
.position(|barrier| barrier.before_command == command)
.unwrap_or_else(|| {
barriers.push(PipelineBarrier {
before_command: command,
memory_barriers: Vec::new(),
resources: tinyvec::TinyVec::new(),
});
barriers.len() - 1
});
&mut barriers[index]
}
fn pipeline_barrier_at_command_indexed<'a>(
barriers: &'a mut Vec<PipelineBarrier>,
index_by_command: &mut [Option<usize>],
command: CommandIndex,
) -> &'a mut PipelineBarrier {
let index = *index_by_command[command.index()].get_or_insert_with(|| {
barriers.push(PipelineBarrier {
before_command: command,
memory_barriers: Vec::new(),
resources: tinyvec::TinyVec::new(),
});
barriers.len() - 1
});
&mut barriers[index]
}
impl Default for RenderGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
enum StagedImportResource {
Buffer(Arc<BufferResource>),
Texture(Arc<ImageResource>),
}
impl RenderGraph {
// Import staging is transactional for ordinary errors. Panics are intentionally not caught;
// callers must not rely on graph rollback after unwinding.
fn import_transaction<T>(
&mut self,
operation: impl FnOnce(&mut Self) -> Result<T, GraphError>,
) -> Result<T, GraphError> {
let resources_len = self.resources.len();
let next_id = self.next_id;
let buffer_ptrs = self.imported_buffers_by_ptr.clone();
let buffer_raw = self.imported_buffers_by_raw.clone();
let texture_ptrs = self.imported_textures_by_ptr.clone();
let texture_raw = self.imported_textures_by_raw.clone();
match operation(self) {
Ok(value) => Ok(value),
Err(error) => {
self.resources.truncate(resources_len);
self.next_id = next_id;
self.imported_buffers_by_ptr = buffer_ptrs;
self.imported_buffers_by_raw = buffer_raw;
self.imported_textures_by_ptr = texture_ptrs;
self.imported_textures_by_raw = texture_raw;
Err(error)
}
}
}
fn find_imported(
&self,
resource: &StagedImportResource,
) -> Option<(ResourceId, ResourceState)> {
let id = match resource {
StagedImportResource::Buffer(resource) => self
.imported_buffers_by_ptr
.get(&(Arc::as_ptr(resource) as usize))
.copied()
.or_else(|| self.imported_buffers_by_raw.get(&resource.raw()).copied()),
StagedImportResource::Texture(resource) => self
.imported_textures_by_ptr
.get(&(Arc::as_ptr(resource) as usize))
.copied()
.or_else(|| self.imported_textures_by_raw.get(&resource.raw()).copied()),
}?;
Some((id, self.resources[id.index()].initial_state()))
}
}
fn map_descriptor_set_error(
error: renderer_resources::descriptors::DescriptorSetError,
) -> GraphError {
match error {
renderer_resources::descriptors::DescriptorSetError::StaleSet { .. } => {
GraphError::StaleDescriptorSet
}
error => GraphError::DescriptorImport(error.to_string()),
}
}
fn same_state(a: ResourceState, b: ResourceState) -> bool {
a.stage == b.stage && a.access == b.access && a.layout == b.layout
}
fn same_imported_identity(a: &StagedImportResource, b: &StagedImportResource) -> bool {
match (a, b) {
(StagedImportResource::Buffer(a), StagedImportResource::Buffer(b)) => {
Arc::ptr_eq(a, b) || a.raw() == b.raw()
}
(StagedImportResource::Texture(a), StagedImportResource::Texture(b)) => {
Arc::ptr_eq(a, b) || a.raw() == b.raw()
}
_ => false,
}
}
fn effective_buffer_range(
buffer_len: u64,
base_offset: u64,
range: u64,
dynamic_offset: u64,
) -> Result<BufferRange, &'static str> {
if base_offset > buffer_len {
return Err("descriptor base offset exceeds buffer length");
}
let base_range = if range == vk::WHOLE_SIZE {
buffer_len - base_offset
} else {
range
};
if base_offset
.checked_add(base_range)
.is_none_or(|end| end > buffer_len)
{
return Err("descriptor base range exceeds buffer length");
}
let offset = base_offset
.checked_add(dynamic_offset)
.ok_or("effective descriptor offset overflow")?;
if offset
.checked_add(base_range)
.is_none_or(|end| end > buffer_len)
{
return Err("effective descriptor range exceeds buffer length");
}
Ok(BufferRange {
offset,
size: base_range,
})
}
fn dynamic_offset_slots(
layout: &[renderer_resources::pipeline::DescriptorSetLayoutBindingDesc],
) -> Vec<(u32, u32, vk::DescriptorType)> {
let mut dynamic_bindings = layout.iter().collect::<Vec<_>>();
dynamic_bindings.sort_by_key(|binding| binding.binding);
dynamic_bindings
.into_iter()
.filter(|binding| {
matches!(
binding.kind,
vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC
| vk::DescriptorType::STORAGE_BUFFER_DYNAMIC
)
})
.flat_map(|binding| {
(0..binding.count).map(move |element| (binding.binding, element, binding.kind))
})
.collect()
}
fn is_dynamic_offset_aligned(offset: u32, alignment: u64) -> bool {
alignment != 0 && u64::from(offset).is_multiple_of(alignment)
}
fn texture_layouts_conflict(
a: vk::ImageSubresourceRange,
layout_a: vk::ImageLayout,
b: vk::ImageSubresourceRange,
layout_b: vk::ImageLayout,
) -> bool {
layout_a != layout_b && a.intersects(&b)
}
fn descriptor_access(
kind: vk::DescriptorType,
) -> Option<(vk::AccessFlags2, Option<vk::ImageLayout>)> {
match kind {
vk::DescriptorType::UNIFORM_BUFFER | vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC => {
Some((vk::AccessFlags2::UNIFORM_READ, None))
}
vk::DescriptorType::STORAGE_BUFFER | vk::DescriptorType::STORAGE_BUFFER_DYNAMIC => Some((
vk::AccessFlags2::SHADER_STORAGE_READ | vk::AccessFlags2::SHADER_STORAGE_WRITE,
None,
)),
vk::DescriptorType::SAMPLED_IMAGE | vk::DescriptorType::COMBINED_IMAGE_SAMPLER => {
Some((vk::AccessFlags2::SHADER_SAMPLED_READ, None))
}
vk::DescriptorType::STORAGE_IMAGE => Some((
vk::AccessFlags2::SHADER_STORAGE_READ | vk::AccessFlags2::SHADER_STORAGE_WRITE,
Some(vk::ImageLayout::GENERAL),
)),
vk::DescriptorType::UNIFORM_TEXEL_BUFFER => Some((vk::AccessFlags2::SHADER_READ, None)),
vk::DescriptorType::STORAGE_TEXEL_BUFFER => Some((
vk::AccessFlags2::SHADER_STORAGE_READ | vk::AccessFlags2::SHADER_STORAGE_WRITE,
None,
)),
_ => None,
}
}
fn state_for_identity(
states: &[(DescriptorResourceIdentity, ResourceState)],
resource: &StagedImportResource,
) -> Option<ResourceState> {
let matches = states.iter().filter_map(|(identity, state)| {
let same = match (resource, identity) {
(StagedImportResource::Buffer(resource), DescriptorResourceIdentity::Buffer(other)) => {
Arc::ptr_eq(resource, other) || resource.raw() == other.raw()
}
(
StagedImportResource::Texture(resource),
DescriptorResourceIdentity::Texture(other),
) => Arc::ptr_eq(resource, other) || resource.raw() == other.raw(),
_ => false,
};
same.then_some(*state)
});
let mut matches = matches;
let first = matches.next()?;
matches
.all(|state| same_state(first, state))
.then_some(first)
}
fn shader_stages_to_pipeline(stages: vk::ShaderStageFlags) -> Option<vk::PipelineStageFlags2> {
if stages == vk::ShaderStageFlags::ALL {
return Some(vk::PipelineStageFlags2::ALL_COMMANDS);
}
let mut pipeline = vk::PipelineStageFlags2::NONE;
let mappings = [
(
vk::ShaderStageFlags::VERTEX,
vk::PipelineStageFlags2::VERTEX_SHADER,
),
(
vk::ShaderStageFlags::TESSELLATION_CONTROL,
vk::PipelineStageFlags2::TESSELLATION_CONTROL_SHADER,
),
(
vk::ShaderStageFlags::TESSELLATION_EVALUATION,
vk::PipelineStageFlags2::TESSELLATION_EVALUATION_SHADER,
),
(
vk::ShaderStageFlags::GEOMETRY,
vk::PipelineStageFlags2::GEOMETRY_SHADER,
),
(
vk::ShaderStageFlags::FRAGMENT,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
),
(
vk::ShaderStageFlags::COMPUTE,
vk::PipelineStageFlags2::COMPUTE_SHADER,
),
(
vk::ShaderStageFlags::TASK_EXT,
vk::PipelineStageFlags2::TASK_SHADER_EXT,
),
(
vk::ShaderStageFlags::MESH_EXT,
vk::PipelineStageFlags2::MESH_SHADER_EXT,
),
];
let mut recognized = vk::ShaderStageFlags::empty();
if stages.contains(vk::ShaderStageFlags::ALL_GRAPHICS) {
recognized |= vk::ShaderStageFlags::ALL_GRAPHICS;
pipeline |= vk::PipelineStageFlags2::ALL_GRAPHICS;
}
for (shader, stage) in mappings {
if stages.intersects(shader) {
recognized |= shader;
pipeline |= stage;
}
}
(recognized == stages && !pipeline.is_empty()).then_some(pipeline)
}
/// A compiled graph retains the resource-hazard DAG's transitive closure for
/// lifetime/alias decisions. After resolution, alias placements are based on
/// memory requirements and split into linear and optimal pools, with barriers
/// at each new occupant's first use. Those guarantees assume the recorded
/// command buffer is submitted to one queue; multi-queue use requires semaphore
/// edges for alias transitions.
pub struct CompiledGraph {
resources: Vec<ResourceDesc>,
resolved: HashMap<ResourceId, ResolvedResource>,
views: HashMap<(ResourceId, ImageViewDesc), Arc<ImageView>>,
created_view_bindings: Vec<(ResourceId, ImageViewDesc, Arc<ImageView>)>,
has_created_views: bool,
placements_resolved: bool,
commands: CommandList,
batches: Vec<Batch>,
execution_order: Vec<CommandIndex>,
barriers: Vec<PipelineBarrier>,
barrier_indices_by_command: BarriersByCommand,
ordinary_barrier_count: usize,
lifetimes: HashMap<ResourceId, ResourceLifetime>,
interference: ResourceInterference,
dependency_edges: Vec<Vec<usize>>,
node_of_command: Vec<usize>,
node_representatives: Vec<CommandIndex>,
happens_before: HappensBefore,
uses_by_resource: HashMap<ResourceId, Vec<ResourceUse>>,
frontiers: ResourceFrontiers,
render_pass_spans: Vec<RenderPassSpan>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PoolClass {
Linear,
Optimal,
}
struct PendingPlacement {
id: ResourceId,
resource: ResolvedResource,
requirements: renderer_resources::memory::MemoryRequirements,
class: PoolClass,
property_flags: vk::MemoryPropertyFlags,
}
struct PlacementPool {
id: u32,
class: PoolClass,
memory_type_index: u32,
property_flags: vk::MemoryPropertyFlags,
size: u64,
members: Vec<(ResourceId, u64)>,
occupied: Vec<(ResourceId, u64)>,
}
enum ResolvedResource {
Buffer(Arc<BufferResource>),
Image(Arc<ImageResource>),
}
impl CompiledGraph {
/// Topological batches of commands. Each returned slice references a
/// contiguous range of the full execution order.
pub fn batches(&self) -> impl Iterator<Item = &[CommandIndex]> {
self.batches
.iter()
.map(|batch| &self.execution_order[batch.start..batch.start + batch.count])
}
/// Full recording order, with each pass expanded as begin, interiors in
/// original record order, then end.
pub fn execution_order(&self) -> &[CommandIndex] {
&self.execution_order
}
/// Render-pass spans in original command-recording order.
pub fn render_pass_spans(&self) -> &[RenderPassSpan] {
&self.render_pass_spans
}
pub fn barriers(&self) -> &[PipelineBarrier] {
&self.barriers
}
/// Returns a cached view created for a resolved graph texture.
pub fn created_view(
&self,
texture: ResourceId,
desc: &ImageViewDesc,
) -> Option<&Arc<ImageView>> {
self.views.get(&(texture, desc.clone()))
}
pub fn resource_lifetimes(&self) -> &HashMap<ResourceId, ResourceLifetime> {
&self.lifetimes
}
pub fn interference(&self) -> &ResourceInterference {
&self.interference
}
/// Whether command `a` is transitively ordered before command `b` by
/// resource-hazard edges. Commands in the same render-pass span map to one
/// node and are deliberately unordered for aliasing/interference analysis.
pub fn happens_before(&self, a: CommandIndex, b: CommandIndex) -> bool {
self.happens_before.contains(a.index(), b.index())
}
/// The compile-time transitive closure of resource-hazard ordering.
pub fn happens_before_closure(&self) -> &HappensBefore {
&self.happens_before
}
/// Node-space adjacency over DAG-node representatives. Render-pass edges
/// refer to the node represented by its begin command index.
pub fn dependency_edges(&self) -> &[Vec<usize>] {
&self.dependency_edges
}
/// Per-resource command uses, with stage/access masks ORed across accesses
/// to that resource within each command.
pub fn resource_uses(&self, resource: ResourceId) -> Option<&[ResourceUse]> {
self.uses_by_resource.get(&resource).map(Vec::as_slice)
}
/// Replace placement-specific alias barriers while retaining compile-time ordinary barriers.
fn rebuild_barriers(&mut self) -> Result<(), GraphError> {
let compiler = GraphCompiler::new_with_nodes(
&self.resources,
self.commands.side_effects.command_accesses(),
self.commands.len(),
&self.commands.side_effects,
self.node_of_command.clone(),
self.node_representatives.clone(),
&self.render_pass_spans,
);
self.barriers.truncate(self.ordinary_barrier_count);
compiler.build_alias_barriers(
&mut self.barriers,
&self.lifetimes,
&self.frontiers,
&self.happens_before,
);
hoist_span_barriers(
&mut self.barriers,
&self.render_pass_spans,
self.commands.len(),
);
validate_intra_pass_barriers(&self.barriers, &self.render_pass_spans)?;
self.barrier_indices_by_command =
grouped_barrier_indices(&self.barriers, self.commands.len());
Ok(())
}
/// Tests whether two use sets can safely reuse a placement in the supplied
/// direction. Stage/access masks are retained for alias barriers; ordering
/// is proven from command ids through the dependency closure.
pub fn aliasing_permitted(&self, uses_a: &[ResourceUse], uses_b: &[ResourceUse]) -> bool {
resources_alias_in_order(uses_a, uses_b, &self.happens_before)
}
/// Tests whether two resources can reuse a placement in either safe order.
pub fn resources_can_alias(&self, a: ResourceId, b: ResourceId) -> bool {
let (Some(uses_a), Some(uses_b)) = (self.resource_uses(a), self.resource_uses(b)) else {
return false;
};
self.aliasing_permitted(uses_a, uses_b) || self.aliasing_permitted(uses_b, uses_a)
}
/// Create and bind used transient resources and finalize alias barriers.
///
/// Aliasing safety assumes all graph commands are submitted from one command
/// buffer to one queue. Cross-queue aliasing is unsupported; when
/// per-command queue assignment exists, alias transitions between queues
/// must become semaphore edges as well as the corresponding memory barriers.
pub fn resolve(&mut self, device: &Device) -> Result<(), GraphError> {
if self.plan_placements(device)? {
self.rebuild_barriers()?;
}
self.resolve_created_views()?;
Ok(())
}
fn resolve_created_views(&mut self) -> Result<(), GraphError> {
if !self.has_created_views {
return Ok(());
}
let mut staged = HashMap::new();
let mut staged_bindings = Vec::new();
for command in &self.commands.commands {
let Some(desc) = command.render_pass_desc() else {
continue;
};
let attachments = desc
.color
.iter()
.map(|attachment| (attachment.texture.id(), &attachment.view))
.chain(
desc.depth
.into_iter()
.map(|attachment| (attachment.texture.id(), &attachment.view)),
);
for (texture, view) in attachments {
let AttachmentView::Created(view_desc) = view else {
continue;
};
if !self.lifetimes.contains_key(&texture)
|| self.views.contains_key(&(texture, view_desc.clone()))
|| staged.contains_key(&(texture, view_desc.clone()))
{
continue;
}
let image = match self.resources.get(texture.index()) {
Some(ResourceDesc::ImportedTexture { resource, .. }) => resource,
_ => match self.resolved.get(&texture) {
Some(ResolvedResource::Image(resource)) => resource,
_ => {
return Err(GraphError::ViewCreationFailed(format!(
"created attachment texture {texture:?} is unresolved"
)));
}
},
};
let view = image
.create_view(view_desc.clone())
.map(Arc::new)
.map_err(|error| GraphError::ViewCreationFailed(error.to_string()))?;
if view.image().as_ref() != image.as_ref() {
return Err(GraphError::ViewCreationFailed(format!(
"created view for texture {texture:?} refers to a different image"
)));
}
staged.insert((texture, view_desc.clone()), view.clone());
let mut binding_desc = view_desc.clone();
binding_desc.name = None;
staged_bindings.push((texture, binding_desc, view));
}
}
self.views.extend(staged);
self.created_view_bindings.extend(staged_bindings);
Ok(())
}
/// Stage resource creation and placements locally; publish only after all binds succeed.
fn plan_placements(&mut self, device: &Device) -> Result<bool, GraphError> {
if self.placements_resolved {
let has_unresolved_created_resource =
self.resources.iter().enumerate().any(|(index, desc)| {
matches!(
desc,
ResourceDesc::CreatedBuffer(_) | ResourceDesc::CreatedTexture(_)
) && self
.lifetimes
.contains_key(&ResourceId::from_u32(index as u32))
&& !self
.resolved
.contains_key(&ResourceId::from_u32(index as u32))
});
if !has_unresolved_created_resource {
return Ok(false);
}
}
let command_accesses = self.commands.side_effects.command_accesses();
let mut accesses_by_resource = vec![Vec::new(); self.resources.len()];
for (command, accesses) in command_accesses.iter().enumerate() {
for access in accesses {
if !self
.commands
.side_effects
.is_lifetime_only(command.into(), access.resource)
{
accesses_by_resource[access.resource.index()].push(*access);
}
}
}
let mut staged_resources = Vec::new();
let mut staged_lifetimes = self.lifetimes.clone();
let mut pooled = Vec::new();
// Create only resources with actual graph uses; unused transients have no lifetime.
for (index, desc) in self.resources.iter().enumerate() {
let id = ResourceId::from_u32(index as u32);
if !should_plan_created_resource(id, &staged_lifetimes, &self.resolved) {
continue;
}
let accesses = accesses_by_resource[id.index()].as_slice();
match desc {
ResourceDesc::CreatedBuffer(desc) => {
let desc = BufferDesc {
flags: vk::BufferCreateFlags::empty(),
name: desc.name.clone(),
size: desc.size,
usage: derive_buffer_usage(accesses, desc.extra_usage),
queue_families: actual_queue_families(
device,
derive_queue_capabilities(accesses),
),
mem_location: desc.mem_location,
// Explicit allocation strategy is selected after the requirements query.
alloc_scheme: AllocationStrategy::AllocatorManaged,
};
let resource = BufferResource::new_unallocated(device.clone(), desc.clone())
.map_err(|_| GraphError::PlacementFailed(id))?;
let requirements = resource
.memory_requirements()
.map_err(|_| GraphError::PlacementFailed(id))?;
if !is_poolable(
desc.mem_location,
allocation_strategy_for_size(desc.size, requirements),
requirements,
) {
drop(resource);
let resource = BufferResource::new(
device.clone(),
BufferDesc {
alloc_scheme: allocation_strategy_for_size(desc.size, requirements),
..desc
},
)
.map_err(|_| GraphError::PlacementFailed(id))?;
set_placement(&mut staged_lifetimes, id, requirements, 0, None, false);
staged_resources.push((id, ResolvedResource::Buffer(Arc::new(resource))));
continue;
}
pooled.push(PendingPlacement {
id,
resource: ResolvedResource::Buffer(Arc::new(resource)),
requirements,
class: PoolClass::Linear,
property_flags: memory_property_flags(desc.mem_location),
});
continue;
}
ResourceDesc::CreatedTexture(desc) => {
let image_desc = ImageDesc {
// Candidates start alias-capable; non-poolable candidates are recreated
// without the alias flag below.
flags: vk::ImageCreateFlags::ALIAS,
name: desc.name.clone(),
format: desc.format,
kind: desc.kind,
mip_levels: desc.mip_levels,
array_layers: desc.array_layers,
samples: desc.samples,
extent: desc.extent,
tiling: desc.tiling,
usage: derive_image_usage(accesses, desc.extra_usage),
queue_families: actual_queue_families(
device,
derive_queue_capabilities(accesses),
),
// Query requirements from a Vulkan-valid undefined candidate; pooled
// eligibility requires the graph's declared initial layout be undefined.
layout: vk::ImageLayout::UNDEFINED,
mem_location: desc.mem_location,
alloc_scheme: AllocationStrategy::AllocatorManaged,
};
let candidate = ImageResource::new_unallocated(device.clone(), image_desc)
.map_err(|_| GraphError::PlacementFailed(id))?;
let requirements = candidate
.memory_requirements()
.map_err(|_| GraphError::PlacementFailed(id))?;
if !is_poolable(
desc.mem_location,
allocation_strategy_for(requirements),
requirements,
) {
// TODO: Consider reusing the candidate
drop(candidate);
let resource = ImageResource::new(
device.clone(),
image_desc_for_graph_texture(
desc,
accesses,
actual_queue_families(device, derive_queue_capabilities(accesses)),
allocation_strategy_for_size(
desc.extent.width.into(),
requirements,
),
vk::ImageCreateFlags::empty(),
),
)
.map_err(|_| GraphError::PlacementFailed(id))?;
let requirements = resource
.memory_requirements()
.map_err(|_| GraphError::PlacementFailed(id))?;
set_placement(&mut staged_lifetimes, id, requirements, 0, None, false);
staged_resources.push((id, ResolvedResource::Image(Arc::new(resource))));
continue;
}
validate_pooled_image_layout(id, desc, requirements)?;
pooled.push(PendingPlacement {
id,
resource: ResolvedResource::Image(Arc::new(candidate)),
requirements,
class: pool_class_for_image(desc.tiling),
property_flags: memory_property_flags(desc.mem_location),
});
continue;
}
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => {
continue;
}
}
}
// Add resources to class/type-specific pools with intersection semantics.
let mut pools: Vec<PlacementPool> = Vec::new();
pooled.sort_by_key(|member| member.id);
for member in pooled {
let compatible_pool = pools.iter().position(|pool| {
pool_accepts_member(
pool,
member.class,
member.property_flags,
member.requirements,
)
});
let pool_index = if let Some(pool_index) = compatible_pool {
pool_index
} else {
let memory_type_index = device
.find_memory_type_index(&member.requirements, member.property_flags)
.map_err(|_| GraphError::PlacementFailed(member.id))?;
let id = u32::try_from(pools.len())
.map_err(|_| GraphError::PlacementFailed(member.id))?;
pools.push(PlacementPool {
id,
class: member.class,
memory_type_index,
property_flags: member.property_flags,
size: 0,
members: Vec::new(),
occupied: Vec::new(),
});
pools.len() - 1
};
let offset = first_fit_placement(
member.id,
member.requirements.size,
member.requirements.alignment,
&pools[pool_index].occupied,
&staged_lifetimes,
&self.interference,
&self.frontiers,
&self.happens_before,
)
.ok_or(GraphError::PlacementFailed(member.id))?;
let end = offset
.checked_add(member.requirements.size)
.ok_or(GraphError::PlacementFailed(member.id))?;
let pool = &mut pools[pool_index];
pool.size = pool.size.max(end);
let mut intersects_unsafe_lifetime = false;
let overlapping = pool
.members
.iter()
.filter_map(|(other_id, other_offset)| {
let overlaps = byte_ranges_overlap(
offset,
member.requirements.size,
*other_offset,
staged_lifetimes[other_id].placement.size,
);
if !overlaps {
return None;
}
let is_ordered = !self.interference.interferes(member.id, *other_id)
&& self.frontiers.alias_in_order_either(
member.id,
*other_id,
&self.happens_before,
);
if !is_ordered {
intersects_unsafe_lifetime = true;
}
Some(*other_id)
})
.collect::<Vec<_>>();
if intersects_unsafe_lifetime {
return Err(GraphError::PlacementFailed(member.id));
}
pool.members.push((member.id, offset));
let occupied_index = pool
.occupied
.binary_search_by_key(&(offset, member.id), |(id, offset)| (*offset, *id))
.unwrap_or_else(|index| index);
pool.occupied.insert(occupied_index, (member.id, offset));
for other_id in &overlapping {
if let Some(lifetime) = staged_lifetimes.get_mut(other_id) {
lifetime.placement.aliased = true;
}
}
set_placement(
&mut staged_lifetimes,
member.id,
member.requirements,
offset,
Some(pool.id),
!overlapping.is_empty(),
);
staged_resources.push((member.id, member.resource));
}
// Allocate one shared block per pool, then transfer references to every bound member.
let mut staged_indices = HashMap::with_capacity(staged_resources.len());
for (index, (id, _)) in staged_resources.iter().enumerate() {
staged_indices.insert(*id, index);
}
for pool in pools {
let first_id = pool.members[0].0;
let alignment = pool
.members
.iter()
.try_fold(1_u64, |alignment, (id, _)| {
checked_alignment_lcm(alignment, staged_lifetimes[id].placement.alignment)
})
.ok_or(GraphError::PlacementFailed(first_id))?;
let requirements = renderer_resources::memory::MemoryRequirements {
size: pool.size,
alignment,
memory_type_bits: 1 << pool.memory_type_index,
requires_dedicated_allocation: false,
prefers_dedicated_allocation: false,
};
let allocation = device
.allocate_memory(&requirements, pool.property_flags)
.map_err(|_| GraphError::PlacementFailed(first_id))?;
for (id, _) in &pool.members {
let resource_index = *staged_indices.get(id).expect("every pool member is staged");
let resource = &mut staged_resources[resource_index];
match &mut resource.1 {
ResolvedResource::Buffer(buffer) => Arc::get_mut(buffer)
.expect("new pooled buffer has a unique owner")
.bind_allocation(allocation.clone(), staged_lifetimes[id].placement.offset)
.map_err(|error| GraphError::from((*id, error)))?,
ResolvedResource::Image(image) => Arc::get_mut(image)
.expect("new pooled image has a unique owner")
.bind_allocation(allocation.clone(), staged_lifetimes[id].placement.offset)
.map_err(|error| GraphError::from((*id, error)))?,
}
}
}
// Commit only after every creation, allocation, and bind has succeeded.
let changed = !self.placements_resolved || !staged_resources.is_empty();
for (id, resource) in staged_resources {
self.resolved.insert(id, resource);
}
self.lifetimes = staged_lifetimes;
self.placements_resolved = true;
Ok(changed)
}
/// Record the compiled order into a real Vulkan command buffer. Imported
/// handles are automatically bound. Created resources must first be
/// resolved with a device.
pub fn record_commands(
&mut self,
recorder: &mut CommandRecorder<'_>,
) -> Result<(), RecordError> {
// Validate every wrapper before resource binding or barriers can emit Vulkan commands.
self.commands.preflight()?;
for (index, desc) in self.resources.iter().enumerate() {
let id = ResourceId::from_u32(index as u32);
if !self.lifetimes.contains_key(&id) {
continue;
}
match desc {
ResourceDesc::ImportedBuffer { resource, .. } => {
recorder.buffers.insert(id, resource.raw());
}
ResourceDesc::ImportedTexture { resource, .. } => {
recorder.images.insert(id, resource.raw());
}
ResourceDesc::CreatedBuffer(_) => match self.resolved.get(&id) {
Some(ResolvedResource::Buffer(resource)) => {
recorder.buffers.insert(id, resource.raw());
}
_ => return Err(RecordError::UnresolvedCreatedBuffer(id)),
},
ResourceDesc::CreatedTexture(_) => match self.resolved.get(&id) {
Some(ResolvedResource::Image(resource)) => {
recorder.images.insert(id, resource.raw());
}
_ => return Err(RecordError::UnresolvedCreatedImage(id)),
},
}
}
for (texture, desc, view) in &self.created_view_bindings {
recorder
.views
.insert((*texture, desc.clone()), view.clone());
}
// Batches currently record into one command buffer, which must be
// submitted to one queue for current aliasing safety. TODO: separate
// per-batch command buffers may be submitted sequentially on that queue;
// parallel/cross-queue execution requires command-to-queue assignment
// and semaphore edges for every cross-queue alias transition.
let mut in_render_pass = false;
for step in recording_steps_cached(
&self.execution_order,
&self.barriers,
&self.barrier_indices_by_command,
) {
match step {
// TODO: Merge `PipelineBarrier.resources` across commands or a batch to reduce vkCmdPipelineBarrier2 calls.
RecordingStep::Barrier(barrier) => recorder.pipeline_barrier(barrier),
RecordingStep::Command(command_index) => {
if let Err(error) =
self.commands.commands[command_index.index()].apply(recorder)
{
if std::mem::replace(&mut in_render_pass, false) {
unsafe {
recorder.device.cmd_end_rendering(recorder.command_buffer);
}
}
return Err(error);
}
match self.commands.commands[command_index.index()].kind() {
CommandKind::Begin => in_render_pass = true,
CommandKind::End => in_render_pass = false,
_ => {}
}
}
}
}
Ok(())
}
}
enum RecordingStep<'a> {
Barrier(&'a PipelineBarrier),
Command(CommandIndex),
}
fn recording_steps<'a>(
execution_order: &'a [CommandIndex],
barriers: &'a [PipelineBarrier],
) -> impl Iterator<Item = RecordingStep<'a>> + 'a {
let command_count = execution_order
.iter()
.map(|command| command.index())
.chain(
barriers
.iter()
.map(|barrier| barrier.before_command.index()),
)
.max()
.unwrap_or_default()
+ 1;
let mut barriers_by_command: Vec<Vec<&PipelineBarrier>> =
(0..command_count).map(|_| Vec::new()).collect();
for barrier in barriers {
barriers_by_command[barrier.before_command.index()].push(barrier);
}
execution_order.iter().flat_map(move |&command_index| {
std::mem::take(&mut barriers_by_command[command_index.index()])
.into_iter()
.map(RecordingStep::Barrier)
.chain(std::iter::once(RecordingStep::Command(command_index)))
})
}
fn grouped_barrier_indices(
barriers: &[PipelineBarrier],
command_count: usize,
) -> BarriersByCommand {
let mut barriers_by_command = BarriersByCommand::new(command_count);
for (index, barrier) in barriers.iter().enumerate() {
barriers_by_command.entries[barrier.before_command.index()].push(index);
}
barriers_by_command
}
fn recording_steps_cached<'a>(
execution_order: &'a [CommandIndex],
barriers: &'a [PipelineBarrier],
barrier_indices_by_command: &'a BarriersByCommand,
) -> impl Iterator<Item = RecordingStep<'a>> + 'a {
execution_order.iter().flat_map(move |&command_index| {
barrier_indices_by_command
.get(command_index)
.iter()
.map(move |&index| RecordingStep::Barrier(&barriers[index]))
.chain(std::iter::once(RecordingStep::Command(command_index)))
})
}
/// Very large resources bypass transient pools and use a dedicated allocation.
const DEDICATED_ALLOCATION_THRESHOLD: u64 = 64 * 1024 * 1024;
fn accesses_for_resource(
id: ResourceId,
command_accesses: &[Vec<crate::recorder::ResourceAccess>],
side_effects: &SideEffectMap,
) -> Vec<crate::recorder::ResourceAccess> {
command_accesses
.iter()
.enumerate()
.flat_map(|(command, accesses)| {
accesses.iter().filter_map(move |access| {
(access.resource == id
&& !side_effects.is_lifetime_only(command.into(), access.resource))
.then_some(*access)
})
})
.collect()
}
/// Derive buffer usages from declared access masks. Unknown access bits add no
/// usage; callers can use `extra_usage` for uses outside the graph's knowledge.
pub fn derive_buffer_usage(
accesses: &[crate::recorder::ResourceAccess],
extra_usage: vk::BufferUsageFlags,
) -> vk::BufferUsageFlags {
accesses.iter().fold(extra_usage, |usage, access| {
let mask = access.scope.access;
let mut usage = usage;
if mask.intersects(vk::AccessFlags2::TRANSFER_READ) {
usage |= vk::BufferUsageFlags::TRANSFER_SRC;
}
if mask.intersects(vk::AccessFlags2::TRANSFER_WRITE) {
usage |= vk::BufferUsageFlags::TRANSFER_DST;
}
if mask.intersects(
vk::AccessFlags2::SHADER_READ
| vk::AccessFlags2::SHADER_STORAGE_READ
| vk::AccessFlags2::SHADER_STORAGE_WRITE,
) {
// Shader reads are conservatively treated as storage-buffer reads;
// add UNIFORM_BUFFER explicitly when the shader uses uniform data.
usage |= vk::BufferUsageFlags::STORAGE_BUFFER;
}
if mask.intersects(vk::AccessFlags2::UNIFORM_READ) {
usage |= vk::BufferUsageFlags::UNIFORM_BUFFER;
}
if mask.intersects(vk::AccessFlags2::SHADER_WRITE | vk::AccessFlags2::SHADER_STORAGE_WRITE)
{
usage |= vk::BufferUsageFlags::STORAGE_BUFFER;
}
if mask.intersects(vk::AccessFlags2::VERTEX_ATTRIBUTE_READ) {
usage |= vk::BufferUsageFlags::VERTEX_BUFFER;
}
if mask.intersects(vk::AccessFlags2::INDEX_READ) {
usage |= vk::BufferUsageFlags::INDEX_BUFFER;
}
if mask.intersects(vk::AccessFlags2::INDIRECT_COMMAND_READ) {
usage |= vk::BufferUsageFlags::INDIRECT_BUFFER;
}
usage
})
}
/// Derive image usages from declared access masks. Shader reads require a
/// sampled or storage image descriptor, so both flags are enabled; unknown
/// access bits add no usage and may be supplied through `extra_usage`.
pub fn derive_image_usage(
accesses: &[crate::recorder::ResourceAccess],
extra_usage: vk::ImageUsageFlags,
) -> vk::ImageUsageFlags {
accesses.iter().fold(extra_usage, |usage, access| {
let mask = access.scope.access;
let mut usage = usage;
if mask.intersects(vk::AccessFlags2::TRANSFER_READ) {
usage |= vk::ImageUsageFlags::TRANSFER_SRC;
}
if mask.intersects(vk::AccessFlags2::TRANSFER_WRITE) {
usage |= vk::ImageUsageFlags::TRANSFER_DST;
}
if mask.intersects(
vk::AccessFlags2::SHADER_READ
| vk::AccessFlags2::SHADER_SAMPLED_READ
| vk::AccessFlags2::SHADER_STORAGE_READ,
) {
usage |= vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::STORAGE;
}
if mask.intersects(vk::AccessFlags2::SHADER_WRITE | vk::AccessFlags2::SHADER_STORAGE_WRITE)
{
usage |= vk::ImageUsageFlags::STORAGE;
}
if mask.intersects(vk::AccessFlags2::INPUT_ATTACHMENT_READ) {
usage |= vk::ImageUsageFlags::INPUT_ATTACHMENT;
}
if mask.intersects(
vk::AccessFlags2::COLOR_ATTACHMENT_READ | vk::AccessFlags2::COLOR_ATTACHMENT_WRITE,
) {
usage |= vk::ImageUsageFlags::COLOR_ATTACHMENT;
}
if mask.intersects(
vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ
| vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE,
) {
usage |= vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT;
}
usage
})
}
/// Map pipeline-stage usage to desired queue capabilities. Graphics stages
/// imply GRAPHICS, compute stages imply ASYNC_COMPUTE, and copy/clear/transfer
/// stages imply TRANSFER. NONE and unrecognized stages contribute nothing.
pub fn derive_queue_capabilities(accesses: &[crate::recorder::ResourceAccess]) -> QueueFlags {
accesses
.iter()
.fold(QueueFlags::empty(), |mut flags, access| {
let stages = access.scope.stage;
if stages.intersects(
vk::PipelineStageFlags2::DRAW_INDIRECT
| vk::PipelineStageFlags2::VERTEX_INPUT
| vk::PipelineStageFlags2::VERTEX_SHADER
| vk::PipelineStageFlags2::TESSELLATION_CONTROL_SHADER
| vk::PipelineStageFlags2::TESSELLATION_EVALUATION_SHADER
| vk::PipelineStageFlags2::GEOMETRY_SHADER
| vk::PipelineStageFlags2::FRAGMENT_SHADER
| vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS
| vk::PipelineStageFlags2::LATE_FRAGMENT_TESTS
| vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT
| vk::PipelineStageFlags2::ALL_GRAPHICS
| vk::PipelineStageFlags2::PRE_RASTERIZATION_SHADERS,
) {
flags |= QueueFlags::GRAPHICS;
}
if stages.intersects(vk::PipelineStageFlags2::COMPUTE_SHADER) {
flags |= QueueFlags::ASYNC_COMPUTE;
}
if stages.intersects(
vk::PipelineStageFlags2::TRANSFER
| vk::PipelineStageFlags2::COPY
| vk::PipelineStageFlags2::CLEAR,
) {
flags |= QueueFlags::TRANSFER;
}
flags
})
}
/// Convert inferred capabilities into the device's queue-role selectors,
/// retaining only roles whose selected family actually supports the inferred
/// capability. If no role matches (or nothing was inferred), use graphics.
fn actual_queue_families(device: &Device, capabilities: QueueFlags) -> QueueFlags {
use renderer_device::queue::QueueFlags as FamilyFlags;
let mut selected = QueueFlags::empty();
if capabilities.contains(QueueFlags::GRAPHICS)
&& device
.queues
.graphics()
.family
.flags
.contains(FamilyFlags::GRAPHICS)
{
selected |= QueueFlags::GRAPHICS;
}
if capabilities.contains(QueueFlags::ASYNC_COMPUTE)
&& device
.queues
.compute()
.family
.flags
.contains(FamilyFlags::COMPUTE)
{
selected |= QueueFlags::ASYNC_COMPUTE;
}
if capabilities.contains(QueueFlags::TRANSFER)
&& device
.queues
.transfer()
.family
.flags
.contains(FamilyFlags::TRANSFER)
{
selected |= QueueFlags::TRANSFER;
}
if selected.is_empty() {
QueueFlags::GRAPHICS
} else {
selected
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
commands::{
AttachmentView, BeginRendering, ClearTexture, ColorAttachment, CopyBuffer, Draw,
DrawArgs, EndRendering,
},
resources::{
BufferRange, BufferSlice, DescriptorResourceIdentity, GraphBufferDesc,
GraphTextureDesc, IndexBinding, IndexFormat, IndirectBinding, ResourceDesc,
ResourceState, VertexBinding,
},
};
use ash::vk::Handle;
use renderer_resources::{
buffers::Buffer as BufferResource,
descriptors::{DescriptorResourceBinding, TrackedDescriptorWrite},
images::{Image as ImageResource, ImageDesc, ImageView},
pipeline::{DescriptorSet, DescriptorSetLayoutBindingDesc, Sampler},
};
fn ordering_from_accesses(
resources: &[ResourceDesc],
accesses: &[Vec<crate::recorder::ResourceAccess>],
) -> CommandOrderings {
let mut commands = CommandList::new();
for command_accesses in accesses {
let command = AllowedAccess(
command_accesses
.iter()
.map(|access| (access.resource, access.scope))
.collect(),
);
commands.push(command);
}
build_command_orderings(resources, &commands.commands, &mut commands.side_effects).unwrap()
}
fn topo_batches_for_test(
compiler: &GraphCompiler<'_>,
) -> Result<Vec<Vec<CommandIndex>>, GraphError> {
let orderings = ordering_from_accesses(compiler.resources, compiler.accesses);
Ok(orderings
.batches
.iter()
.map(|batch| orderings.execution_order[batch.start..batch.start + batch.count].to_vec())
.collect())
}
static RECORDING_MARKER_APPLIED: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
struct RecordingMarker(Buffer);
impl crate::commands::Command for RecordingMarker {
fn side_effects(
&self,
command_index: CommandIndex,
map: &mut crate::recorder::SideEffectMap,
) {
map.insert(
command_index,
self.0.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::TRANSFER,
access: vk::AccessFlags2::TRANSFER_READ,
subresource: SubresourceScopeInner::Buffer(BufferRange {
offset: 0,
size: 32,
}),
},
);
}
}
impl crate::recorder::RecordedCommand for RecordingMarker {
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
RECORDING_MARKER_APPLIED.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(())
}
}
struct AllowedAccess(Vec<(ResourceId, SubresourceScope)>);
impl crate::commands::Command for AllowedAccess {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
for &(resource, scope) in &self.0 {
map.insert(command_index, resource, scope);
}
}
}
impl crate::recorder::RecordedCommand for AllowedAccess {
fn allowed_in_render_pass(&self) -> bool {
true
}
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
Ok(())
}
}
fn test_buffer(handle: u64, size: u64) -> Arc<BufferResource> {
Arc::new(BufferResource::for_testing(
vk::Buffer::from_raw(handle),
size,
))
}
fn test_image(handle: u64) -> Arc<ImageResource> {
Arc::new(ImageResource::for_testing(
vk::Image::from_raw(handle),
ImageDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 16,
height: 16,
depth: 1,
},
usage: vk::ImageUsageFlags::SAMPLED,
..Default::default()
},
))
}
fn empty_render_pass() -> BeginRendering {
BeginRendering {
color: Vec::new(),
depth: None,
}
}
fn created_color_attachment(texture: Texture, layout: vk::ImageLayout) -> ColorAttachment {
ColorAttachment {
texture,
view: AttachmentView::Created(renderer_resources::images::ImageViewDesc::color_2d()),
layout,
load_op: vk::AttachmentLoadOp::DONT_CARE,
store_op: vk::AttachmentStoreOp::STORE,
clear_value: vk::ClearValue::default(),
range: texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
}
}
fn test_view(image: Arc<ImageResource>) -> Arc<ImageView> {
Arc::new(ImageView::for_testing(
vk::ImageView::from_raw(1),
image,
texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
))
}
fn layout_binding(binding: u32, kind: vk::DescriptorType) -> DescriptorSetLayoutBindingDesc {
DescriptorSetLayoutBindingDesc {
binding,
count: 1,
kind,
stage: vk::ShaderStageFlags::FRAGMENT,
flags: None,
}
}
fn test_set(
handle: u64,
layout: Vec<DescriptorSetLayoutBindingDesc>,
bindings: Vec<TrackedDescriptorWrite>,
) -> Arc<DescriptorSet> {
Arc::new(DescriptorSet::for_testing(
vk::DescriptorSet::from_raw(handle),
layout,
0,
0,
bindings,
))
}
#[test]
fn pool_classes_separate_linear_buffers_from_optimal_images() {
let pool = PlacementPool {
id: 0,
class: PoolClass::Linear,
memory_type_index: 0,
property_flags: vk::MemoryPropertyFlags::DEVICE_LOCAL,
size: 64,
members: Vec::new(),
occupied: Vec::new(),
};
let requirements = renderer_resources::memory::MemoryRequirements {
size: 16,
alignment: 8,
memory_type_bits: 1,
requires_dedicated_allocation: false,
prefers_dedicated_allocation: false,
};
assert_eq!(
pool_class_for_image(vk::ImageTiling::LINEAR),
PoolClass::Linear
);
assert_eq!(
pool_class_for_image(vk::ImageTiling::OPTIMAL),
PoolClass::Optimal
);
assert_ne!(PoolClass::Linear, PoolClass::Optimal);
assert!(!pool_accepts_member(
&pool,
pool_class_for_image(vk::ImageTiling::OPTIMAL),
vk::MemoryPropertyFlags::DEVICE_LOCAL,
requirements,
));
}
#[test]
fn pool_groups_require_a_common_memory_type_for_every_member() {
let requirements = |bits| renderer_resources::memory::MemoryRequirements {
size: 16,
alignment: 8,
memory_type_bits: bits,
requires_dedicated_allocation: false,
prefers_dedicated_allocation: false,
};
let pool = PlacementPool {
id: 0,
class: PoolClass::Linear,
memory_type_index: 1,
property_flags: vk::MemoryPropertyFlags::DEVICE_LOCAL,
size: 16,
members: vec![(ResourceId::from_u32(0), 0)],
occupied: vec![(ResourceId::from_u32(0), 0)],
};
assert!(pool_accepts_member(
&pool,
PoolClass::Linear,
vk::MemoryPropertyFlags::DEVICE_LOCAL,
requirements(0b0010),
));
assert!(!pool_accepts_member(
&pool,
PoolClass::Linear,
vk::MemoryPropertyFlags::DEVICE_LOCAL,
requirements(0b0001),
));
}
#[test]
fn first_fit_checks_every_occupied_range_and_finds_earliest_gap() {
let id = ResourceId::from_u32(2);
let occupied = [(ResourceId::from_u32(0), 0), (ResourceId::from_u32(1), 16)];
let lifetimes = HashMap::from([
(ResourceId::from_u32(0), placed_lifetime(0, 32, false)),
(ResourceId::from_u32(1), placed_lifetime(16, 32, false)),
(id, placed_lifetime(0, 0, false)),
]);
let uses = HashMap::from([
(
ResourceId::from_u32(0),
vec![ResourceUse {
command: CommandIndex(0),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}],
),
(
ResourceId::from_u32(1),
vec![ResourceUse {
command: CommandIndex(1),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}],
),
(
id,
vec![ResourceUse {
command: CommandIndex(2),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}],
),
]);
let closure = HappensBefore::from_command_dag(
&[vec![2], vec![2], vec![]],
&[
vec![CommandIndex(0), CommandIndex(1)],
vec![CommandIndex(2)],
],
);
let mut interference = ResourceInterference {
edges: vec![BTreeSet::new(); 3],
};
for old in [ResourceId::from_u32(0), ResourceId::from_u32(1)] {
interference.edges[id.index()].insert(old);
interference.edges[old.index()].insert(id);
}
assert_eq!(
first_fit_placement(
id,
32,
16,
&occupied,
&lifetimes,
&interference,
&ResourceFrontiers::from_uses(&uses, &closure),
&closure,
),
Some(48)
);
}
#[test]
fn first_fit_advances_past_all_unsafe_intersecting_ranges() {
let candidate = ResourceId::from_u32(2);
let occupied = [(ResourceId::from_u32(0), 0), (ResourceId::from_u32(1), 16)];
let lifetimes = HashMap::from([
(ResourceId::from_u32(0), placed_lifetime(0, 32, false)),
(ResourceId::from_u32(1), placed_lifetime(16, 32, false)),
(candidate, placed_lifetime(0, 0, false)),
]);
let uses = HashMap::from([
(
ResourceId::from_u32(0),
vec![ResourceUse {
command: CommandIndex(0),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}],
),
(
ResourceId::from_u32(1),
vec![ResourceUse {
command: CommandIndex(1),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}],
),
(
candidate,
vec![ResourceUse {
command: CommandIndex(2),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}],
),
]);
let closure = HappensBefore::from_command_dag(
&[vec![], vec![], vec![]],
&[vec![CommandIndex(0), CommandIndex(1), CommandIndex(2)]],
);
let mut interference = ResourceInterference {
edges: vec![BTreeSet::new(); 3],
};
for old in [ResourceId::from_u32(0), ResourceId::from_u32(1)] {
interference.edges[candidate.index()].insert(old);
interference.edges[old.index()].insert(candidate);
}
assert_eq!(
first_fit_placement(
candidate,
16,
16,
&occupied,
&lifetimes,
&interference,
&ResourceFrontiers::from_uses(&uses, &closure),
&closure,
),
Some(48)
);
}
#[test]
fn dedicated_and_host_visible_resources_are_not_poolable() {
let requirements = renderer_resources::memory::MemoryRequirements {
size: 128,
alignment: 16,
memory_type_bits: 1,
requires_dedicated_allocation: false,
prefers_dedicated_allocation: false,
};
assert!(!is_poolable(
MemoryLocation::GpuOnly,
AllocationStrategy::Dedicated,
requirements,
));
assert!(!is_poolable(
MemoryLocation::CpuToGpu,
AllocationStrategy::AllocatorManaged,
requirements,
));
assert!(!is_poolable(
MemoryLocation::GpuToCpu,
AllocationStrategy::AllocatorManaged,
requirements,
));
assert!(!is_poolable(
MemoryLocation::GpuOnly,
AllocationStrategy::AllocatorManaged,
renderer_resources::memory::MemoryRequirements {
requires_dedicated_allocation: true,
..requirements
},
));
assert!(is_poolable(
MemoryLocation::GpuOnly,
AllocationStrategy::AllocatorManaged,
requirements,
));
}
#[test]
fn allocator_managed_strategy_remains_poolable_until_requirements_select_dedicated() {
let requirements = renderer_resources::memory::MemoryRequirements {
size: 128,
alignment: 16,
memory_type_bits: 1,
requires_dedicated_allocation: false,
prefers_dedicated_allocation: false,
};
assert_eq!(
allocation_strategy_for(requirements),
AllocationStrategy::AllocatorManaged
);
assert_eq!(
allocation_strategy_for(renderer_resources::memory::MemoryRequirements {
requires_dedicated_allocation: true,
..requirements
}),
AllocationStrategy::Dedicated
);
assert_eq!(
allocation_strategy_for(renderer_resources::memory::MemoryRequirements {
size: DEDICATED_ALLOCATION_THRESHOLD,
..requirements
}),
AllocationStrategy::Dedicated
);
}
#[test]
fn unused_transients_have_no_lifetime_and_are_not_candidates() {
let unused = ResourceId::from_u32(0);
let used = ResourceId::from_u32(1);
let resources = [
ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 1024,
..Default::default()
}),
ResourceDesc::CreatedBuffer(GraphBufferDesc::default()),
];
let accesses = vec![vec![resource_access_for(used)]];
let orderings = ordering_from_accesses(&resources, &accesses);
let batches = orderings
.batches
.iter()
.map(|batch| orderings.execution_order[batch.start..batch.start + batch.count].to_vec())
.collect::<Vec<_>>();
let closure = HappensBefore::from_command_dag(&[vec![]], &batches);
let compiler = GraphCompiler::new_with_nodes(
&resources,
&accesses,
1,
&SideEffectMap::default(),
orderings.node_of_command,
orderings.node_representatives,
&orderings.spans,
);
let uses = compiler.resource_uses();
let frontiers = ResourceFrontiers::from_uses(&uses, &closure);
let uses_by_resource = compiler.resource_uses();
let (lifetimes, _) = compiler.bind_lifetimes_for_test(
&batches,
&frontiers,
&closure,
&uses_by_resource,
&[],
);
assert!(!lifetimes.contains_key(&unused));
assert!(lifetimes.contains_key(&used));
assert!(!should_plan_created_resource(
unused,
&lifetimes,
&HashMap::new(),
));
assert!(should_plan_created_resource(
used,
&lifetimes,
&HashMap::new(),
));
}
#[test]
fn pooled_image_layout_validation_rejects_non_undefined_but_allows_dedicated() {
let id = ResourceId::from_u32(7);
let desc = GraphTextureDesc {
layout: vk::ImageLayout::GENERAL,
..Default::default()
};
let requirements = renderer_resources::memory::MemoryRequirements {
size: 128,
alignment: 16,
memory_type_bits: 1,
requires_dedicated_allocation: false,
prefers_dedicated_allocation: false,
};
assert_eq!(
validate_pooled_image_layout(id, &desc, requirements),
Err(GraphError::InvalidPooledImageLayout(id))
);
validate_pooled_image_layout(
id,
&desc,
renderer_resources::memory::MemoryRequirements {
size: DEDICATED_ALLOCATION_THRESHOLD,
..requirements
},
)
.expect("non-pooled images retain the existing initial-layout behavior");
}
fn buffer_entry(
binding: u32,
kind: vk::DescriptorType,
buffer: Arc<BufferResource>,
offset: u64,
range: u64,
) -> TrackedDescriptorWrite {
TrackedDescriptorWrite {
binding,
array_element: 0,
descriptor_type: kind,
stages: vk::ShaderStageFlags::FRAGMENT,
resource: DescriptorResourceBinding::Buffer {
buffer,
offset,
range,
dynamic: matches!(
kind,
vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC
| vk::DescriptorType::STORAGE_BUFFER_DYNAMIC
),
},
}
}
fn image_entry(
binding: u32,
kind: vk::DescriptorType,
view: Arc<ImageView>,
layout: vk::ImageLayout,
) -> TrackedDescriptorWrite {
TrackedDescriptorWrite {
binding,
array_element: 0,
descriptor_type: kind,
stages: vk::ShaderStageFlags::FRAGMENT,
resource: DescriptorResourceBinding::Image {
range: view.subresource_range(),
view,
sampler: None,
layout,
},
}
}
fn import_buffer_state(
buffer: &Arc<BufferResource>,
state: ResourceState,
) -> (DescriptorResourceIdentity, ResourceState) {
(DescriptorResourceIdentity::Buffer(buffer.clone()), state)
}
fn import_texture_state(
image: &Arc<ImageResource>,
state: ResourceState,
) -> (DescriptorResourceIdentity, ResourceState) {
(DescriptorResourceIdentity::Texture(image.clone()), state)
}
#[test]
fn managed_descriptor_import_transitively_retains_buffer_and_image_owners() {
let buffer = test_buffer(101, 128);
let image = test_image(201);
let view = test_view(image.clone());
let buffer_weak = Arc::downgrade(&buffer);
let image_weak = Arc::downgrade(&image);
let view_weak = Arc::downgrade(&view);
let set = test_set(
1,
vec![
layout_binding(0, vk::DescriptorType::UNIFORM_BUFFER),
layout_binding(1, vk::DescriptorType::SAMPLED_IMAGE),
],
vec![
buffer_entry(0, vk::DescriptorType::UNIFORM_BUFFER, buffer.clone(), 8, 32),
image_entry(
1,
vk::DescriptorType::SAMPLED_IMAGE,
view.clone(),
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
),
],
);
drop(buffer);
drop(image);
drop(view);
let buffer = buffer_weak.upgrade().unwrap();
let image = image_weak.upgrade().unwrap();
let state = ResourceState::default();
let mut graph = RenderGraph::new();
let imported = graph
.import_descriptor_set(
set.clone(),
&[
import_buffer_state(&buffer, state),
import_texture_state(&image, state),
],
&[],
)
.unwrap();
assert!(buffer_weak.strong_count() >= 2);
assert!(image_weak.strong_count() >= 2);
assert!(view_weak.strong_count() >= 1);
assert_eq!(graph.resources.len(), 2);
assert_eq!(imported.accesses.len(), 2);
assert!(graph.resources.iter().any(|desc| matches!(
desc,
ResourceDesc::ImportedBuffer { resource, .. } if Arc::ptr_eq(resource, &buffer)
)));
assert!(graph.resources.iter().any(|desc| matches!(
desc,
ResourceDesc::ImportedTexture { resource, .. } if Arc::ptr_eq(resource, &image)
)));
}
#[test]
fn managed_descriptor_import_deduplicates_buffer_in_both_import_orders() {
let state = ResourceState::default();
let buffer = test_buffer(102, 64);
let set = test_set(
2,
vec![layout_binding(0, vk::DescriptorType::UNIFORM_BUFFER)],
vec![buffer_entry(
0,
vk::DescriptorType::UNIFORM_BUFFER,
buffer.clone(),
0,
32,
)],
);
let mut graph = RenderGraph::new();
let direct = graph.import_buffer(buffer.clone(), state).unwrap();
let imported = graph
.import_descriptor_set(set, &[import_buffer_state(&buffer, state)], &[])
.unwrap();
let DescriptorResourceAccess::Buffer {
buffer: transitive, ..
} = imported.accesses[0]
else {
panic!("buffer descriptor import produces a buffer scope");
};
assert_eq!(direct, transitive);
assert_eq!(graph.resources.len(), 1);
let buffer = test_buffer(103, 64);
let set = test_set(
3,
vec![layout_binding(0, vk::DescriptorType::UNIFORM_BUFFER)],
vec![buffer_entry(
0,
vk::DescriptorType::UNIFORM_BUFFER,
buffer.clone(),
0,
32,
)],
);
let mut graph = RenderGraph::new();
let imported = graph
.import_descriptor_set(set, &[import_buffer_state(&buffer, state)], &[])
.unwrap();
let DescriptorResourceAccess::Buffer {
buffer: transitive, ..
} = imported.accesses[0]
else {
panic!("buffer descriptor import produces a buffer scope");
};
let direct = graph.import_buffer(buffer, state).unwrap();
assert_eq!(direct, transitive);
assert_eq!(graph.resources.len(), 1);
}
#[test]
fn managed_descriptor_import_deduplicates_texture_in_both_import_orders() {
let state = ResourceState::default();
let image = test_image(202);
let view = test_view(image.clone());
let set = test_set(
4,
vec![layout_binding(0, vk::DescriptorType::SAMPLED_IMAGE)],
vec![image_entry(
0,
vk::DescriptorType::SAMPLED_IMAGE,
view,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
)],
);
let mut graph = RenderGraph::new();
let direct = graph.import_texture(image.clone(), state).unwrap();
let imported = graph
.import_descriptor_set(set, &[import_texture_state(&image, state)], &[])
.unwrap();
let DescriptorResourceAccess::Texture {
texture: transitive,
..
} = imported.accesses[0]
else {
panic!("image descriptor import produces a texture scope");
};
assert_eq!(direct, transitive);
assert_eq!(graph.resources.len(), 1);
let image = test_image(203);
let view = test_view(image.clone());
let set = test_set(
5,
vec![layout_binding(0, vk::DescriptorType::SAMPLED_IMAGE)],
vec![image_entry(
0,
vk::DescriptorType::SAMPLED_IMAGE,
view,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
)],
);
let mut graph = RenderGraph::new();
let imported = graph
.import_descriptor_set(set, &[import_texture_state(&image, state)], &[])
.unwrap();
let DescriptorResourceAccess::Texture {
texture: transitive,
..
} = imported.accesses[0]
else {
panic!("image descriptor import produces a texture scope");
};
let direct = graph.import_texture(image, state).unwrap();
assert_eq!(direct, transitive);
assert_eq!(graph.resources.len(), 1);
}
#[test]
fn descriptor_import_rejects_conflicting_initial_state_in_both_orders() {
let buffer_state = ResourceState::default();
let conflicting = ResourceState {
stage: vk::PipelineStageFlags2::HOST,
access: vk::AccessFlags2::HOST_WRITE,
layout: None,
};
let buffer = test_buffer(104, 64);
let set = test_set(
6,
vec![layout_binding(0, vk::DescriptorType::UNIFORM_BUFFER)],
vec![buffer_entry(
0,
vk::DescriptorType::UNIFORM_BUFFER,
buffer.clone(),
0,
32,
)],
);
let mut graph = RenderGraph::new();
let direct = graph.import_buffer(buffer.clone(), buffer_state).unwrap();
assert!(matches!(
graph.import_descriptor_set(
set,
&[import_buffer_state(&buffer, conflicting)],
&[],
),
Err(GraphError::ConflictingInitialState(id)) if id == direct.id()
));
let buffer = test_buffer(105, 64);
let set = test_set(
7,
vec![layout_binding(0, vk::DescriptorType::UNIFORM_BUFFER)],
vec![buffer_entry(
0,
vk::DescriptorType::UNIFORM_BUFFER,
buffer.clone(),
0,
32,
)],
);
let mut graph = RenderGraph::new();
let imported = graph
.import_descriptor_set(set, &[import_buffer_state(&buffer, buffer_state)], &[])
.unwrap();
let DescriptorResourceAccess::Buffer { buffer: id, .. } = imported.accesses[0] else {
panic!("buffer descriptor import produces a buffer scope");
};
assert_eq!(
graph.import_buffer(buffer, conflicting),
Err(GraphError::ConflictingInitialState(id.id()))
);
}
#[test]
fn stale_managed_descriptor_set_is_rejected_at_import() {
let buffer = test_buffer(106, 64);
let set = Arc::new(DescriptorSet::for_testing(
vk::DescriptorSet::from_raw(8),
vec![layout_binding(0, vk::DescriptorType::UNIFORM_BUFFER)],
3,
4,
vec![buffer_entry(
0,
vk::DescriptorType::UNIFORM_BUFFER,
buffer.clone(),
0,
32,
)],
));
let mut graph = RenderGraph::new();
assert!(matches!(
graph.import_descriptor_set(
set,
&[import_buffer_state(&buffer, ResourceState::default())],
&[],
),
Err(GraphError::StaleDescriptorSet)
));
}
#[test]
fn recording_preflight_rejects_stale_draw_before_graph_recording_steps() {
let buffer = test_buffer(109, 64);
let state = ResourceState {
stage: vk::PipelineStageFlags2::HOST,
access: vk::AccessFlags2::HOST_WRITE,
layout: None,
};
let mut graph = RenderGraph::new();
let imported = graph.import_buffer(buffer.clone(), state).unwrap();
let stale_set = Arc::new(DescriptorSet::for_testing(
vk::DescriptorSet::from_raw(9),
vec![layout_binding(0, vk::DescriptorType::UNIFORM_BUFFER)],
1,
2,
vec![buffer_entry(
0,
vk::DescriptorType::UNIFORM_BUFFER,
buffer,
0,
32,
)],
));
graph.push(RecordingMarker(imported));
graph.push(BeginRendering {
color: Vec::new(),
depth: None,
});
graph.push(Draw {
args: DrawArgs::Direct {
vertex_count: 3,
instance_count: 1,
first_vertex: 0,
first_instance: 0,
},
pipeline_layout: vk::PipelineLayout::null(),
vertices: Vec::new(),
index: None,
indirect: None,
descriptor_sets: vec![GraphDescriptorSet {
raw: vk::DescriptorSet::from_raw(9),
accesses: Vec::new(),
managed: Some(stale_set),
dynamic_offsets: Vec::new(),
}],
push_constants: Vec::new(),
});
graph.push(EndRendering);
let mut compiled = graph.compile().unwrap();
assert!(compiled.resource_lifetimes()[&imported.id()].imported);
assert!(compiled.barriers().iter().any(|barrier| {
barrier.before_command == CommandIndex(0) && !barrier.resources.is_empty()
}));
static BARRIER_CALLS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
unsafe extern "system" fn count_barrier(
_device: vk::Device,
_command_buffer: vk::CommandBuffer,
_dependency: *const vk::DependencyInfo<'_>,
) {
BARRIER_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
let device = unsafe {
ash::Device::load_with(
|name| {
if name.to_bytes() == b"vkCmdPipelineBarrier2" {
count_barrier as *const () as *const std::ffi::c_void
} else {
std::ptr::null()
}
},
vk::Device::null(),
)
};
RECORDING_MARKER_APPLIED.store(0, std::sync::atomic::Ordering::SeqCst);
BARRIER_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
let mut recorder = CommandRecorder::new(&device, vk::CommandBuffer::null());
assert_eq!(
compiled.record_commands(&mut recorder),
Err(RecordError::StaleDescriptorSet)
);
assert_eq!(BARRIER_CALLS.load(std::sync::atomic::Ordering::SeqCst), 0);
assert_eq!(
RECORDING_MARKER_APPLIED.load(std::sync::atomic::Ordering::SeqCst),
0
);
}
#[test]
fn failed_descriptor_import_does_not_append_earlier_valid_resources() {
let buffer = test_buffer(110, 64);
let set = test_set(
16,
vec![
layout_binding(0, vk::DescriptorType::UNIFORM_BUFFER),
layout_binding(1, vk::DescriptorType::INPUT_ATTACHMENT),
],
vec![
buffer_entry(0, vk::DescriptorType::UNIFORM_BUFFER, buffer.clone(), 0, 32),
TrackedDescriptorWrite {
binding: 1,
array_element: 0,
descriptor_type: vk::DescriptorType::INPUT_ATTACHMENT,
stages: vk::ShaderStageFlags::FRAGMENT,
resource: DescriptorResourceBinding::Sampler {
sampler: Arc::new(Sampler::for_testing(vk::Sampler::from_raw(3))),
},
},
],
);
let mut graph = RenderGraph::new();
assert!(matches!(
graph.import_descriptor_set(
set,
&[import_buffer_state(&buffer, ResourceState::default())],
&[],
),
Err(GraphError::DescriptorImport(_))
));
assert!(graph.resources.is_empty());
assert_eq!(graph.next_id, 0);
assert!(graph.imported_buffers_by_ptr.is_empty());
assert!(graph.imported_buffers_by_raw.is_empty());
}
#[test]
fn failed_import_transaction_restores_exact_raw_identity_mapping() {
let first = test_buffer(111, 64);
let second = test_buffer(111, 64);
assert!(!Arc::ptr_eq(&first, &second));
let state = ResourceState::default();
let mut graph = RenderGraph::new();
let first_id = graph.import_buffer(first.clone(), state).unwrap().id();
// Model an existing graph state with two pointer identities sharing a raw handle; the
// raw-key choice is deliberately not the resource-order reconstruction's last entry.
let second_id = graph.add_resource(ResourceDesc::ImportedBuffer {
resource: second.clone(),
initial: state,
});
graph
.imported_buffers_by_ptr
.insert(Arc::as_ptr(&second) as usize, second_id);
graph.imported_buffers_by_raw.insert(first.raw(), first_id);
let ptr_before = graph.imported_buffers_by_ptr.clone();
let raw_before = graph.imported_buffers_by_raw.clone();
let resources_before = graph.resources.len();
let failed = graph.import_transaction::<()>(|graph| {
// Pointer identity selects the second resource and updates its shared raw-key entry;
// the subsequent import error must restore the prior raw-key choice exactly.
graph.import_buffer(second.clone(), state)?;
Err(GraphError::ConflictingDescriptorInitialStates)
});
assert!(matches!(
failed,
Err(GraphError::ConflictingDescriptorInitialStates)
));
assert_eq!(graph.resources.len(), resources_before);
assert_eq!(graph.imported_buffers_by_ptr, ptr_before);
assert_eq!(graph.imported_buffers_by_raw, raw_before);
assert_eq!(
graph.imported_buffers_by_raw.get(&first.raw()),
Some(&first_id)
);
// Pointer identity still selects the second resource, while a fresh Arc using the raw
// handle follows the exact pre-transaction raw index.
let fresh = test_buffer(111, 64);
assert_eq!(graph.import_buffer(fresh, state).unwrap().id(), first_id);
assert_eq!(graph.import_buffer(second, state).unwrap().id(), second_id);
}
#[test]
fn descriptor_bound_uniform_copy_orders_before_draw_and_infers_usage() {
let buffer = test_buffer(107, 64);
let set = test_set(
9,
vec![layout_binding(0, vk::DescriptorType::UNIFORM_BUFFER)],
vec![buffer_entry(
0,
vk::DescriptorType::UNIFORM_BUFFER,
buffer.clone(),
0,
32,
)],
);
let state = ResourceState::default();
let mut graph = RenderGraph::new();
let destination = graph
.import_descriptor_set(set.clone(), &[import_buffer_state(&buffer, state)], &[])
.unwrap();
let DescriptorResourceAccess::Buffer {
buffer: uniform, ..
} = destination.accesses[0]
else {
panic!("uniform descriptor maps to buffer access");
};
let source = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let range = BufferRange {
offset: 0,
size: 32,
};
graph.push(CopyBuffer {
src: source,
src_range: range,
dst: uniform,
dst_range: range,
});
graph.push(BeginRendering {
color: Vec::new(),
depth: None,
});
graph.push(Draw {
args: DrawArgs::Direct {
vertex_count: 3,
instance_count: 1,
first_vertex: 0,
first_instance: 0,
},
pipeline_layout: vk::PipelineLayout::null(),
vertices: Vec::new(),
index: None,
indirect: None,
descriptor_sets: vec![destination],
push_constants: Vec::new(),
});
graph.push(EndRendering);
let accesses = graph.commands.side_effects.command_accesses();
assert!(
derive_buffer_usage(
&accesses_for_resource(uniform.id(), accesses, &graph.commands.side_effects),
vk::BufferUsageFlags::empty()
)
.contains(vk::BufferUsageFlags::UNIFORM_BUFFER)
);
let compiled = graph.compile().unwrap();
let batches = compiled.batches().collect::<Vec<_>>();
assert_eq!(
batches,
[
&[CommandIndex(0)][..],
&[CommandIndex(1), CommandIndex(2), CommandIndex(3)][..],
]
);
let barrier = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(1))
.unwrap();
assert!(barrier.resources.iter().any(|resource| {
resource.id == uniform.id()
&& resource.dst_access == vk::AccessFlags2::UNIFORM_READ
&& resource.dst_stage == vk::PipelineStageFlags2::FRAGMENT_SHADER
}));
}
#[test]
fn sampled_image_import_sets_usage_and_layout_barrier() {
let image = test_image(204);
let view = test_view(image.clone());
let set = test_set(
10,
vec![layout_binding(0, vk::DescriptorType::SAMPLED_IMAGE)],
vec![image_entry(
0,
vk::DescriptorType::SAMPLED_IMAGE,
view,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
)],
);
let initial = ResourceState {
stage: vk::PipelineStageFlags2::TRANSFER,
access: vk::AccessFlags2::TRANSFER_WRITE,
layout: Some(vk::ImageLayout::GENERAL),
};
let mut graph = RenderGraph::new();
let descriptor_set = graph
.import_descriptor_set(set, &[import_texture_state(&image, initial)], &[])
.unwrap();
let DescriptorResourceAccess::Texture { texture, .. } = descriptor_set.accesses[0] else {
panic!("sampled image maps to texture access");
};
graph.push(BeginRendering {
color: Vec::new(),
depth: None,
});
graph.push(Draw {
args: DrawArgs::Direct {
vertex_count: 3,
instance_count: 1,
first_vertex: 0,
first_instance: 0,
},
pipeline_layout: vk::PipelineLayout::null(),
vertices: Vec::new(),
index: None,
indirect: None,
descriptor_sets: vec![descriptor_set],
push_constants: Vec::new(),
});
graph.push(EndRendering);
let accesses = graph.commands.side_effects.command_accesses();
assert!(
derive_image_usage(
&accesses_for_resource(texture.id(), accesses, &graph.commands.side_effects),
vk::ImageUsageFlags::empty()
)
.contains(vk::ImageUsageFlags::SAMPLED)
);
let compiled = graph.compile().unwrap();
let barrier = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(0))
.expect("initial sampled-image transition is hoisted before BeginRendering");
assert_eq!(barrier.resources.len(), 1);
let barrier = &barrier.resources[0];
assert_eq!(barrier.old_layout, Some(vk::ImageLayout::GENERAL));
assert_eq!(
barrier.new_layout,
Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
);
assert_eq!(barrier.dst_access, vk::AccessFlags2::SHADER_SAMPLED_READ);
}
#[test]
fn sampler_only_managed_binding_has_no_graph_resource_or_memory_access() {
let sampler = Arc::new(Sampler::for_testing(vk::Sampler::from_raw(1)));
let set = test_set(
11,
vec![layout_binding(0, vk::DescriptorType::SAMPLER)],
vec![TrackedDescriptorWrite {
binding: 0,
array_element: 0,
descriptor_type: vk::DescriptorType::SAMPLER,
stages: vk::ShaderStageFlags::FRAGMENT,
resource: DescriptorResourceBinding::Sampler { sampler },
}],
);
let mut graph = RenderGraph::new();
let imported = graph.import_descriptor_set(set, &[], &[]).unwrap();
assert!(imported.accesses.is_empty());
assert!(graph.resources.is_empty());
}
#[test]
fn dynamic_offsets_across_sets_follow_binding_order_and_validate_effective_ranges() {
let buffer = test_buffer(108, 256);
let first = test_set(
12,
vec![
DescriptorSetLayoutBindingDesc {
binding: 8,
count: 1,
..layout_binding(8, vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
},
DescriptorSetLayoutBindingDesc {
binding: 2,
count: 1,
..layout_binding(2, vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
},
],
vec![
buffer_entry(
2,
vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC,
buffer.clone(),
16,
32,
),
buffer_entry(
8,
vk::DescriptorType::STORAGE_BUFFER_DYNAMIC,
buffer.clone(),
64,
32,
),
],
);
let second = test_set(
13,
vec![layout_binding(
1,
vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC,
)],
vec![buffer_entry(
1,
vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC,
buffer.clone(),
128,
32,
)],
);
let state = ResourceState::default();
let mut graph = RenderGraph::new();
let initial = [import_buffer_state(&buffer, state)];
let first = graph
.import_descriptor_set(first, &initial, &[8, 16])
.unwrap();
let second = graph
.import_descriptor_set(second, &initial, &[24])
.unwrap();
assert_eq!(first.dynamic_offsets(), &[8, 16]);
assert_eq!(second.dynamic_offsets(), &[24]);
assert_eq!(
first
.dynamic_offsets()
.iter()
.chain(second.dynamic_offsets())
.copied()
.collect::<Vec<_>>(),
[8, 16, 24]
);
let ranges = first
.accesses
.iter()
.chain(&second.accesses)
.map(|access| match access {
DescriptorResourceAccess::Buffer { range, .. } => *range,
_ => panic!("dynamic buffer descriptor maps to buffer range"),
})
.collect::<Vec<_>>();
assert_eq!(
ranges,
[
BufferRange {
offset: 24,
size: 32
},
BufferRange {
offset: 80,
size: 32
},
BufferRange {
offset: 152,
size: 32
},
]
);
let out_of_bounds = test_set(
14,
vec![layout_binding(
0,
vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC,
)],
vec![buffer_entry(
0,
vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC,
buffer.clone(),
224,
32,
)],
);
assert!(
graph
.import_descriptor_set(out_of_bounds, &initial, &[8])
.is_err()
);
}
#[test]
fn managed_descriptor_import_rejects_unsupported_descriptor_kind() {
let set = test_set(
15,
vec![layout_binding(0, vk::DescriptorType::INPUT_ATTACHMENT)],
vec![TrackedDescriptorWrite {
binding: 0,
array_element: 0,
descriptor_type: vk::DescriptorType::INPUT_ATTACHMENT,
stages: vk::ShaderStageFlags::FRAGMENT,
resource: DescriptorResourceBinding::Sampler {
sampler: Arc::new(Sampler::for_testing(vk::Sampler::from_raw(2))),
},
}],
);
assert!(matches!(
RenderGraph::new().import_descriptor_set(set, &[], &[]),
Err(GraphError::DescriptorImport(_))
));
}
#[test]
fn kahn_emits_dag_in_deterministic_order() {
let mut kahn = Kahn::new(4);
kahn.add_edge(0, 2);
kahn.add_edge(0, 2);
kahn.add_edge(1, 2);
kahn.add_edge(2, 3);
let mut batches = Vec::new();
while !kahn.is_complete() {
let batch = kahn.pop_batch();
assert!(!batch.is_empty());
for &node in &batch {
kahn.complete(node);
}
batches.push(batch);
}
assert_eq!(batches, [vec![0, 1], vec![2], vec![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);
for node in kahn.pop_batch() {
kahn.complete(node);
}
assert_eq!(kahn.emitted, 1);
assert!(!kahn.is_complete());
}
#[test]
fn kahn_handles_holes() {
let mut kahn = Kahn::new(6);
kahn.add_edge(3, 2);
kahn.add_edge(4, 2);
kahn.add_edge(2, 1);
kahn.add_node(5);
assert_eq!(vec![3, 4, 5], kahn.pop_batch());
for node in [3, 4, 5] {
kahn.complete(node);
}
assert_eq!(vec![2], kahn.pop_batch());
for node in [2] {
kahn.complete(node);
}
assert_eq!(vec![1], kahn.pop_batch());
for node in [1] {
kahn.complete(node);
}
dbg!(&kahn);
assert!(kahn.is_complete());
}
#[test]
fn interference_tracks_overlapping_inclusive_lifetimes() {
let lifetimes = HashMap::from([
(
ResourceId::from_u32(0),
ResourceLifetime {
first_batch: 0,
last_batch: 2,
placement: Placement {
offset: 0,
size: 1,
alignment: 1,
pool: None,
aliased: false,
},
imported: false,
},
),
(
ResourceId::from_u32(1),
ResourceLifetime {
first_batch: 2,
last_batch: 3,
placement: Placement {
offset: 0,
size: 1,
alignment: 1,
pool: None,
aliased: false,
},
imported: false,
},
),
(
ResourceId::from_u32(2),
ResourceLifetime {
first_batch: 4,
last_batch: 5,
placement: Placement {
offset: 0,
size: 1,
alignment: 1,
pool: None,
aliased: false,
},
imported: false,
},
),
]);
let interference = ResourceInterference::from_lifetimes(
lifetimes.iter().map(|(&id, &lifetime)| (id, lifetime)),
);
assert!(interference.interferes(ResourceId::from_u32(0), ResourceId::from_u32(1)));
assert!(interference.interferes(ResourceId::from_u32(1), ResourceId::from_u32(0)));
assert!(!interference.interferes(ResourceId::from_u32(0), ResourceId::from_u32(2)));
assert!(!interference.interferes(ResourceId::from_u32(1), ResourceId::from_u32(2)));
}
#[test]
fn lifetime_analysis_tracks_batches_and_command_level_interference() {
let resources = vec![
ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 8,
..Default::default()
}),
ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 8,
..Default::default()
}),
ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 8,
..Default::default()
}),
];
let accesses = vec![
vec![resource_access_for(ResourceId::from_u32(0))],
vec![
resource_access_for(ResourceId::from_u32(0)),
resource_access_for(ResourceId::from_u32(1)),
],
vec![resource_access_for(ResourceId::from_u32(0))],
vec![resource_access_for(ResourceId::from_u32(2))],
];
let orderings = ordering_from_accesses(&resources, &accesses);
let batches = orderings
.batches
.iter()
.map(|batch| orderings.execution_order[batch.start..batch.start + batch.count].to_vec())
.collect::<Vec<_>>();
let happens_before = HappensBefore::from_command_dag(&orderings.dependency_edges, &batches);
let compiler = GraphCompiler::new_with_nodes(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
orderings.node_of_command,
orderings.node_representatives,
&orderings.spans,
);
let uses_by_resource = compiler.resource_uses();
let (lifetimes, interference) = compiler.bind_lifetimes_for_test(
&batches,
&ResourceFrontiers::from_uses(&uses_by_resource, &happens_before),
&happens_before,
&uses_by_resource,
&[],
);
assert_eq!(lifetimes[&ResourceId::from_u32(0)].first_batch, 0);
assert_eq!(lifetimes[&ResourceId::from_u32(0)].last_batch, 2);
assert!(interference.interferes(ResourceId::from_u32(0), ResourceId::from_u32(1)));
assert!(interference.interferes(ResourceId::from_u32(0), ResourceId::from_u32(2)));
assert!(interference.interferes(ResourceId::from_u32(1), ResourceId::from_u32(2)));
}
#[test]
fn happens_before_closure_covers_direct_and_transitive_edges() {
let edges = vec![vec![1], vec![2], vec![], vec![]];
let batches = vec![
vec![CommandIndex(0), CommandIndex(3)],
vec![CommandIndex(1)],
vec![CommandIndex(2)],
];
let happens_before = HappensBefore::from_command_dag(&edges, &batches);
assert!(happens_before.happens_before(CommandIndex(0), CommandIndex(1)));
assert!(happens_before.happens_before(CommandIndex(0), CommandIndex(2)));
assert!(happens_before.happens_before(CommandIndex(1), CommandIndex(2)));
assert!(!happens_before.happens_before(CommandIndex(2), CommandIndex(0)));
assert!(!happens_before.happens_before(CommandIndex(0), CommandIndex(3)));
assert!(!happens_before.happens_before(CommandIndex(0), CommandIndex(0)));
}
#[test]
fn same_batch_resource_uses_interfere() {
let edges = vec![vec![], vec![]];
let batches = vec![vec![CommandIndex(0), CommandIndex(1)]];
let happens_before = HappensBefore::from_command_dag(&edges, &batches);
let a = [ResourceUse {
command: CommandIndex(0),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}];
let b = [ResourceUse {
command: CommandIndex(1),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}];
let use_sets = HashMap::from([
(ResourceId::from_u32(0), a.to_vec()),
(ResourceId::from_u32(1), b.to_vec()),
]);
let interference = ResourceInterference::from_frontiers(
&ResourceFrontiers::from_uses(&use_sets, &happens_before),
&happens_before,
);
assert!(interference.interferes(ResourceId::from_u32(0), ResourceId::from_u32(1)));
assert!(!resources_alias_in_order(&a, &b, &happens_before));
assert!(!resources_alias_in_order(&b, &a, &happens_before));
}
#[test]
fn unordered_distinct_batches_interfere_even_without_interval_overlap() {
// This reproduces the plan's ready-set shape: 0 and 2 are batch-ready;
// 1 depends on 0, but 2 has no edge to 1.
let edges = vec![vec![1], vec![], vec![]];
let batches = vec![
vec![CommandIndex(0), CommandIndex(2)],
vec![CommandIndex(1)],
];
let happens_before = HappensBefore::from_command_dag(&edges, &batches);
let resource_a = [ResourceUse {
command: CommandIndex(2),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}];
let resource_b = [ResourceUse {
command: CommandIndex(1),
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
}];
assert!(!happens_before.happens_before(CommandIndex(2), CommandIndex(1)));
assert!(!resources_alias_in_order(
&resource_a,
&resource_b,
&happens_before,
));
let use_sets = HashMap::from([
(ResourceId::from_u32(0), resource_a.to_vec()),
(ResourceId::from_u32(1), resource_b.to_vec()),
]);
let interference = ResourceInterference::from_frontiers(
&ResourceFrontiers::from_uses(&use_sets, &happens_before),
&happens_before,
);
assert!(interference.interferes(ResourceId::from_u32(0), ResourceId::from_u32(1)));
}
#[test]
fn dependency_chain_proves_alias_reuse_safe() {
let resources = (0..5)
.map(|_| {
ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 8,
..Default::default()
})
})
.collect::<Vec<_>>();
let accesses = vec![
vec![
resource_access_for(ResourceId::from_u32(0)),
resource_access_for(ResourceId::from_u32(2)),
],
vec![
resource_access_for(ResourceId::from_u32(2)),
resource_access_for(ResourceId::from_u32(3)),
],
vec![
resource_access_for(ResourceId::from_u32(3)),
resource_access_for(ResourceId::from_u32(1)),
],
];
let orderings = ordering_from_accesses(&resources, &accesses);
let batches = orderings
.batches
.iter()
.map(|batch| orderings.execution_order[batch.start..batch.start + batch.count].to_vec())
.collect::<Vec<_>>();
let happens_before = HappensBefore::from_command_dag(&orderings.dependency_edges, &batches);
let compiler = GraphCompiler::new_with_nodes(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
orderings.node_of_command,
orderings.node_representatives,
&orderings.spans,
);
let uses = compiler.resource_uses();
assert!(happens_before.happens_before(CommandIndex(0), CommandIndex(2)));
assert!(resources_alias_in_order(
&uses[&ResourceId::from_u32(0)],
&uses[&ResourceId::from_u32(1)],
&happens_before,
));
let uses_a = &uses[&ResourceId::from_u32(0)];
let uses_b = &uses[&ResourceId::from_u32(1)];
assert!(aliasing_permitted(uses_a, uses_b, &happens_before));
}
#[test]
fn copy_buffer_compiles_with_dependency_and_barrier() {
let mut graph = RenderGraph::new();
let src = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let dst = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
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.batches().flatten().copied().collect::<Vec<_>>(),
vec![CommandIndex(0)]
);
assert!(compiled.barriers().is_empty());
assert_eq!(compiled.resource_lifetimes().len(), 2);
assert!(compiled.interference().interferes(src.id(), dst.id()));
assert!(!compiled.resource_lifetimes()[&src.id()].imported);
}
#[test]
fn render_pass_spans_validate_classification_and_balance() {
let mut graph = RenderGraph::new();
graph.push(BeginRendering {
color: Vec::new(),
depth: None,
});
graph.push(EndRendering);
let compiled = graph.compile().unwrap();
assert_eq!(compiled.render_pass_spans().len(), 1);
assert_eq!(compiled.render_pass_spans()[0].begin, CommandIndex(0));
assert_eq!(compiled.render_pass_spans()[0].end, CommandIndex(1));
let mut unbalanced = RenderGraph::new();
unbalanced.push(EndRendering);
assert!(matches!(
unbalanced.compile(),
Err(GraphError::UnbalancedRenderPass)
));
let mut unclosed = RenderGraph::new();
unclosed.push(empty_render_pass());
assert!(matches!(
unclosed.compile(),
Err(GraphError::UnbalancedRenderPass)
));
let mut nested = RenderGraph::new();
nested.push(empty_render_pass());
nested.push(empty_render_pass());
assert!(matches!(
nested.compile(),
Err(GraphError::NestedRenderPass)
));
}
#[test]
fn render_pass_span_is_one_node_and_expands_in_record_order() {
let mut graph = RenderGraph::new();
let source = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let shared = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let range = BufferRange { offset: 0, size: 8 };
graph.push(CopyBuffer {
src: source,
src_range: range,
dst: shared,
dst_range: range,
});
graph.push(empty_render_pass());
graph.push(Draw {
args: DrawArgs::Direct {
vertex_count: 3,
instance_count: 1,
first_vertex: 0,
first_instance: 0,
},
pipeline_layout: vk::PipelineLayout::null(),
vertices: Vec::new(),
index: None,
indirect: None,
descriptor_sets: Vec::new(),
push_constants: Vec::new(),
});
graph.push(EndRendering);
graph.push(CopyBuffer {
src: source,
src_range: range,
dst: shared,
dst_range: range,
});
graph.commands.side_effects.insert(
CommandIndex(2),
shared.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
access: vk::AccessFlags2::SHADER_READ,
subresource: SubresourceScopeInner::Buffer(range),
},
);
let compiled = graph.compile().expect("graph compiles");
assert_eq!(
compiled.execution_order(),
[
CommandIndex(0),
CommandIndex(1),
CommandIndex(2),
CommandIndex(3),
CommandIndex(4),
]
);
assert_eq!(
compiled.batches().collect::<Vec<_>>(),
[
&[CommandIndex(0)][..],
&[CommandIndex(1), CommandIndex(2), CommandIndex(3)][..],
&[CommandIndex(4)][..],
]
);
assert_eq!(compiled.dependency_edges().len(), 3);
assert!(compiled.happens_before(CommandIndex(0), CommandIndex(2)));
assert!(compiled.happens_before(CommandIndex(2), CommandIndex(4)));
assert!(!compiled.happens_before(CommandIndex(1), CommandIndex(2)));
assert!(!compiled.happens_before(CommandIndex(2), CommandIndex(3)));
assert_eq!(compiled.resource_lifetimes()[&shared.id()].first_batch, 0);
assert_eq!(compiled.resource_lifetimes()[&shared.id()].last_batch, 2);
}
#[test]
fn render_pass_rejects_graphics_outside_and_transfer_inside() {
let mut outside = RenderGraph::new();
outside.push(Draw {
args: DrawArgs::Direct {
vertex_count: 3,
instance_count: 1,
first_vertex: 0,
first_instance: 0,
},
pipeline_layout: vk::PipelineLayout::null(),
vertices: Vec::new(),
index: None,
indirect: None,
descriptor_sets: Vec::new(),
push_constants: Vec::new(),
});
assert!(matches!(
outside.compile(),
Err(GraphError::DrawOutsideRenderPass(CommandIndex(0)))
));
let mut inside = RenderGraph::new();
let source = inside.create_buffer(GraphBufferDesc {
size: 16,
..Default::default()
});
let destination = inside.create_buffer(GraphBufferDesc {
size: 16,
..Default::default()
});
inside.push(empty_render_pass());
inside.push(CopyBuffer {
src: source,
src_range: BufferRange {
offset: 0,
size: 16,
},
dst: destination,
dst_range: BufferRange {
offset: 0,
size: 16,
},
});
inside.push(EndRendering);
assert!(matches!(
inside.compile(),
Err(GraphError::CommandNotAllowedInRenderPass(CommandIndex(1)))
));
}
#[test]
fn render_pass_attachment_validation_and_lifetime_access() {
let mut graph = RenderGraph::new();
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
let mut begin = empty_render_pass();
begin.color.push(created_color_attachment(
texture,
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
));
graph.push(begin);
graph.push(EndRendering);
let compiled = graph.compile().unwrap();
assert!(
compiled
.commands
.side_effects
.is_lifetime_only(CommandIndex(1), texture.id())
);
let uses = compiled.resource_uses(texture.id()).unwrap();
assert!(uses.iter().any(|use_| use_.command == CommandIndex(1)));
assert!(!compiled.barriers().iter().any(|barrier| {
barrier.before_command == CommandIndex(1)
&& barrier
.resources
.iter()
.any(|resource| resource.id == texture.id())
}));
let mut duplicate = RenderGraph::new();
let texture = duplicate.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
let mut begin = empty_render_pass();
begin
.color
.push(created_color_attachment(texture, vk::ImageLayout::GENERAL));
begin
.color
.push(created_color_attachment(texture, vk::ImageLayout::GENERAL));
duplicate.push(begin);
duplicate.push(EndRendering);
assert!(matches!(
duplicate.compile(),
Err(GraphError::InvalidRenderPassAttachment(_))
));
let mut invalid_layout = RenderGraph::new();
let texture = invalid_layout.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
let mut begin = empty_render_pass();
begin.color.push(created_color_attachment(
texture,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
));
invalid_layout.push(begin);
invalid_layout.push(EndRendering);
assert!(matches!(
invalid_layout.compile(),
Err(GraphError::InvalidRenderPassAttachment(_))
));
let mut sentinel = RenderGraph::new();
let texture = sentinel.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
let mut begin = empty_render_pass();
let mut attachment = created_color_attachment(texture, vk::ImageLayout::GENERAL);
let AttachmentView::Created(view) = &mut attachment.view else {
unreachable!();
};
view.mip_range.end = vk::REMAINING_MIP_LEVELS;
begin.color.push(attachment);
sentinel.push(begin);
sentinel.push(EndRendering);
assert!(matches!(
sentinel.compile(),
Err(GraphError::InvalidRenderPassAttachment(_))
));
let mut mismatch = RenderGraph::new();
let graph_image = test_image(602);
let foreign_image = test_image(603);
let texture = mismatch
.import_texture(graph_image, ResourceState::default())
.unwrap();
let mut begin = empty_render_pass();
begin.color.push(ColorAttachment::imported(
texture,
test_view(foreign_image),
vk::ImageLayout::GENERAL,
vk::AttachmentLoadOp::DONT_CARE,
vk::AttachmentStoreOp::STORE,
vk::ClearValue::default(),
));
mismatch.push(begin);
mismatch.push(EndRendering);
assert!(matches!(
mismatch.compile(),
Err(GraphError::InvalidRenderPassAttachment(_))
));
}
#[test]
fn render_pass_feedback_loop_rejects_overlapping_interior_texture_access() {
let mut graph = RenderGraph::new();
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
mip_levels: 2,
..Default::default()
});
let mut begin = empty_render_pass();
begin
.color
.push(created_color_attachment(texture, vk::ImageLayout::GENERAL));
graph.push(begin);
struct TextureAccess(Texture);
impl crate::commands::Command for TextureAccess {
fn side_effects(&self, command: CommandIndex, map: &mut SideEffectMap) {
map.insert(
command,
self.0.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
access: vk::AccessFlags2::SHADER_READ,
subresource: SubresourceScopeInner::Texture {
range: texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
layout: vk::ImageLayout::GENERAL,
},
},
);
}
}
impl crate::recorder::RecordedCommand for TextureAccess {
fn allowed_in_render_pass(&self) -> bool {
true
}
fn apply(&mut self, _: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
Ok(())
}
}
graph.push(TextureAccess(texture));
graph.push(EndRendering);
assert!(matches!(
graph.compile(),
Err(GraphError::RenderPassFeedbackLoop(_))
));
}
#[test]
fn intra_pass_same_layout_barriers_are_allowed_but_layout_transitions_are_rejected() {
struct ImageUse {
texture: Texture,
layout: vk::ImageLayout,
access: vk::AccessFlags2,
}
impl crate::commands::Command for ImageUse {
fn side_effects(&self, command: CommandIndex, map: &mut SideEffectMap) {
map.insert(
command,
self.texture.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: self.access,
subresource: SubresourceScopeInner::Texture {
range: texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
layout: self.layout,
},
},
);
}
}
impl crate::recorder::RecordedCommand for ImageUse {
fn allowed_in_render_pass(&self) -> bool {
true
}
fn apply(&mut self, _: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
Ok(())
}
}
let make_graph = |second_layout| {
let mut graph = RenderGraph::new();
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
graph.push(empty_render_pass());
graph.push(ImageUse {
texture,
layout: vk::ImageLayout::GENERAL,
access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
});
graph.push(ImageUse {
texture,
layout: second_layout,
access: vk::AccessFlags2::SHADER_STORAGE_READ,
});
graph.push(EndRendering);
graph
};
let same_layout = make_graph(vk::ImageLayout::GENERAL)
.compile()
.expect("same-layout in-pass barrier is supported");
assert!(same_layout.barriers().iter().any(|barrier| {
barrier.before_command == CommandIndex(2)
&& barrier.resources.iter().any(|resource| {
resource.old_layout == Some(vk::ImageLayout::GENERAL)
&& resource.new_layout == Some(vk::ImageLayout::GENERAL)
})
}));
assert!(matches!(
make_graph(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL).compile(),
Err(GraphError::UnsupportedIntraPassDependency)
));
}
#[test]
fn hazard_free_foreign_command_is_scheduled_outside_render_pass_span() {
let mut graph = RenderGraph::new();
graph.push(empty_render_pass());
graph.push(AllowedAccess(Vec::new()));
graph.push(EndRendering);
graph.push(AllowedAccess(Vec::new()));
let compiled = graph.compile().expect("graph compiles");
let begin = compiled
.execution_order()
.iter()
.position(|&command| command == CommandIndex(0))
.unwrap();
let end = compiled
.execution_order()
.iter()
.position(|&command| command == CommandIndex(2))
.unwrap();
let foreign = compiled
.execution_order()
.iter()
.position(|&command| command == CommandIndex(3))
.unwrap();
assert!(foreign < begin || foreign > end);
}
#[test]
fn interior_access_barriers_are_hoisted_and_coalesced() {
let mut graph = RenderGraph::new();
let source = graph.create_buffer(GraphBufferDesc {
size: 32,
..Default::default()
});
let produced = graph.create_buffer(GraphBufferDesc {
size: 32,
..Default::default()
});
let range = BufferRange {
offset: 0,
size: 32,
};
let producer = graph.push(CopyBuffer {
src: source,
src_range: range,
dst: produced,
dst_range: range,
});
graph.push(empty_render_pass());
graph.push(AllowedAccess(vec![(
produced.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
access: vk::AccessFlags2::SHADER_READ,
subresource: SubresourceScopeInner::Buffer(range),
},
)]));
graph.push(AllowedAccess(vec![(
produced.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
access: vk::AccessFlags2::UNIFORM_READ,
subresource: SubresourceScopeInner::Buffer(range),
},
)]));
graph.push(EndRendering);
let compiled = graph.compile().expect("graph compiles");
let begin = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(1))
.expect("outside-producer dependency is hoisted to begin");
let resource = begin
.resources
.iter()
.find(|resource| resource.id == produced.id())
.expect("producer buffer barrier is present");
assert_eq!(resource.origin, BarrierOrigin::Command(producer));
let consumers = begin
.resources
.iter()
.filter(|resource| resource.id == produced.id())
.collect::<Vec<_>>();
assert_eq!(consumers.len(), 1);
assert_eq!(
consumers[0].dst_stage,
vk::PipelineStageFlags2::VERTEX_SHADER | vk::PipelineStageFlags2::FRAGMENT_SHADER
);
assert_eq!(
consumers[0].dst_access,
vk::AccessFlags2::SHADER_READ | vk::AccessFlags2::UNIFORM_READ
);
}
#[test]
fn initial_storage_image_transition_is_hoisted_before_begin() {
let mut graph = RenderGraph::new();
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
extra_usage: vk::ImageUsageFlags::STORAGE,
..Default::default()
});
graph.push(empty_render_pass());
graph.push(AllowedAccess(vec![(
texture.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
subresource: SubresourceScopeInner::Texture {
range: texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
layout: vk::ImageLayout::GENERAL,
},
},
)]));
graph.push(EndRendering);
let compiled = graph.compile().expect("graph compiles");
let begin = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(0))
.expect("initial image state transition is hoisted to begin");
let resource = begin
.resources
.iter()
.find(|resource| resource.id == texture.id())
.expect("storage image transition is present");
assert_eq!(resource.origin, BarrierOrigin::Initial);
assert_eq!(resource.old_layout, Some(vk::ImageLayout::UNDEFINED));
assert_eq!(resource.new_layout, Some(vk::ImageLayout::GENERAL));
}
#[test]
fn outside_image_transition_to_interior_readers_is_coalesced() {
let mut graph = RenderGraph::new();
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
let range = texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1);
let producer = graph.push(AllowedAccess(vec![(
texture.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
subresource: SubresourceScopeInner::Texture {
range,
layout: vk::ImageLayout::GENERAL,
},
},
)]));
graph.push(empty_render_pass());
graph.push(AllowedAccess(vec![(
texture.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
access: vk::AccessFlags2::SHADER_READ,
subresource: SubresourceScopeInner::Texture {
range,
layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
},
},
)]));
graph.push(AllowedAccess(vec![(
texture.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
access: vk::AccessFlags2::SHADER_SAMPLED_READ,
subresource: SubresourceScopeInner::Texture {
range,
layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
},
},
)]));
graph.push(EndRendering);
let compiled = graph.compile().expect("outside transition is hoisted");
let begin = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(1))
.expect("transition is keyed at begin");
let resources = begin
.resources
.iter()
.filter(|resource| resource.id == texture.id())
.collect::<Vec<_>>();
assert_eq!(resources.len(), 1);
assert_eq!(resources[0].origin, BarrierOrigin::Command(producer));
assert_eq!(resources[0].old_layout, Some(vk::ImageLayout::GENERAL));
assert_eq!(
resources[0].new_layout,
Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
);
assert_eq!(
resources[0].dst_stage,
vk::PipelineStageFlags2::VERTEX_SHADER | vk::PipelineStageFlags2::FRAGMENT_SHADER
);
assert_eq!(
resources[0].dst_access,
vk::AccessFlags2::SHADER_READ | vk::AccessFlags2::SHADER_SAMPLED_READ
);
}
#[test]
fn interior_storage_buffer_hazard_emits_barrier_without_splitting_pass() {
let mut graph = RenderGraph::new();
let buffer = graph.create_buffer(GraphBufferDesc {
size: 32,
extra_usage: vk::BufferUsageFlags::STORAGE_BUFFER,
..Default::default()
});
let range = BufferRange {
offset: 0,
size: 32,
};
graph.push(empty_render_pass());
graph.push(AllowedAccess(vec![(
buffer.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
subresource: SubresourceScopeInner::Buffer(range),
},
)]));
graph.push(AllowedAccess(vec![(
buffer.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: vk::AccessFlags2::SHADER_STORAGE_READ,
subresource: SubresourceScopeInner::Buffer(range),
},
)]));
graph.push(EndRendering);
let compiled = graph.compile().expect("same-pass buffer barrier is legal");
assert_eq!(
compiled.batches().collect::<Vec<_>>(),
[&[
CommandIndex(0),
CommandIndex(1),
CommandIndex(2),
CommandIndex(3),
][..]]
);
assert_eq!(compiled.render_pass_spans().len(), 1);
assert!(compiled.barriers().iter().any(|barrier| {
barrier.before_command == CommandIndex(2)
&& barrier.resources.iter().any(|resource| {
resource.id == buffer.id()
&& resource.src_access == vk::AccessFlags2::SHADER_STORAGE_WRITE
&& resource.dst_access == vk::AccessFlags2::SHADER_STORAGE_READ
})
}));
}
#[test]
fn interior_storage_image_same_layout_hazard_emits_image_barrier() {
let mut graph = RenderGraph::new();
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
extra_usage: vk::ImageUsageFlags::STORAGE,
..Default::default()
});
let range = texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1);
graph.push(empty_render_pass());
graph.push(AllowedAccess(vec![(
texture.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
subresource: SubresourceScopeInner::Texture {
range,
layout: vk::ImageLayout::GENERAL,
},
},
)]));
graph.push(AllowedAccess(vec![(
texture.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: vk::AccessFlags2::SHADER_STORAGE_READ,
subresource: SubresourceScopeInner::Texture {
range,
layout: vk::ImageLayout::GENERAL,
},
},
)]));
graph.push(EndRendering);
let compiled = graph.compile().expect("same-layout image barrier is legal");
assert!(compiled.barriers().iter().any(|barrier| {
barrier.before_command == CommandIndex(2)
&& barrier.resources.iter().any(|resource| {
resource.id == texture.id()
&& resource.old_layout == Some(vk::ImageLayout::GENERAL)
&& resource.new_layout == Some(vk::ImageLayout::GENERAL)
})
}));
}
#[test]
fn interior_only_resource_lifetime_maps_to_pass_batch() {
let mut graph = RenderGraph::new();
let buffer = graph.create_buffer(GraphBufferDesc {
size: 16,
..Default::default()
});
graph.push(empty_render_pass());
graph.push(AllowedAccess(vec![(
buffer.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
access: vk::AccessFlags2::SHADER_READ,
subresource: SubresourceScopeInner::Buffer(BufferRange {
offset: 0,
size: 16,
}),
},
)]));
graph.push(EndRendering);
let compiled = graph.compile().expect("graph compiles");
let batch = compiled
.batches()
.position(|commands| commands.contains(&CommandIndex(0)))
.unwrap();
let lifetime = compiled.resource_lifetimes()[&buffer.id()];
assert_eq!(lifetime.first_batch, batch);
assert_eq!(lifetime.last_batch, batch);
assert_ne!(lifetime.first_batch, usize::MAX);
}
#[test]
fn render_pass_attachment_and_interior_image_interfere_until_end() {
let mut graph = RenderGraph::new();
let attachment = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
let storage = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
extra_usage: vk::ImageUsageFlags::STORAGE,
..Default::default()
});
let mut begin = empty_render_pass();
begin.color.push(created_color_attachment(
attachment,
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
));
let begin_index = graph.push(begin);
let interior = graph.push(AllowedAccess(vec![(
storage.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
subresource: SubresourceScopeInner::Texture {
range: texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
layout: vk::ImageLayout::GENERAL,
},
},
)]));
let end_index = graph.push(EndRendering);
let compiled = graph.compile().expect("graph compiles");
assert!(
compiled
.interference()
.interferes(attachment.id(), storage.id())
);
assert!(!compiled.barriers().iter().any(|barrier| {
barrier.before_command == interior
&& barrier
.resources
.iter()
.any(|resource| resource.id == attachment.id() || resource.id == storage.id())
}));
let uses = compiled.resource_uses(attachment.id()).unwrap();
assert!(uses.iter().any(|use_| use_.command == begin_index));
assert!(uses.iter().any(|use_| use_.command == end_index));
let execution_order = compiled.execution_order();
let end_position = execution_order
.iter()
.position(|&command| command == end_index)
.unwrap();
let last_attachment_use = uses
.iter()
.map(|use_| {
execution_order
.iter()
.position(|&command| command == use_.command)
.unwrap()
})
.max()
.unwrap();
assert_eq!(last_attachment_use, end_position);
}
#[test]
fn dependencies_order_commands_around_the_whole_pass_but_not_within_it() {
let mut graph = RenderGraph::new();
let source = graph.create_buffer(GraphBufferDesc {
size: 16,
..Default::default()
});
let shared = graph.create_buffer(GraphBufferDesc {
size: 16,
..Default::default()
});
let range = BufferRange {
offset: 0,
size: 16,
};
let producer = graph.push(CopyBuffer {
src: source,
src_range: range,
dst: shared,
dst_range: range,
});
graph.push(empty_render_pass());
let interior_a = graph.push(AllowedAccess(vec![(
shared.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
access: vk::AccessFlags2::SHADER_READ,
subresource: SubresourceScopeInner::Buffer(range),
},
)]));
let interior_b = graph.push(AllowedAccess(vec![(
shared.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
access: vk::AccessFlags2::SHADER_READ,
subresource: SubresourceScopeInner::Buffer(range),
},
)]));
graph.push(EndRendering);
let consumer = graph.push(AllowedAccess(vec![(
shared.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
subresource: SubresourceScopeInner::Buffer(range),
},
)]));
let compiled = graph.compile().expect("graph compiles");
assert!(compiled.happens_before(producer, interior_a));
assert!(compiled.happens_before(interior_a, consumer));
assert!(!compiled.happens_before(interior_a, interior_b));
}
#[test]
fn draw_vertex_index_and_indirect_reads_follow_copies_and_derive_usage() {
let mut graph = RenderGraph::new();
let source = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let vertex = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let index = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let indirect = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let range = BufferRange {
offset: 0,
size: 64,
};
for dst in [vertex, index, indirect] {
graph.push(CopyBuffer {
src: source,
src_range: range,
dst,
dst_range: range,
});
}
graph.push(BeginRendering {
color: Vec::new(),
depth: None,
});
graph.push(Draw {
args: DrawArgs::Indexed {
index_count: 3,
instance_count: 1,
first_index: 0,
vertex_offset: 0,
first_instance: 0,
},
pipeline_layout: vk::PipelineLayout::null(),
vertices: vec![VertexBinding {
binding: 0,
slice: BufferSlice {
buffer: vertex,
range,
},
}],
index: Some(IndexBinding {
slice: BufferSlice {
buffer: index,
range,
},
format: IndexFormat::Uint16,
}),
indirect: Some(IndirectBinding {
slice: BufferSlice {
buffer: indirect,
range,
},
draw_count: 1,
stride: 20,
}),
descriptor_sets: Vec::new(),
push_constants: Vec::new(),
});
graph.push(EndRendering);
let accesses = graph.commands.side_effects.command_accesses();
assert_eq!(
derive_buffer_usage(
&accesses_for_resource(vertex.id(), accesses, &graph.commands.side_effects),
vk::BufferUsageFlags::empty()
),
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::VERTEX_BUFFER
);
assert_eq!(
derive_buffer_usage(
&accesses_for_resource(index.id(), accesses, &graph.commands.side_effects),
vk::BufferUsageFlags::empty()
),
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::INDEX_BUFFER
);
assert_eq!(
derive_buffer_usage(
&accesses_for_resource(indirect.id(), accesses, &graph.commands.side_effects),
vk::BufferUsageFlags::empty()
),
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::INDIRECT_BUFFER
);
let compiled = graph.compile().expect("graph compiles");
let batches = compiled.batches().collect::<Vec<_>>();
assert_eq!(batches.len(), 2);
assert_eq!(
batches[0],
[CommandIndex(0), CommandIndex(1), CommandIndex(2)]
);
assert_eq!(
batches[1],
[CommandIndex(3), CommandIndex(4), CommandIndex(5)]
);
let draw_barrier = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(3))
.expect("draw input barriers are generated");
assert_eq!(draw_barrier.resources.len(), 3);
assert!(
draw_barrier
.resources
.iter()
.any(|resource| resource.id == vertex.id())
);
assert!(
draw_barrier
.resources
.iter()
.any(|resource| resource.id == index.id())
);
assert!(
draw_barrier
.resources
.iter()
.any(|resource| resource.id == indirect.id())
);
}
#[test]
fn descriptor_set_declarations_create_buffer_and_texture_accesses() {
let mut graph = RenderGraph::new();
let source = graph.create_buffer(GraphBufferDesc {
size: 32,
..Default::default()
});
let uniform = graph.create_buffer(GraphBufferDesc {
size: 32,
..Default::default()
});
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
let buffer_range = BufferRange {
offset: 0,
size: 32,
};
graph.push(CopyBuffer {
src: source,
src_range: buffer_range,
dst: uniform,
dst_range: buffer_range,
});
graph.push(ClearTexture {
texture,
range: texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
color: [0.0; 4],
});
let set = graph.track_descriptor_set(
vk::DescriptorSet::null(),
vec![
DescriptorResourceAccess::Buffer {
buffer: uniform,
range: buffer_range,
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
access: vk::AccessFlags2::UNIFORM_READ,
},
DescriptorResourceAccess::Texture {
texture,
range: texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
access: vk::AccessFlags2::SHADER_SAMPLED_READ,
},
],
);
graph.push(BeginRendering {
color: Vec::new(),
depth: None,
});
graph.push(Draw {
args: DrawArgs::Direct {
vertex_count: 3,
instance_count: 1,
first_vertex: 0,
first_instance: 0,
},
pipeline_layout: vk::PipelineLayout::null(),
vertices: Vec::new(),
index: None,
indirect: None,
descriptor_sets: vec![set],
push_constants: Vec::new(),
});
graph.push(EndRendering);
let accesses = graph.commands.side_effects.command_accesses();
assert!(
derive_buffer_usage(
&accesses_for_resource(uniform.id(), accesses, &graph.commands.side_effects),
vk::BufferUsageFlags::empty()
)
.contains(vk::BufferUsageFlags::UNIFORM_BUFFER)
);
assert!(
derive_image_usage(
&accesses_for_resource(texture.id(), accesses, &graph.commands.side_effects),
vk::ImageUsageFlags::empty()
)
.contains(vk::ImageUsageFlags::SAMPLED)
);
let compiled = graph.compile().expect("graph compiles");
let draw_barrier = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(2))
.expect("descriptor accesses produce barriers");
assert_eq!(draw_barrier.resources.len(), 2);
let texture_barrier = draw_barrier
.resources
.iter()
.find(|resource| resource.id == texture.id())
.expect("sampled texture barrier is present");
assert_eq!(
texture_barrier.new_layout,
Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
);
assert_eq!(
texture_barrier.dst_access,
vk::AccessFlags2::SHADER_SAMPLED_READ
);
assert!(compiled.resource_lifetimes().contains_key(&uniform.id()));
assert!(compiled.resource_lifetimes().contains_key(&texture.id()));
assert!(!compiled.resource_lifetimes()[&uniform.id()].imported);
assert!(!compiled.resource_lifetimes()[&texture.id()].imported);
assert!(
compiled
.interference()
.interferes(uniform.id(), texture.id())
);
}
#[test]
fn descriptor_import_helpers_validate_effective_ranges_and_dynamic_order() {
assert_eq!(
effective_buffer_range(128, 16, 32, 64),
Ok(BufferRange {
offset: 80,
size: 32
})
);
assert!(effective_buffer_range(128, 96, 32, 16).is_err());
assert!(effective_buffer_range(128, u64::MAX, 16, 1).is_err());
assert!(effective_buffer_range(128, 16, vk::WHOLE_SIZE, 0).is_ok());
assert!(effective_buffer_range(128, 16, vk::WHOLE_SIZE, 4).is_err());
assert!(is_dynamic_offset_aligned(64, 32));
assert!(!is_dynamic_offset_aligned(16, 32));
assert!(!is_dynamic_offset_aligned(0, 0));
let layout = [
renderer_resources::pipeline::DescriptorSetLayoutBindingDesc {
binding: 9,
count: 2,
kind: vk::DescriptorType::STORAGE_BUFFER_DYNAMIC,
..Default::default()
},
renderer_resources::pipeline::DescriptorSetLayoutBindingDesc {
binding: 2,
count: 1,
kind: vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC,
..Default::default()
},
];
assert_eq!(
dynamic_offset_slots(&layout),
[
(2, 0, vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC),
(9, 0, vk::DescriptorType::STORAGE_BUFFER_DYNAMIC),
(9, 1, vk::DescriptorType::STORAGE_BUFFER_DYNAMIC),
]
);
assert_eq!(
descriptor_access(vk::DescriptorType::UNIFORM_BUFFER),
Some((vk::AccessFlags2::UNIFORM_READ, None))
);
assert_eq!(
descriptor_access(vk::DescriptorType::SAMPLED_IMAGE),
Some((vk::AccessFlags2::SHADER_SAMPLED_READ, None))
);
assert_eq!(
descriptor_access(vk::DescriptorType::STORAGE_IMAGE),
Some((
vk::AccessFlags2::SHADER_STORAGE_READ | vk::AccessFlags2::SHADER_STORAGE_WRITE,
Some(vk::ImageLayout::GENERAL),
))
);
assert_eq!(descriptor_access(vk::DescriptorType::SAMPLER), None);
assert_eq!(
shader_stages_to_pipeline(
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT
),
Some(vk::PipelineStageFlags2::VERTEX_SHADER | vk::PipelineStageFlags2::FRAGMENT_SHADER)
);
assert_eq!(
shader_stages_to_pipeline(vk::ShaderStageFlags::ALL),
Some(vk::PipelineStageFlags2::ALL_COMMANDS)
);
assert!(texture_layouts_conflict(
texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
vk::ImageLayout::GENERAL,
texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
));
assert!(!texture_layouts_conflict(
texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
vk::ImageLayout::GENERAL,
texture_range(vk::ImageAspectFlags::COLOR, 1, 1, 0, 1),
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
));
}
#[test]
fn raw_and_managed_descriptor_binding_offsets_are_concatenated_by_set_order() {
let first = GraphDescriptorSet {
raw: vk::DescriptorSet::null(),
accesses: Vec::new(),
managed: None,
dynamic_offsets: vec![4, 8],
};
let second = GraphDescriptorSet {
raw: vk::DescriptorSet::null(),
accesses: Vec::new(),
managed: None,
dynamic_offsets: vec![12],
};
let combined = [first, second]
.iter()
.flat_map(|set| set.dynamic_offsets().iter().copied())
.collect::<Vec<_>>();
assert_eq!(combined, [4, 8, 12]);
}
#[test]
fn write_then_read_creates_order_and_barrier() {
let mut graph = RenderGraph::new();
let a = graph.create_buffer(GraphBufferDesc {
size: 32,
..Default::default()
});
let b = graph.create_buffer(GraphBufferDesc {
size: 32,
..Default::default()
});
let c = graph.create_buffer(GraphBufferDesc {
size: 32,
..Default::default()
});
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.batches().flatten().copied().collect::<Vec<_>>(),
vec![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());
}
#[test]
fn write_read_write_emits_barriers_from_each_most_recent_conflict() {
let resource = ResourceId::from_u32(0);
let resources = [ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 64,
..Default::default()
})];
let accesses = vec![
vec![scoped_access(
resource,
0,
64,
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
)],
vec![scoped_access(
resource,
0,
64,
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
)],
vec![scoped_access(
resource,
0,
64,
vk::PipelineStageFlags2::COMPUTE_SHADER,
vk::AccessFlags2::SHADER_WRITE,
)],
];
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let batches = topo_batches_for_test(&compiler).expect("graph is acyclic");
let execution_order = flatten_batches(&batches);
let barriers = compiler.build_barriers(&execution_order);
assert_eq!(barriers.len(), 2);
assert_eq!(barriers[0].before_command, CommandIndex(1));
assert_eq!(
barriers[0].resources[0].src_stage,
vk::PipelineStageFlags2::COPY
);
assert_eq!(
barriers[0].resources[0].src_access,
vk::AccessFlags2::TRANSFER_WRITE
);
assert_eq!(
barriers[0].resources[0].dst_stage,
vk::PipelineStageFlags2::VERTEX_SHADER
);
assert_eq!(
barriers[0].resources[0].dst_access,
vk::AccessFlags2::SHADER_READ
);
let second_write_barrier = barriers
.iter()
.find(|barrier| barrier.before_command == CommandIndex(2))
.expect("second write has a barrier");
assert_eq!(
second_write_barrier.resources[0].src_stage,
vk::PipelineStageFlags2::VERTEX_SHADER
);
assert_eq!(
second_write_barrier.resources[0].src_access,
vk::AccessFlags2::SHADER_READ
);
}
#[test]
fn alias_buffer_barrier_aggregates_terminal_write_and_first_use_scopes() {
let old = ResourceId::from_u32(0);
let new = ResourceId::from_u32(1);
let resources = [
ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 64,
..Default::default()
}),
ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 64,
..Default::default()
}),
];
let accesses = vec![
vec![
scoped_access(
old,
0,
64,
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
),
scoped_access(
old,
0,
64,
vk::PipelineStageFlags2::COMPUTE_SHADER,
vk::AccessFlags2::SHADER_STORAGE_WRITE,
),
],
vec![scoped_access(
new,
0,
64,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::AccessFlags2::UNIFORM_READ,
)],
];
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let batches = vec![vec![CommandIndex(0)], vec![CommandIndex(1)]];
let happens_before = HappensBefore::from_command_dag(&[vec![1], vec![]], &batches);
let uses = compiler.resource_uses();
let frontiers = ResourceFrontiers::from_uses(&uses, &happens_before);
let lifetimes = HashMap::from([
(old, placed_lifetime(0, 64, false)),
(new, placed_lifetime(0, 64, true)),
]);
let mut barriers = compiler.build_barriers(&flatten_batches(&batches));
compiler.build_alias_barriers(&mut barriers, &lifetimes, &frontiers, &happens_before);
let barrier = barriers
.iter()
.find(|barrier| barrier.before_command == CommandIndex(1))
.unwrap();
assert!(barrier.memory_barriers.is_empty());
let alias = barrier
.resources
.iter()
.find(|resource| resource.id == new)
.unwrap();
assert_eq!(
alias.src_stage,
vk::PipelineStageFlags2::COPY | vk::PipelineStageFlags2::COMPUTE_SHADER
);
assert_eq!(
alias.src_access,
vk::AccessFlags2::TRANSFER_WRITE | vk::AccessFlags2::SHADER_STORAGE_WRITE
);
assert_eq!(alias.dst_stage, vk::PipelineStageFlags2::FRAGMENT_SHADER);
assert_eq!(alias.dst_access, vk::AccessFlags2::UNIFORM_READ);
assert!(matches!(
alias.range,
Subresource::Buffer(BufferRange {
offset: 0,
size: vk::WHOLE_SIZE,
})
));
assert_eq!(alias.old_layout, None);
assert_eq!(alias.new_layout, None);
assert_eq!(alias.queue_transfer, None);
assert_eq!(alias.origin, BarrierOrigin::Initial);
// Existing first-use resource dependencies retain their destination/range/origin,
// while alias source masks are added to their own source masks.
let existing_resource = BarrierResource {
id: new,
range: Subresource::Buffer(BufferRange {
offset: 8,
size: 16,
}),
origin: BarrierOrigin::Command(CommandIndex(0)),
src_stage: vk::PipelineStageFlags2::VERTEX_SHADER,
src_access: vk::AccessFlags2::SHADER_READ,
dst_stage: vk::PipelineStageFlags2::COPY,
dst_access: vk::AccessFlags2::TRANSFER_WRITE,
old_layout: None,
new_layout: None,
queue_transfer: None,
};
let mut barriers = vec![PipelineBarrier {
before_command: CommandIndex(1),
memory_barriers: Vec::new(),
resources: tinyvec::tiny_vec!([BarrierResource; 2] => existing_resource),
}];
compiler.build_alias_barriers(&mut barriers, &lifetimes, &frontiers, &happens_before);
let alias = barriers
.iter()
.find(|barrier| barrier.before_command == CommandIndex(1))
.unwrap()
.resources
.iter()
.find(|resource| resource.id == new)
.unwrap();
assert_eq!(
alias.src_stage,
vk::PipelineStageFlags2::VERTEX_SHADER
| vk::PipelineStageFlags2::COPY
| vk::PipelineStageFlags2::COMPUTE_SHADER
);
assert_eq!(
alias.src_access,
vk::AccessFlags2::SHADER_READ
| vk::AccessFlags2::TRANSFER_WRITE
| vk::AccessFlags2::SHADER_STORAGE_WRITE
);
assert_eq!(alias.dst_stage, vk::PipelineStageFlags2::COPY);
assert_eq!(alias.dst_access, vk::AccessFlags2::TRANSFER_WRITE);
assert!(matches!(
alias.range,
Subresource::Buffer(BufferRange {
offset: 8,
size: 16,
})
));
assert_eq!(alias.origin, BarrierOrigin::Command(CommandIndex(0)));
}
#[test]
fn alias_buffer_read_only_source_uses_execution_only_scoped_barrier() {
let old = ResourceId::from_u32(0);
let new = ResourceId::from_u32(1);
let resources = [
ResourceDesc::CreatedBuffer(GraphBufferDesc::default()),
ResourceDesc::CreatedBuffer(GraphBufferDesc::default()),
];
let accesses = vec![
vec![scoped_access(
old,
0,
8,
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
)],
vec![scoped_access(
new,
0,
8,
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
)],
];
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let batches = vec![vec![CommandIndex(0)], vec![CommandIndex(1)]];
let happens_before = HappensBefore::from_command_dag(&[vec![1], vec![]], &batches);
let uses = compiler.resource_uses();
let frontiers = ResourceFrontiers::from_uses(&uses, &happens_before);
let lifetimes = HashMap::from([
(old, placed_lifetime(0, 8, false)),
(new, placed_lifetime(0, 8, true)),
]);
let mut barriers = compiler.build_barriers(&flatten_batches(&batches));
compiler.build_alias_barriers(&mut barriers, &lifetimes, &frontiers, &happens_before);
let barrier = barriers
.iter()
.find(|barrier| barrier.before_command == CommandIndex(1))
.unwrap();
assert!(barrier.memory_barriers.is_empty());
let alias = barrier
.resources
.iter()
.find(|resource| resource.id == new)
.unwrap();
assert_eq!(alias.src_stage, vk::PipelineStageFlags2::VERTEX_SHADER);
assert_eq!(alias.src_access, vk::AccessFlags2::NONE);
assert_eq!(alias.dst_stage, vk::PipelineStageFlags2::COPY);
assert_eq!(alias.dst_access, vk::AccessFlags2::TRANSFER_WRITE);
}
#[test]
fn alias_image_scope_is_folded_into_undefined_layout_transition() {
let old = ResourceId::from_u32(0);
let new = ResourceId::from_u32(1);
let resources = [
ResourceDesc::CreatedTexture(GraphTextureDesc::default()),
ResourceDesc::CreatedTexture(GraphTextureDesc::default()),
];
let range = texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1);
let accesses = vec![
vec![texture_scoped_access(
old,
range,
vk::ImageLayout::GENERAL,
vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
vk::AccessFlags2::COLOR_ATTACHMENT_WRITE,
)],
vec![texture_scoped_access(
new,
range,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::AccessFlags2::SHADER_SAMPLED_READ,
)],
];
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let batches = vec![vec![CommandIndex(0)], vec![CommandIndex(1)]];
let happens_before = HappensBefore::from_command_dag(&[vec![1], vec![]], &batches);
let uses = compiler.resource_uses();
let frontiers = ResourceFrontiers::from_uses(&uses, &happens_before);
let lifetimes = HashMap::from([
(old, placed_lifetime(0, 128, false)),
(new, placed_lifetime(0, 128, true)),
]);
let mut barriers = compiler.build_barriers(&flatten_batches(&batches));
compiler.build_alias_barriers(&mut barriers, &lifetimes, &frontiers, &happens_before);
let barrier = barriers
.iter()
.find(|barrier| barrier.before_command == CommandIndex(1))
.unwrap();
assert!(barrier.memory_barriers.is_empty());
let image = barrier
.resources
.iter()
.find(|resource| resource.id == new)
.unwrap();
assert_eq!(image.old_layout, Some(vk::ImageLayout::UNDEFINED));
assert_eq!(image.origin, BarrierOrigin::Initial);
assert_eq!(
image.src_stage,
vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT
);
assert_eq!(image.src_access, vk::AccessFlags2::COLOR_ATTACHMENT_WRITE);
assert_eq!(image.dst_stage, vk::PipelineStageFlags2::FRAGMENT_SHADER);
}
#[test]
fn alias_range_barriers_include_every_intersecting_occupant() {
let old_ids = [
ResourceId::from_u32(0),
ResourceId::from_u32(1),
ResourceId::from_u32(2),
ResourceId::from_u32(3),
];
let new = ResourceId::from_u32(4);
let resources = (0..5)
.map(|_| ResourceDesc::CreatedBuffer(GraphBufferDesc::default()))
.collect::<Vec<_>>();
let old_stages = [
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::PipelineStageFlags2::COMPUTE_SHADER,
vk::PipelineStageFlags2::COPY,
];
let mut accesses = old_ids
.iter()
.zip(old_stages)
.map(|(&id, stage)| {
vec![scoped_access(
id,
0,
8,
stage,
vk::AccessFlags2::TRANSFER_WRITE,
)]
})
.collect::<Vec<_>>();
accesses.push(vec![scoped_access(
new,
0,
8,
vk::PipelineStageFlags2::COMPUTE_SHADER,
vk::AccessFlags2::SHADER_WRITE,
)]);
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let batches = (0..5)
.map(|index| vec![CommandIndex(index)])
.collect::<Vec<_>>();
let edges = vec![vec![4], vec![4], vec![4], vec![4], vec![]];
let happens_before = HappensBefore::from_command_dag(&edges, &batches);
let uses = compiler.resource_uses();
let frontiers = ResourceFrontiers::from_uses(&uses, &happens_before);
let lifetimes = HashMap::from([
(old_ids[0], placed_lifetime(0, 8, false)),
(old_ids[1], placed_lifetime(4, 8, false)),
(old_ids[2], placed_lifetime(8, 8, false)),
(old_ids[3], placed_lifetime(32, 8, false)),
(new, placed_lifetime(8, 16, true)),
]);
let mut barriers = compiler.build_barriers(&flatten_batches(&batches));
compiler.build_alias_barriers(&mut barriers, &lifetimes, &frontiers, &happens_before);
let barrier = barriers
.iter()
.find(|barrier| barrier.before_command == CommandIndex(4))
.unwrap();
// Source masks are unioned across all intersecting occupants into one barrier.
assert!(barrier.memory_barriers.is_empty());
let alias = barrier
.resources
.iter()
.find(|resource| resource.id == new)
.unwrap();
assert_eq!(
alias.src_stage,
vk::PipelineStageFlags2::FRAGMENT_SHADER | vk::PipelineStageFlags2::COMPUTE_SHADER
);
assert_eq!(alias.src_access, vk::AccessFlags2::TRANSFER_WRITE);
assert_eq!(alias.dst_stage, vk::PipelineStageFlags2::COMPUTE_SHADER);
assert_eq!(alias.dst_access, vk::AccessFlags2::SHADER_WRITE);
assert!(matches!(
alias.range,
Subresource::Buffer(BufferRange {
offset: 0,
size: vk::WHOLE_SIZE,
})
));
}
fn placed_lifetime(offset: u64, size: u64, aliased: bool) -> ResourceLifetime {
ResourceLifetime {
first_batch: 0,
last_batch: 0,
placement: Placement {
offset,
size,
alignment: 1,
pool: Some(0),
aliased,
},
imported: false,
}
}
#[test]
fn imported_texture_read_with_different_layout_gets_initial_transition() {
let resource = ResourceId::from_u32(0);
let resources = [ResourceDesc::CreatedTexture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
mip_levels: 2,
array_layers: 2,
..Default::default()
})];
let initial_states = [crate::resources::ResourceState {
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
access: vk::AccessFlags2::SHADER_READ,
layout: Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL),
}];
// Supply the imported image's initial state directly so this stays
// device-free and does not need to construct a Vulkan Image wrapper.
let accesses = vec![
vec![texture_scoped_access(
resource,
texture_range(vk::ImageAspectFlags::COLOR, 0, 2, 0, 2),
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
)],
vec![texture_scoped_access(
resource,
texture_range(vk::ImageAspectFlags::COLOR, 1, 1, 1, 1),
vk::ImageLayout::GENERAL,
vk::PipelineStageFlags2::COMPUTE_SHADER,
vk::AccessFlags2::SHADER_SAMPLED_READ,
)],
];
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let batches = topo_batches_for_test(&compiler).expect("graph is acyclic");
let barriers = compiler
.build_barriers_with_initial_states(&flatten_batches(&batches), &initial_states);
assert_eq!(barriers.len(), 1);
assert_eq!(barriers[0].before_command, CommandIndex(1));
assert_eq!(
barriers[0].resources[0].old_layout,
Some(initial_states[0].layout.unwrap())
);
assert_eq!(
barriers[0].resources[0].new_layout,
Some(vk::ImageLayout::GENERAL)
);
assert_eq!(
barriers[0].resources[0].dst_access,
vk::AccessFlags2::SHADER_SAMPLED_READ
);
}
#[test]
fn overlapping_texture_reads_with_different_layouts_get_layout_transition() {
let resource = ResourceId::from_u32(0);
let resources = [ResourceDesc::CreatedTexture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
})];
let range = texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1);
let accesses = vec![
vec![texture_scoped_access(
resource,
range,
vk::ImageLayout::GENERAL,
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
)],
vec![texture_scoped_access(
resource,
range,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
)],
vec![texture_scoped_access(
resource,
range,
vk::ImageLayout::GENERAL,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::AccessFlags2::SHADER_SAMPLED_READ,
)],
];
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let batches = topo_batches_for_test(&compiler).expect("graph is acyclic");
let barriers = compiler.build_barriers(&flatten_batches(&batches));
assert_eq!(barriers.len(), 3);
let current_read_barrier = &barriers[2];
assert_eq!(current_read_barrier.before_command, CommandIndex(2));
let barrier = &current_read_barrier.resources[0];
assert_eq!(barrier.dst_stage, vk::PipelineStageFlags2::FRAGMENT_SHADER);
assert_eq!(barrier.dst_access, vk::AccessFlags2::SHADER_SAMPLED_READ);
assert_eq!(
barrier.old_layout,
Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
);
assert_eq!(barrier.new_layout, Some(vk::ImageLayout::GENERAL));
}
#[test]
fn same_target_readers_chain_the_current_image_layout() {
let resource = ResourceId::from_u32(0);
let resources = [ResourceDesc::CreatedTexture(GraphTextureDesc::default())];
let range = texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1);
let accesses = vec![
vec![texture_scoped_access(
resource,
range,
vk::ImageLayout::GENERAL,
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
)],
vec![texture_scoped_access(
resource,
range,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
)],
vec![texture_scoped_access(
resource,
range,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::AccessFlags2::SHADER_READ,
)],
];
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let order = [CommandIndex(0), CommandIndex(1), CommandIndex(2)];
let barriers = compiler.build_barriers(&order);
let second_reader = barriers
.iter()
.find(|barrier| barrier.before_command == CommandIndex(2))
.expect("the producer conflicts with the second reader");
assert_eq!(second_reader.resources.len(), 1);
assert_eq!(
second_reader.resources[0].old_layout,
Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
);
assert_eq!(
second_reader.resources[0].new_layout,
Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
);
assert_eq!(
second_reader.resources[0].origin,
BarrierOrigin::Command(CommandIndex(0))
);
}
#[test]
fn hoisted_equal_image_transitions_union_destination_scopes_in_order() {
let id = ResourceId::from_u32(0);
let range = texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1);
let mut barriers = vec![
PipelineBarrier {
before_command: CommandIndex(2),
memory_barriers: Vec::new(),
resources: [BarrierResource {
id,
range: Subresource::Texture(range),
origin: BarrierOrigin::Command(CommandIndex(0)),
src_stage: vk::PipelineStageFlags2::TRANSFER,
src_access: vk::AccessFlags2::TRANSFER_WRITE,
dst_stage: vk::PipelineStageFlags2::VERTEX_SHADER,
dst_access: vk::AccessFlags2::SHADER_READ,
old_layout: Some(vk::ImageLayout::GENERAL),
new_layout: Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL),
queue_transfer: None,
}]
.into_iter()
.collect(),
},
PipelineBarrier {
before_command: CommandIndex(3),
memory_barriers: Vec::new(),
resources: [BarrierResource {
id,
range: Subresource::Texture(range),
origin: BarrierOrigin::Command(CommandIndex(0)),
src_stage: vk::PipelineStageFlags2::TRANSFER,
src_access: vk::AccessFlags2::TRANSFER_WRITE,
dst_stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
dst_access: vk::AccessFlags2::SHADER_SAMPLED_READ,
old_layout: Some(vk::ImageLayout::GENERAL),
new_layout: Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL),
queue_transfer: None,
}]
.into_iter()
.collect(),
},
];
let spans = [RenderPassSpan {
begin: CommandIndex(1),
end: CommandIndex(4),
}];
hoist_span_barriers(&mut barriers, &spans, 5);
let merged = barriers
.iter()
.find(|barrier| barrier.before_command == CommandIndex(1))
.unwrap();
assert_eq!(merged.resources.len(), 1);
assert_eq!(
merged.resources[0].dst_stage,
vk::PipelineStageFlags2::VERTEX_SHADER | vk::PipelineStageFlags2::FRAGMENT_SHADER
);
assert_eq!(
merged.resources[0].dst_access,
vk::AccessFlags2::SHADER_READ | vk::AccessFlags2::SHADER_SAMPLED_READ
);
}
#[test]
fn overlapping_pure_reads_without_layout_change_need_no_barrier() {
let resource = ResourceId::from_u32(0);
let resources = [ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 64,
..Default::default()
})];
let accesses = vec![
vec![scoped_access(
resource,
0,
64,
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
)],
vec![scoped_access(
resource,
16,
16,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::AccessFlags2::UNIFORM_READ,
)],
];
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let batches = topo_batches_for_test(&compiler).expect("graph is acyclic");
assert!(
compiler
.build_barriers(&flatten_batches(&batches))
.is_empty()
);
}
#[test]
fn write_before_contained_read_emits_barrier_before_read() {
let resource = ResourceId::from_u32(0);
let resources = [ResourceDesc::CreatedBuffer(GraphBufferDesc {
size: 64,
..Default::default()
})];
let accesses = vec![
vec![scoped_access(
resource,
0,
64,
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
)],
vec![scoped_access(
resource,
16,
16,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::AccessFlags2::UNIFORM_READ,
)],
];
let compiler = GraphCompiler::new(
&resources,
&accesses,
accesses.len(),
&SideEffectMap::default(),
&[],
);
let batches = topo_batches_for_test(&compiler).expect("graph is acyclic");
let barriers = compiler.build_barriers(&flatten_batches(&batches));
assert_eq!(batches, [vec![CommandIndex(0)], vec![CommandIndex(1)]]);
assert_eq!(barriers.len(), 1);
assert_eq!(barriers[0].before_command, CommandIndex(1));
assert_eq!(
barriers[0].resources[0].src_access,
vk::AccessFlags2::TRANSFER_WRITE
);
assert_eq!(
barriers[0].resources[0].dst_stage,
vk::PipelineStageFlags2::FRAGMENT_SHADER
);
assert_eq!(
barriers[0].resources[0].dst_access,
vk::AccessFlags2::UNIFORM_READ
);
}
#[test]
fn independent_commands_share_a_batch_and_dependencies_advance_to_later_batches() {
let mut graph = RenderGraph::new();
let a = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let shared = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let b = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let independent_src = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let independent_dst = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
graph.push(CopyBuffer {
src: a,
src_range: BufferRange { offset: 0, size: 8 },
dst: shared,
dst_range: BufferRange { offset: 0, size: 8 },
});
graph.push(CopyBuffer {
src: shared,
src_range: BufferRange { offset: 0, size: 8 },
dst: b,
dst_range: BufferRange { offset: 0, size: 8 },
});
graph.push(CopyBuffer {
src: independent_src,
src_range: BufferRange { offset: 0, size: 8 },
dst: independent_dst,
dst_range: BufferRange { offset: 0, size: 8 },
});
let compiled = graph.compile().expect("graph compiles");
assert_eq!(
compiled.batches().collect::<Vec<_>>(),
[
&[CommandIndex(0), CommandIndex(2)][..],
&[CommandIndex(1)][..]
]
);
assert_eq!(
compiled.batches().flatten().copied().collect::<Vec<_>>(),
vec![CommandIndex(0), CommandIndex(2), CommandIndex(1)]
);
assert_eq!(compiled.barriers().len(), 1);
assert_eq!(compiled.barriers()[0].before_command, CommandIndex(1));
let batch_for = |command| {
compiled
.batches()
.position(|batch| batch.contains(&command))
.expect("command appears in a batch")
};
for barrier in compiled.barriers() {
assert!(
batch_for(CommandIndex(0)) < batch_for(barrier.before_command),
"barrier target follows its conflicting source in another batch"
);
}
}
#[test]
fn recording_steps_place_barriers_immediately_before_their_target_command() {
let first_barrier = PipelineBarrier {
before_command: CommandIndex(1),
memory_barriers: Vec::new(),
resources: tinyvec::TinyVec::new(),
};
let second_barrier = PipelineBarrier {
before_command: CommandIndex(1),
memory_barriers: Vec::new(),
resources: tinyvec::TinyVec::new(),
};
let barriers = [first_barrier, second_barrier];
let batches = [vec![CommandIndex(0), CommandIndex(1)]];
let execution_order = flatten_batches(&batches);
let steps = recording_steps(&execution_order, &barriers).collect::<Vec<_>>();
assert!(matches!(steps[0], RecordingStep::Command(CommandIndex(0))));
assert!(
matches!(steps[1], RecordingStep::Barrier(barrier) if std::ptr::eq(barrier, &barriers[0]))
);
assert!(
matches!(steps[2], RecordingStep::Barrier(barrier) if std::ptr::eq(barrier, &barriers[1]))
);
assert!(matches!(steps[3], RecordingStep::Command(CommandIndex(1))));
}
#[test]
fn barrier_for_later_command_follows_independent_command_in_same_batch() {
let mut graph = RenderGraph::new();
let first_src = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let first_shared = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let first_dst = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let second_src = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let second_shared = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let second_dst = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let copy = |src, dst| CopyBuffer {
src,
src_range: BufferRange { offset: 0, size: 8 },
dst,
dst_range: BufferRange { offset: 0, size: 8 },
};
graph.push(copy(first_src, first_shared));
graph.push(copy(second_src, second_shared));
graph.push(copy(second_shared, second_dst));
graph.push(copy(first_shared, first_dst));
let compiled = graph.compile().expect("graph compiles");
assert_eq!(
compiled.batches().collect::<Vec<_>>(),
[
&[CommandIndex(0), CommandIndex(1)][..],
&[CommandIndex(2), CommandIndex(3)][..],
]
);
let steps =
recording_steps(compiled.execution_order(), compiled.barriers()).collect::<Vec<_>>();
let command_two_position = steps
.iter()
.position(|step| matches!(step, RecordingStep::Command(CommandIndex(2))))
.expect("independent command is recorded");
let barrier_three_position = steps
.iter()
.position(|step| {
matches!(step, RecordingStep::Barrier(barrier) if barrier.before_command == CommandIndex(3))
})
.expect("barrier for command three is recorded");
assert!(command_two_position < barrier_three_position);
}
#[test]
fn dependency_chain_occupies_three_batches() {
let mut graph = RenderGraph::new();
let a = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let b = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let c = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let d = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
graph.push(CopyBuffer {
src: a,
src_range: BufferRange { offset: 0, size: 8 },
dst: b,
dst_range: BufferRange { offset: 0, size: 8 },
});
graph.push(CopyBuffer {
src: b,
src_range: BufferRange { offset: 0, size: 8 },
dst: c,
dst_range: BufferRange { offset: 0, size: 8 },
});
graph.push(CopyBuffer {
src: c,
src_range: BufferRange { offset: 0, size: 8 },
dst: d,
dst_range: BufferRange { offset: 0, size: 8 },
});
let compiled = graph.compile().expect("graph compiles");
assert_eq!(
compiled.batches().collect::<Vec<_>>(),
[
&[CommandIndex(0)][..],
&[CommandIndex(1)][..],
&[CommandIndex(2)][..],
]
);
assert!(compiled.happens_before(CommandIndex(0), CommandIndex(2)));
assert!(compiled.resources_can_alias(a.id(), d.id()));
for barrier in compiled.barriers() {
let source_command = CommandIndex(barrier.before_command.index() - 1);
let source_batch = compiled
.batches()
.position(|batch| batch.contains(&source_command))
.expect("conflicting source appears in a batch");
let target_batch = compiled
.batches()
.position(|batch| batch.contains(&barrier.before_command))
.expect("barrier target appears in a batch");
assert!(source_batch < target_batch);
}
}
#[test]
fn texture_lifetime_size_waits_for_device_requirements() {
let mut graph = RenderGraph::new();
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 4,
height: 4,
depth: 1,
},
..Default::default()
});
let buffer = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
graph.push(ClearTexture {
texture,
range: vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
},
layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
color: [0.0; 4],
});
// Keep the buffer live in the same command, so its slot follows the image.
graph.commands.side_effects.insert(
CommandIndex(0),
buffer.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::CLEAR,
access: vk::AccessFlags2::TRANSFER_WRITE,
subresource: SubresourceScopeInner::Buffer(BufferRange { offset: 0, size: 8 }),
},
);
let compiled = graph.compile().expect("graph compiles");
let texture_placement = compiled.resource_lifetimes()[&texture.id()].placement;
let buffer_placement = compiled.resource_lifetimes()[&buffer.id()].placement;
assert_eq!(texture_placement.size, 0);
assert_eq!(buffer_placement.size, 0);
assert_eq!(texture_placement.offset, 0);
assert_eq!(buffer_placement.offset, 0);
}
#[test]
fn copy_buffer_derives_transfer_usages_for_source_and_destination() {
let mut graph = RenderGraph::new();
let src = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let dst = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
graph.push(CopyBuffer {
src,
src_range: BufferRange {
offset: 0,
size: 64,
},
dst,
dst_range: BufferRange {
offset: 0,
size: 64,
},
});
graph.push(CopyBuffer {
src: dst,
src_range: BufferRange {
offset: 0,
size: 64,
},
dst: src,
dst_range: BufferRange {
offset: 0,
size: 64,
},
});
let accesses = graph.commands.side_effects.command_accesses();
let src_accesses = accesses_for_resource(src.id(), accesses, &graph.commands.side_effects);
let dst_accesses = accesses_for_resource(dst.id(), accesses, &graph.commands.side_effects);
assert_eq!(
derive_buffer_usage(&src_accesses, vk::BufferUsageFlags::empty()),
vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST
);
assert_eq!(
derive_buffer_usage(&dst_accesses, vk::BufferUsageFlags::empty()),
vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST
);
}
#[test]
fn clear_texture_derives_transfer_destination_usage() {
let mut graph = RenderGraph::new();
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
graph.push(ClearTexture {
texture,
range: vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
},
layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
color: [0.0; 4],
});
let accesses = accesses_for_resource(
texture.id(),
graph.commands.side_effects.command_accesses(),
&graph.commands.side_effects,
);
assert_eq!(
derive_image_usage(&accesses, vk::ImageUsageFlags::empty()),
vk::ImageUsageFlags::TRANSFER_DST
);
}
#[test]
fn extra_usage_is_preserved_and_combined_with_derived_usage() {
let mut graph = RenderGraph::new();
let buffer = graph.create_buffer(GraphBufferDesc {
size: 16,
extra_usage: vk::BufferUsageFlags::UNIFORM_BUFFER,
..Default::default()
});
graph.push(CopyBuffer {
src: buffer,
src_range: BufferRange {
offset: 0,
size: 16,
},
dst: buffer,
dst_range: BufferRange {
offset: 0,
size: 16,
},
});
let accesses = accesses_for_resource(
buffer.id(),
graph.commands.side_effects.command_accesses(),
&graph.commands.side_effects,
);
assert_eq!(
derive_buffer_usage(&accesses, vk::BufferUsageFlags::UNIFORM_BUFFER),
vk::BufferUsageFlags::TRANSFER_SRC
| vk::BufferUsageFlags::TRANSFER_DST
| vk::BufferUsageFlags::UNIFORM_BUFFER
);
}
#[test]
fn pipeline_stages_map_to_queue_capabilities() {
let accesses = [
resource_access(
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
),
resource_access(
vk::PipelineStageFlags2::COMPUTE_SHADER,
vk::AccessFlags2::SHADER_WRITE,
),
resource_access(
vk::PipelineStageFlags2::COPY | vk::PipelineStageFlags2::CLEAR,
vk::AccessFlags2::TRANSFER_WRITE,
),
resource_access(vk::PipelineStageFlags2::NONE, vk::AccessFlags2::NONE),
];
assert_eq!(
derive_queue_capabilities(&accesses),
QueueFlags::GRAPHICS | QueueFlags::ASYNC_COMPUTE | QueueFlags::TRANSFER
);
}
fn resource_access(
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
) -> crate::recorder::ResourceAccess {
crate::recorder::ResourceAccess {
resource: ResourceId::from_u32(0),
scope: SubresourceScope {
stage,
access,
subresource: SubresourceScopeInner::Buffer(BufferRange::full()),
},
}
}
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()),
},
}
}
fn scoped_access(
resource: ResourceId,
offset: u64,
size: u64,
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
) -> crate::recorder::ResourceAccess {
crate::recorder::ResourceAccess {
resource,
scope: SubresourceScope {
stage,
access,
subresource: SubresourceScopeInner::Buffer(BufferRange { offset, size }),
},
}
}
fn texture_range(
aspect_mask: vk::ImageAspectFlags,
base_mip_level: u32,
level_count: u32,
base_array_layer: u32,
layer_count: u32,
) -> vk::ImageSubresourceRange {
vk::ImageSubresourceRange {
aspect_mask,
base_mip_level,
level_count,
base_array_layer,
layer_count,
}
}
fn texture_scoped_access(
resource: ResourceId,
range: vk::ImageSubresourceRange,
layout: vk::ImageLayout,
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
) -> crate::recorder::ResourceAccess {
crate::recorder::ResourceAccess {
resource,
scope: SubresourceScope {
stage,
access,
subresource: SubresourceScopeInner::Texture { range, layout },
},
}
}
}