blob: a2ca514690ef96b835b34a39ea7852b596b8286b [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.
//! Traits and types for managing the allocation of command buffers and command pools.
//!
//! In Vulkano, creating a command buffer requires passing an implementation of the
//! [`CommandBufferAllocator`] trait. You can implement this trait yourself, or use the
//! Vulkano-provided [`StandardCommandBufferAllocator`].
use super::{
pool::{
CommandBufferAllocateInfo, CommandPool, CommandPoolAlloc, CommandPoolCreateInfo,
CommandPoolCreationError,
},
CommandBufferLevel,
};
use crate::{
device::{Device, DeviceOwned},
OomError,
};
use crossbeam_queue::ArrayQueue;
use smallvec::{IntoIter, SmallVec};
use std::{
cell::{Cell, UnsafeCell},
error::Error,
fmt::Display,
marker::PhantomData,
mem::ManuallyDrop,
sync::Arc,
thread,
};
use thread_local::ThreadLocal;
const MAX_POOLS: usize = 32;
/// Types that manage the memory of command buffers.
///
/// # Safety
///
/// A Vulkan command pool must be externally synchronized as if it owned the command buffers that
/// were allocated from it. This includes allocating from the pool, freeing from the pool, resetting
/// the pool or individual command buffers, and most importantly recording commands to command
/// buffers. The implementation of `CommandBufferAllocator` is expected to manage this.
///
/// The destructors of the [`CommandBufferBuilderAlloc`] and the [`CommandBufferAlloc`] are expected
/// to free the command buffer, reset the command buffer, or add it to a pool so that it gets
/// reused. If the implementation frees or resets the command buffer, it must not forget that this
/// operation must be externally synchronized.
pub unsafe trait CommandBufferAllocator: DeviceOwned {
/// See [`allocate`](Self::allocate).
type Iter: Iterator<Item = Self::Builder>;
/// Represents a command buffer that has been allocated and that is currently being built.
type Builder: CommandBufferBuilderAlloc<Alloc = Self::Alloc>;
/// Represents a command buffer that has been allocated and that is pending execution or is
/// being executed.
type Alloc: CommandBufferAlloc;
/// Allocates command buffers.
///
/// Returns an iterator that contains the requested amount of allocated command buffers.
fn allocate(
&self,
queue_family_index: u32,
level: CommandBufferLevel,
command_buffer_count: u32,
) -> Result<Self::Iter, OomError>;
}
/// A command buffer allocated from a pool and that can be recorded.
///
/// # Safety
///
/// See [`CommandBufferAllocator`] for information about safety.
pub unsafe trait CommandBufferBuilderAlloc: DeviceOwned {
/// Return type of `into_alloc`.
type Alloc: CommandBufferAlloc;
/// Returns the internal object that contains the command buffer.
fn inner(&self) -> &CommandPoolAlloc;
/// Turns this builder into a command buffer that is pending execution.
fn into_alloc(self) -> Self::Alloc;
/// Returns the index of the queue family that the pool targets.
fn queue_family_index(&self) -> u32;
}
/// A command buffer allocated from a pool that has finished being recorded.
///
/// # Safety
///
/// See [`CommandBufferAllocator`] for information about safety.
pub unsafe trait CommandBufferAlloc: DeviceOwned + Send + Sync + 'static {
/// Returns the internal object that contains the command buffer.
fn inner(&self) -> &CommandPoolAlloc;
/// Returns the index of the queue family that the pool targets.
fn queue_family_index(&self) -> u32;
}
/// Standard implementation of a command buffer allocator.
///
/// The intended way to use this allocator is to have one that is used globally for the duration of
/// the program, in order to avoid creating and destroying [`CommandPool`]s, as that is expensive.
/// Alternatively, you can have one locally on a thread for the duration of the thread.
///
/// Internally, this allocator keeps one or more `CommandPool`s per queue family index per thread,
/// using Thread-Local Storage. When a thread first allocates, an entry is reserved for the thread
/// and queue family combination. After a thread exits and the allocator wasn't dropped yet, its
/// entries are freed, but the pools it used are not dropped. The next time a new thread allocates
/// for the first time, the entries are reused along with the pools. If all threads drop their
/// reference to the allocator, all entries along with the allocator are dropped, even if the
/// threads didn't exit yet, which is why you should keep the allocator alive for as long as you
/// need to allocate so that the pools can keep being reused.
///
/// This allocator only needs to lock when a thread first allocates or when a thread that
/// previously allocated exits. In all other cases, allocation is lock-free.
///
/// Command buffers can't be moved between threads during the building process, but finished command
/// buffers can. When a command buffer is dropped, it is returned back to the pool for reuse.
#[derive(Debug)]
pub struct StandardCommandBufferAllocator {
device: Arc<Device>,
// Each queue family index points directly to its entry.
pools: ThreadLocal<SmallVec<[UnsafeCell<Option<Entry>>; 8]>>,
create_info: StandardCommandBufferAllocatorCreateInfo,
}
impl StandardCommandBufferAllocator {
/// Creates a new `StandardCommandBufferAllocator`.
#[inline]
pub fn new(device: Arc<Device>, create_info: StandardCommandBufferAllocatorCreateInfo) -> Self {
StandardCommandBufferAllocator {
device,
pools: ThreadLocal::new(),
create_info,
}
}
/// Tries to reset the [`CommandPool`] that's currently in use for the given queue family index
/// on the current thread.
///
/// If successful, the memory of the pool can be reused again along with all command buffers
/// allocated from it. This is only possible if all command buffers allocated from the pool
/// have been dropped.
///
/// This has no effect if the entry wasn't initialized yet or if the entry was [cleared].
///
/// # Panics
///
/// - Panics if `queue_family_index` is not less than the number of queue families.
///
/// [cleared]: Self::clear
#[inline]
pub fn try_reset_pool(
&self,
queue_family_index: u32,
release_resources: bool,
) -> Result<(), CommandPoolResetError> {
if let Some(entry) = unsafe { &mut *self.entry(queue_family_index) }.as_mut() {
entry.try_reset_pool(release_resources)
} else {
Ok(())
}
}
/// Clears the entry for the given queue family index and the current thread. This does not
/// mean that the pools are dropped immediately. A pool is kept alive for as long as command
/// buffers allocated from it exist.
///
/// This has no effect if the entry was not initialized yet.
///
/// # Panics
///
/// - Panics if `queue_family_index` is not less than the number of queue families.
#[inline]
pub fn clear(&self, queue_family_index: u32) {
unsafe { *self.entry(queue_family_index) = None };
}
fn entry(&self, queue_family_index: u32) -> *mut Option<Entry> {
let pools = self.pools.get_or(|| {
self.device
.physical_device()
.queue_family_properties()
.iter()
.map(|_| UnsafeCell::new(None))
.collect()
});
pools[queue_family_index as usize].get()
}
}
unsafe impl CommandBufferAllocator for StandardCommandBufferAllocator {
type Iter = IntoIter<[StandardCommandBufferBuilderAlloc; 1]>;
type Builder = StandardCommandBufferBuilderAlloc;
type Alloc = StandardCommandBufferAlloc;
/// Allocates command buffers.
///
/// Returns an iterator that contains the requested amount of allocated command buffers.
///
/// # Panics
///
/// - Panics if the queue family index is not active on the device.
/// - Panics if `command_buffer_count` exceeds the count configured for the pool corresponding
/// to `level`.
#[inline]
fn allocate(
&self,
queue_family_index: u32,
level: CommandBufferLevel,
command_buffer_count: u32,
) -> Result<Self::Iter, OomError> {
// VUID-vkCreateCommandPool-queueFamilyIndex-01937
assert!(self
.device
.active_queue_family_indices()
.contains(&queue_family_index));
let entry = unsafe { &mut *self.entry(queue_family_index) };
if entry.is_none() {
let reserve = Arc::new(ArrayQueue::new(MAX_POOLS));
*entry = Some(Entry {
pool: Pool::new(
self.device.clone(),
queue_family_index,
reserve.clone(),
&self.create_info,
)?,
reserve,
});
}
let entry = entry.as_mut().unwrap();
// First try to allocate from existing command buffers.
if let Some(allocs) = entry.pool.allocate(level, command_buffer_count) {
return Ok(allocs);
}
// Else try to reset the pool.
if entry.try_reset_pool(false).is_err() {
// If that fails too try to grab a pool from the reserve.
entry.pool = if let Some(inner) = entry.reserve.pop() {
Arc::new(Pool {
inner: ManuallyDrop::new(inner),
reserve: entry.reserve.clone(),
})
} else {
// Else we are unfortunately forced to create a new pool.
Pool::new(
self.device.clone(),
queue_family_index,
entry.reserve.clone(),
&self.create_info,
)?
};
}
Ok(entry.pool.allocate(level, command_buffer_count).unwrap())
}
}
unsafe impl CommandBufferAllocator for Arc<StandardCommandBufferAllocator> {
type Iter = IntoIter<[StandardCommandBufferBuilderAlloc; 1]>;
type Builder = StandardCommandBufferBuilderAlloc;
type Alloc = StandardCommandBufferAlloc;
#[inline]
fn allocate(
&self,
queue_family_index: u32,
level: CommandBufferLevel,
command_buffer_count: u32,
) -> Result<Self::Iter, OomError> {
(**self).allocate(queue_family_index, level, command_buffer_count)
}
}
unsafe impl DeviceOwned for StandardCommandBufferAllocator {
#[inline]
fn device(&self) -> &Arc<Device> {
&self.device
}
}
#[derive(Debug)]
struct Entry {
// Contains the actual Vulkan command pool that is currently in use.
pool: Arc<Pool>,
// When a `Pool` is dropped, it returns itself here for reuse.
reserve: Arc<ArrayQueue<PoolInner>>,
}
// This is needed because of the blanket impl of `Send` on `Arc<T>`, which requires that `T` is
// `Send + Sync`. `Pool` is `Send + !Sync` because `CommandPool` is `!Sync`. That's fine however
// because we never access the Vulkan command pool concurrently. Same goes for the `Cell`s.
unsafe impl Send for Entry {}
impl Entry {
fn try_reset_pool(&mut self, release_resources: bool) -> Result<(), CommandPoolResetError> {
if let Some(pool) = Arc::get_mut(&mut self.pool) {
unsafe { pool.inner.inner.reset(release_resources) }
.map_err(|_| CommandPoolResetError::OutOfDeviceMemory)?;
*pool.inner.primary_allocations.get_mut() = 0;
*pool.inner.secondary_allocations.get_mut() = 0;
Ok(())
} else {
Err(CommandPoolResetError::InUse)
}
}
}
#[derive(Debug)]
struct Pool {
inner: ManuallyDrop<PoolInner>,
// Where we return the `PoolInner` in our `Drop` impl.
reserve: Arc<ArrayQueue<PoolInner>>,
}
#[derive(Debug)]
struct PoolInner {
// The Vulkan pool specific to a device's queue family.
inner: CommandPool,
// List of existing primary command buffers that are available for reuse.
primary_pool: Option<ArrayQueue<CommandPoolAlloc>>,
// List of existing secondary command buffers that are available for reuse.
secondary_pool: Option<ArrayQueue<CommandPoolAlloc>>,
// How many command buffers have been allocated from `self.primary_pool`.
primary_allocations: Cell<usize>,
// How many command buffers have been allocated from `self.secondary_pool`.
secondary_allocations: Cell<usize>,
}
impl Pool {
fn new(
device: Arc<Device>,
queue_family_index: u32,
reserve: Arc<ArrayQueue<PoolInner>>,
create_info: &StandardCommandBufferAllocatorCreateInfo,
) -> Result<Arc<Self>, OomError> {
let inner = CommandPool::new(
device,
CommandPoolCreateInfo {
queue_family_index,
..Default::default()
},
)
.map_err(|err| match err {
CommandPoolCreationError::OomError(err) => err,
// We check that the provided queue family index is active on the device, so it can't
// be out of range.
CommandPoolCreationError::QueueFamilyIndexOutOfRange { .. } => unreachable!(),
})?;
let primary_pool = if create_info.primary_buffer_count > 0 {
let pool = ArrayQueue::new(create_info.primary_buffer_count);
for alloc in inner.allocate_command_buffers(CommandBufferAllocateInfo {
level: CommandBufferLevel::Primary,
command_buffer_count: create_info.primary_buffer_count as u32,
..Default::default()
})? {
let _ = pool.push(alloc);
}
Some(pool)
} else {
None
};
let secondary_pool = if create_info.secondary_buffer_count > 0 {
let pool = ArrayQueue::new(create_info.secondary_buffer_count);
for alloc in inner.allocate_command_buffers(CommandBufferAllocateInfo {
level: CommandBufferLevel::Secondary,
command_buffer_count: create_info.secondary_buffer_count as u32,
..Default::default()
})? {
let _ = pool.push(alloc);
}
Some(pool)
} else {
None
};
Ok(Arc::new(Pool {
inner: ManuallyDrop::new(PoolInner {
inner,
primary_pool,
secondary_pool,
primary_allocations: Cell::new(0),
secondary_allocations: Cell::new(0),
}),
reserve,
}))
}
fn allocate(
self: &Arc<Self>,
level: CommandBufferLevel,
command_buffer_count: u32,
) -> Option<IntoIter<[StandardCommandBufferBuilderAlloc; 1]>> {
let command_buffer_count = command_buffer_count as usize;
match level {
CommandBufferLevel::Primary => {
if let Some(pool) = &self.inner.primary_pool {
let count = self.inner.primary_allocations.get();
if count + command_buffer_count <= pool.capacity() {
let mut output = SmallVec::<[_; 1]>::with_capacity(command_buffer_count);
for _ in 0..command_buffer_count {
output.push(StandardCommandBufferBuilderAlloc {
inner: StandardCommandBufferAlloc {
inner: ManuallyDrop::new(pool.pop().unwrap()),
pool: self.clone(),
},
_marker: PhantomData,
});
}
self.inner
.primary_allocations
.set(count + command_buffer_count);
Some(output.into_iter())
} else if command_buffer_count > pool.capacity() {
panic!(
"command buffer count ({}) exceeds the capacity of the primary command \
buffer pool ({})",
command_buffer_count, pool.capacity(),
);
} else {
None
}
} else {
panic!(
"attempted to allocate a primary command buffer when the primary command \
buffer pool was configured to be empty",
);
}
}
CommandBufferLevel::Secondary => {
if let Some(pool) = &self.inner.secondary_pool {
let count = self.inner.secondary_allocations.get();
if count + command_buffer_count <= pool.capacity() {
let mut output = SmallVec::<[_; 1]>::with_capacity(command_buffer_count);
for _ in 0..command_buffer_count {
output.push(StandardCommandBufferBuilderAlloc {
inner: StandardCommandBufferAlloc {
inner: ManuallyDrop::new(pool.pop().unwrap()),
pool: self.clone(),
},
_marker: PhantomData,
});
}
self.inner
.secondary_allocations
.set(count + command_buffer_count);
Some(output.into_iter())
} else if command_buffer_count > pool.capacity() {
panic!(
"command buffer count ({}) exceeds the capacity of the secondary \
command buffer pool ({})",
command_buffer_count,
pool.capacity(),
);
} else {
None
}
} else {
panic!(
"attempted to allocate a secondary command buffer when the secondary \
command buffer pool was configured to be empty",
);
}
}
}
}
}
impl Drop for Pool {
fn drop(&mut self) {
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
if thread::panicking() {
return;
}
unsafe { inner.inner.reset(false) }.unwrap();
inner.primary_allocations.set(0);
inner.secondary_allocations.set(0);
// If there is not enough space in the reserve, we destroy the pool. The only way this can
// happen is if something is resource hogging, forcing new pools to be created such that
// the number exceeds `MAX_POOLS`, and then drops them all at once.
let _ = self.reserve.push(inner);
}
}
/// Parameters to create a new [`StandardCommandBufferAllocator`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StandardCommandBufferAllocatorCreateInfo {
/// How many primary command buffers should be allocated per pool.
///
/// Each time a thread allocates using some queue family index, and either no pools were
/// initialized yet or all pools are full, a new pool is created for that thread and queue
/// family combination. This option tells the allocator how many primary command buffers should
/// be allocated for that pool. It always allocates exactly this many command buffers at once
/// for the pool, as that is more performant than allocating them one-by-one. What this means
/// is that you should make sure that this is not too large, so that you don't end up wasting
/// too much memory. You also don't want this to be too low, because that on the other hand
/// would mean that the pool would have to be reset more often, or that more pools would need
/// to be created, depending on the lifetime of the command buffers.
///
/// The default value is `256`.
pub primary_buffer_count: usize,
/// Same as `primary_buffer_count` except for secondary command buffers.
///
/// The default value is `256`.
pub secondary_buffer_count: usize,
pub _ne: crate::NonExhaustive,
}
impl Default for StandardCommandBufferAllocatorCreateInfo {
#[inline]
fn default() -> Self {
StandardCommandBufferAllocatorCreateInfo {
primary_buffer_count: 256,
secondary_buffer_count: 256,
_ne: crate::NonExhaustive(()),
}
}
}
/// Command buffer allocated from a [`StandardCommandBufferAllocator`] that is currently being
/// built.
pub struct StandardCommandBufferBuilderAlloc {
// The only difference between a `StandardCommandBufferBuilder` and a
// `StandardCommandBufferAlloc` is that the former must not implement `Send` and `Sync`.
// Therefore we just share the structs.
inner: StandardCommandBufferAlloc,
// Unimplemented `Send` and `Sync` from the builder.
_marker: PhantomData<*const ()>,
}
unsafe impl CommandBufferBuilderAlloc for StandardCommandBufferBuilderAlloc {
type Alloc = StandardCommandBufferAlloc;
#[inline]
fn inner(&self) -> &CommandPoolAlloc {
self.inner.inner()
}
#[inline]
fn into_alloc(self) -> Self::Alloc {
self.inner
}
#[inline]
fn queue_family_index(&self) -> u32 {
self.inner.queue_family_index()
}
}
unsafe impl DeviceOwned for StandardCommandBufferBuilderAlloc {
#[inline]
fn device(&self) -> &Arc<Device> {
self.inner.device()
}
}
/// Command buffer allocated from a [`StandardCommandBufferAllocator`].
pub struct StandardCommandBufferAlloc {
// The actual command buffer. Extracted in the `Drop` implementation.
inner: ManuallyDrop<CommandPoolAlloc>,
// We hold a reference to the pool for our destructor.
pool: Arc<Pool>,
}
// It's fine to share `Pool` between threads because we never access the Vulkan command pool
// concurrently. Same goes for the `Cell`s.
unsafe impl Send for StandardCommandBufferAlloc {}
unsafe impl Sync for StandardCommandBufferAlloc {}
unsafe impl CommandBufferAlloc for StandardCommandBufferAlloc {
#[inline]
fn inner(&self) -> &CommandPoolAlloc {
&self.inner
}
#[inline]
fn queue_family_index(&self) -> u32 {
self.pool.inner.inner.queue_family_index()
}
}
unsafe impl DeviceOwned for StandardCommandBufferAlloc {
#[inline]
fn device(&self) -> &Arc<Device> {
self.pool.inner.inner.device()
}
}
impl Drop for StandardCommandBufferAlloc {
#[inline]
fn drop(&mut self) {
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
let pool = match inner.level() {
CommandBufferLevel::Primary => &self.pool.inner.primary_pool,
CommandBufferLevel::Secondary => &self.pool.inner.secondary_pool,
};
// This can't panic, because if an allocation from a particular kind of pool was made, then
// the pool must exist.
let _ = pool.as_ref().unwrap().push(inner);
}
}
/// Error that can be returned when resetting a [`CommandPool`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CommandPoolResetError {
/// The `CommandPool` is still in use.
InUse,
/// Out of device memory.
OutOfDeviceMemory,
}
impl Error for CommandPoolResetError {}
impl Display for CommandPoolResetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InUse => write!(f, "the `CommandPool` is still in use"),
Self::OutOfDeviceMemory => write!(f, "out of device memory"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::VulkanObject;
use std::thread;
#[test]
fn threads_use_different_pools() {
let (device, queue) = gfx_dev_and_queue!();
let allocator = StandardCommandBufferAllocator::new(device, Default::default());
let pool1 = allocator
.allocate(queue.queue_family_index(), CommandBufferLevel::Primary, 1)
.unwrap()
.next()
.unwrap()
.into_alloc()
.pool
.inner
.inner
.handle();
thread::spawn(move || {
let pool2 = allocator
.allocate(queue.queue_family_index(), CommandBufferLevel::Primary, 1)
.unwrap()
.next()
.unwrap()
.into_alloc()
.pool
.inner
.inner
.handle();
assert_ne!(pool1, pool2);
})
.join()
.unwrap();
}
}