diff options
Diffstat (limited to 'src/command_buffer/synced/builder.rs')
-rw-r--r-- | src/command_buffer/synced/builder.rs | 748 |
1 files changed, 748 insertions, 0 deletions
diff --git a/src/command_buffer/synced/builder.rs b/src/command_buffer/synced/builder.rs new file mode 100644 index 0000000..541ccbb --- /dev/null +++ b/src/command_buffer/synced/builder.rs @@ -0,0 +1,748 @@ +// Copyright (c) 2016 The vulkano developers +// Licensed under the Apache License, Version 2.0 +// <LICENSE-APACHE or +// https://www.apache.org/licenses/LICENSE-2.0> or the MIT +// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, +// at your option. All files in the project carrying such +// notice may not be copied, modified, or distributed except +// according to those terms. + +pub use self::commands::SyncCommandBufferBuilderBindDescriptorSets; +pub use self::commands::SyncCommandBufferBuilderBindVertexBuffer; +pub use self::commands::SyncCommandBufferBuilderExecuteCommands; +use super::Command; +use super::ResourceFinalState; +use super::ResourceKey; +use super::ResourceLocation; +use super::SyncCommandBuffer; +use crate::buffer::BufferAccess; +use crate::command_buffer::pool::UnsafeCommandPoolAlloc; +use crate::command_buffer::sys::UnsafeCommandBufferBuilder; +use crate::command_buffer::sys::UnsafeCommandBufferBuilderPipelineBarrier; +use crate::command_buffer::CommandBufferExecError; +use crate::command_buffer::CommandBufferLevel; +use crate::command_buffer::CommandBufferUsage; +use crate::command_buffer::ImageUninitializedSafe; +use crate::descriptor_set::DescriptorSet; +use crate::device::Device; +use crate::device::DeviceOwned; +use crate::image::ImageLayout; +use crate::pipeline::{ComputePipelineAbstract, GraphicsPipelineAbstract, PipelineBindPoint}; +use crate::render_pass::FramebufferAbstract; +use crate::sync::AccessFlags; +use crate::sync::PipelineMemoryAccess; +use crate::sync::PipelineStages; +use crate::OomError; +use fnv::FnvHashMap; +use std::borrow::Cow; +use std::collections::hash_map::Entry; +use std::error; +use std::fmt; +use std::sync::Arc; + +#[path = "commands.rs"] +mod commands; + +/// Wrapper around `UnsafeCommandBufferBuilder` that handles synchronization for you. +/// +/// Each method of the `UnsafeCommandBufferBuilder` has an equivalent in this wrapper, except +/// for `pipeline_layout` which is automatically handled. This wrapper automatically builds +/// pipeline barriers, keeps used resources alive and implements the `CommandBuffer` trait. +/// +/// Since the implementation needs to cache commands in a `Vec`, most methods have additional +/// `Send + Sync + 'static` trait requirements on their generics. +/// +/// If this builder finds out that a command isn't valid because of synchronization reasons (eg. +/// trying to copy from a buffer to an image which share the same memory), then an error is +/// returned. +/// Note that all methods are still unsafe, because this builder doesn't check the validity of +/// the commands except for synchronization purposes. The builder may panic if you pass invalid +/// commands. +pub struct SyncCommandBufferBuilder { + // The actual Vulkan command buffer builder. + inner: UnsafeCommandBufferBuilder, + + // Stores all the commands that were added to the sync builder. Some of them are maybe not + // submitted to the inner builder yet. + // Each command owns the resources it uses (buffers, images, pipelines, descriptor sets etc.), + // references to any of these must be indirect in the form of a command index + resource id. + commands: Vec<Arc<dyn Command + Send + Sync>>, + + // Prototype for the pipeline barrier that must be submitted before flushing the commands + // in `commands`. + pending_barrier: UnsafeCommandBufferBuilderPipelineBarrier, + + // Locations within commands that pipeline barriers were inserted. For debugging purposes. + // TODO: present only in cfg(debug_assertions)? + barriers: Vec<usize>, + + // Only the commands before `first_unflushed` have already been sent to the inner + // `UnsafeCommandBufferBuilder`. + first_unflushed: usize, + + // If we're currently inside a render pass, contains the index of the `CmdBeginRenderPass` + // command. + latest_render_pass_enter: Option<usize>, + + // Stores the current state of buffers and images that are in use by the command buffer. + resources: FnvHashMap<ResourceKey, ResourceState>, + + // Resources and their accesses. Used for executing secondary command buffers in a primary. + buffers: Vec<(ResourceLocation, PipelineMemoryAccess)>, + images: Vec<( + ResourceLocation, + PipelineMemoryAccess, + ImageLayout, + ImageLayout, + ImageUninitializedSafe, + )>, + + // State of bindings. + bindings: BindingState, + + // `true` if the builder has been put in an inconsistent state. This happens when + // `append_command` throws an error, because some changes to the internal state have already + // been made at that point and can't be reverted. + // TODO: throw the error in `append_command` _before_ any state changes are made, + // so that this is no longer needed. + is_poisoned: bool, + + // True if we're a secondary command buffer. + is_secondary: bool, +} + +impl SyncCommandBufferBuilder { + /// Builds a new `SyncCommandBufferBuilder`. The parameters are the same as the + /// `UnsafeCommandBufferBuilder::new` function. + /// + /// # Safety + /// + /// See `UnsafeCommandBufferBuilder::new()`. + pub unsafe fn new<F>( + pool_alloc: &UnsafeCommandPoolAlloc, + level: CommandBufferLevel<F>, + usage: CommandBufferUsage, + ) -> Result<SyncCommandBufferBuilder, OomError> + where + F: FramebufferAbstract, + { + let (is_secondary, inside_render_pass) = match level { + CommandBufferLevel::Primary => (false, false), + CommandBufferLevel::Secondary(ref inheritance) => { + (true, inheritance.render_pass.is_some()) + } + }; + + let cmd = UnsafeCommandBufferBuilder::new(pool_alloc, level, usage)?; + Ok(SyncCommandBufferBuilder::from_unsafe_cmd( + cmd, + is_secondary, + inside_render_pass, + )) + } + + /// Builds a `SyncCommandBufferBuilder` from an existing `UnsafeCommandBufferBuilder`. + /// + /// # Safety + /// + /// See `UnsafeCommandBufferBuilder::new()`. + /// + /// In addition to this, the `UnsafeCommandBufferBuilder` should be empty. If it isn't, then + /// you must take into account the fact that the `SyncCommandBufferBuilder` won't be aware of + /// any existing resource usage. + #[inline] + pub unsafe fn from_unsafe_cmd( + cmd: UnsafeCommandBufferBuilder, + is_secondary: bool, + inside_render_pass: bool, + ) -> SyncCommandBufferBuilder { + let latest_render_pass_enter = if inside_render_pass { Some(0) } else { None }; + + SyncCommandBufferBuilder { + inner: cmd, + commands: Vec::new(), + pending_barrier: UnsafeCommandBufferBuilderPipelineBarrier::new(), + barriers: Vec::new(), + first_unflushed: 0, + latest_render_pass_enter, + resources: FnvHashMap::default(), + buffers: Vec::new(), + images: Vec::new(), + bindings: Default::default(), + is_poisoned: false, + is_secondary, + } + } + + // Adds a command to be processed by the builder. + // + // The `resources` argument should contain each buffer or image used by the command. + // The function will take care of handling the pipeline barrier or flushing. + // + // - The index of the resource within the `resources` slice maps to the resource accessed + // through `Command::buffer(..)` or `Command::image(..)`. + // - `PipelineMemoryAccess` must match the way the resource has been used. + // - `start_layout` and `end_layout` designate the image layout that the image is expected to be + // in when the command starts, and the image layout that the image will be transitioned to + // during the command. When it comes to buffers, you should pass `Undefined` for both. + #[inline] + fn append_command<C>( + &mut self, + command: C, + resources: &[( + KeyTy, + Option<( + PipelineMemoryAccess, + ImageLayout, + ImageLayout, + ImageUninitializedSafe, + )>, + )], + ) -> Result<(), SyncCommandBufferBuilderError> + where + C: Command + Send + Sync + 'static, + { + // TODO: see comment for the `is_poisoned` member in the struct + assert!( + !self.is_poisoned, + "The builder has been put in an inconsistent state by a previous error" + ); + + // Note that we don't submit the command to the inner command buffer yet. + let (latest_command_id, end) = { + self.commands.push(Arc::new(command)); + let latest_command_id = self.commands.len() - 1; + let end = self.latest_render_pass_enter.unwrap_or(latest_command_id); + (latest_command_id, end) + }; + let mut last_cmd_buffer = 0; + let mut last_cmd_image = 0; + + for &(resource_ty, resource) in resources { + if let Some((memory, start_layout, end_layout, image_uninitialized_safe)) = resource { + // Anti-dumbness checks. + debug_assert!(memory.exclusive || start_layout == end_layout); + debug_assert!(memory.access.is_compatible_with(&memory.stages)); + debug_assert!(resource_ty != KeyTy::Image || end_layout != ImageLayout::Undefined); + debug_assert!( + resource_ty != KeyTy::Buffer || start_layout == ImageLayout::Undefined + ); + debug_assert!(resource_ty != KeyTy::Buffer || end_layout == ImageLayout::Undefined); + debug_assert_ne!(end_layout, ImageLayout::Preinitialized); + + let (resource_key, resource_index) = match resource_ty { + KeyTy::Buffer => { + let buffer = self.commands[latest_command_id].buffer(last_cmd_buffer); + (ResourceKey::from(buffer), last_cmd_buffer) + } + KeyTy::Image => { + let image = self.commands[latest_command_id].image(last_cmd_image); + (ResourceKey::from(image), last_cmd_image) + } + }; + + match self.resources.entry(resource_key) { + // Situation where this resource was used before in this command buffer. + Entry::Occupied(mut entry) => { + // `collision_cmd_ids` contains the IDs of the commands that we are potentially + // colliding with. + let collision_cmd_ids = &entry.get().command_ids; + debug_assert!(collision_cmd_ids.iter().all(|id| *id <= latest_command_id)); + + let entry_key_resource_index = entry.get().resource_index; + + // Find out if we have a collision with the pending commands. + if memory.exclusive + || entry.get().memory.exclusive + || entry.get().current_layout != start_layout + { + // Collision found between `latest_command_id` and `collision_cmd_id`. + + // We now want to modify the current pipeline barrier in order to handle the + // collision. But since the pipeline barrier is going to be submitted before + // the flushed commands, it would be a mistake if `collision_cmd_id` hasn't + // been flushed yet. + let first_unflushed_cmd_id = self.first_unflushed; + + if collision_cmd_ids + .iter() + .any(|command_id| *command_id >= first_unflushed_cmd_id) + || entry.get().current_layout != start_layout + { + unsafe { + // Flush the pending barrier. + self.inner.pipeline_barrier(&self.pending_barrier); + self.pending_barrier = + UnsafeCommandBufferBuilderPipelineBarrier::new(); + + // Flush the commands if possible, or return an error if not possible. + { + let start = self.first_unflushed; + self.barriers.push(start); // Track inserted barriers + + if let Some(collision_cmd_id) = collision_cmd_ids + .iter() + .find(|command_id| **command_id >= end) + { + // TODO: see comment for the `is_poisoned` member in the struct + self.is_poisoned = true; + + let cmd1 = &self.commands[*collision_cmd_id]; + let cmd2 = &self.commands[latest_command_id]; + + return Err(SyncCommandBufferBuilderError::Conflict { + command1_name: cmd1.name(), + command1_param: match resource_ty { + KeyTy::Buffer => { + cmd1.buffer_name(entry_key_resource_index) + } + KeyTy::Image => { + cmd1.image_name(entry_key_resource_index) + } + }, + command1_offset: *collision_cmd_id, + + command2_name: cmd2.name(), + command2_param: match resource_ty { + KeyTy::Buffer => { + cmd2.buffer_name(resource_index) + } + KeyTy::Image => cmd2.image_name(resource_index), + }, + command2_offset: latest_command_id, + }); + } + for command in &mut self.commands[start..end] { + command.send(&mut self.inner); + } + self.first_unflushed = end; + } + } + } + + entry.get_mut().command_ids.push(latest_command_id); + let entry = entry.into_mut(); + + // Modify the pipeline barrier to handle the collision. + unsafe { + match resource_ty { + KeyTy::Buffer => { + let buf = + self.commands[latest_command_id].buffer(resource_index); + + let b = &mut self.pending_barrier; + b.add_buffer_memory_barrier( + buf, + entry.memory.stages, + entry.memory.access, + memory.stages, + memory.access, + true, + None, + 0, + buf.size(), + ); + } + + KeyTy::Image => { + let img = + self.commands[latest_command_id].image(resource_index); + + let b = &mut self.pending_barrier; + b.add_image_memory_barrier( + img, + img.current_miplevels_access(), + img.current_layer_levels_access(), + entry.memory.stages, + entry.memory.access, + memory.stages, + memory.access, + true, + None, + entry.current_layout, + start_layout, + ); + } + }; + } + + // Update state. + entry.memory = memory; + entry.exclusive_any = true; + if memory.exclusive || end_layout != ImageLayout::Undefined { + // Only modify the layout in case of a write, because buffer operations + // pass `Undefined` for the layout. While a buffer write *must* set the + // layout to `Undefined`, a buffer read must not touch it. + entry.current_layout = end_layout; + } + } else { + // There is no collision. Simply merge the stages and accesses. + // TODO: what about simplifying the newly-constructed stages/accesses? + // this would simplify the job of the driver, but is it worth it? + let entry = entry.into_mut(); + entry.memory.stages |= memory.stages; + entry.memory.access |= memory.access; + } + } + + // Situation where this is the first time we use this resource in this command buffer. + Entry::Vacant(entry) => { + // We need to perform some tweaks if the initial layout requirement of the image + // is different from the first layout usage. + let mut actually_exclusive = memory.exclusive; + let mut actual_start_layout = start_layout; + + if !self.is_secondary + && resource_ty == KeyTy::Image + && start_layout != ImageLayout::Undefined + && start_layout != ImageLayout::Preinitialized + { + let img = self.commands[latest_command_id].image(resource_index); + let initial_layout_requirement = img.initial_layout_requirement(); + + // Checks if the image is initialized and transitions it + // if it isn't + let is_layout_initialized = img.is_layout_initialized(); + + if initial_layout_requirement != start_layout || !is_layout_initialized + { + // Note that we transition from `bottom_of_pipe`, which means that we + // wait for all the previous commands to be entirely finished. This is + // suboptimal, but: + // + // - If we're at the start of the command buffer we have no choice anyway, + // because we have no knowledge about what comes before. + // - If we're in the middle of the command buffer, this pipeline is going + // to be merged with an existing barrier. While it may still be + // suboptimal in some cases, in the general situation it will be ok. + // + unsafe { + let from_layout = if is_layout_initialized { + actually_exclusive = true; + initial_layout_requirement + } else { + if img.preinitialized_layout() { + ImageLayout::Preinitialized + } else { + ImageLayout::Undefined + } + }; + if initial_layout_requirement != start_layout { + actual_start_layout = initial_layout_requirement; + } + let b = &mut self.pending_barrier; + b.add_image_memory_barrier( + img, + img.current_miplevels_access(), + img.current_layer_levels_access(), + PipelineStages { + bottom_of_pipe: true, + ..PipelineStages::none() + }, + AccessFlags::none(), + memory.stages, + memory.access, + true, + None, + from_layout, + start_layout, + ); + img.layout_initialized(); + } + } + } + + entry.insert(ResourceState { + command_ids: vec![latest_command_id], + resource_index, + + memory: PipelineMemoryAccess { + stages: memory.stages, + access: memory.access, + exclusive: actually_exclusive, + }, + exclusive_any: actually_exclusive, + initial_layout: actual_start_layout, + current_layout: end_layout, // TODO: what if we reach the end with Undefined? that's not correct? + image_uninitialized_safe, + }); + } + } + + // Add the resources to the lists + // TODO: Perhaps any barriers for a resource in the secondary command buffer will "protect" + // its accesses so the primary needs less strict barriers. + // Less barriers is more efficient, so worth investigating! + let location = ResourceLocation { + command_id: latest_command_id, + resource_index, + }; + + match resource_ty { + KeyTy::Buffer => { + self.buffers.push((location, memory)); + last_cmd_buffer += 1; + } + KeyTy::Image => { + self.images.push(( + location, + memory, + start_layout, + end_layout, + image_uninitialized_safe, + )); + last_cmd_image += 1; + } + } + } else { + match resource_ty { + KeyTy::Buffer => { + last_cmd_buffer += 1; + } + KeyTy::Image => { + last_cmd_image += 1; + } + } + } + } + + Ok(()) + } + + /// Builds the command buffer and turns it into a `SyncCommandBuffer`. + #[inline] + pub fn build(mut self) -> Result<SyncCommandBuffer, OomError> { + // TODO: see comment for the `is_poisoned` member in the struct + assert!( + !self.is_poisoned, + "The builder has been put in an inconsistent state by a previous error" + ); + + debug_assert!(self.latest_render_pass_enter.is_none() || self.pending_barrier.is_empty()); + + // The commands that haven't been sent to the inner command buffer yet need to be sent. + unsafe { + self.inner.pipeline_barrier(&self.pending_barrier); + let start = self.first_unflushed; + self.barriers.push(start); // Track inserted barriers + for command in &mut self.commands[start..] { + command.send(&mut self.inner); + } + } + + // Transition images to their desired final layout. + if !self.is_secondary { + unsafe { + // TODO: this could be optimized by merging the barrier with the barrier above? + let mut barrier = UnsafeCommandBufferBuilderPipelineBarrier::new(); + + for (key, state) in self + .resources + .iter_mut() + .filter(|(key, _)| matches!(key, ResourceKey::Image(..))) + { + let img = self.commands[state.command_ids[0]].image(state.resource_index); + let requested_layout = img.final_layout_requirement(); + if requested_layout == state.current_layout { + continue; + } + + barrier.add_image_memory_barrier( + img, + img.current_miplevels_access(), + img.current_layer_levels_access(), + state.memory.stages, + state.memory.access, + PipelineStages { + top_of_pipe: true, + ..PipelineStages::none() + }, + AccessFlags::none(), + true, + None, // TODO: queue transfers? + state.current_layout, + requested_layout, + ); + + state.exclusive_any = true; + state.current_layout = requested_layout; + } + + self.inner.pipeline_barrier(&barrier); + } + } + + // Build the final resources states. + let final_resources_states: FnvHashMap<_, _> = { + self.resources + .into_iter() + .map(|(resource, state)| { + let final_state = ResourceFinalState { + command_ids: state.command_ids, + resource_index: state.resource_index, + final_stages: state.memory.stages, + final_access: state.memory.access, + exclusive: state.exclusive_any, + initial_layout: state.initial_layout, + final_layout: state.current_layout, + image_uninitialized_safe: state.image_uninitialized_safe, + }; + (resource, final_state) + }) + .collect() + }; + + Ok(SyncCommandBuffer { + inner: self.inner.build()?, + buffers: self.buffers, + images: self.images, + resources: final_resources_states, + commands: self.commands, + barriers: self.barriers, + }) + } + + /// Returns the descriptor set currently bound to a given set number, or `None` if nothing has + /// been bound yet. + pub(crate) fn bound_descriptor_set( + &self, + pipeline_bind_point: PipelineBindPoint, + set_num: u32, + ) -> Option<(&dyn DescriptorSet, &[u32])> { + self.bindings + .descriptor_sets + .get(&pipeline_bind_point) + .and_then(|sets| { + sets.get(&set_num) + .map(|cmd| cmd.bound_descriptor_set(set_num)) + }) + } + + /// Returns the index buffer currently bound, or `None` if nothing has been bound yet. + pub(crate) fn bound_index_buffer(&self) -> Option<&dyn BufferAccess> { + self.bindings + .index_buffer + .as_ref() + .map(|cmd| cmd.bound_index_buffer()) + } + + /// Returns the compute pipeline currently bound, or `None` if nothing has been bound yet. + pub(crate) fn bound_pipeline_compute(&self) -> Option<&dyn ComputePipelineAbstract> { + self.bindings + .pipeline_compute + .as_ref() + .map(|cmd| cmd.bound_pipeline_compute()) + } + + /// Returns the graphics pipeline currently bound, or `None` if nothing has been bound yet. + pub(crate) fn bound_pipeline_graphics(&self) -> Option<&dyn GraphicsPipelineAbstract> { + self.bindings + .pipeline_graphics + .as_ref() + .map(|cmd| cmd.bound_pipeline_graphics()) + } + + /// Returns the vertex buffer currently bound to a given binding slot number, or `None` if + /// nothing has been bound yet. + pub(crate) fn bound_vertex_buffer(&self, binding_num: u32) -> Option<&dyn BufferAccess> { + self.bindings + .vertex_buffers + .get(&binding_num) + .map(|cmd| cmd.bound_vertex_buffer(binding_num)) + } +} + +unsafe impl DeviceOwned for SyncCommandBufferBuilder { + #[inline] + fn device(&self) -> &Arc<Device> { + self.inner.device() + } +} + +impl fmt::Debug for SyncCommandBufferBuilder { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} + +/// Error returned if the builder detects that there's an unsolvable conflict. +#[derive(Debug, Clone)] +pub enum SyncCommandBufferBuilderError { + /// Unsolvable conflict. + Conflict { + command1_name: &'static str, + command1_param: Cow<'static, str>, + command1_offset: usize, + + command2_name: &'static str, + command2_param: Cow<'static, str>, + command2_offset: usize, + }, + + ExecError(CommandBufferExecError), +} + +impl error::Error for SyncCommandBufferBuilderError {} + +impl fmt::Display for SyncCommandBufferBuilderError { + #[inline] + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match self { + SyncCommandBufferBuilderError::Conflict { .. } => write!(fmt, "unsolvable conflict"), + SyncCommandBufferBuilderError::ExecError(err) => err.fmt(fmt), + } + } +} + +impl From<CommandBufferExecError> for SyncCommandBufferBuilderError { + #[inline] + fn from(val: CommandBufferExecError) -> Self { + SyncCommandBufferBuilderError::ExecError(val) + } +} + +/// Type of resource whose state is to be tracked. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum KeyTy { + Buffer, + Image, +} + +// State of a resource during the building of the command buffer. +#[derive(Debug, Clone)] +struct ResourceState { + // Indices of the commands that contain the resource. + command_ids: Vec<usize>, + + // Index of the resource within the first command in `command_ids`. + resource_index: usize, + + // Memory access of the command that last used this resource. + memory: PipelineMemoryAccess, + + // True if the resource was used in exclusive mode at any point during the building of the + // command buffer. Also true if an image layout transition or queue transfer has been performed. + exclusive_any: bool, + + // Layout at the first use of the resource by the command buffer. Can be `Undefined` if we + // don't care. + initial_layout: ImageLayout, + + // Current layout at this stage of the building. + current_layout: ImageLayout, + + // Extra context of how the image will be used + image_uninitialized_safe: ImageUninitializedSafe, +} + +/// Holds the index of the most recent command that binds a particular resource, or `None` if +/// nothing has been bound yet. +#[derive(Debug, Default)] +struct BindingState { + descriptor_sets: FnvHashMap<PipelineBindPoint, FnvHashMap<u32, Arc<dyn Command + Send + Sync>>>, + index_buffer: Option<Arc<dyn Command + Send + Sync>>, + pipeline_compute: Option<Arc<dyn Command + Send + Sync>>, + pipeline_graphics: Option<Arc<dyn Command + Send + Sync>>, + vertex_buffers: FnvHashMap<u32, Arc<dyn Command + Send + Sync>>, +} |