vidya/crates/renderer_rendergraph/src/graph.rs

2106 lines
72 KiB
Rust

use std::{
cmp::Reverse,
collections::{BTreeSet, BinaryHeap, HashMap, VecDeque},
sync::Arc,
};
use ash::vk;
use ash_formats::FormatExt;
use renderer_resources::{
Device,
buffers::{Buffer as BufferResource, BufferDesc},
device::{AllocationStrategy, QueueFlags},
images::{Image as ImageResource, ImageDesc},
};
use renderer_device::QueueFamilyIndex;
use crate::{
recorder::{CommandIndex, CommandList, CommandRecorder, RecordError, scope_range},
resources::{
Buffer, DescriptorResourceAccess, GraphBufferDesc, GraphDescriptorSet, GraphTextureDesc,
ResourceDesc, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner, Texture,
access_flag_is_write,
},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GraphError {
UnknownResource(ResourceId),
DependencyCycle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueueTransfer {
pub src: QueueFamilyIndex,
pub dst: QueueFamilyIndex,
}
#[derive(Debug, Clone, Copy)]
pub struct BarrierResource {
pub id: ResourceId,
pub range: Subresource,
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>,
}
#[derive(Debug, Clone)]
pub struct PipelineBarrier {
pub before_command: CommandIndex,
pub resources: Vec<BarrierResource>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResourceLifetime {
pub first_command: CommandIndex,
pub last_command: CommandIndex,
/// Placeholder allocator binding; resources with disjoint lifetimes can
/// share an offset when their descriptor sizes permit it.
pub memory_offset: u64,
}
impl ResourceLifetime {
fn overlaps(&self, other: &Self) -> bool {
self.first_command <= other.last_command && other.first_command <= self.last_command
}
}
/// The resource-level interference graph used by transient-resource aliasing.
/// An edge means the two resources are live at the same command index.
#[derive(Debug, Clone, Default)]
pub struct ResourceInterference {
edges: Vec<BTreeSet<ResourceId>>,
}
impl ResourceInterference {
/// Build a symmetric interference graph from inclusive resource lifetimes.
pub fn from_lifetimes(
lifetimes: impl IntoIterator<Item = (ResourceId, ResourceLifetime)>,
) -> Self {
let mut max_id = ResourceId(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_command);
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_command < lifetime.first_command {
resources.swap_remove_front(j);
index -= 1;
} else {
j += 1;
}
}
index += 1;
// resources is no longer sorted in the interval ..index, but because of the invariant `last_command >= first_command`, it is still partitioned by `a.last_command <= b.first_command`.
let active = resources
.partition_point(|&(_, other)| other.first_command <= lifetime.last_command);
for &(other_id, _) in resources.range(..active) {
edges[id.index()].insert(other_id);
edges[other_id.index()].insert(id);
}
}
Self { edges }
}
/// Returns whether both resources are live at any common command index.
pub fn interferes(&self, a: ResourceId, b: ResourceId) -> bool {
self.edges[a.index()].contains(&b)
}
}
/// Deterministic Kahn topological-sort state for command dependencies.
struct Kahn {
edges: Vec<Vec<usize>>,
indegree: Vec<usize>,
ready: BinaryHeap<Reverse<usize>>,
// TODO: bitvec
is_ready: Vec<bool>,
emitted: usize,
total: usize,
}
impl Kahn {
fn new(node_count: usize) -> Self {
Self {
edges: vec![Vec::new(); node_count],
indegree: vec![0; node_count],
ready: (0..node_count).map(Reverse).collect(),
is_ready: vec![true; node_count],
emitted: 0,
total: node_count,
}
}
fn add_edge(&mut self, from: usize, to: usize) {
let edges = &mut self.edges[from];
if let Err(index) = edges.binary_search(&to) {
edges.insert(index, to);
self.indegree[to] += 1;
if self.is_ready[to] {
self.is_ready[to] = false;
}
}
}
fn pop_batch(&mut self) -> Vec<usize> {
let mut batch = Vec::new();
while let Some(Reverse(node)) = self.ready.pop() {
if self.is_ready[node] {
self.is_ready[node] = false;
batch.push(node);
}
}
batch
}
fn complete(&mut self, node: usize) {
self.emitted += 1;
for &dependent in &self.edges[node] {
self.indegree[dependent] -= 1;
if self.indegree[dependent] == 0 {
self.is_ready[dependent] = true;
self.ready.push(Reverse(dependent));
}
}
}
fn is_complete(&self) -> bool {
self.emitted == self.total
}
}
pub struct RenderGraph {
next_id: u64,
resources: Vec<ResourceDesc>,
pub commands: CommandList,
}
impl RenderGraph {
pub fn new() -> Self {
Self {
next_id: 0,
resources: Vec::new(),
commands: CommandList::new(),
}
}
fn add_resource(&mut self, desc: ResourceDesc) -> ResourceId {
let id = ResourceId(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,
) -> Buffer {
Buffer(self.add_resource(ResourceDesc::ImportedBuffer {
resource: resource.into(),
initial,
}))
}
pub fn import_texture(
&mut self,
resource: impl Into<Arc<ImageResource>>,
initial: crate::resources::ResourceState,
) -> Texture {
Texture(self.add_resource(ResourceDesc::ImportedTexture {
resource: resource.into(),
initial,
}))
}
/// Associate graph-visible resource accesses with a raw Vulkan descriptor set.
///
/// The caller is responsible for declaring every resource referenced by the
/// set accurately; compilation validates that each declared resource belongs
/// to this graph, but cannot inspect the raw descriptor contents.
pub fn track_descriptor_set(
&self,
raw: vk::DescriptorSet,
accesses: Vec<DescriptorResourceAccess>,
) -> GraphDescriptorSet {
GraphDescriptorSet { raw, accesses }
}
pub fn push<C: crate::commands::Command + 'static>(&mut self, command: C) -> CommandIndex {
self.commands.push(command)
}
pub fn compile(self) -> Result<CompiledGraph, GraphError> {
let command_count = self.commands.len();
let accesses = self.commands.side_effects.command_accesses();
let compiler = GraphCompiler::new(&self.resources, accesses, command_count);
let batches = compiler.topo_batches()?;
compiler.validate_resources()?;
let barriers = compiler.build_barriers(&batches);
let (lifetimes, interference) = compiler.bind_lifetimes();
Ok(CompiledGraph {
resources: self.resources,
resolved: HashMap::new(),
commands: self.commands,
batches,
barriers,
lifetimes,
interference,
})
}
}
struct GraphCompiler<'a> {
resources: &'a [ResourceDesc],
accesses: &'a [Vec<crate::recorder::ResourceAccess>],
command_count: usize,
}
impl<'a> GraphCompiler<'a> {
fn new(
resources: &'a [ResourceDesc],
accesses: &'a [Vec<crate::recorder::ResourceAccess>],
command_count: usize,
) -> Self {
Self {
resources,
accesses,
command_count,
}
}
fn dependencies(&self) -> Kahn {
let mut kahn = Kahn::new(self.command_count);
for current in 0..self.command_count {
for prior in 0..current {
let dependency = self.accesses[prior].iter().any(|earlier| {
self.accesses[current].iter().any(|later| {
earlier.resource == later.resource && earlier.scope.conflicts(&later.scope)
})
});
if dependency {
kahn.add_edge(prior, current);
}
}
}
kahn
}
fn topo_batches(&self) -> Result<Vec<Vec<CommandIndex>>, GraphError> {
let mut kahn = self.dependencies();
let mut batches = Vec::new();
while !kahn.is_complete() {
let batch = kahn.pop_batch();
if batch.is_empty() {
return Err(GraphError::DependencyCycle);
}
let batch = batch
.into_iter()
.inspect(|&index| kahn.complete(index))
.map(CommandIndex::from)
.collect();
batches.push(batch);
}
Ok(batches)
}
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, batches: &[Vec<CommandIndex>]) -> Vec<PipelineBarrier> {
let initial_states = self
.resources
.iter()
.map(ResourceDesc::initial_state)
.collect::<Vec<_>>();
self.build_barriers_with_initial_states(batches, &initial_states)
}
fn build_barriers_with_initial_states(
&self,
batches: &[Vec<CommandIndex>],
initial_states: &[crate::resources::ResourceState],
) -> Vec<PipelineBarrier> {
let mut batch_indices = vec![usize::MAX; self.command_count];
for (batch_index, batch) in batches.iter().enumerate() {
for command in batch {
batch_indices[command.index()] = batch_index;
}
}
let mut result: Vec<PipelineBarrier> = Vec::new();
for (ordered_index, &command_index) in batches.iter().flatten().enumerate() {
let mut barrier_resources = Vec::new();
for current in &self.accesses[command_index.index()] {
// The most recent conflicting access is the complete source scope
// for this barrier; prior barrier masks are not carried forward.
let previous = batches
.iter()
.flatten()
.take(ordered_index)
.filter_map(|&prior_command| {
self.accesses[prior_command.index()]
.iter()
.rev()
.find(|prior| {
prior.resource == current.resource
&& prior.scope.conflicts(&current.scope)
})
.map(|prior| (prior_command, prior))
})
.last();
if let Some((previous_command, previous)) = previous {
debug_assert!(
batch_indices[previous_command.index()]
< batch_indices[command_index.index()]
);
// Layouts follow the latest conflicting access, which is
// the state the resource actually reached most recently.
let (old_layout, new_layout) = Self::layouts(previous.scope, current.scope);
barrier_resources.push(BarrierResource {
id: current.resource,
range: scope_range(current.scope),
src_stage: previous.scope.stage,
src_access: previous.scope.access,
dst_stage: current.scope.stage,
dst_access: current.scope.access,
old_layout,
new_layout,
queue_transfer: None,
});
continue;
}
let desc = &self.resources[current.resource.0 as usize];
let initial = initial_states[current.resource.0 as usize];
let (old_layout, new_layout) = Self::layouts_from_state(initial, current.scope);
let layout_transition = old_layout != new_layout && new_layout.is_some();
let initial_hazard = matches!(
desc,
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. }
) && !initial.access.is_empty()
&& (access_flag_is_write(initial.access)
|| access_flag_is_write(current.scope.access));
if layout_transition || initial_hazard {
barrier_resources.push(BarrierResource {
id: current.resource,
range: scope_range(current.scope),
src_stage: initial.stage,
src_access: initial.access,
dst_stage: current.scope.stage,
dst_access: current.scope.access,
old_layout,
new_layout,
queue_transfer: None,
});
}
}
if !barrier_resources.is_empty() {
result.push(PipelineBarrier {
before_command: command_index,
resources: barrier_resources,
});
}
}
result
}
fn layouts(
previous: SubresourceScope,
current: SubresourceScope,
) -> (Option<vk::ImageLayout>, Option<vk::ImageLayout>) {
let layout = |scope: SubresourceScope| match scope.subresource {
SubresourceScopeInner::Texture { layout, .. } => Some(layout),
SubresourceScopeInner::Buffer(_) => None,
};
(layout(previous), layout(current))
}
fn layouts_from_state(
initial: crate::resources::ResourceState,
current: SubresourceScope,
) -> (Option<vk::ImageLayout>, Option<vk::ImageLayout>) {
let current_layout = match current.subresource {
SubresourceScopeInner::Texture { layout, .. } => Some(layout),
SubresourceScopeInner::Buffer(_) => None,
};
(initial.layout, current_layout)
}
fn bind_lifetimes(&self) -> (HashMap<ResourceId, ResourceLifetime>, ResourceInterference) {
let mut lifetimes = HashMap::new();
for (command, command_accesses) in self.accesses.iter().enumerate() {
for access in command_accesses {
lifetimes
.entry(access.resource)
.and_modify(|lifetime: &mut ResourceLifetime| {
lifetime.last_command = command.into()
})
.or_insert(ResourceLifetime {
first_command: command.into(),
last_command: command.into(),
memory_offset: 0,
});
}
}
let interference = ResourceInterference::from_lifetimes(
lifetimes
.iter()
// TODO: consider resource usage across multiple queues.
// when two resources do not overlap in time but their last-first commands are not connected via an edge in the dag, they must be marked as interfering.
.filter(|&(id, _)| self.resources[id.0 as usize].is_created())
.map(|(&id, &lifetime)| (id, lifetime)),
);
// Skeleton aliasing policy: resources with non-overlapping lifetimes may
// share a heap offset. The graph test is equivalent to the previous
// strict `last_use < first_command` check (intervals are inclusive).
// Actual size/alignment-aware Vulkan allocation is TODO.
let mut slots: Vec<(u64, ResourceId, u64)> = Vec::new(); // (offset, last occupant, capacity)
let mut next_offset = 0;
let mut ids: Vec<_> = lifetimes.keys().copied().collect();
ids.sort_by_key(|id| lifetimes[id].first_command);
for id in ids {
let lifetime = lifetimes.get_mut(&id).expect("lifetime key exists");
let desc = &self.resources[id.0 as usize];
let size = match desc {
ResourceDesc::CreatedBuffer(desc) => desc.size,
ResourceDesc::CreatedTexture(desc) => texture_size_estimate(desc),
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => 0,
};
if desc.is_created()
&& let Some((offset, last_use, capacity)) = slots
.iter_mut()
.find(|(_, last, cap)| !interference.interferes(*last, id) && *cap >= size)
{
lifetime.memory_offset = *offset;
*last_use = id;
let _ = capacity;
} else {
lifetime.memory_offset = next_offset;
if desc.is_created() {
slots.push((next_offset, id, size));
next_offset = next_offset.saturating_add(size);
}
}
}
(lifetimes, interference)
}
}
impl Default for RenderGraph {
fn default() -> Self {
Self::new()
}
}
pub struct CompiledGraph {
resources: Vec<ResourceDesc>,
resolved: HashMap<ResourceId, ResolvedResource>,
commands: CommandList,
batches: Vec<Vec<CommandIndex>>,
barriers: Vec<PipelineBarrier>,
lifetimes: HashMap<ResourceId, ResourceLifetime>,
interference: ResourceInterference,
}
enum ResolvedResource {
Buffer(Arc<BufferResource>),
Image(Arc<ImageResource>),
}
impl CompiledGraph {
pub fn batches(&self) -> &[Vec<CommandIndex>] {
&self.batches
}
pub fn barriers(&self) -> &[PipelineBarrier] {
&self.barriers
}
pub fn resource_lifetimes(&self) -> &HashMap<ResourceId, ResourceLifetime> {
&self.lifetimes
}
pub fn interference(&self) -> &ResourceInterference {
&self.interference
}
/// Create the graph's transient resources. Compilation remains independent
/// of a Vulkan device, while command recording requires this resolution step
/// for any created resources used by commands.
pub fn resolve(&mut self, device: &Device) -> renderer_resources::Result<()> {
for (index, desc) in self.resources.iter().enumerate() {
let id = ResourceId(index as u64);
if self.resolved.contains_key(&id) {
continue;
}
let accesses = accesses_for_resource(id, self.commands.side_effects.command_accesses());
let resource = match desc {
ResourceDesc::CreatedBuffer(desc) => {
let usage = derive_buffer_usage(&accesses, desc.extra_usage);
let queue_families =
actual_queue_families(device, derive_queue_capabilities(&accesses));
let resource_size = desc.size;
ResolvedResource::Buffer(Arc::new(BufferResource::new(
device.clone(),
BufferDesc {
flags: vk::BufferCreateFlags::empty(),
name: desc.name.clone(),
size: desc.size,
usage,
queue_families,
mem_location: desc.mem_location,
alloc_scheme: allocation_strategy(resource_size),
},
)?))
}
ResourceDesc::CreatedTexture(desc) => {
let usage = derive_image_usage(&accesses, desc.extra_usage);
let queue_families =
actual_queue_families(device, derive_queue_capabilities(&accesses));
let resource_size = texture_size_estimate(desc);
ResolvedResource::Image(Arc::new(ImageResource::new(
device.clone(),
ImageDesc {
flags: vk::ImageCreateFlags::empty(),
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,
queue_families,
layout: desc.layout,
mem_location: desc.mem_location,
alloc_scheme: allocation_strategy(resource_size),
},
)?))
}
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => {
continue;
}
};
self.resolved.insert(id, resource);
}
Ok(())
}
/// 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> {
for (index, desc) in self.resources.iter().enumerate() {
let id = ResourceId(index as u64);
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)),
},
}
}
// TODO: `batches` are currently recorded sequentially into one command buffer;
// record per-batch command buffers and submit sequentially/in parallel, using split barriers/events or timeline semaphores for overlap.
for step in recording_steps(&self.batches, &self.barriers) {
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) => {
self.commands.commands[command_index.index()].apply(recorder)?;
}
}
}
Ok(())
}
}
enum RecordingStep<'a> {
Barrier(&'a PipelineBarrier),
Command(CommandIndex),
}
fn recording_steps<'a>(
batches: &'a [Vec<CommandIndex>],
barriers: &'a [PipelineBarrier],
) -> impl Iterator<Item = RecordingStep<'a>> + 'a {
batches.iter().flatten().flat_map(move |&command_index| {
barriers
.iter()
.filter(move |barrier| barrier.before_command == command_index)
.map(RecordingStep::Barrier)
.chain(std::iter::once(RecordingStep::Command(command_index)))
})
}
/// The skeleton switches to a dedicated allocation at 64 MiB. This is a
/// placeholder heuristic until resource lifetime/aliasing allocation is real.
const DEDICATED_ALLOCATION_THRESHOLD: u64 = 64 * 1024 * 1024;
fn allocation_strategy(size: u64) -> AllocationStrategy {
if size >= DEDICATED_ALLOCATION_THRESHOLD {
AllocationStrategy::Dedicated
} else {
AllocationStrategy::AllocatorManaged
}
}
fn accesses_for_resource(
id: ResourceId,
command_accesses: &[Vec<crate::recorder::ResourceAccess>],
) -> Vec<crate::recorder::ResourceAccess> {
command_accesses
.iter()
.flatten()
.filter(|access| access.resource == id)
.copied()
.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
}
}
fn texture_size_estimate(desc: &GraphTextureDesc) -> u64 {
let texels_per_block = u64::from(desc.format.texels_per_block()).max(1);
let block_size = u64::from(desc.format.block_size());
let mut size = 0u64;
let (mut width, mut height, mut depth) = (
u64::from(desc.extent.width),
u64::from(desc.extent.height),
u64::from(desc.extent.depth),
);
for _ in 0..desc.mip_levels {
size = size.saturating_add(
width
.saturating_mul(height)
.saturating_mul(depth)
.div_ceil(texels_per_block)
.saturating_mul(block_size),
);
width = (width / 2).max(1);
height = (height / 2).max(1);
depth = (depth / 2).max(1);
}
size.saturating_mul(u64::from(desc.array_layers))
.saturating_mul(u64::from(desc.samples.as_raw().max(1)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
commands::{ClearTexture, CopyBuffer, Draw, DrawArgs},
resources::{
BufferRange, BufferSlice, GraphBufferDesc, GraphTextureDesc, IndexBinding, IndexFormat,
IndirectBinding, ResourceDesc, VertexBinding,
},
};
#[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 interference_tracks_overlapping_inclusive_lifetimes() {
let lifetimes = HashMap::from([
(
ResourceId(0),
ResourceLifetime {
first_command: CommandIndex(0),
last_command: CommandIndex(2),
memory_offset: 0,
},
),
(
ResourceId(1),
ResourceLifetime {
first_command: CommandIndex(2),
last_command: CommandIndex(3),
memory_offset: 0,
},
),
(
ResourceId(2),
ResourceLifetime {
first_command: CommandIndex(4),
last_command: CommandIndex(5),
memory_offset: 0,
},
),
]);
let interference = ResourceInterference::from_lifetimes(
lifetimes.iter().map(|(&id, &lifetime)| (id, lifetime)),
);
assert!(interference.interferes(ResourceId(0), ResourceId(1)));
assert!(interference.interferes(ResourceId(1), ResourceId(0)));
assert!(!interference.interferes(ResourceId(0), ResourceId(2)));
assert!(!interference.interferes(ResourceId(1), ResourceId(2)));
}
#[test]
fn aliasing_reuses_non_interfering_slot_and_separates_interfering_resources() {
let resources = 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(0))],
vec![
resource_access_for(ResourceId(0)),
resource_access_for(ResourceId(1)),
],
vec![resource_access_for(ResourceId(0))],
vec![resource_access_for(ResourceId(2))],
];
let compiler = GraphCompiler::new(&resources, &accesses, accesses.len());
let (lifetimes, interference) = compiler.bind_lifetimes();
let long_lived_offset = lifetimes[&ResourceId(0)].memory_offset;
let overlapping_offset = lifetimes[&ResourceId(1)].memory_offset;
let later_offset = lifetimes[&ResourceId(2)].memory_offset;
assert!(interference.interferes(ResourceId(0), ResourceId(1)));
assert_ne!(long_lived_offset, overlapping_offset);
assert!(!interference.interferes(ResourceId(0), ResourceId(2)));
assert!(!interference.interferes(ResourceId(1), ResourceId(2)));
assert!(later_offset == long_lived_offset || later_offset == overlapping_offset);
}
#[test]
fn copy_buffer_compiles_with_dependency_and_barrier() {
let mut graph = RenderGraph::new();
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()
.iter()
.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()));
}
#[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(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(),
});
let accesses = graph.commands.side_effects.command_accesses();
assert_eq!(
derive_buffer_usage(
&accesses_for_resource(vertex.id(), accesses),
vk::BufferUsageFlags::empty()
),
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::VERTEX_BUFFER
);
assert_eq!(
derive_buffer_usage(
&accesses_for_resource(index.id(), accesses),
vk::BufferUsageFlags::empty()
),
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::INDEX_BUFFER
);
assert_eq!(
derive_buffer_usage(
&accesses_for_resource(indirect.id(), accesses),
vk::BufferUsageFlags::empty()
),
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::INDIRECT_BUFFER
);
let compiled = graph.compile().expect("graph compiles");
assert_eq!(compiled.batches().len(), 2);
assert_eq!(
compiled.batches()[0],
[CommandIndex(0), CommandIndex(1), CommandIndex(2)]
);
assert_eq!(compiled.batches()[1], [CommandIndex(3)]);
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(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(),
});
let accesses = graph.commands.side_effects.command_accesses();
assert!(
derive_buffer_usage(
&accesses_for_resource(uniform.id(), accesses),
vk::BufferUsageFlags::empty()
)
.contains(vk::BufferUsageFlags::UNIFORM_BUFFER)
);
assert!(
derive_image_usage(
&accesses_for_resource(texture.id(), accesses),
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
.interference()
.interferes(uniform.id(), texture.id())
);
}
#[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()
.iter()
.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(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());
let batches = compiler.topo_batches().expect("graph is acyclic");
let barriers = compiler.build_barriers(&batches);
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 imported_texture_read_with_different_layout_gets_initial_transition() {
let resource = ResourceId(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());
let batches = compiler.topo_batches().expect("graph is acyclic");
let barriers = compiler.build_barriers_with_initial_states(&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(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());
let batches = compiler.topo_batches().expect("graph is acyclic");
let barriers = compiler.build_barriers(&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 overlapping_pure_reads_without_layout_change_need_no_barrier() {
let resource = ResourceId(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());
let batches = compiler.topo_batches().expect("graph is acyclic");
assert!(compiler.build_barriers(&batches).is_empty());
}
#[test]
fn write_before_contained_read_emits_barrier_before_read() {
let resource = ResourceId(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());
let batches = compiler.topo_batches().expect("graph is acyclic");
let barriers = compiler.build_barriers(&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(),
&[
vec![CommandIndex(0), CommandIndex(2)],
vec![CommandIndex(1)]
]
);
assert_eq!(
compiled
.batches()
.iter()
.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()
.iter()
.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),
resources: Vec::new(),
};
let second_barrier = PipelineBarrier {
before_command: CommandIndex(1),
resources: Vec::new(),
};
let barriers = [first_barrier, second_barrier];
let batches = [vec![CommandIndex(0), CommandIndex(1)]];
let steps = recording_steps(&batches, &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(),
&[
vec![CommandIndex(0), CommandIndex(1)],
vec![CommandIndex(2), CommandIndex(3)]
]
);
let steps = recording_steps(compiled.batches(), 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(),
&[
vec![CommandIndex(0)],
vec![CommandIndex(1)],
vec![CommandIndex(2)]
]
);
for barrier in compiled.barriers() {
let source_command = CommandIndex(barrier.before_command.index() - 1);
let source_batch = compiled
.batches()
.iter()
.position(|batch| batch.contains(&source_command))
.expect("conflicting source appears in a batch");
let target_batch = compiled
.batches()
.iter()
.position(|batch| batch.contains(&barrier.before_command))
.expect("barrier target appears in a batch");
assert!(source_batch < target_batch);
}
}
#[test]
fn texture_aliasing_size_uses_format_block_size() {
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_offset = compiled.resource_lifetimes()[&texture.id()].memory_offset;
let buffer_offset = compiled.resource_lifetimes()[&buffer.id()].memory_offset;
// Equal first-use indices may be assigned in either order by the
// lifetime map, but the image's estimated 64-byte size is respected.
assert!(
texture_offset == 0 && buffer_offset == 64 || buffer_offset == 0 && texture_offset == 8
);
}
#[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);
let dst_accesses = accesses_for_resource(dst.id(), accesses);
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());
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());
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(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 },
},
}
}
}