Skip to main content

wgpu_hal/vulkan/
descriptor.rs

1use alloc::vec::Vec;
2use arrayvec::ArrayVec;
3use ash::vk;
4use hashbrown::{HashMap, HashSet};
5
6const POOL_MIN_SETS: u32 = 64;
7const POOL_MAX_SETS: u32 = 512;
8
9/// The dynamic counts are tracked separately since dynamic textures/buffers
10/// have distinct descriptor types compared to their non-dynamic counterparts.
11#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
12pub struct DescriptorCounts {
13    pub sampler: u32,
14    pub sampled_image: u32,
15    pub storage_image: u32,
16    pub uniform_buffer: u32,
17    pub uniform_buffer_dynamic: u32,
18    pub storage_buffer: u32,
19    pub storage_buffer_dynamic: u32,
20    pub acceleration_structure: u32,
21}
22
23impl DescriptorCounts {
24    fn total(&self) -> u32 {
25        self.sampler
26            + self.sampled_image
27            + self.storage_image
28            + self.uniform_buffer
29            + self.uniform_buffer_dynamic
30            + self.storage_buffer
31            + self.storage_buffer_dynamic
32            + self.acceleration_structure
33    }
34}
35
36#[derive(Debug)]
37pub struct DescriptorSet {
38    raw: vk::DescriptorSet,
39    bucket_key: BucketKey,
40    pool_index: usize,
41}
42
43impl DescriptorSet {
44    pub fn raw(&self) -> vk::DescriptorSet {
45        self.raw
46    }
47}
48
49#[derive(Debug, PartialEq, Eq, Hash)]
50struct BucketKey {
51    counts: DescriptorCounts,
52    update_after_bind: bool,
53}
54
55struct Pool {
56    raw: vk::DescriptorPool,
57    capacity: u32,
58    available: u32,
59    /// Set when the driver refused to allocate from this pool despite it
60    /// having free capacity, making `alloc` skip it until the pool is empty
61    /// again and can be reset.
62    fragmented: bool,
63}
64
65/// Keeps track of all pools created with this bucket's [`BucketKey`].
66#[derive(Default)]
67struct Bucket {
68    /// We keep track of all descriptor set layouts that might use this bucket.
69    /// If the set becomes empty, the bucket is destroyed.
70    layouts: HashSet<vk::DescriptorSetLayout>,
71    pools: Vec<Pool>,
72    available_sets: u32,
73    allocated_sets: u32,
74}
75
76impl Bucket {
77    fn create_pool(
78        &mut self,
79        device: &ash::Device,
80        key: &BucketKey,
81        capacity_hint: u32,
82    ) -> Result<(usize, &mut Pool), crate::DeviceError> {
83        let index = self.pools.len();
84        let pool = create_descriptor_pool(device, key, capacity_hint)?;
85        self.available_sets += pool.capacity;
86        self.pools.push(pool);
87        Ok((index, self.pools.last_mut().unwrap()))
88    }
89}
90
91pub struct DescriptorAllocator {
92    buckets: HashMap<BucketKey, Bucket>,
93    max_update_after_bind_descriptors_in_all_pools: u32,
94    update_after_bind_descriptors_in_all_pools: u32,
95}
96
97impl super::BindGroupLayout {
98    fn bucket_key(&self) -> BucketKey {
99        let update_after_bind = self.contains_binding_arrays;
100        let counts = self.desc_count.clone();
101        BucketKey {
102            counts,
103            update_after_bind,
104        }
105    }
106}
107
108impl DescriptorAllocator {
109    pub fn new(max_update_after_bind_descriptors_in_all_pools: u32) -> Self {
110        DescriptorAllocator {
111            buckets: HashMap::default(),
112            max_update_after_bind_descriptors_in_all_pools,
113            update_after_bind_descriptors_in_all_pools: 0,
114        }
115    }
116
117    pub fn register_layout(
118        &mut self,
119        device: &ash::Device,
120        layout: &super::BindGroupLayout,
121    ) -> Result<(), crate::DeviceError> {
122        let key = layout.bucket_key();
123        let bucket = match self.buckets.entry(key) {
124            hashbrown::hash_map::Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
125            hashbrown::hash_map::Entry::Vacant(vacant_entry) => {
126                let mut bucket = Bucket::default();
127                // Create at least 1 pool upfront instead of doing this when
128                // creating the first bind group.
129                bucket.create_pool(device, vacant_entry.key(), POOL_MIN_SETS)?;
130                vacant_entry.insert(bucket)
131            }
132        };
133
134        assert!(bucket.layouts.insert(layout.raw));
135
136        Ok(())
137    }
138
139    pub fn unregister_layout(&mut self, device: &ash::Device, layout: &super::BindGroupLayout) {
140        let key = layout.bucket_key();
141        let bucket = self.buckets.get_mut(&key).unwrap();
142
143        assert!(bucket.layouts.remove(&layout.raw));
144
145        if bucket.layouts.is_empty() {
146            // Remove the bucket and destroy any remaining pools.
147            let bucket = self.buckets.remove(&key).unwrap();
148            for pool in bucket.pools {
149                assert_eq!(
150                    pool.available, pool.capacity,
151                    "pool is not empty, at least one DescriptorSet has not been freed"
152                );
153                unsafe { device.destroy_descriptor_pool(pool.raw, None) };
154            }
155        }
156    }
157
158    pub unsafe fn alloc(
159        &mut self,
160        device: &ash::Device,
161        layout: &super::BindGroupLayout,
162    ) -> Result<DescriptorSet, crate::DeviceError> {
163        let update_after_bind = layout.contains_binding_arrays;
164        let total_descriptors = layout.desc_count.total();
165
166        if update_after_bind
167            && self.max_update_after_bind_descriptors_in_all_pools
168                - self.update_after_bind_descriptors_in_all_pools
169                < total_descriptors
170        {
171            return Err(crate::DeviceError::OutOfMemory);
172        }
173
174        let key = layout.bucket_key();
175        let bucket = self.buckets.get_mut(&key).unwrap();
176        let capacity_hint = bucket.allocated_sets;
177
178        // Prefer smaller/older/fuller pools for new allocations to prevent
179        // fragmentation and possibly fragmentation of hardware resources
180        // (VK_ERROR_FRAGMENTATION)
181        let mut candidate = bucket
182            .pools
183            .iter()
184            .position(|pool| !pool.fragmented && pool.available != 0);
185
186        let (raw, pool_index) = loop {
187            let index = match candidate {
188                Some(index) => index,
189                None => bucket.create_pool(device, &key, capacity_hint)?.0,
190            };
191            let created_pool = candidate.is_none();
192
193            let vk_info = vk::DescriptorSetAllocateInfo::default()
194                .descriptor_pool(bucket.pools[index].raw)
195                .set_layouts(core::slice::from_ref(&layout.raw));
196
197            // From https://docs.vulkan.org/refpages/latest/refpages/source/VkDescriptorPoolCreateInfo.html#_description
198            // > if all sets allocated from the pool since it was created or
199            // > most recently reset use the same number of descriptors
200            // > (of each type) and the requested allocation also uses that
201            // > same number of descriptors (of each type), then
202            // > fragmentation must not cause an allocation failure
203            //
204            // These pools satisfy that condition, but some drivers (e.g.
205            // Mali) fail such allocations anyway once a pool has served
206            // a number of allocations; freeing sets does not make the
207            // pool usable again, only resetting it does.
208            //
209            // Recover instead of failing: reset a pool that has no live sets,
210            // otherwise mark the pool `fragmented` so that allocations skip
211            // it. A fragmented pool is reset once its last set is freed.
212            //
213            // See https://github.com/gfx-rs/wgpu/pull/10264
214            match unsafe { device.allocate_descriptor_sets(&vk_info) } {
215                Ok(sets) => break (sets[0], index),
216                Err(vk::Result::ERROR_FRAGMENTED_POOL | vk::Result::ERROR_OUT_OF_POOL_MEMORY) => {
217                    if created_pool {
218                        // A fresh pool failing to allocate even a single set leaves
219                        // no pool left to fall back to, so drop it and give up.
220                        let pool = bucket.pools.pop().unwrap();
221                        bucket.available_sets -= pool.capacity;
222                        unsafe { device.destroy_descriptor_pool(pool.raw, None) };
223                        log::error!(
224                            "descriptor set allocation from a fresh pool failed unexpectedly"
225                        );
226                        return Err(crate::DeviceError::OutOfMemory);
227                    }
228
229                    // Resetting a pool restores the spec guarantee that
230                    // fragmentation cannot cause an allocation failure, but it
231                    // implicitly frees all of the pool's sets, so only reset a
232                    // pool that has no live sets.
233                    let pool = &bucket.pools[index];
234                    if pool.available == pool.capacity {
235                        let reset_result = unsafe {
236                            device.reset_descriptor_pool(
237                                pool.raw,
238                                vk::DescriptorPoolResetFlags::empty(),
239                            )
240                        };
241                        if reset_result.is_ok() {
242                            match unsafe { device.allocate_descriptor_sets(&vk_info) } {
243                                Ok(sets) => break (sets[0], index),
244                                Err(
245                                    vk::Result::ERROR_FRAGMENTED_POOL
246                                    | vk::Result::ERROR_OUT_OF_POOL_MEMORY,
247                                ) => {}
248                                Err(err) => return Err(super::map_host_device_oom_err(err)),
249                            }
250                        }
251                    }
252
253                    let available = bucket.pools[index].available;
254                    let capacity = bucket.pools[index].capacity;
255                    bucket.pools[index].fragmented = true;
256                    bucket.available_sets -= available;
257                    log::debug!(
258                        "Retiring fragmented descriptor pool ({available}/{capacity} sets free), retrying on another pool"
259                    );
260                    candidate = bucket
261                        .pools
262                        .iter()
263                        .position(|pool| !pool.fragmented && pool.available != 0);
264                }
265                Err(err) => return Err(super::map_host_device_oom_err(err)),
266            }
267        };
268
269        let pool = &mut bucket.pools[pool_index];
270        pool.available -= 1;
271        bucket.available_sets -= 1;
272        bucket.allocated_sets += 1;
273        if update_after_bind {
274            self.update_after_bind_descriptors_in_all_pools += total_descriptors;
275        }
276
277        Ok(DescriptorSet {
278            raw,
279            bucket_key: key,
280            pool_index,
281        })
282    }
283
284    pub unsafe fn free(&mut self, device: &ash::Device, set: DescriptorSet) {
285        let bucket = self.buckets.get_mut(&set.bucket_key).unwrap();
286        let pool = bucket.pools.get_mut(set.pool_index).unwrap();
287
288        let result =
289            unsafe { device.free_descriptor_sets(pool.raw, core::slice::from_ref(&set.raw())) };
290        if let Err(err) = result {
291            // vkFreeDescriptorSets is documented to return:
292            // - VK_ERROR_UNKNOWN
293            // - VK_ERROR_VALIDATION_FAILED (we shouldn't encounter this one)
294            // wgpu-hal doesn't currently report errors in destroy functions.
295            // Panic here for now. It might be ok to ignore the error but the
296            // proper solution would probably be to lose the device.
297            panic!("vkFreeDescriptorSets error: {err}, please report this error");
298        }
299
300        pool.available += 1;
301
302        if !pool.fragmented {
303            bucket.available_sets += 1;
304        } else if pool.available == pool.capacity {
305            // A fragmented pool's free sets are unusable until it is reset, so
306            // they do not count towards `available_sets`.
307            // On Mali, freeing sets does not make such a pool usable again;
308            // resetting it is what does, and that is only safe once no sets are live.
309            let reset_result = unsafe {
310                device.reset_descriptor_pool(pool.raw, vk::DescriptorPoolResetFlags::empty())
311            };
312            if reset_result.is_ok() {
313                pool.fragmented = false;
314                bucket.available_sets += pool.capacity;
315            }
316        }
317        bucket.allocated_sets -= 1;
318        if set.bucket_key.update_after_bind {
319            self.update_after_bind_descriptors_in_all_pools -= set.bucket_key.counts.total();
320        }
321
322        // Do not immediately destroy empty pools since they might have
323        // been recently created.
324        // Destroy a pool only if it's empty and we have 1/4th its capacity
325        // in other pools.
326        // Note that this logic will never destroy the last pool.
327        let last = bucket.pools.last().unwrap();
328        let last_available = last.available;
329        let last_capacity = last.capacity;
330        // Only a non-fragmented pool contributes its capacity to
331        // `available_sets`.
332        let last_contribution = if last.fragmented { 0 } else { last_capacity };
333        if last_available == last_capacity
334            && bucket.available_sets - last_contribution > last_capacity / 4
335        {
336            let pool = bucket.pools.pop().unwrap();
337            unsafe { device.destroy_descriptor_pool(pool.raw, None) };
338            bucket.available_sets -= last_contribution;
339        }
340    }
341}
342
343impl Drop for DescriptorAllocator {
344    fn drop(&mut self) {
345        if !std::thread::panicking() {
346            assert!(
347                self.buckets.is_empty(),
348                "buckets are not empty, at least one BGL has not been unregistered"
349            )
350        }
351    }
352}
353
354fn create_descriptor_pool(
355    device: &ash::Device,
356    key: &BucketKey,
357    capacity_hint: u32,
358) -> Result<Pool, crate::DeviceError> {
359    let counts = &key.counts;
360
361    const NR_OF_DESCRIPTOR_TYPES: usize = 8;
362
363    use vk::DescriptorType as Dt;
364    let counts: [_; NR_OF_DESCRIPTOR_TYPES] = [
365        (Dt::SAMPLER, counts.sampler),
366        (Dt::SAMPLED_IMAGE, counts.sampled_image),
367        (Dt::STORAGE_IMAGE, counts.storage_image),
368        (Dt::UNIFORM_BUFFER, counts.uniform_buffer),
369        (Dt::UNIFORM_BUFFER_DYNAMIC, counts.uniform_buffer_dynamic),
370        (Dt::STORAGE_BUFFER, counts.storage_buffer),
371        (Dt::STORAGE_BUFFER_DYNAMIC, counts.storage_buffer_dynamic),
372        (
373            Dt::ACCELERATION_STRUCTURE_KHR,
374            counts.acceleration_structure,
375        ),
376    ];
377
378    // bounded doubling growth strategy
379    let mut capacity = capacity_hint
380        .clamp(POOL_MIN_SETS, POOL_MAX_SETS)
381        .next_power_of_two();
382
383    // Lower capacity to avoid overflowing `count * capacity` calculations
384    // below. The resulting `capacity` might not be a power of 2 but that's
385    // ok.
386    for (_, count) in counts {
387        capacity = (u32::MAX / count.max(1)).min(capacity);
388    }
389
390    let pool_sizes = counts
391        .into_iter()
392        .filter(|&(_, count)| count != 0)
393        .map(|(ty, count)| vk::DescriptorPoolSize {
394            ty,
395            descriptor_count: count * capacity,
396        })
397        .collect::<ArrayVec<_, NR_OF_DESCRIPTOR_TYPES>>();
398
399    // Prior to Vulkan 1.3.215 `poolSizeCount` was required to be non-zero, so
400    // add a single small sampler entry if `pool_sizes` would otherwise be
401    // empty.
402    let mut pool_sizes = pool_sizes;
403    if pool_sizes.is_empty() {
404        pool_sizes.push(vk::DescriptorPoolSize {
405            ty: Dt::SAMPLER,
406            descriptor_count: 1,
407        });
408    }
409
410    let mut flags = vk::DescriptorPoolCreateFlags::FREE_DESCRIPTOR_SET;
411    if key.update_after_bind {
412        flags |= vk::DescriptorPoolCreateFlags::UPDATE_AFTER_BIND;
413    };
414
415    let vk_info = vk::DescriptorPoolCreateInfo::default()
416        .flags(flags)
417        .max_sets(capacity)
418        .pool_sizes(&pool_sizes);
419
420    let raw = unsafe { device.create_descriptor_pool(&vk_info, None) }
421        .map_err(super::map_host_device_oom_and_fragmentation_err)?;
422
423    Ok(Pool {
424        raw,
425        capacity,
426        available: capacity,
427        fragmented: false,
428    })
429}