fix(window): size the swapchain for its present mode, and key frames in flight by frame instead of image
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>
This commit is contained in:
parent
6f471ca6b1
commit
234fcce099
29 changed files with 1378 additions and 550 deletions
|
|
@ -61,7 +61,10 @@ export namespace Crafter {
|
|||
// 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.
|
||||
// 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;
|
||||
|
|
@ -77,12 +80,116 @@ export namespace Crafter {
|
|||
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<VkImageView> setImageViews[Window::numFrames];
|
||||
// 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;
|
||||
|
|
@ -105,12 +212,10 @@ export namespace Crafter {
|
|||
// 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<VkImageView>& views : setImageViews) {
|
||||
for (VkImageView view : views) {
|
||||
vkDestroyImageView(Device::device, view, nullptr);
|
||||
}
|
||||
views.clear();
|
||||
for (VkImageView view : setImageViews) {
|
||||
vkDestroyImageView(Device::device, view, nullptr);
|
||||
}
|
||||
setImageViews.clear();
|
||||
for (VkSampler sampler : setSamplers) {
|
||||
vkDestroySampler(Device::device, sampler, nullptr);
|
||||
}
|
||||
|
|
@ -224,6 +329,8 @@ export namespace Crafter {
|
|||
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
|
||||
|
|
@ -272,6 +379,8 @@ export namespace Crafter {
|
|||
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
|
||||
|
|
@ -347,9 +456,7 @@ export namespace Crafter {
|
|||
};
|
||||
Device::CheckVkResult(vkAllocateDescriptorSets(Device::device, &allocInfo, sets));
|
||||
|
||||
for (std::vector<VkImageView>& views : setImageViews) {
|
||||
views.assign(imageCount, VK_NULL_HANDLE);
|
||||
}
|
||||
setImageViews.assign(imageCount, VK_NULL_HANDLE);
|
||||
setSamplers.assign(samplerCount, VK_NULL_HANDLE);
|
||||
rollback.heap = nullptr;
|
||||
}
|
||||
|
|
@ -364,30 +471,8 @@ export namespace Crafter {
|
|||
// 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);
|
||||
}
|
||||
// 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.
|
||||
|
|
@ -395,11 +480,8 @@ export namespace Crafter {
|
|||
const VkImageViewCreateInfo& viewInfo, VkImageLayout layout) {
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
Device::CheckVkResult(vkCreateImageView(Device::device, &viewInfo, nullptr, &view));
|
||||
for (std::vector<VkImageView>& views : setImageViews) {
|
||||
vkDestroyImageView(Device::device, views[slot], nullptr);
|
||||
views[slot] = VK_NULL_HANDLE;
|
||||
}
|
||||
setImageViews[0][slot] = view;
|
||||
vkDestroyImageView(Device::device, setImageViews[slot], nullptr);
|
||||
setImageViews[slot] = view;
|
||||
|
||||
const VkDescriptorImageInfo imageInfo { .imageView = view, .imageLayout = layout };
|
||||
VkWriteDescriptorSet writes[Window::numFrames];
|
||||
|
|
@ -543,7 +625,12 @@ export namespace Crafter {
|
|||
// 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue