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, pub new_layout: Option, pub queue_transfer: Option, } #[derive(Debug, Clone)] pub struct PipelineBarrier { pub before_command: CommandIndex, pub resources: Vec, } #[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>, } impl ResourceInterference { /// Build a symmetric interference graph from inclusive resource lifetimes. pub fn from_lifetimes( lifetimes: impl IntoIterator, ) -> 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>, indegree: Vec, ready: BinaryHeap>, // TODO: bitvec is_ready: Vec, 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 { 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, 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>, initial: crate::resources::ResourceState, ) -> Buffer { Buffer(self.add_resource(ResourceDesc::ImportedBuffer { resource: resource.into(), initial, })) } pub fn import_texture( &mut self, resource: impl Into>, 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, ) -> GraphDescriptorSet { GraphDescriptorSet { raw, accesses } } pub fn push(&mut self, command: C) -> CommandIndex { self.commands.push(command) } pub fn compile(self) -> Result { 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], command_count: usize, } impl<'a> GraphCompiler<'a> { fn new( resources: &'a [ResourceDesc], accesses: &'a [Vec], 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>, 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]) -> Vec { let initial_states = self .resources .iter() .map(ResourceDesc::initial_state) .collect::>(); self.build_barriers_with_initial_states(batches, &initial_states) } fn build_barriers_with_initial_states( &self, batches: &[Vec], initial_states: &[crate::resources::ResourceState], ) -> Vec { 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 = 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, Option) { 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, Option) { let current_layout = match current.subresource { SubresourceScopeInner::Texture { layout, .. } => Some(layout), SubresourceScopeInner::Buffer(_) => None, }; (initial.layout, current_layout) } fn bind_lifetimes(&self) -> (HashMap, 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, resolved: HashMap, commands: CommandList, batches: Vec>, barriers: Vec, lifetimes: HashMap, interference: ResourceInterference, } enum ResolvedResource { Buffer(Arc), Image(Arc), } impl CompiledGraph { pub fn batches(&self) -> &[Vec] { &self.batches } pub fn barriers(&self) -> &[PipelineBarrier] { &self.barriers } pub fn resource_lifetimes(&self) -> &HashMap { &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], barriers: &'a [PipelineBarrier], ) -> impl Iterator> + '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], ) -> Vec { 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![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![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![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::>(); 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::>(); 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 }, }, } } }