blob: 541ccbb428885279e4b7b1dc8646dbdc1185284c [file] [log] [blame]
// 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>>,
}