//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #include "vulkan/vulkan.h" #endif // !CRAFTER_GRAPHICS_WINDOW_DOM export module Crafter.Graphics:DescriptorHeapVulkan; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM import std; import :Device; import :Window; import :Types; import :VulkanBuffer; export namespace Crafter { struct ImageSlotRange { std::uint16_t firstElement; std::uint16_t count; }; struct BufferSlotRange { std::uint16_t firstElement; std::uint16_t count; }; struct SamplerSlotRange { std::uint16_t firstElement; std::uint16_t count; }; struct DescriptorHeapVulkan { VulkanBuffer resourceHeap[Window::numFrames]; VulkanBuffer samplerHeap[Window::numFrames]; std::uint32_t bufferStartOffset; std::uint16_t bufferStartElement; std::uint16_t imageCapacity = 0; std::uint16_t bufferCapacity = 0; std::uint16_t samplerCapacity = 0; std::uint16_t imageNext = 0; std::uint16_t bufferNext = 0; std::uint16_t samplerNext = 0; // Per-pool freelists of recyclable slot indices (raw, pre-bufferStartElement // for the buffer pool). Populated by Free*Slots, consumed by Allocate*Slots // before falling through to the bump pointer. Single-slot allocations only — // multi-slot allocations need contiguity and bypass the freelist. std::vector imageFreelist; std::vector bufferFreelist; std::vector samplerFreelist; // ─── Sets mode ────────────────────────────────────────────────── // Used instead of the heap buffers above when the driver has no // VK_EXT_descriptor_heap (Device::descriptorHeapSupported == false). // The same slots then live in one bindless descriptor set per frame, // mirroring the per-frame heap copies, so slot allocation, the slot // handles and every shader-side index keep their meaning: // binding 0 STORAGE_IMAGE[] image slots written as storage images // binding 1 SAMPLED_IMAGE[] image slots written as sampled images // binding 2 SAMPLER[] sampler slots // binding 3 STORAGE_BUFFER[] buffer slots (bufferStartElement is 0) // These numbers are the UI_HEAP_* macros in shaders/ui-shared.glsl. // Bindings 0 and 1 share the image slot space the way the two typed // views of the resource heap do; a slot is valid in whichever of the // two it was written to. // // All bindings are PARTIALLY_BOUND | UPDATE_AFTER_BIND | // UPDATE_UNUSED_WHILE_PENDING: most slots are never written, and a // slot is written into every frame's set at once while earlier frames // may still be executing with their set bound. static constexpr std::uint32_t storageImageBinding = 0; static constexpr std::uint32_t sampledImageBinding = 1; static constexpr std::uint32_t samplerBinding = 2; static constexpr std::uint32_t storageBufferBinding = 3; VkDescriptorPool setPool = VK_NULL_HANDLE; VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; // Shared by every compute pipeline created against this heap // (ComputeShader::Load), so the set Window::Render binds once per // frame stays bound across all of them. Carries one compute // push-constant range covering the device's whole push-constant space. VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; VkDescriptorSet sets[Window::numFrames] {}; // Descriptor sets reference VkImageView / VkSampler objects, where the // heap takes create-infos, so in sets mode this struct creates and // owns them. setImageViews[frame][slot] is the view frame's set uses // at that image slot; a view shared by all frames is stored under // frame 0 only. setSamplers[slot] likewise. std::vector setImageViews[Window::numFrames]; std::vector setSamplers; DescriptorHeapVulkan() = default; DescriptorHeapVulkan(const DescriptorHeapVulkan&) = delete; DescriptorHeapVulkan& operator=(const DescriptorHeapVulkan&) = delete; // Heap mode owns nothing beyond its VulkanBuffers, which release // themselves. Sets mode owns the objects above. The frame loop keeps // frames in flight and nothing waits for them at shutdown, so wait // here: destroying a view or the pool under a still-executing command // buffer is a use-after-free on the GPU. ~DescriptorHeapVulkan() { if (setLayout == VK_NULL_HANDLE) { return; } vkQueueWaitIdle(Device::queue); DestroySets(); } // Destroys every sets-mode object and returns to the un-initialized // state. The caller guarantees nothing on the GPU still uses them. // Safe on a partly built heap: destroying VK_NULL_HANDLE is a no-op. void DestroySets() noexcept { vkDestroyDescriptorPool(Device::device, setPool, nullptr); for (std::vector& views : setImageViews) { for (VkImageView view : views) { vkDestroyImageView(Device::device, view, nullptr); } views.clear(); } for (VkSampler sampler : setSamplers) { vkDestroySampler(Device::device, sampler, nullptr); } setSamplers.clear(); vkDestroyDescriptorSetLayout(Device::device, setLayout, nullptr); vkDestroyPipelineLayout(Device::device, pipelineLayout, nullptr); setPool = VK_NULL_HANDLE; setLayout = VK_NULL_HANDLE; pipelineLayout = VK_NULL_HANDLE; std::ranges::fill(sets, VkDescriptorSet{VK_NULL_HANDLE}); } // Call once, before any shader is loaded against this heap. In heap // mode a second call simply re-sizes the heap buffers; in sets mode it // throws, because the pipelines already built against the old layout // could not be used with a new one. void Initialize(std::uint16_t images, std::uint16_t buffers, std::uint16_t samplers) { if (!Device::descriptorHeapSupported) { InitializeSets(images, buffers, samplers); return; } std::uint32_t descriptorRegion = images * Device::descriptorHeapProperties.imageDescriptorSize + buffers * Device::descriptorHeapProperties.bufferDescriptorSize; std::uint32_t alignedDescriptorRegion = (descriptorRegion + Device::descriptorHeapProperties.imageDescriptorAlignment - 1) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1); std::uint32_t resourceSize = alignedDescriptorRegion + Device::descriptorHeapProperties.minResourceHeapReservedRange; std::uint32_t samplerSize = samplers * Device::descriptorHeapProperties.samplerDescriptorSize + Device::descriptorHeapProperties.minSamplerHeapReservedRange; bufferStartElement = images * Device::descriptorHeapProperties.imageDescriptorSize / Device::descriptorHeapProperties.bufferDescriptorSize; if(images > 0 && bufferStartElement == 0) { bufferStartElement = 1; } bufferStartOffset = bufferStartElement * Device::descriptorHeapProperties.bufferDescriptorSize; imageCapacity = images; bufferCapacity = buffers; samplerCapacity = samplers; imageNext = 0; bufferNext = 0; samplerNext = 0; // Reserve so Free*Slots' push_back is noexcept (slot handle dtors // call Free*Slots and must not throw). At most `capacity` slots // can ever be free at one time. imageFreelist.clear(); imageFreelist.reserve(images); bufferFreelist.clear(); bufferFreelist.reserve(buffers); samplerFreelist.clear(); samplerFreelist.reserve(samplers); // The heaps are written by the CPU every frame (HOST_VISIBLE is // mandatory) and read by the GPU, so DEVICE_LOCAL is a perf // preference, not a requirement: devices without a host-visible // device-local heap (no resizable BAR) get a host-visible-only // allocation instead of a hard allocation failure. for(std::uint8_t i = 0; i < Window::numFrames; i++) { resourceHeap[i].Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, resourceSize, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); samplerHeap[i].Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, samplerSize, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); } } // Sets-mode half of Initialize. Kept apart so the heap sizing above, // which divides by descriptor sizes that are zero without the // extension, never runs here. void InitializeSets(std::uint16_t images, std::uint16_t buffers, std::uint16_t samplers) { // A second Initialize would leak the first pool and layouts and // leave every pipeline created against the old layout // incompatible with the new sets. if (setLayout != VK_NULL_HANDLE) { throw std::runtime_error("DescriptorHeapVulkan: Initialize called twice (descriptor-set mode cannot be re-initialized)"); } // If a step below throws, undo the ones that succeeded. A // half-built heap would otherwise keep setLayout set, so a retry // would report "Initialize called twice" instead of the real // error, and nothing would release what was created. struct Rollback { DescriptorHeapVulkan* heap; ~Rollback() { if (heap != nullptr) heap->DestroySets(); } } rollback{this}; // Buffer slots are plain array indices into binding 3, so // BufferSlot's conversion adds nothing. bufferStartElement = 0; bufferStartOffset = 0; imageCapacity = images; bufferCapacity = buffers; samplerCapacity = samplers; imageNext = 0; bufferNext = 0; samplerNext = 0; imageFreelist.clear(); imageFreelist.reserve(images); bufferFreelist.clear(); bufferFreelist.reserve(buffers); samplerFreelist.clear(); samplerFreelist.reserve(samplers); // At least one descriptor per binding even when a pool was sized // zero (Initialize(1, 1, 0) is common): a zero-count pool size is // invalid, and the UI shaders statically reference all four arrays. const std::uint32_t imageCount = std::max(images, 1); const std::uint32_t bufferCount = std::max(buffers, 1); const std::uint32_t samplerCount = std::max(samplers, 1); const VkDescriptorSetLayoutBinding bindings[] = { { storageImageBinding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, imageCount, VK_SHADER_STAGE_COMPUTE_BIT, nullptr }, { sampledImageBinding, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, imageCount, VK_SHADER_STAGE_COMPUTE_BIT, nullptr }, { samplerBinding, VK_DESCRIPTOR_TYPE_SAMPLER, samplerCount, VK_SHADER_STAGE_COMPUTE_BIT, nullptr }, { storageBufferBinding, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bufferCount, VK_SHADER_STAGE_COMPUTE_BIT, nullptr }, }; constexpr VkDescriptorBindingFlags slotFlags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT | VK_DESCRIPTOR_BINDING_UPDATE_UNUSED_WHILE_PENDING_BIT; const VkDescriptorBindingFlags bindingFlags[] = { slotFlags, slotFlags, slotFlags, slotFlags }; const VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO, .bindingCount = static_cast(std::size(bindingFlags)), .pBindingFlags = bindingFlags, }; const VkDescriptorSetLayoutCreateInfo layoutInfo { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .pNext = &bindingFlagsInfo, .flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT, .bindingCount = static_cast(std::size(bindings)), .pBindings = bindings, }; Device::CheckVkResult(vkCreateDescriptorSetLayout(Device::device, &layoutInfo, nullptr, &setLayout)); // One range over the whole push-constant space rather than a // per-shader size: the layout is shared, and each Dispatch pushes // only the bytes its shader declares. const VkPushConstantRange pushRange { .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, .offset = 0, .size = Device::deviceProperties.limits.maxPushConstantsSize & ~3u, }; const VkPipelineLayoutCreateInfo pipelineLayoutInfo { .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1, .pSetLayouts = &setLayout, .pushConstantRangeCount = 1, .pPushConstantRanges = &pushRange, }; Device::CheckVkResult(vkCreatePipelineLayout(Device::device, &pipelineLayoutInfo, nullptr, &pipelineLayout)); const VkDescriptorPoolSize poolSizes[] = { { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, imageCount * Window::numFrames }, { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, imageCount * Window::numFrames }, { VK_DESCRIPTOR_TYPE_SAMPLER, samplerCount * Window::numFrames }, { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bufferCount * Window::numFrames }, }; const VkDescriptorPoolCreateInfo poolInfo { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, .flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, .maxSets = Window::numFrames, .poolSizeCount = static_cast(std::size(poolSizes)), .pPoolSizes = poolSizes, }; Device::CheckVkResult(vkCreateDescriptorPool(Device::device, &poolInfo, nullptr, &setPool)); VkDescriptorSetLayout setLayouts[Window::numFrames]; std::ranges::fill(setLayouts, setLayout); const VkDescriptorSetAllocateInfo allocInfo { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .descriptorPool = setPool, .descriptorSetCount = Window::numFrames, .pSetLayouts = setLayouts, }; Device::CheckVkResult(vkAllocateDescriptorSets(Device::device, &allocInfo, sets)); for (std::vector& views : setImageViews) { views.assign(imageCount, VK_NULL_HANDLE); } setSamplers.assign(samplerCount, VK_NULL_HANDLE); rollback.heap = nullptr; } // ─── Sets-mode descriptor writes ──────────────────────────────── // The counterparts of vkWriteResourceDescriptorsEXT / // vkWriteSamplerDescriptorsEXT into the heap buffers. Call only when // !Device::descriptorHeapSupported. `slot` is the raw index the // Allocate*Slots call returned. // // Each write replaces whatever view or sampler the slot held and // destroys it immediately. That relies on the same contract as slot // reuse in heap mode: no frame in flight may still reference the slot // (true after the queue drain that precedes Window::onResize, and for // a slot that was freed at a scene-switch barrier). // Writes `type` (STORAGE_IMAGE or SAMPLED_IMAGE) at an image slot of // one frame's set, for images that differ per frame: the swapchain. // A slot written this way must be written for every frame. void WriteImageSet(std::uint8_t frame, std::uint16_t slot, VkDescriptorType type, const VkImageViewCreateInfo& viewInfo, VkImageLayout layout) { VkImageView view = VK_NULL_HANDLE; Device::CheckVkResult(vkCreateImageView(Device::device, &viewInfo, nullptr, &view)); vkDestroyImageView(Device::device, setImageViews[frame][slot], nullptr); setImageViews[frame][slot] = view; const VkDescriptorImageInfo imageInfo { .imageView = view, .imageLayout = layout }; const VkWriteDescriptorSet write { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = sets[frame], .dstBinding = type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? storageImageBinding : sampledImageBinding, .dstArrayElement = slot, .descriptorCount = 1, .descriptorType = type, .pImageInfo = &imageInfo, }; vkUpdateDescriptorSets(Device::device, 1, &write, 0, nullptr); } // Writes the same image at an image slot of every frame's set, through // one shared view. void WriteImageSets(std::uint16_t slot, VkDescriptorType type, const VkImageViewCreateInfo& viewInfo, VkImageLayout layout) { VkImageView view = VK_NULL_HANDLE; Device::CheckVkResult(vkCreateImageView(Device::device, &viewInfo, nullptr, &view)); for (std::vector& views : setImageViews) { vkDestroyImageView(Device::device, views[slot], nullptr); views[slot] = VK_NULL_HANDLE; } setImageViews[0][slot] = view; const VkDescriptorImageInfo imageInfo { .imageView = view, .imageLayout = layout }; VkWriteDescriptorSet writes[Window::numFrames]; for (std::uint8_t f = 0; f < Window::numFrames; ++f) { writes[f] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = sets[f], .dstBinding = type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? storageImageBinding : sampledImageBinding, .dstArrayElement = slot, .descriptorCount = 1, .descriptorType = type, .pImageInfo = &imageInfo, }; } vkUpdateDescriptorSets(Device::device, Window::numFrames, writes, 0, nullptr); } // Writes a storage buffer at a buffer slot of every frame's set. The // whole buffer is bound; the shader's runtime array is bounded by the // item count the dispatch passes, as in heap mode. void WriteBufferSets(std::uint16_t slot, VkBuffer buffer) { const VkDescriptorBufferInfo bufferInfo { .buffer = buffer, .offset = 0, .range = VK_WHOLE_SIZE }; VkWriteDescriptorSet writes[Window::numFrames]; for (std::uint8_t f = 0; f < Window::numFrames; ++f) { writes[f] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = sets[f], .dstBinding = storageBufferBinding, .dstArrayElement = slot, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .pBufferInfo = &bufferInfo, }; } vkUpdateDescriptorSets(Device::device, Window::numFrames, writes, 0, nullptr); } // Creates the sampler and writes it at a sampler slot of every // frame's set. void WriteSamplerSets(std::uint16_t slot, const VkSamplerCreateInfo& info) { VkSampler sampler = VK_NULL_HANDLE; Device::CheckVkResult(vkCreateSampler(Device::device, &info, nullptr, &sampler)); vkDestroySampler(Device::device, setSamplers[slot], nullptr); setSamplers[slot] = sampler; const VkDescriptorImageInfo samplerInfo { .sampler = sampler }; VkWriteDescriptorSet writes[Window::numFrames]; for (std::uint8_t f = 0; f < Window::numFrames; ++f) { writes[f] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = sets[f], .dstBinding = samplerBinding, .dstArrayElement = slot, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER, .pImageInfo = &samplerInfo, }; } vkUpdateDescriptorSets(Device::device, Window::numFrames, writes, 0, nullptr); } ImageSlotRange AllocateImageSlots(std::uint16_t count) { if (count == 1 && !imageFreelist.empty()) { std::uint16_t s = imageFreelist.back(); imageFreelist.pop_back(); return {s, 1}; } if (imageNext + count > imageCapacity) { std::uint16_t remaining = static_cast((imageCapacity - imageNext) + imageFreelist.size()); throw std::runtime_error(std::format("DescriptorHeapVulkan: out of image slots ({} requested, {} remaining of {})", count, remaining, imageCapacity)); } ImageSlotRange r{imageNext, count}; imageNext += count; return r; } BufferSlotRange AllocateBufferSlots(std::uint16_t count) { if (count == 1 && !bufferFreelist.empty()) { std::uint16_t s = bufferFreelist.back(); bufferFreelist.pop_back(); return {s, 1}; } if (bufferNext + count > bufferCapacity) { std::uint16_t remaining = static_cast((bufferCapacity - bufferNext) + bufferFreelist.size()); throw std::runtime_error(std::format("DescriptorHeapVulkan: out of buffer slots ({} requested, {} remaining of {})", count, remaining, bufferCapacity)); } BufferSlotRange r{bufferNext, count}; bufferNext += count; return r; } SamplerSlotRange AllocateSamplerSlots(std::uint16_t count) { if (count == 1 && !samplerFreelist.empty()) { std::uint16_t s = samplerFreelist.back(); samplerFreelist.pop_back(); return {s, 1}; } if (samplerNext + count > samplerCapacity) { std::uint16_t remaining = static_cast((samplerCapacity - samplerNext) + samplerFreelist.size()); throw std::runtime_error(std::format("DescriptorHeapVulkan: out of sampler slots ({} requested, {} remaining of {})", count, remaining, samplerCapacity)); } SamplerSlotRange r{samplerNext, count}; samplerNext += count; return r; } // Return slots to the per-pool freelist. The descriptors at these slots // are NOT zeroed; the next allocation that reuses a slot overwrites it // via WriteSampledImageDescriptor / WriteBufferDescriptor / sampler write. // Caller MUST ensure no in-flight GPU frame still references the freed // slots — typically vkDeviceWaitIdle (or a scene-switch barrier) before // freeing. Multi-slot ranges are decomposed into their individual slots. // noexcept: capacity is reserved at Initialize so push_back never reallocates. void FreeImageSlots(ImageSlotRange r) noexcept { for (std::uint16_t i = 0; i < r.count; ++i) { imageFreelist.push_back(static_cast(r.firstElement + i)); } } void FreeBufferSlots(BufferSlotRange r) noexcept { for (std::uint16_t i = 0; i < r.count; ++i) { bufferFreelist.push_back(static_cast(r.firstElement + i)); } } void FreeSamplerSlots(SamplerSlotRange r) noexcept { for (std::uint16_t i = 0; i < r.count; ++i) { samplerFreelist.push_back(static_cast(r.firstElement + i)); } } std::uint32_t ImageByteOffset(std::uint16_t firstElement) const { return firstElement * Device::descriptorHeapProperties.imageDescriptorSize; } std::uint32_t BufferByteOffset(std::uint16_t firstElement) const { return bufferStartOffset + firstElement * Device::descriptorHeapProperties.bufferDescriptorSize; } std::uint32_t SamplerByteOffset(std::uint16_t firstElement) const { return firstElement * Device::descriptorHeapProperties.samplerDescriptorSize; } // Both are 0 in sets mode: buffer slots are plain indices into their // own binding, and the heap descriptor sizes divided by below are zero. inline static std::uint32_t GetBufferOffset(std::uint16_t images, std::uint16_t buffers) { if (!Device::descriptorHeapSupported) { return 0; } std::uint32_t bufferStartElement = images * Device::descriptorHeapProperties.imageDescriptorSize / Device::descriptorHeapProperties.bufferDescriptorSize; if(images > 0 && bufferStartElement == 0) { bufferStartElement = 1; } return bufferStartElement * Device::descriptorHeapProperties.bufferDescriptorSize; } inline static std::uint16_t GetBufferOffsetElement(std::uint16_t images, std::uint16_t buffers) { if (!Device::descriptorHeapSupported) { return 0; } std::uint16_t bufferStartElement = images * Device::descriptorHeapProperties.imageDescriptorSize / Device::descriptorHeapProperties.bufferDescriptorSize; if(images > 0 && bufferStartElement == 0) { bufferStartElement = 1; } return bufferStartElement; } }; // ─── RAII slot handles ────────────────────────────────────────────── // // Move-only owners of a single bindless slot. Destructor returns the slot // to the heap's freelist (noexcept — Free*Slots can't allocate because // Initialize reserved the freelist). Implicitly convertible to std::uint16_t // for use with FillHeader / WriteFooDescriptor / etc.; for buffer slots // this conversion folds in the heap's bufferStartElement so callers always // get the absolute heap index that GLSL `descriptor_heap` expects. // // Lifetime: the handle MUST be destroyed (or moved-from then destroyed) // before the DescriptorHeapVulkan it references. The handle MUST NOT be // destroyed while a frame in-flight on the GPU still references its slot — // free at scene-switch boundaries (vkDeviceWaitIdle), not mid-frame. // // An empty (default-constructed or moved-from) handle is a no-op on // destruction and converts to the sentinel 0xFFFF. struct ImageSlot { ImageSlot() = default; ImageSlot(DescriptorHeapVulkan* heap, std::uint16_t raw) noexcept : heap_(heap), raw_(raw) {} ImageSlot(const ImageSlot&) = delete; ImageSlot& operator=(const ImageSlot&) = delete; ImageSlot(ImageSlot&& o) noexcept : heap_(o.heap_), raw_(o.raw_) { o.heap_ = nullptr; } ImageSlot& operator=(ImageSlot&& o) noexcept { if (this != &o) { Reset(); heap_ = o.heap_; raw_ = o.raw_; o.heap_ = nullptr; } return *this; } ~ImageSlot() noexcept { Reset(); } void Reset() noexcept { if (heap_) { heap_->FreeImageSlots({raw_, 1}); heap_ = nullptr; } } operator std::uint16_t() const noexcept { return heap_ ? raw_ : std::uint16_t{0xFFFF}; } explicit operator bool() const noexcept { return heap_ != nullptr; } private: DescriptorHeapVulkan* heap_ = nullptr; std::uint16_t raw_ = 0; }; struct BufferSlot { BufferSlot() = default; BufferSlot(DescriptorHeapVulkan* heap, std::uint16_t raw) noexcept : heap_(heap), raw_(raw) {} BufferSlot(const BufferSlot&) = delete; BufferSlot& operator=(const BufferSlot&) = delete; BufferSlot(BufferSlot&& o) noexcept : heap_(o.heap_), raw_(o.raw_) { o.heap_ = nullptr; } BufferSlot& operator=(BufferSlot&& o) noexcept { if (this != &o) { Reset(); heap_ = o.heap_; raw_ = o.raw_; o.heap_ = nullptr; } return *this; } ~BufferSlot() noexcept { Reset(); } void Reset() noexcept { if (heap_) { heap_->FreeBufferSlots({raw_, 1}); heap_ = nullptr; } } // Absolute heap index = bufferStartElement + raw. GLSL descriptor_heap // SSBOs are indexed in buffer-descriptor units from heap byte 0; the // raw bump-allocator index is relative to the buffer region's start. operator std::uint16_t() const noexcept { return heap_ ? static_cast(heap_->bufferStartElement + raw_) : std::uint16_t{0xFFFF}; } explicit operator bool() const noexcept { return heap_ != nullptr; } private: DescriptorHeapVulkan* heap_ = nullptr; std::uint16_t raw_ = 0; }; struct SamplerSlot { SamplerSlot() = default; SamplerSlot(DescriptorHeapVulkan* heap, std::uint16_t raw) noexcept : heap_(heap), raw_(raw) {} SamplerSlot(const SamplerSlot&) = delete; SamplerSlot& operator=(const SamplerSlot&) = delete; SamplerSlot(SamplerSlot&& o) noexcept : heap_(o.heap_), raw_(o.raw_) { o.heap_ = nullptr; } SamplerSlot& operator=(SamplerSlot&& o) noexcept { if (this != &o) { Reset(); heap_ = o.heap_; raw_ = o.raw_; o.heap_ = nullptr; } return *this; } ~SamplerSlot() noexcept { Reset(); } void Reset() noexcept { if (heap_) { heap_->FreeSamplerSlots({raw_, 1}); heap_ = nullptr; } } operator std::uint16_t() const noexcept { return heap_ ? raw_ : std::uint16_t{0xFFFF}; } explicit operator bool() const noexcept { return heap_ != nullptr; } private: DescriptorHeapVulkan* heap_ = nullptr; std::uint16_t raw_ = 0; }; } #endif // !CRAFTER_GRAPHICS_WINDOW_DOM