// Copyright (c) 2016 The vulkano developers // Licensed under the Apache License, Version 2.0 // or the MIT // license , // 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; /// Represents a command buffer that has been allocated and that is currently being built. type Builder: CommandBufferBuilderAlloc; /// 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; } /// 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, // Each queue family index points directly to its entry. pools: ThreadLocal>; 8]>>, create_info: StandardCommandBufferAllocatorCreateInfo, } impl StandardCommandBufferAllocator { /// Creates a new `StandardCommandBufferAllocator`. #[inline] pub fn new(device: Arc, 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 { 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 { // 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 { 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).allocate(queue_family_index, level, command_buffer_count) } } unsafe impl DeviceOwned for StandardCommandBufferAllocator { #[inline] fn device(&self) -> &Arc { &self.device } } #[derive(Debug)] struct Entry { // Contains the actual Vulkan command pool that is currently in use. pool: Arc, // When a `Pool` is dropped, it returns itself here for reuse. reserve: Arc>, } // This is needed because of the blanket impl of `Send` on `Arc`, 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, // Where we return the `PoolInner` in our `Drop` impl. reserve: Arc>, } #[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>, // List of existing secondary command buffers that are available for reuse. secondary_pool: Option>, // How many command buffers have been allocated from `self.primary_pool`. primary_allocations: Cell, // How many command buffers have been allocated from `self.secondary_pool`. secondary_allocations: Cell, } impl Pool { fn new( device: Arc, queue_family_index: u32, reserve: Arc>, create_info: &StandardCommandBufferAllocatorCreateInfo, ) -> Result, 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, level: CommandBufferLevel, command_buffer_count: u32, ) -> Option> { 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 { self.inner.device() } } /// Command buffer allocated from a [`StandardCommandBufferAllocator`]. pub struct StandardCommandBufferAlloc { // The actual command buffer. Extracted in the `Drop` implementation. inner: ManuallyDrop, // We hold a reference to the pool for our destructor. pool: Arc, } // 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 { 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(); } }