Skip to main content

vk_graph/pool/
mod.rs

1//! Resource pooling, requesting, and caching types.
2//!
3//! Resource pools provide caching for buffer, image, and acceleration structure resources. Pooled
4//! resources may be requested from a pool using their corresponding information structure.
5//!
6//! Leased resources may be bound directly to a [`Graph`](crate::Graph) and used in the same manner
7//! as regular resources. After execution has completed pooled resources are automatically returned
8//! to their pool for reuse.
9//!
10//! # Buckets
11//!
12//! The provided [`Pool`] implementations store resources in buckets, with each implementation
13//! offering a different strategy which balances performance (_more buckets_) with memory efficiency
14//! (_fewer buckets_).
15//!
16//! _vk-graph_'s pools can be grouped into two major categories:
17//!
18//! * Single-bucket: [`FifoPool`](self::fifo::FifoPool)
19//! * Multi-bucket: [`LazyPool`](self::lazy::LazyPool), [`HashPool`](self::hash::HashPool)
20//!
21//! # Examples
22//!
23//! Leasing an image:
24//!
25//! ```no_run
26//! # use std::sync::Arc;
27//! # use ash::vk;
28//! # use vk_graph::driver::DriverError;
29//! # use vk_graph::driver::device::{Device, DeviceInfo};
30//! # use vk_graph::driver::image::{ImageInfo};
31//! # use vk_graph::pool::{Pool};
32//! # use vk_graph::pool::lazy::{LazyPool};
33//! # fn main() -> Result<(), DriverError> {
34//! # let device = Device::create(DeviceInfo::default())?;
35//! let mut pool = LazyPool::new(&device);
36//!
37//! let info = ImageInfo::image_2d(8, 8, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::STORAGE);
38//! let my_image = pool.resource(info)?;
39//!
40//! assert!(my_image.info.usage.contains(vk::ImageUsageFlags::STORAGE));
41//! # Ok(()) }
42//! ```
43//!
44//! # When Should You Use Which Pool?
45//!
46//! These are fairly high-level break-downs of when each pool should be considered. You may need
47//! to investigate each type of pool individually to provide the absolute best fit for your purpose.
48//!
49//! ### Use a [`FifoPool`](self::fifo::FifoPool) when:
50//! * Low memory usage is most important
51//! * Automatic bucket management is desired
52//!
53//! ### Use a [`LazyPool`](self::lazy::LazyPool) when:
54//! * Resources have different attributes each frame
55//!
56//! ### Use a [`HashPool`](self::hash::HashPool) when:
57//! * High performance is most important
58//! * Resources have consistent attributes each frame
59//!
60//! # When Should You Use Resource Caching?
61//!
62//! Wrapping any pool using [`cache::Cache::new`] enables resource caching, which prevents excess
63//! resources from being created even when different parts of your code request compatible
64//! resources.
65//!
66//! **_NOTE:_** Graph submission will automatically attempt to re-order submitted commands to
67//! reduce contention between individual resources.
68//!
69//! **_NOTE:_** In cases where multiple cached resources using identical request information are
70//! used in the same graph command, ensure they come from different cache tags or different pool
71//! wrappers. Otherwise, two requests may resolve to the same underlying resource and trigger
72//! Vulkan validation warnings when reading from and writing to the same images.
73//!
74//! ### Pros:
75//!
76//! * Fewer resources are created overall
77//! * Wrapped pools behave like and retain all functionality of unwrapped pools
78//! * Easy to experiment with and benchmark in your existing code
79//!
80//! ### Cons:
81//!
82//! * Non-zero cost: atomic load and compatibility check per active cached resource
83//! * May cause GPU stalling if there is not enough work being submitted
84//! * Cached resources are typed `Arc<Lease<T>>` and are not guaranteed to be mutable or unique
85
86pub mod cache;
87pub mod fifo;
88pub mod hash;
89pub mod lazy;
90
91use {
92    crate::driver::{
93        DriverError,
94        accel_struct::{
95            AccelerationStructure, AccelerationStructureInfo, AccelerationStructureInfoBuilder,
96        },
97        buffer::{Buffer, BufferInfo, BufferInfoBuilder},
98        descriptor_set::{DescriptorPool, DescriptorPoolInfo},
99        image::{Image, ImageInfo, ImageInfoBuilder},
100        render_pass::{RenderPass, RenderPassInfo},
101    },
102    derive_builder::{Builder, UninitializedFieldError},
103    std::{
104        fmt::Debug,
105        mem::ManuallyDrop,
106        ops::{Deref, DerefMut},
107        sync::{Arc, Weak},
108        thread::panicking,
109    },
110};
111
112#[derive(Clone, Copy)]
113enum BufferHostMappingCompatibility {
114    Exact,
115    Superset,
116}
117
118fn compatible_buffer_info(
119    item_info: &BufferInfo,
120    requested_info: &BufferInfo,
121    host_mapping: BufferHostMappingCompatibility,
122) -> bool {
123    (item_info.alloc_dedicated & requested_info.alloc_dedicated) == requested_info.alloc_dedicated
124        && compatible_buffer_host_mapping(item_info, requested_info, host_mapping)
125        && item_info.alignment >= requested_info.alignment
126        && item_info.sharing_mode == requested_info.sharing_mode
127        && item_info.size >= requested_info.size
128        && item_info.usage.contains(requested_info.usage)
129}
130
131fn compatible_buffer_host_mapping(
132    item_info: &BufferInfo,
133    requested_info: &BufferInfo,
134    compatibility: BufferHostMappingCompatibility,
135) -> bool {
136    match compatibility {
137        BufferHostMappingCompatibility::Exact => {
138            item_info.host_readable == requested_info.host_readable
139                && item_info.host_writable == requested_info.host_writable
140        }
141        BufferHostMappingCompatibility::Superset => {
142            (item_info.host_readable & requested_info.host_readable) == requested_info.host_readable
143                && (item_info.host_writable & requested_info.host_writable)
144                    == requested_info.host_writable
145        }
146    }
147}
148
149fn compatible_image_info(item_info: &ImageInfo, requested_info: &ImageInfo) -> bool {
150    item_info.array_layer_count == requested_info.array_layer_count
151        && item_info.alloc_dedicated == requested_info.alloc_dedicated
152        && item_info.depth == requested_info.depth
153        && item_info.format == requested_info.format
154        && item_info.height == requested_info.height
155        && item_info.host_readable == requested_info.host_readable
156        && item_info.host_writable == requested_info.host_writable
157        && item_info.mip_level_count == requested_info.mip_level_count
158        && item_info.sample_count == requested_info.sample_count
159        && item_info.sharing_mode == requested_info.sharing_mode
160        && item_info.tiling == requested_info.tiling
161        && item_info.image_type == requested_info.image_type
162        && item_info.width == requested_info.width
163        && item_info.flags.contains(requested_info.flags)
164        && item_info.usage.contains(requested_info.usage)
165}
166
167#[cfg(feature = "parking_lot")]
168use parking_lot::Mutex;
169
170#[cfg(not(feature = "parking_lot"))]
171use std::sync::Mutex;
172
173type Cache<T> = Arc<Mutex<Vec<T>>>;
174type CacheRef<T> = Weak<Mutex<Vec<T>>>;
175
176fn with_cache<T, R>(cache: &Cache<T>, f: impl FnOnce(&mut Vec<T>) -> R) -> R {
177    let cache = cache.lock();
178
179    #[cfg(not(feature = "parking_lot"))]
180    let cache = cache.expect("poisoned cache lock");
181
182    let mut cache = cache;
183
184    f(&mut cache)
185}
186
187/// Holds a pooled resource and implements `Drop` in order to return the resource.
188///
189/// This simple wrapper type implements only the `AsRef`, `AsMut`, `Deref` and `DerefMut` traits
190/// and provides no other functionality. A freshly obtained resource is guaranteed to have no other
191/// owners and may be mutably accessed.
192#[derive(Debug)]
193pub struct Lease<T> {
194    cache_ref: CacheRef<T>,
195    item: ManuallyDrop<T>,
196}
197
198/*
199The following debug_name functions take a self of Lease<T> and return Self.
200This allows pooled resources to have the same `.debug_name("bugs")` chaining.
201*/
202
203impl Lease<AccelerationStructure> {
204    /// Sets the debugging name assigned to this acceleration structure.
205    pub fn with_debug_name(self, name: impl AsRef<str>) -> Self {
206        self.set_debug_name(name);
207
208        self
209    }
210}
211
212impl Lease<Buffer> {
213    /// Sets the debugging name assigned to this buffer.
214    pub fn with_debug_name(self, name: impl AsRef<str>) -> Self {
215        self.set_debug_name(name);
216
217        self
218    }
219}
220
221impl Lease<Image> {
222    /// Sets the debugging name assigned to this image.
223    pub fn with_debug_name(self, name: impl AsRef<str>) -> Self {
224        self.set_debug_name(name);
225
226        self
227    }
228}
229
230impl<T> Lease<T> {
231    fn new(cache_ref: CacheRef<T>, item: T) -> Self {
232        Self {
233            cache_ref,
234            item: ManuallyDrop::new(item),
235        }
236    }
237}
238
239impl<T> AsRef<T> for Lease<T> {
240    fn as_ref(&self) -> &T {
241        self
242    }
243}
244
245impl<T> Deref for Lease<T> {
246    type Target = T;
247
248    fn deref(&self) -> &Self::Target {
249        &self.item
250    }
251}
252
253impl<T> DerefMut for Lease<T> {
254    fn deref_mut(&mut self) -> &mut Self::Target {
255        &mut self.item
256    }
257}
258
259impl<T> Drop for Lease<T> {
260    #[profiling::function]
261    fn drop(&mut self) {
262        if panicking() {
263            return;
264        }
265
266        // If the pool cache has been dropped we must manually drop the item, otherwise it goes back
267        // into the pool
268        if let Some(cache) = self.cache_ref.upgrade() {
269            with_cache(&cache, |cache| {
270                if cache.len() >= cache.capacity() {
271                    cache.pop();
272                }
273
274                cache.push(unsafe { ManuallyDrop::take(&mut self.item) });
275            });
276        } else {
277            unsafe {
278                ManuallyDrop::drop(&mut self.item);
279            }
280        }
281    }
282}
283
284/// Allows requesting resources using driver information structures.
285pub trait Pool<I, T> {
286    /// Request a resource.
287    fn resource(&mut self, info: I) -> Result<Lease<T>, DriverError>;
288}
289
290/// Pool capability required by graph submission scheduling.
291///
292/// This sealed trait is implemented by the built-in pools. It covers internal descriptor-pool and
293/// render-pass leases without exposing their cache-key types in public API bounds.
294#[allow(private_bounds)]
295pub trait SubmissionPool: submission_pool_private::SubmissionPoolSealed {}
296
297impl<T> SubmissionPool for T where T: submission_pool_private::SubmissionPoolSealed {}
298
299pub(crate) mod submission_pool_private {
300    use super::*;
301
302    pub(crate) trait SubmissionPoolSealed {
303        fn descriptor_pool(
304            &mut self,
305            info: DescriptorPoolInfo,
306        ) -> Result<Lease<DescriptorPool>, DriverError>;
307
308        fn render_pass(&mut self, info: RenderPassInfo) -> Result<Lease<RenderPass>, DriverError>;
309    }
310
311    impl<T> SubmissionPoolSealed for T
312    where
313        T: Pool<DescriptorPoolInfo, DescriptorPool> + Pool<RenderPassInfo, RenderPass>,
314    {
315        fn descriptor_pool(
316            &mut self,
317            info: DescriptorPoolInfo,
318        ) -> Result<Lease<DescriptorPool>, DriverError> {
319            self.resource(info)
320        }
321
322        fn render_pass(&mut self, info: RenderPassInfo) -> Result<Lease<RenderPass>, DriverError> {
323            self.resource(info)
324        }
325    }
326}
327
328// Enable requesting items using their info builder type for convenience
329macro_rules! lease_builder {
330    ($info:ident => $item:ident) => {
331        paste::paste! {
332            impl<T> Pool<[<$info Builder>], $item> for T where T: Pool<$info, $item> {
333                fn resource(
334                    &mut self,
335                    builder: [<$info Builder>],
336                ) -> Result<Lease<$item>, DriverError> {
337                    let info = builder.build();
338
339                    self.resource(info)
340                }
341            }
342        }
343    };
344}
345
346lease_builder!(AccelerationStructureInfo => AccelerationStructure);
347lease_builder!(BufferInfo => Buffer);
348lease_builder!(ImageInfo => Image);
349
350/// Information used to create a [`FifoPool`](self::fifo::FifoPool),
351/// [`HashPool`](self::hash::HashPool) or [`LazyPool`](self::lazy::LazyPool) instance.
352#[derive(Builder, Clone, Copy, Debug, Eq, PartialEq)]
353#[builder(
354    build_fn(private, name = "fallible_build", error = "UninitializedFieldError"),
355    derive(Clone, Copy, Debug),
356    pattern = "owned"
357)]
358pub struct PoolConfig {
359    /// The maximum size of a single bucket of acceleration structure resource instances. The
360    /// default value is [`PoolConfig::DEFAULT_RESOURCE_CAPACITY`].
361    ///
362    /// # Note
363    ///
364    /// Individual [`Pool`] implementations store varying numbers of buckets. Read the
365    /// documentation of each implementation to understand how this affects total number of
366    /// stored acceleration structure instances.
367    #[builder(
368        default = "PoolConfig::DEFAULT_RESOURCE_CAPACITY",
369        setter(strip_option)
370    )]
371    pub accel_struct_capacity: usize,
372
373    /// The maximum size of a single bucket of buffer resource instances. The default value is
374    /// [`PoolConfig::DEFAULT_RESOURCE_CAPACITY`].
375    ///
376    /// # Note
377    ///
378    /// Individual [`Pool`] implementations store varying numbers of buckets. Read the
379    /// documentation of each implementation to understand how this affects total number of
380    /// stored buffer instances.
381    #[builder(
382        default = "PoolConfig::DEFAULT_RESOURCE_CAPACITY",
383        setter(strip_option)
384    )]
385    pub buffer_capacity: usize,
386
387    /// The maximum size of a single bucket of image resource instances. The default value is
388    /// [`PoolConfig::DEFAULT_RESOURCE_CAPACITY`].
389    ///
390    /// # Note
391    ///
392    /// Individual [`Pool`] implementations store varying numbers of buckets. Read the
393    /// documentation of each implementation to understand how this affects total number of
394    /// stored image instances.
395    #[builder(
396        default = "PoolConfig::DEFAULT_RESOURCE_CAPACITY",
397        setter(strip_option)
398    )]
399    pub image_capacity: usize,
400}
401
402impl PoolConfig {
403    /// The maximum size of a single bucket of resource instances.
404    pub const DEFAULT_RESOURCE_CAPACITY: usize = 16;
405
406    /// Creates a default `PoolConfigBuilder`.
407    pub fn builder() -> PoolConfigBuilder {
408        Default::default()
409    }
410
411    fn default_cache<T>() -> Cache<T> {
412        Cache::new(Mutex::new(Vec::with_capacity(
413            Self::DEFAULT_RESOURCE_CAPACITY,
414        )))
415    }
416
417    fn explicit_cache<T>(capacity: usize) -> Cache<T> {
418        Cache::new(Mutex::new(Vec::with_capacity(capacity)))
419    }
420
421    /// Converts a `PoolConfig` into a `PoolConfigBuilder`.
422    pub fn into_builder(self) -> PoolConfigBuilder {
423        PoolConfigBuilder {
424            accel_struct_capacity: Some(self.accel_struct_capacity),
425            buffer_capacity: Some(self.buffer_capacity),
426            image_capacity: Some(self.image_capacity),
427        }
428    }
429
430    /// Constructs a new `PoolConfig` with the given acceleration structure, buffer and image
431    /// resource capacity for any single bucket.
432    pub const fn with_capacity(resource_capacity: usize) -> Self {
433        Self {
434            accel_struct_capacity: resource_capacity,
435            buffer_capacity: resource_capacity,
436            image_capacity: resource_capacity,
437        }
438    }
439}
440
441impl Default for PoolConfig {
442    fn default() -> Self {
443        PoolConfigBuilder::default().into()
444    }
445}
446
447impl From<PoolConfigBuilder> for PoolConfig {
448    fn from(info: PoolConfigBuilder) -> Self {
449        info.build()
450    }
451}
452
453impl From<usize> for PoolConfig {
454    fn from(value: usize) -> Self {
455        Self {
456            accel_struct_capacity: value,
457            buffer_capacity: value,
458            image_capacity: value,
459        }
460    }
461}
462
463// HACK: https://github.com/colin-kiegel/rust-derive-builder/issues/56
464impl PoolConfigBuilder {
465    /// Builds a new `PoolConfig`.
466    pub fn build(self) -> PoolConfig {
467        self.fallible_build().expect("invalid pool config")
468    }
469}
470
471#[cfg(test)]
472mod test {
473    use super::*;
474    use crate::driver::ash::vk;
475
476    type Info = PoolConfig;
477    type Builder = PoolConfigBuilder;
478
479    #[test]
480    pub fn pool_info() {
481        let info = Info::default();
482        let builder = info.into_builder().build();
483
484        assert_eq!(info, builder);
485    }
486
487    #[test]
488    pub fn pool_info_builder() {
489        let info = Info {
490            accel_struct_capacity: 1,
491            buffer_capacity: 2,
492            image_capacity: 3,
493        };
494        let builder = Builder::default()
495            .accel_struct_capacity(1)
496            .buffer_capacity(2)
497            .image_capacity(3)
498            .build();
499
500        assert_eq!(info, builder);
501    }
502
503    #[test]
504    fn buffer_info_compatibility_rejects_different_sharing_mode() {
505        let exclusive = BufferInfo::device_mem(64, vk::BufferUsageFlags::STORAGE_BUFFER);
506        let concurrent = BufferInfo {
507            sharing_mode: vk::SharingMode::CONCURRENT,
508            ..exclusive
509        };
510
511        assert!(!compatible_buffer_info(
512            &exclusive,
513            &concurrent,
514            BufferHostMappingCompatibility::Exact,
515        ));
516        assert!(!compatible_buffer_info(
517            &exclusive,
518            &concurrent,
519            BufferHostMappingCompatibility::Superset,
520        ));
521    }
522
523    #[test]
524    fn image_info_compatibility_rejects_different_sharing_mode() {
525        let exclusive = ImageInfo::image_2d(
526            16,
527            16,
528            vk::Format::R8G8B8A8_UNORM,
529            vk::ImageUsageFlags::STORAGE,
530        );
531        let concurrent = ImageInfo {
532            sharing_mode: vk::SharingMode::CONCURRENT,
533            ..exclusive
534        };
535
536        assert!(!compatible_image_info(&exclusive, &concurrent));
537    }
538}