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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -295,6 +295,20 @@ export namespace Crafter {
|
|||
inline static bool rayTracingSupported = false;
|
||||
// VK_EXT_device_fault: the VK_ERROR_DEVICE_LOST report in CheckVkResult.
|
||||
inline static bool deviceFaultSupported = false;
|
||||
// VK_KHR_surface_maintenance1 (or its EXT form) with
|
||||
// VK_KHR_get_surface_capabilities2, instance extensions enabled where
|
||||
// the loader has them: a surface's capabilities for one present mode.
|
||||
// Its image counts can differ from the surface's answer for all modes,
|
||||
// and Window::CreateSwapchain asks for the mode it uses (#160).
|
||||
inline static bool surfaceMaintenance1Supported = false;
|
||||
inline static PFN_vkGetPhysicalDeviceSurfaceCapabilities2KHR vkGetPhysicalDeviceSurfaceCapabilities2KHR = nullptr;
|
||||
// VK_KHR_swapchain_maintenance1 (or its EXT form) with its
|
||||
// swapchainMaintenance1 feature, a device group enabled where the
|
||||
// driver has it. With it a swapchain is made naming its present mode
|
||||
// (VkSwapchainPresentModesCreateInfoKHR), which is what lets it have
|
||||
// that mode's image count where the surface's count for all modes is
|
||||
// higher (Turnip's FIFO: 3 where MAILBOX needs 4).
|
||||
inline static bool swapchainMaintenance1Supported = false;
|
||||
// Pipeline stages that are valid on this device: everything, minus
|
||||
// RAY_TRACING_SHADER when ray tracing is unsupported. AND it into any
|
||||
// stage mask built from a constant that names the RT stage.
|
||||
|
|
@ -340,6 +354,12 @@ export namespace Crafter {
|
|||
// over, and throws listing every GPU's gaps when none qualifies. Pure
|
||||
// logic apart from that print, driven by tests/DeviceFeatures.
|
||||
static std::size_t ChooseDevice(std::span<const DeviceChoice> choices);
|
||||
// `instance`'s physical devices grouped by GPU, in the loader's order:
|
||||
// a GPU two drivers both list (a Flatpak sees the runtime's and the GL
|
||||
// extension's manifests; RADV beside AMDVLK) has one device UUID and
|
||||
// two handles. CRAFTER_GRAPHICS_DEVICE numbers these groups, so one
|
||||
// number means the same GPU inside a sandbox and out.
|
||||
static std::vector<std::vector<VkPhysicalDevice>> DistinctGpus(VkInstance instance);
|
||||
// Throws when `spirv` declares, directly or implicitly (Int64Atomics
|
||||
// declares Int64), a SPIR-V capability that nothing enabled: a core
|
||||
// feature that is off, a subgroup operation the GPU lacks, or one of
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ export namespace Crafter {
|
|||
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
|
||||
struct RenderPass {
|
||||
// `frameIdx` is the frame's slot among the frames in flight
|
||||
// (Window::currentFrame), for what lives per frame: the heap copy
|
||||
// bound, RenderingElement3D::tlases. It is not the swapchain image,
|
||||
// which is window.images[window.currentImage] natively: the two stopped
|
||||
// being one index once the swapchain could have more images than
|
||||
// frames in flight (#160).
|
||||
virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
|
||||
virtual ~RenderPass() = default;
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
|
|
|
|||
|
|
@ -151,11 +151,11 @@ export namespace Crafter {
|
|||
// The build writes tlases[index] in place: its instance and metadata
|
||||
// buffers on the host as it records, its acceleration structure and
|
||||
// scratch on the GPU. So call it only once every earlier submission
|
||||
// that used tlases[index] has completed. Window::Render runs onUpdate
|
||||
// after waiting for the fence of the image it renders, so building
|
||||
// tlases[currentBuffer] there is safe. Earlier builds, traces and
|
||||
// compute ray queries in the same command buffer are ordered before
|
||||
// the build by the build itself.
|
||||
// that used tlases[index] has completed. The TLASes are per frame in
|
||||
// flight, and Window::Render runs onUpdate after waiting for the fence
|
||||
// of its frame slot, so building tlases[currentFrame] there is safe.
|
||||
// Earlier builds, traces and compute ray queries in the same command
|
||||
// buffer are ordered before the build by the build itself.
|
||||
//
|
||||
// A rebuild for a new instance count or preference builds into the
|
||||
// same acceleration structure while it fits its storage, so
|
||||
|
|
|
|||
|
|
@ -429,10 +429,7 @@ export namespace Crafter {
|
|||
bool firstDispatchThisFrame_ = true;
|
||||
#endif
|
||||
|
||||
Crafter::EventListener<void> resizeSub_;
|
||||
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
void WriteSwapchainDescriptors();
|
||||
void WriteFontAtlasDescriptor();
|
||||
void WriteBufferDescriptor(std::uint16_t slot, VkBuffer buffer, VkDeviceAddress address, std::uint32_t size);
|
||||
void WriteSampledImageDescriptor(std::uint16_t slot,
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ export namespace Crafter {
|
|||
// has. Mailbox replaces a frame still waiting to be shown, so presenting
|
||||
// never waits, which suits a window that updates on its own clock
|
||||
// (Window::updateInterval). Immediate shows each frame at once and may
|
||||
// tear. A mode the surface lacks falls back to Fifo, as does one whose
|
||||
// swapchain the driver makes with more images than the frame loop's
|
||||
// Window::numFrames (Mesa's Mailbox has at least four). In the browser
|
||||
// the page presents the canvas and the mode has no effect.
|
||||
// tear. A mode the surface lacks falls back to Fifo. The mode decides how
|
||||
// many images the swapchain has (Mesa's Mailbox needs at least four),
|
||||
// which the frame loop takes as they come. In the browser the page
|
||||
// presents the canvas and the mode has no effect.
|
||||
enum class PresentMode {
|
||||
Fifo,
|
||||
Mailbox,
|
||||
|
|
@ -79,10 +79,12 @@ export namespace Crafter {
|
|||
Event<void> onClose;
|
||||
Event<void> onBeforeUpdate;
|
||||
Event<FrameTime> onUpdate;
|
||||
// Fires when the swapchain has been recreated for a new size.
|
||||
// width/height already reflect the new size. Passes that hold
|
||||
// descriptors referring to window.imageViews[] must re-write them
|
||||
// here (the old VkImage handles have been destroyed).
|
||||
// Fires when the swapchain has been recreated, for a new size or a
|
||||
// new present mode. width/height already reflect the new size, and
|
||||
// the old images have been destroyed: anything that holds one of
|
||||
// them, or is sized after the window, is made again here. The heap
|
||||
// slots that name the swapchain image follow by themselves (see
|
||||
// DescriptorHeapVulkan::AddSwapchainSlot).
|
||||
Event<void> onResize;
|
||||
bool open = true;
|
||||
bool updating = false;
|
||||
|
|
@ -208,7 +210,10 @@ export namespace Crafter {
|
|||
void StartUpdate();
|
||||
void StopUpdate();
|
||||
// Makes the swapchain again with the new mode and fires onResize, as
|
||||
// a resize does, so call it outside StartInit/FinishInit. The
|
||||
// a resize does, so call it outside StartInit/FinishInit. Called while
|
||||
// a frame is being recorded (from onUpdate, a pass, an input action
|
||||
// ticked there), the new swapchain is made once that frame has been
|
||||
// presented: the frame still draws into an image of the old one. The
|
||||
// environment variable CRAFTER_PRESENT_IMMEDIATE overrides the mode,
|
||||
// for benchmarks.
|
||||
void SetPresentMode(PresentMode mode);
|
||||
|
|
@ -221,6 +226,9 @@ export namespace Crafter {
|
|||
// pixels, or that none has. Remembered: it is applied again whenever
|
||||
// the keyboard focus comes back to this window.
|
||||
void SetTextInput(bool active, std::int32_t x = 0, std::int32_t y = 0, std::int32_t width = 0, std::int32_t height = 0, TextInputPurpose purpose = TextInputPurpose::Normal);
|
||||
// Like SetPresentMode, a resize asked for while a frame is being
|
||||
// recorded takes effect once that frame has been presented, width and
|
||||
// height included, which the rest of the frame still draws at.
|
||||
void Resize(std::uint32_t width, std::uint32_t height);
|
||||
void Render();
|
||||
void Update();
|
||||
|
|
@ -337,13 +345,35 @@ export namespace Crafter {
|
|||
void FinishInit();
|
||||
VkCommandBuffer GetCmd();
|
||||
void EndCmd(VkCommandBuffer cmd);
|
||||
// Makes the swapchain, after the old one's images have stopped being
|
||||
// used: every caller has waited for the queue (or has no frames yet).
|
||||
void CreateSwapchain();
|
||||
// Tear-and-rebuild helper used by Resize() and the OUT_OF_DATE
|
||||
// recovery in Render(). Calls CreateSwapchain() and re-issues the
|
||||
// initial PRESENT_SRC_KHR layout transition for the new images so
|
||||
// Render()'s barriers (which assume oldLayout = PRESENT_SRC_KHR)
|
||||
// stay valid. Does NOT fire onResize — callers do that.
|
||||
// The capabilities a swapchain of `mode` on `surface` is sized by. With
|
||||
// surface_maintenance1 they are the surface's for that mode, which may
|
||||
// differ from its answer for all modes (Mesa on Wayland: FIFO 3, all
|
||||
// modes 4, because MAILBOX needs 4). Those are the ones that apply when
|
||||
// the swapchain is made naming its mode, which `modePassedAlong` says
|
||||
// (swapchain_maintenance1's VkSwapchainPresentModesCreateInfoKHR).
|
||||
// Without that the counts for all modes bind as well, so the minimum
|
||||
// and maximum are the stricter of the two. Asks for the mode last, as
|
||||
// the validation layer checks minImageCount against the surface's
|
||||
// last answer. Static so a test sizes swapchains the way the window
|
||||
// does.
|
||||
static VkSurfaceCapabilitiesKHR SwapchainCapabilities(VkPhysicalDevice gpu, VkSurfaceKHR surface,
|
||||
VkPresentModeKHR mode, bool modePassedAlong);
|
||||
// The minImageCount CreateSwapchain asks for, from those capabilities:
|
||||
// numFrames, or the surface's minimum where that is more, and never
|
||||
// more than its maximum (0 means no maximum). The driver may still
|
||||
// make more. Static so a test can feed it the numbers drivers report.
|
||||
static std::uint32_t SwapchainMinImageCount(const VkSurfaceCapabilitiesKHR& capabilities);
|
||||
// Calls CreateSwapchain(). The new images start in UNDEFINED, and
|
||||
// Render() transitions each on its first acquire (imageInitialised).
|
||||
// Does NOT fire onResize — callers do that.
|
||||
void RecreateSwapchainAndImages();
|
||||
// What Resize and SetPresentMode do once no frame is being recorded:
|
||||
// waits for the queue, frees the deletion queue, makes the swapchain
|
||||
// again at width x height and fires onResize.
|
||||
void RebuildSwapchain();
|
||||
|
||||
// Saves the frame the next Render() draws to a PNG at `path`, for
|
||||
// visual regression tests and screenshots: that Render copies the
|
||||
|
|
@ -353,9 +383,46 @@ export namespace Crafter {
|
|||
// acquired again.
|
||||
void CaptureNextFrame(std::filesystem::path path);
|
||||
std::optional<std::filesystem::path> pendingCapture;
|
||||
|
||||
// ── Frames and images ───────────────────────────────────────────
|
||||
// The frame loop keeps numFrames frames in flight, and that number is
|
||||
// its own: the swapchain has however many images the driver made for
|
||||
// the present mode, which may be fewer or more (Mesa's Mailbox has at
|
||||
// least four; issue #160). Two indices follow from that, set by
|
||||
// Render() before onUpdate:
|
||||
//
|
||||
// currentFrame this frame's slot, frameCounter % numFrames. It
|
||||
// picks everything that lives per frame in flight:
|
||||
// the command buffer, the fence and acquire
|
||||
// semaphore, the descriptor heap copy (or set) the
|
||||
// frame binds, RenderingElement3D::tlases, the
|
||||
// frameIdx a RenderPass is recorded with.
|
||||
// currentImage the swapchain image the frame acquired and draws
|
||||
// into: images[currentImage].
|
||||
//
|
||||
// The two go together differently from frame to frame, so a heap copy
|
||||
// can't hold "its" swapchain image: Render() writes the acquired one
|
||||
// into the frame's copy, at the slots DescriptorHeapVulkan::
|
||||
// AddSwapchainSlot lists for this window, before onUpdate.
|
||||
static constexpr std::uint8_t numFrames = 3;
|
||||
std::uint32_t currentFrame = 0;
|
||||
std::uint32_t currentImage = 0;
|
||||
// True from a frame's acquire to its present: images[currentImage]
|
||||
// and the frame's heap copy are this frame's, and a swapchain rebuild
|
||||
// (Resize, SetPresentMode) waits for the present. What is waiting:
|
||||
// a new present mode, or a present that found the swapchain out of
|
||||
// date; and the last size a resize asked for, 0 when none did.
|
||||
bool recording = false;
|
||||
bool swapchainRebuildPending = false;
|
||||
std::uint32_t deferredWidth = 0;
|
||||
std::uint32_t deferredHeight = 0;
|
||||
VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE;
|
||||
VkSwapchainKHR swapChain = VK_NULL_HANDLE;
|
||||
// Bumped from a process-wide count whenever CreateSwapchain makes a
|
||||
// swapchain, so it names one swapchain for good: a new swapchain's
|
||||
// handle and images may reuse the old one's handle values. The heap
|
||||
// keys what it wrote into each copy on it.
|
||||
std::uint64_t swapchainGeneration = 0;
|
||||
// What the swapchain images were created for: STORAGE | TRANSFER_SRC,
|
||||
// plus TRANSFER_DST where the surface allows it, so a pass can blit or
|
||||
// clear into the image instead of only writing it from a shader.
|
||||
|
|
@ -363,10 +430,6 @@ export namespace Crafter {
|
|||
// The mode the swapchain was made with: presentMode, unless the
|
||||
// surface lacks it or CRAFTER_PRESENT_IMMEDIATE is set.
|
||||
VkPresentModeKHR swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR;
|
||||
// The mode presentMode asks for made a swapchain with more images than
|
||||
// numFrames once, so swapchains are FIFO until SetPresentMode is
|
||||
// called again, rather than being made twice at every resize.
|
||||
bool presentModeUnfit = false;
|
||||
VkFormat colorFormat;
|
||||
VkColorSpaceKHR colorSpace;
|
||||
// Supported composite-alpha mode for vulkanSurface. A surface property
|
||||
|
|
@ -374,8 +437,14 @@ export namespace Crafter {
|
|||
// once at construction rather than re-derived on every CreateSwapchain
|
||||
// (i.e. on every resize configure).
|
||||
VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
||||
VkImage images[numFrames];
|
||||
VkImageViewCreateInfo imageViews[numFrames];
|
||||
// One entry per swapchain image, as many as vkGetSwapchainImagesKHR
|
||||
// returned; remade with the swapchain. imageViewInfos are the views a
|
||||
// heap descriptor takes (VkImageDescriptorInfoEXT::pView); imageViews
|
||||
// the same views made, which a descriptor set takes, owned by the
|
||||
// window and destroyed with its swapchain.
|
||||
std::vector<VkImage> images;
|
||||
std::vector<VkImageViewCreateInfo> imageViewInfos;
|
||||
std::vector<VkImageView> imageViews;
|
||||
// Tracks whether each swapchain image has been rendered (and thus
|
||||
// left in PRESENT_SRC_KHR) at least once. Freshly created swapchain
|
||||
// images start in VK_IMAGE_LAYOUT_UNDEFINED, so the first per-frame
|
||||
|
|
@ -383,17 +452,18 @@ export namespace Crafter {
|
|||
// Reset in CreateSwapchain(). A presentable image may only be touched
|
||||
// after it has been acquired, so this initial transition happens
|
||||
// lazily in Render() (post-acquire) rather than up front.
|
||||
std::array<bool, numFrames> imageInitialised{};
|
||||
std::vector<bool> imageInitialised;
|
||||
std::thread thread;
|
||||
// Per frame in flight.
|
||||
VkCommandBuffer drawCmdBuffers[numFrames] = {};
|
||||
// Per-frame Vulkan info structs whose contents are almost entirely
|
||||
// invariant. Initialised once in the constructor; Render() patches only
|
||||
// the few fields that change each frame (the two barriers' image and
|
||||
// the acquire barrier's oldLayout, and — for the multi-frame-in-flight
|
||||
// sync below — submitInfo/presentInfo's wait/signal semaphores plus
|
||||
// submitInfo's command buffer, which follow currentBuffer/frameCounter).
|
||||
// submitInfo's command buffer, which follow currentFrame/currentImage).
|
||||
// presentInfo.pImageIndices and pSwapchains point at the
|
||||
// currentBuffer/swapChain members, so they track value changes
|
||||
// currentImage/swapChain members, so they track value changes
|
||||
// (including swapchain recreation) automatically.
|
||||
VkSubmitInfo submitInfo;
|
||||
VkCommandBufferBeginInfo cmdBufInfo;
|
||||
|
|
@ -402,14 +472,9 @@ export namespace Crafter {
|
|||
VkPresentInfoKHR presentInfo;
|
||||
|
||||
// ── Multi-frame-in-flight synchronisation (issue #40) ──────────────
|
||||
// The renderer keeps up to numFrames frames in flight, so a single
|
||||
// semaphore pair + a per-frame wait-idle is no longer enough. Note
|
||||
// that drawCmdBuffers, the per-frame descriptor heap slots, and the
|
||||
// swapchain images are ALL keyed by the acquired image index
|
||||
// (currentBuffer) — WriteSwapchainDescriptors bakes heap slot i to
|
||||
// write imageViews[i], so the index that selects a command buffer /
|
||||
// heap slot must equal the acquired image index. Everything below
|
||||
// follows from that.
|
||||
// Render() waits for its frame slot's fence before it acquires, so
|
||||
// the command buffer, the heap copy and the acquire semaphore of that
|
||||
// slot are free again, whichever image the acquire then returns.
|
||||
|
||||
// Signaled by the queue submit, waited by vkQueuePresentKHR. Keyed by
|
||||
// the acquired IMAGE index: the presentation engine keeps this
|
||||
|
|
@ -417,29 +482,27 @@ export namespace Crafter {
|
|||
// semaphore is the only safe key. Keying it per-CPU-frame trips
|
||||
// VUID-vkQueueSubmit-pSignalSemaphores-00067.
|
||||
// https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html
|
||||
std::array<VkSemaphore, numFrames> renderFinishedSemaphores{};
|
||||
// Grown by CreateSwapchain to the image count and never shrunk: a
|
||||
// present of the old swapchain may still wait on one, and nothing
|
||||
// reports when that wait is done, so they live as long as the window.
|
||||
std::vector<VkSemaphore> renderFinishedSemaphores;
|
||||
|
||||
// Signaled by the queue submit, waited + reset before
|
||||
// drawCmdBuffers[image] and that image's descriptor-heap slot are
|
||||
// re-recorded. Keyed by image index. Created signaled so the first
|
||||
// wait on each image passes through instead of deadlocking.
|
||||
// drawCmdBuffers[frame] and that frame's descriptor-heap copy are
|
||||
// re-recorded. Keyed by frame slot. Created signaled so the first
|
||||
// wait on each slot passes through instead of deadlocking.
|
||||
std::array<VkFence, numFrames> waitFences{};
|
||||
|
||||
// Signaled by vkAcquireNextImageKHR, waited by the queue submit. Keyed
|
||||
// by a free-running CPU frame counter, because the image index isn't
|
||||
// known until acquire returns. Sized numFrames+1: at most numFrames
|
||||
// frames are ever in flight (each image's command buffer is gated by
|
||||
// its own fence, and there are numFrames images), so numFrames+1
|
||||
// distinct acquire semaphores guarantees the one being reused has no
|
||||
// pending operation — required by
|
||||
// VUID-vkAcquireNextImageKHR-semaphore-01779.
|
||||
static constexpr std::uint8_t numAcquireSemaphores = numFrames + 1;
|
||||
std::array<VkSemaphore, numAcquireSemaphores> imageAcquiredSemaphores{};
|
||||
// by frame slot: the slot's fence is waited before the acquire, and
|
||||
// that fence follows the submit that waited on the semaphore, so the
|
||||
// semaphore has no pending operation when it is reused
|
||||
// (VUID-vkAcquireNextImageKHR-semaphore-01779).
|
||||
std::array<VkSemaphore, numFrames> imageAcquiredSemaphores{};
|
||||
|
||||
// Free-running count of Render() calls; drives the acquire-semaphore
|
||||
// slot (frameCounter % numAcquireSemaphores).
|
||||
// Free-running count of Render() calls; currentFrame is this modulo
|
||||
// numFrames.
|
||||
std::uint64_t frameCounter = 0;
|
||||
std::uint32_t currentBuffer = 0;
|
||||
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
std::vector<RenderPass*> passes;
|
||||
DescriptorHeapVulkan* descriptorHeap = nullptr;
|
||||
|
|
@ -450,7 +513,7 @@ export namespace Crafter {
|
|||
// holds the per-Window state Crafter::Window users expect:
|
||||
// a list of render passes and a pointer to the descriptor heap.
|
||||
static constexpr std::uint8_t numFrames = 1;
|
||||
std::uint32_t currentBuffer = 0;
|
||||
std::uint32_t currentFrame = 0;
|
||||
std::vector<RenderPass*> passes;
|
||||
DescriptorHeapWebGPU* descriptorHeap = nullptr;
|
||||
std::optional<std::array<float, 4>> clearColor;
|
||||
|
|
|
|||
Loading…
Reference in a new issue