2106 lines
72 KiB
Rust
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(¤t.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 = ¤t_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 },
|
|
},
|
|
}
|
|
}
|
|
}
|