DescriptorHeapVulkan: a second heap-mode Initialize corrupts the slots still held (latent) #172

Open
opened 2026-10-31 14:57:41 +00:00 by jorijnvdgraaf · 0 comments

Summary

DescriptorHeapVulkan::Initialize says that in heap mode "a second call simply re-sizes the heap buffers". It also resets the bump pointers, empties the freelists and moves the buffer region, while the ImageSlot, BufferSlot and SamplerSlot handles from the first call are still alive. Those handles then alias new allocations and convert to the wrong absolute index. Their slots can also be handed out again past the new capacity, where a descriptor write lands in the implementation's reserved range. Latent: nothing in the tree calls Initialize twice on one heap.

Line numbers refer to interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm at 6e94cc6.

What a second call does

  • :145-156 set imageNext, bufferNext and samplerNext to 0 and clear the freelists. The next allocation hands out slot 0 again while an old handle still owns it, such as a UIRenderer's swapchain image slot.
  • The buffer region moves with the new image count, so a surviving BufferSlot converts to bufferStartElement + raw (:625) against the new start.
  • When an old handle is destroyed, Release puts its raw slot on the freelist. AllocateBufferSlots pops a freelist entry without checking it against the new capacity (:434), so a slot past the new heap is handed out and written.

Example, NVIDIA (32-byte image and 16-byte buffer descriptors): Initialize(16, 16, 4) while a UIRenderer holds buffer slots 0 to 10, then Initialize(4, 4, 2). That fits the existing allocation, so it resizes in place: the buffers now start at byte 128 and the reserved range at 192. Destroying the handle of raw slot 10 frees it, the next AllocateBufferSlots(1) returns it, and its descriptor is written at BufferByteOffset(10) = 128 + 160 = 288, inside the reserved range.

Descriptor-set mode already refuses a second call (:176). Window::Render also cached its heap-bind structs on the heap pointer, which a reallocating second call left stale; the #163 change removes that cache.

Suggested fix

Refuse a second heap-mode Initialize the way InitializeSets does, and fix the comment: a heap of another size is a new DescriptorHeapVulkan. If resizing in place is wanted, it has to require that no slot handle is alive, and check it.

Found in a code review of the #163 change.

## Summary `DescriptorHeapVulkan::Initialize` says that in heap mode "a second call simply re-sizes the heap buffers". It also resets the bump pointers, empties the freelists and moves the buffer region, while the `ImageSlot`, `BufferSlot` and `SamplerSlot` handles from the first call are still alive. Those handles then alias new allocations and convert to the wrong absolute index. Their slots can also be handed out again past the new capacity, where a descriptor write lands in the implementation's reserved range. Latent: nothing in the tree calls `Initialize` twice on one heap. Line numbers refer to `interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm` at `6e94cc6`. ## What a second call does - `:145-156` set `imageNext`, `bufferNext` and `samplerNext` to 0 and clear the freelists. The next allocation hands out slot 0 again while an old handle still owns it, such as a `UIRenderer`'s swapchain image slot. - The buffer region moves with the new image count, so a surviving `BufferSlot` converts to `bufferStartElement + raw` (`:625`) against the new start. - When an old handle is destroyed, `Release` puts its raw slot on the freelist. `AllocateBufferSlots` pops a freelist entry without checking it against the new capacity (`:434`), so a slot past the new heap is handed out and written. Example, NVIDIA (32-byte image and 16-byte buffer descriptors): `Initialize(16, 16, 4)` while a `UIRenderer` holds buffer slots 0 to 10, then `Initialize(4, 4, 2)`. That fits the existing allocation, so it resizes in place: the buffers now start at byte 128 and the reserved range at 192. Destroying the handle of raw slot 10 frees it, the next `AllocateBufferSlots(1)` returns it, and its descriptor is written at `BufferByteOffset(10)` = 128 + 160 = 288, inside the reserved range. Descriptor-set mode already refuses a second call (`:176`). `Window::Render` also cached its heap-bind structs on the heap pointer, which a reallocating second call left stale; the #163 change removes that cache. ## Suggested fix Refuse a second heap-mode `Initialize` the way `InitializeSets` does, and fix the comment: a heap of another size is a new `DescriptorHeapVulkan`. If resizing in place is wanted, it has to require that no slot handle is alive, and check it. Found in a code review of the #163 change.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Catcrafts/Crafter.Graphics#172
No description provided.