Window - CreateSwapchain asked for exactly numFrames (3) images whatever the surface said, read them into a three-element array, and keyed the command buffers, the fences and the descriptor-heap copy holding "its" swapchain image by the acquired image index. A swapchain of another size fell back to FIFO (asked for MAILBOX or IMMEDIATE) or threw, and a surface whose minimum is above three got a create info below it (#160). Mesa's MAILBOX needs four images: Turnip on phoc, llvmpipe on any Wayland compositor. - The present mode is picked first, then the swapchain is sized by SwapchainCapabilities: the surface's counts for that mode where the instance has surface_maintenance1. With swapchain_maintenance1 the swapchain names its mode (VkSwapchainPresentModesCreateInfoKHR), which is what makes the mode's own count the one that applies, at creation and to the forward progress of a UINT64_MAX acquire; without it the stricter of the mode's and the surface's counts for all modes. It asks for numFrames or that minimum where more, never past the maximum (SwapchainMinImageCount). Turnip's FIFO keeps its three images and MAILBOX gets four. The FIFO fallback and presentModeUnfit are gone. - images, imageViewInfos (was imageViews: they are create-infos), the made imageViews that descriptor sets take, imageInitialised and the render-finished semaphores are per image, as many as the driver made. The views are made once per swapchain. The semaphores only grow, since a present of the old swapchain may still wait on one. - Frames in flight stay numFrames, keyed by currentFrame, frameCounter % numFrames: the command buffer, the fence, the acquire semaphore, the heap copy (or set), RenderingElement3D::tlases, the frameIdx a pass is recorded with. currentImage is the acquired image. currentBuffer is gone, so code that used it stops compiling instead of picking the wrong one; RenderPass::Record's frameIdx keeps its name and is now the frame slot, as RenderPass.cppm says. - The frame's fence is waited before the acquire. The acquire semaphores need no spare then, and a frame waits for exactly the one numFrames before it, which the deletion queue and the staging rings count on and fences keyed by image did not promise once images come back out of order. The swapchain slots are written before the fence is reset. - From the acquire to the present the window is recording: a Resize or SetPresentMode from a listener (a VSync key ticked in onUpdate) takes effect after the present, through RebuildSwapchain, together with a present that found the swapchain out of date. Rebuilding mid-frame used to destroy the image the frame was drawing into; with images sized per swapchain it would also index past the new one's end. DescriptorHeapVulkan - Frames and images pair up differently from frame to frame, so no heap copy can keep one swapchain image. AddSwapchainSlot(slot, window) lists an image slot as that window's swapchain image, and Window::Render writes the image a frame acquired into its window's listed slots in that frame's copy, or its set, before onUpdate, skipping a slot that holds it already. A slot is one window's, so two windows can share a heap without writing each other's slots under each other's frames in flight. A slot listed during a frame its window records is written at once. A slot past the heap's image slots, an empty ImageSlot's 0xFFFF among them, is refused. Freeing a slot unlists it, as does initialising the heap in either binding model. WriteImageSet, the one use of which this replaces, is gone, and with it the per-frame dimension of setImageViews. Device - VK_KHR_surface_maintenance1 (or EXT) and VK_KHR_get_surface_capabilities2 are enabled where the loader has them (surfaceMaintenance1Supported, vkGetPhysicalDeviceSurfaceCapabilities2KHR), and the matching swapchain_maintenance1 with its feature where the device has it (swapchainMaintenance1Supported). - DistinctGpus groups the physical devices by GPU as CRAFTER_GRAPHICS_DEVICE numbers them, for ChooseGpu and tests. UI - UIRenderer lists its output slot instead of writing every copy itself, and again on every resize; the resize listener is gone on both backends. The headers' frameIdx is currentFrame. Examples - VulkanTriangle, Sponza, SponzaBench and RTVolume list their output slot and write only the per-frame TLASes. They wrote imageViews[f] into copy f once, which a resize left pointing at destroyed images. Tests - SwapchainImageCount: SwapchainMinImageCount on the numbers drivers report; every offered mode keeps its mode and gets what the window asks for; every frame presents the colour it drew, read back a texel per frame; a switch and a resize asked for during a frame wait for its present; a slot listed during a frame holds its image at once; two windows on one heap draw their own colours. The frame checks run again in child processes, through CRAFTER_GRAPHICS_DEVICE, on a GPU whose swapchains have more images than frames (this one, else llvmpipe), in each binding model the parent didn't cover; a child skips only for a GPU that can't run the library, and checks it runs the model it was asked for. - FrameLoopSync checks the frame slot follows the frame counter and counts the swapchain's own images. HeapASRead, HeapDynamicIndexHit and RTPassPushData list their swapchain slot and drop their resize rewrites. Docs - README, AGENTS.md and AGENTS.consumer.md: frames and images, the swapchain slots, rebuilds held to the present, the new optional groups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
835 lines
44 KiB
C++
835 lines
44 KiB
C++
//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<std::uint8_t, true> resourceHeap[Window::numFrames];
|
|
VulkanBuffer<std::uint8_t, true> samplerHeap[Window::numFrames];
|
|
std::uint32_t bufferStartOffset;
|
|
std::uint16_t bufferStartElement;
|
|
// Where each heap's reserved range starts; it runs to the heap's end.
|
|
// Heap mode only. Bind passes them on.
|
|
std::uint32_t resourceReservedRangeOffset = 0;
|
|
std::uint32_t samplerReservedRangeOffset = 0;
|
|
|
|
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<std::uint16_t> imageFreelist;
|
|
std::vector<std::uint16_t> bufferFreelist;
|
|
std::vector<std::uint16_t> 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. A swapchain slot too
|
|
// is written into a set another window's frame in flight is bound
|
|
// to, and when listed during a frame, into the set that frame has
|
|
// already 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[slot] is the view every frame's set uses
|
|
// at that image slot, setSamplers[slot] likewise. The swapchain's
|
|
// views are its window's (Window::imageViews).
|
|
std::vector<VkImageView> setImageViews;
|
|
std::vector<VkSampler> setSamplers;
|
|
|
|
// ─── Swapchain slots ────────────────────────────────────────────
|
|
// Image slots that name a window's swapchain image. A window's
|
|
// swapchain images are not its frames: it keeps Window::numFrames
|
|
// frames in flight over however many images the driver made, and
|
|
// which one a frame draws into is known only once it has been
|
|
// acquired (#160). So each frame, after the acquire and before
|
|
// onUpdate, Window::Render writes the image it acquired into the slots
|
|
// listed here for it, as a storage image in GENERAL, in that frame's
|
|
// heap copy (or descriptor set), through WriteSwapchainImage. A shader
|
|
// reads the image it draws into from one fixed slot, and nothing has
|
|
// to be written again after a resize. Each slot is one window's, so
|
|
// two windows can share a heap: neither writes the other's slots,
|
|
// which the other's frames in flight may be reading. UIRenderer lists
|
|
// its output slot; a pass of your own lists its own with
|
|
// AddSwapchainSlot. Freeing a slot takes it off the list.
|
|
struct SwapchainSlot {
|
|
std::uint16_t slot = 0;
|
|
// Compared, never dereferenced: a window gone takes nothing down.
|
|
const Window* window = nullptr;
|
|
// What each frame's copy holds at the slot, so a frame whose image
|
|
// hasn't changed writes nothing: the image's index in the
|
|
// swapchain of that Window::swapchainGeneration, which starts at
|
|
// 1, so generation 0 means nothing yet.
|
|
struct Written {
|
|
std::uint64_t generation = 0;
|
|
std::uint32_t image = 0;
|
|
};
|
|
std::array<Written, Window::numFrames> written {};
|
|
};
|
|
std::vector<SwapchainSlot> swapchainSlots;
|
|
|
|
// Lists an image slot, raw as AllocateImageSlots returned it, as one
|
|
// that names `window`'s swapchain image. Every frame's copy gets it
|
|
// from that frame on; a slot listed during a frame `window` records
|
|
// with this heap gets that frame's image at once, so a pass set up
|
|
// mid-frame draws into it the same frame. Listing a slot again moves
|
|
// it to `window`. Throws std::invalid_argument for a slot past the
|
|
// heap's image slots, which an empty ImageSlot (0xFFFF) is.
|
|
void AddSwapchainSlot(std::uint16_t slot, const Window& window) {
|
|
if (slot >= imageCapacity) {
|
|
throw std::invalid_argument(std::format(
|
|
"DescriptorHeapVulkan::AddSwapchainSlot: image slot {} is not one of the heap's {} image slots", slot, imageCapacity));
|
|
}
|
|
std::erase_if(swapchainSlots, [slot](const SwapchainSlot& listed) { return listed.slot == slot; });
|
|
swapchainSlots.push_back({ .slot = slot, .window = &window });
|
|
if (window.recording && window.descriptorHeap == this) {
|
|
WriteSwapchainImage(window);
|
|
}
|
|
}
|
|
|
|
// Writes window.imageViewInfos[window.currentImage] (in sets mode,
|
|
// window.imageViews[window.currentImage]) into window's swapchain
|
|
// slots in the copy of window.currentFrame. Window::Render calls it
|
|
// once the frame's fence has been waited, so none of window's frames
|
|
// still reads that copy, and other windows' frames don't read these
|
|
// slots.
|
|
void WriteSwapchainImage(const Window& window) {
|
|
for (SwapchainSlot& listed : swapchainSlots) {
|
|
if (listed.window != &window) {
|
|
continue;
|
|
}
|
|
SwapchainSlot::Written& written = listed.written[window.currentFrame];
|
|
if (written.generation == window.swapchainGeneration && written.image == window.currentImage) {
|
|
continue;
|
|
}
|
|
if (Device::descriptorHeapSupported) {
|
|
const VkImageDescriptorInfoEXT imageInfo {
|
|
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
.pView = &window.imageViewInfos[window.currentImage],
|
|
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
|
};
|
|
const VkResourceDescriptorInfoEXT resource {
|
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
.data = { .pImage = &imageInfo },
|
|
};
|
|
VulkanBuffer<std::uint8_t, true>& heap = resourceHeap[window.currentFrame];
|
|
const std::uint32_t size = Device::descriptorHeapProperties.imageDescriptorSize;
|
|
const VkHostAddressRangeEXT destination {
|
|
.address = heap.value + ImageByteOffset(listed.slot),
|
|
.size = size,
|
|
};
|
|
Device::vkWriteResourceDescriptorsEXT(Device::device, 1, &resource, &destination);
|
|
heap.FlushDevice(ImageByteOffset(listed.slot), size);
|
|
} else {
|
|
const VkDescriptorImageInfo imageInfo {
|
|
.imageView = window.imageViews[window.currentImage],
|
|
.imageLayout = VK_IMAGE_LAYOUT_GENERAL,
|
|
};
|
|
const VkWriteDescriptorSet write {
|
|
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
|
|
.dstSet = sets[window.currentFrame],
|
|
.dstBinding = storageImageBinding,
|
|
.dstArrayElement = listed.slot,
|
|
.descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
.pImageInfo = &imageInfo,
|
|
};
|
|
vkUpdateDescriptorSets(Device::device, 1, &write, 0, nullptr);
|
|
}
|
|
written = { .generation = window.swapchainGeneration, .image = window.currentImage };
|
|
}
|
|
}
|
|
|
|
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 (VkImageView view : setImageViews) {
|
|
vkDestroyImageView(Device::device, view, nullptr);
|
|
}
|
|
setImageViews.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});
|
|
}
|
|
|
|
// Where Initialize puts things in each frame's two heaps: image
|
|
// descriptors from byte 0, the buffer descriptors after them, then
|
|
// the implementation's reserved range, with which the heap ends;
|
|
// sampler descriptors from byte 0 of the sampler heap, then its
|
|
// reserved range.
|
|
struct HeapLayout {
|
|
std::uint16_t bufferStartElement;
|
|
std::uint32_t bufferStartOffset;
|
|
std::uint32_t resourceReservedRangeOffset;
|
|
std::uint32_t resourceHeapSize;
|
|
std::uint32_t samplerReservedRangeOffset;
|
|
std::uint32_t samplerHeapSize;
|
|
|
|
bool operator==(const HeapLayout&) const = default;
|
|
};
|
|
|
|
// The layout of a heap with `images`, `buffers` and `samplers` slots
|
|
// under these descriptor sizes and alignments. Heap mode only:
|
|
// without the extension the sizes are zero. Pure, so the arithmetic
|
|
// can be checked against sizes no local driver reports.
|
|
//
|
|
// A descriptor_heap array indexes the heap from byte 0 in strides of
|
|
// its descriptor size (its ArrayStrideIdEXT is the OpConstantSizeOfEXT
|
|
// of the descriptor type), so buffer slot 0 has to sit on a whole
|
|
// buffer descriptor: the first one past the image descriptors. The
|
|
// start used to be rounded down (#163), which put the buffers over the
|
|
// last image descriptors wherever those were not a whole number of
|
|
// buffer descriptors, and where they were less than one, sized the
|
|
// heap as if the buffers began right after them, so the last buffer
|
|
// descriptors landed in the reserved range, which belongs to the
|
|
// implementation. Both need image descriptors smaller than buffer
|
|
// descriptors; NVIDIA's and llvmpipe's are not. Every descriptor size
|
|
// and alignment is a power of two, each alignment at most its size,
|
|
// so a whole number of descriptors is aligned for its kind. The
|
|
// reserved range has to start on a multiple of both resource
|
|
// alignments (VUID-vkCmdBindResourceHeapEXT-pBindInfo-11435 and
|
|
// -11436), which for powers of two is a multiple of the larger. The
|
|
// heap ends with the reserved range.
|
|
static HeapLayout ComputeLayout(const VkPhysicalDeviceDescriptorHeapPropertiesEXT& properties,
|
|
std::uint16_t images, std::uint16_t buffers, std::uint16_t samplers) {
|
|
const std::uint64_t imageEnd = images * properties.imageDescriptorSize;
|
|
// Without buffer slots there is no buffer region to place, and its
|
|
// start is 0, as in sets mode.
|
|
const std::uint64_t bufferStart = buffers == 0 ? 0 : (imageEnd + properties.bufferDescriptorSize - 1) / properties.bufferDescriptorSize;
|
|
// Shaders and BufferSlot carry absolute buffer indices in 16 bits,
|
|
// and 0xFFFF is the empty slot.
|
|
if (bufferStart + buffers > 0xFFFF) {
|
|
throw std::runtime_error(std::format("DescriptorHeapVulkan: {} image and {} buffer slots do not fit 16-bit slot indices (with {}-byte image and {}-byte buffer descriptors the buffer slots start at index {})",
|
|
images, buffers, properties.imageDescriptorSize, properties.bufferDescriptorSize, bufferStart));
|
|
}
|
|
const std::uint64_t bufferStartOffset = bufferStart * properties.bufferDescriptorSize;
|
|
// The reserved range follows whichever descriptors end last. Neither
|
|
// heap is ever empty, though: a zero-size buffer is invalid
|
|
// (VUID-VkBufferCreateInfo-size-00912), and on a driver that
|
|
// reserves nothing (llvmpipe) the common Initialize(1, 1, 0) made
|
|
// the sampler heap one. Each holds at least one descriptor, as each
|
|
// binding does in InitializeSets.
|
|
const std::uint64_t descriptorEnd = std::max({ imageEnd, bufferStartOffset + buffers * properties.bufferDescriptorSize,
|
|
properties.bufferDescriptorSize });
|
|
const std::uint64_t resourceReserved = AlignUp(descriptorEnd, std::max(properties.imageDescriptorAlignment, properties.bufferDescriptorAlignment));
|
|
const std::uint64_t samplerReserved = std::max<std::uint64_t>(samplers, 1) * properties.samplerDescriptorSize;
|
|
return {
|
|
.bufferStartElement = static_cast<std::uint16_t>(bufferStart),
|
|
.bufferStartOffset = static_cast<std::uint32_t>(bufferStartOffset),
|
|
.resourceReservedRangeOffset = static_cast<std::uint32_t>(resourceReserved),
|
|
.resourceHeapSize = static_cast<std::uint32_t>(resourceReserved + properties.minResourceHeapReservedRange),
|
|
.samplerReservedRangeOffset = static_cast<std::uint32_t>(samplerReserved),
|
|
.samplerHeapSize = static_cast<std::uint32_t>(samplerReserved + properties.minSamplerHeapReservedRange),
|
|
};
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
const HeapLayout layout = ComputeLayout(Device::descriptorHeapProperties, images, buffers, samplers);
|
|
bufferStartElement = layout.bufferStartElement;
|
|
bufferStartOffset = layout.bufferStartOffset;
|
|
resourceReservedRangeOffset = layout.resourceReservedRangeOffset;
|
|
samplerReservedRangeOffset = layout.samplerReservedRangeOffset;
|
|
|
|
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);
|
|
// Every slot is unallocated again, and the new heaps hold nothing.
|
|
swapchainSlots.clear();
|
|
|
|
// 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, layout.resourceHeapSize, 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, layout.samplerHeapSize, 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);
|
|
// Every slot is unallocated again, as in heap mode.
|
|
swapchainSlots.clear();
|
|
|
|
// 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<std::uint32_t>(images, 1);
|
|
const std::uint32_t bufferCount = std::max<std::uint32_t>(buffers, 1);
|
|
const std::uint32_t samplerCount = std::max<std::uint32_t>(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::uint32_t>(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::uint32_t>(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::uint32_t>(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));
|
|
|
|
setImageViews.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). The swapchain
|
|
// image is written per frame by WriteSwapchainImage instead.
|
|
|
|
// 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));
|
|
vkDestroyImageView(Device::device, setImageViews[slot], nullptr);
|
|
setImageViews[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);
|
|
}
|
|
|
|
// Takes `count` consecutive slots out of `freelist`, the first of
|
|
// them returned, if it holds a run that long: freed ranges and freed
|
|
// single slots alike come back as ranges, rather than only fresh
|
|
// slots serving a range. The freelists are kept sorted (Free), which
|
|
// puts neighbours side by side. A freed slot is handed out again at
|
|
// once, which is why the Free* calls below require that no frame in
|
|
// flight still uses it.
|
|
static std::optional<std::uint16_t> TakeRun(std::vector<std::uint16_t>& freelist, std::uint16_t count) {
|
|
if (count == 0 || freelist.size() < count) return std::nullopt;
|
|
for (std::size_t i = 0; i + count <= freelist.size(); ++i) {
|
|
// Sorted and without repeats: the ends `count - 1` apart are a run.
|
|
if (freelist[i + count - 1] - freelist[i] == count - 1) {
|
|
const std::uint16_t first = freelist[i];
|
|
freelist.erase(freelist.begin() + static_cast<std::ptrdiff_t>(i), freelist.begin() + static_cast<std::ptrdiff_t>(i + count));
|
|
return first;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
ImageSlotRange AllocateImageSlots(std::uint16_t count) {
|
|
if (count == 1 && !imageFreelist.empty()) {
|
|
std::uint16_t s = imageFreelist.back();
|
|
imageFreelist.pop_back();
|
|
return {s, 1};
|
|
}
|
|
if (const std::optional<std::uint16_t> first = TakeRun(imageFreelist, count)) {
|
|
return {*first, count};
|
|
}
|
|
if (imageNext + count > imageCapacity) {
|
|
std::uint16_t remaining = static_cast<std::uint16_t>((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 (const std::optional<std::uint16_t> first = TakeRun(bufferFreelist, count)) {
|
|
return {*first, count};
|
|
}
|
|
if (bufferNext + count > bufferCapacity) {
|
|
std::uint16_t remaining = static_cast<std::uint16_t>((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 (const std::optional<std::uint16_t> first = TakeRun(samplerFreelist, count)) {
|
|
return {*first, count};
|
|
}
|
|
if (samplerNext + count > samplerCapacity) {
|
|
std::uint16_t remaining = static_cast<std::uint16_t>((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,
|
|
// each put in its sorted place (TakeRun looks for runs).
|
|
// noexcept: capacity is reserved at Initialize so inserting never reallocates.
|
|
// A freed swapchain slot stops being one, so the window doesn't write
|
|
// the swapchain image over the slot's next owner.
|
|
void FreeImageSlots(ImageSlotRange r) noexcept {
|
|
std::erase_if(swapchainSlots, [r](const SwapchainSlot& listed) {
|
|
return listed.slot >= r.firstElement && listed.slot - r.firstElement < r.count;
|
|
});
|
|
Release(imageFreelist, r.firstElement, r.count);
|
|
}
|
|
|
|
void FreeBufferSlots(BufferSlotRange r) noexcept {
|
|
Release(bufferFreelist, r.firstElement, r.count);
|
|
}
|
|
|
|
void FreeSamplerSlots(SamplerSlotRange r) noexcept {
|
|
Release(samplerFreelist, r.firstElement, r.count);
|
|
}
|
|
|
|
static void Release(std::vector<std::uint16_t>& freelist, std::uint16_t first, std::uint16_t count) noexcept {
|
|
const auto at = std::ranges::lower_bound(freelist, first);
|
|
const std::ptrdiff_t index = at - freelist.begin();
|
|
for (std::uint16_t i = 0; i < count; ++i) {
|
|
freelist.insert(freelist.begin() + index + i, static_cast<std::uint16_t>(first + 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;
|
|
}
|
|
|
|
// Binds frame `frame`'s heaps into `cmd`, with the reserved ranges
|
|
// where Initialize put them, or in sets mode the frame's descriptor
|
|
// set at the compute bind point. Window::Render does it for every
|
|
// frame it records; a command buffer outside the frame loop that
|
|
// dispatches against the heap does it itself (examples/RTVolume).
|
|
void Bind(VkCommandBuffer cmd, std::uint32_t frame) const {
|
|
if (!Device::descriptorHeapSupported) {
|
|
// Every compute pipeline loaded against this heap shares
|
|
// pipelineLayout, so one bind stays valid across all of them,
|
|
// as the heap bind does.
|
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &sets[frame], 0, nullptr);
|
|
return;
|
|
}
|
|
const VkBindHeapInfoEXT resourceInfo {
|
|
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
|
|
.heapRange = { .address = resourceHeap[frame].address, .size = resourceHeap[frame].size },
|
|
.reservedRangeOffset = resourceReservedRangeOffset,
|
|
.reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange,
|
|
};
|
|
const VkBindHeapInfoEXT samplerInfo {
|
|
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
|
|
.heapRange = { .address = samplerHeap[frame].address, .size = samplerHeap[frame].size },
|
|
.reservedRangeOffset = samplerReservedRangeOffset,
|
|
.reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange,
|
|
};
|
|
Device::vkCmdBindResourceHeapEXT(cmd, &resourceInfo);
|
|
Device::vkCmdBindSamplerHeapEXT(cmd, &samplerInfo);
|
|
}
|
|
};
|
|
|
|
// ─── 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<std::uint16_t>(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
|