Merge remote-tracking branch 'origin/master' into claude/issue-40

# Conflicts:
#	implementations/Crafter.Graphics-Window.cpp
#	interfaces/Crafter.Graphics-Window.cppm
This commit is contained in:
catbot 2026-06-16 15:41:59 +00:00
commit 2d6052ec67
3 changed files with 195 additions and 126 deletions

View file

@ -405,6 +405,26 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
colorFormat = selectedFormat.format; colorFormat = selectedFormat.format;
colorSpace = selectedFormat.colorSpace; colorSpace = selectedFormat.colorSpace;
// Select a supported composite alpha format (not all devices support alpha
// opaque). This is a surface property, so pick it once here rather than on
// every CreateSwapchain. Simply select the first available.
{
VkSurfaceCapabilitiesKHR surfCaps;
Device::CheckVkResult(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device::physDevice, vulkanSurface, &surfCaps));
const VkCompositeAlphaFlagBitsKHR compositeAlphaFlags[] = {
VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR,
VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
};
for (const VkCompositeAlphaFlagBitsKHR compositeAlphaFlag : compositeAlphaFlags) {
if (surfCaps.supportedCompositeAlpha & compositeAlphaFlag) {
compositeAlpha = compositeAlphaFlag;
break;
}
}
}
CreateSwapchain(); CreateSwapchain();
VkCommandBufferAllocateInfo cmdBufAllocateInfo {}; VkCommandBufferAllocateInfo cmdBufAllocateInfo {};
@ -432,6 +452,70 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
Device::CheckVkResult(vkCreateSemaphore(Device::device, &semaphoreCreateInfo, nullptr, &imageAcquiredSemaphores[i])); Device::CheckVkResult(vkCreateSemaphore(Device::device, &semaphoreCreateInfo, nullptr, &imageAcquiredSemaphores[i]));
} }
// Per-frame info structs: everything that never varies between frames is
// set here once. Render() patches the barriers' image/oldLayout and the
// sync handles that follow currentBuffer/frameCounter (submitInfo's
// wait/signal semaphores + command buffer, presentInfo's wait semaphore).
cmdBufInfo = {};
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
// Submit: stage mask + the 1/1/1 counts are invariant; the wait/signal
// semaphore handles and the command buffer are patched per frame.
submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.pNext = VK_NULL_HANDLE;
submitInfo.pWaitDstStageMask = &submitPipelineStages;
submitInfo.waitSemaphoreCount = 1;
submitInfo.signalSemaphoreCount = 1;
submitInfo.commandBufferCount = 1;
const VkImageSubresourceRange range {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = VK_REMAINING_MIP_LEVELS,
.baseArrayLayer = 0,
.layerCount = VK_REMAINING_ARRAY_LAYERS,
};
// Transition into VK_IMAGE_LAYOUT_GENERAL before recording passes. image
// and oldLayout are patched per frame (oldLayout depends on first use).
acquireBarrier = {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = 0,
.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = VK_NULL_HANDLE,
.subresourceRange = range,
};
// Transition back to PRESENT_SRC_KHR after the passes. Only image varies.
presentBarrier = {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = 0,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = VK_NULL_HANDLE,
.subresourceRange = range,
};
// pImageIndices/pSwapchains point at members, so they follow value changes
// (currentBuffer each frame, swapChain across recreation) on their own. The
// wait semaphore is the per-image render-finished semaphore, patched per
// frame in Render() (it follows currentBuffer).
presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.pNext = NULL;
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapChain;
presentInfo.pImageIndices = &currentBuffer;
presentInfo.waitSemaphoreCount = 1;
lastMousePos = {0,0}; lastMousePos = {0,0};
mouseDelta = {0,0}; mouseDelta = {0,0};
currentMousePos = {0,0}; currentMousePos = {0,0};
@ -659,11 +743,12 @@ void Window::Update() {
#ifdef CRAFTER_TIMING #ifdef CRAFTER_TIMING
frameEnd = std::chrono::high_resolution_clock::now(); frameEnd = std::chrono::high_resolution_clock::now();
frameTimes.push_back(totalUpdate+totalRender); // Ring buffer: overwrite the oldest entry once full. Order doesn't matter
// to LogTiming, so we never need to shift elements.
// Keep only the last 100 frame times frameTimes[frameTimesHead] = totalUpdate+totalRender;
if (frameTimes.size() > 100) { frameTimesHead = (frameTimesHead + 1) % frameTimeCapacity;
frameTimes.erase(frameTimes.begin()); if (frameTimesCount < frameTimeCapacity) {
++frameTimesCount;
} }
#endif #endif
lastFrameBegin = startTime; lastFrameBegin = startTime;
@ -675,8 +760,7 @@ void Window::Render() {
// between Render calls), recreate and retry once. // between Render calls), recreate and retry once.
// Pick this frame's acquire semaphore from a free-running CPU counter: the // Pick this frame's acquire semaphore from a free-running CPU counter: the
// image index (currentBuffer) isn't known until acquire returns, so the // image index (currentBuffer) isn't known until acquire returns, so the
// acquire's signal semaphore can't be keyed by the image. Held in a local // acquire's signal semaphore can't be keyed by the image.
// so submitInfo below can point at the same handle.
const std::uint32_t acquireSlot = static_cast<std::uint32_t>(frameCounter % numAcquireSemaphores); const std::uint32_t acquireSlot = static_cast<std::uint32_t>(frameCounter % numAcquireSemaphores);
VkSemaphore imageAcquired = imageAcquiredSemaphores[acquireSlot]; VkSemaphore imageAcquired = imageAcquiredSemaphores[acquireSlot];
{ {
@ -702,18 +786,8 @@ void Window::Render() {
Device::CheckVkResult(vkWaitForFences(Device::device, 1, &waitFences[currentBuffer], VK_TRUE, UINT64_MAX)); Device::CheckVkResult(vkWaitForFences(Device::device, 1, &waitFences[currentBuffer], VK_TRUE, UINT64_MAX));
Device::CheckVkResult(vkResetFences(Device::device, 1, &waitFences[currentBuffer])); Device::CheckVkResult(vkResetFences(Device::device, 1, &waitFences[currentBuffer]));
VkCommandBufferBeginInfo cmdBufInfo {};
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo)); Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));
VkImageSubresourceRange range{};
range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
range.baseMipLevel = 0;
range.levelCount = VK_REMAINING_MIP_LEVELS;
range.baseArrayLayer = 0;
range.layerCount = VK_REMAINING_ARRAY_LAYERS;
// On an image's first use after (re)creating the swapchain it is still in // On an image's first use after (re)creating the swapchain it is still in
// VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in // VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in
// PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation // PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation
@ -722,20 +796,11 @@ void Window::Render() {
const bool firstUse = !imageInitialised[currentBuffer]; const bool firstUse = !imageInitialised[currentBuffer];
imageInitialised[currentBuffer] = true; imageInitialised[currentBuffer] = true;
VkImageMemoryBarrier image_memory_barrier { acquireBarrier.oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
.srcAccessMask = 0, acquireBarrier.image = images[currentBuffer];
.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
.oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED
: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = images[currentBuffer],
.subresourceRange = range
};
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier); vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier);
// Synthesise key-repeat events before listeners run, so the focused // Synthesise key-repeat events before listeners run, so the focused
// widget's OnTextInput / OnKeyDown sees them in the same frame. // widget's OnTextInput / OnKeyDown sees them in the same frame.
@ -748,27 +813,35 @@ void Window::Render() {
// Pass-side dispatches still run with the same heaps bound — moving // Pass-side dispatches still run with the same heaps bound — moving
// the bind earlier doesn't change anything for them. // the bind earlier doesn't change anything for them.
if (descriptorHeap) { if (descriptorHeap) {
VkBindHeapInfoEXT resourceHeapInfo = { // Rebuild the cached per-frame bind structs only when the heap changes.
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, // The address/size of each frame's heap and the reservedRangeOffset
.heapRange = { // bitmask are stable for a given heap, so this runs once per assignment
.address = descriptorHeap->resourceHeap[currentBuffer].address, // rather than every frame.
.size = static_cast<std::uint32_t>(descriptorHeap->resourceHeap[currentBuffer].size) if (descriptorHeap != cachedDescriptorHeap) {
}, for (std::uint8_t i = 0; i < numFrames; ++i) {
.reservedRangeOffset = (descriptorHeap->resourceHeap[currentBuffer].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1), resourceHeapInfos[i] = {
.reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
}; .heapRange = {
Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfo); .address = descriptorHeap->resourceHeap[i].address,
.size = static_cast<std::uint32_t>(descriptorHeap->resourceHeap[i].size)
VkBindHeapInfoEXT samplerHeapInfo = { },
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, .reservedRangeOffset = (descriptorHeap->resourceHeap[i].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1),
.heapRange = { .reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange
.address = descriptorHeap->samplerHeap[currentBuffer].address, };
.size = static_cast<std::uint32_t>(descriptorHeap->samplerHeap[currentBuffer].size) samplerHeapInfos[i] = {
}, .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
.reservedRangeOffset = descriptorHeap->samplerHeap[currentBuffer].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange, .heapRange = {
.reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange .address = descriptorHeap->samplerHeap[i].address,
}; .size = static_cast<std::uint32_t>(descriptorHeap->samplerHeap[i].size)
Device::vkCmdBindSamplerHeapEXT(drawCmdBuffers[currentBuffer], &samplerHeapInfo); },
.reservedRangeOffset = descriptorHeap->samplerHeap[i].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange,
.reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange
};
}
cachedDescriptorHeap = descriptorHeap;
}
Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfos[currentBuffer]);
Device::vkCmdBindSamplerHeapEXT(drawCmdBuffers[currentBuffer], &samplerHeapInfos[currentBuffer]);
} }
onUpdate.Invoke({startTime, startTime-lastFrameBegin}); onUpdate.Invoke({startTime, startTime-lastFrameBegin});
@ -800,45 +873,25 @@ void Window::Render() {
} }
} }
VkImageMemoryBarrier image_memory_barrier2 { presentBarrier.image = images[currentBuffer];
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = 0,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = images[currentBuffer],
.subresourceRange = range
};
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier2); vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier);
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer])); Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
// Submit waits on this frame's acquire semaphore and signals this image's // Patch the per-frame fields of the cached submitInfo/presentInfo (the
// render-finished semaphore; the fence (keyed by image index) lets a // invariant fields were set once in the constructor): submit waits on this
// future Render() reuse this image's command buffer + heap slot safely. // frame's acquire semaphore and signals this image's render-finished
VkSubmitInfo submitInfo{}; // semaphore, present waits on that same render-finished semaphore. The
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; // fence (keyed by image index) lets a future Render() reuse this image's
submitInfo.pNext = VK_NULL_HANDLE; // command buffer + heap slot safely. The semaphore handles live in member
submitInfo.pWaitDstStageMask = &submitPipelineStages; // arrays, so taking their addresses here is stable for the call.
submitInfo.waitSemaphoreCount = 1; submitInfo.pWaitSemaphores = &imageAcquiredSemaphores[acquireSlot];
submitInfo.pWaitSemaphores = &imageAcquired;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentBuffer]; submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentBuffer];
submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, waitFences[currentBuffer])); Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, waitFences[currentBuffer]));
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.pNext = NULL;
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapChain;
presentInfo.pImageIndices = &currentBuffer;
presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentBuffer]; presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentBuffer];
presentInfo.waitSemaphoreCount = 1;
VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo); VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo);
if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) { if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) {
@ -873,21 +926,22 @@ void Window::LogTiming() {
std::cout << std::format("Total: {}", duration_cast<std::chrono::milliseconds>(totalUpdate+totalRender)) << std::endl; std::cout << std::format("Total: {}", duration_cast<std::chrono::milliseconds>(totalUpdate+totalRender)) << std::endl;
std::cout << std::format("Vblank: {}", duration_cast<std::chrono::milliseconds>(vblank)) << std::endl; std::cout << std::format("Vblank: {}", duration_cast<std::chrono::milliseconds>(vblank)) << std::endl;
// Add 100-frame average and min-max timing info // Add 100-frame average and min-max timing info. Only the first
if (!frameTimes.empty()) { // frameTimesCount entries of the ring buffer hold valid samples.
if (frameTimesCount != 0) {
// Calculate average // Calculate average
std::chrono::nanoseconds sum(0); std::chrono::nanoseconds sum(0);
for (const auto& frameTime : frameTimes) { for (std::size_t i = 0; i < frameTimesCount; ++i) {
sum += frameTime; sum += frameTimes[i];
} }
auto average = sum / frameTimes.size(); auto average = sum / frameTimesCount;
// Find min and max // Find min and max
auto min = frameTimes.front(); auto min = frameTimes[0];
auto max = frameTimes.front(); auto max = frameTimes[0];
for (const auto& frameTime : frameTimes) { for (std::size_t i = 0; i < frameTimesCount; ++i) {
if (frameTime < min) min = frameTime; if (frameTimes[i] < min) min = frameTimes[i];
if (frameTime > max) max = frameTime; if (frameTimes[i] > max) max = frameTimes[i];
} }
std::cout << std::format("Last 100 Frame Times - Avg: {}, Min: {}, Max: {}", std::cout << std::format("Last 100 Frame Times - Avg: {}, Min: {}, Max: {}",
@ -924,18 +978,6 @@ void Window::CreateSwapchain()
} }
// Select a present mode for the swapchain
uint32_t presentModeCount;
Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, NULL));
assert(presentModeCount > 0);
std::vector<VkPresentModeKHR> presentModes(presentModeCount);
Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, presentModes.data()));
// The VK_PRESENT_MODE_FIFO_KHR mode must always be present as per spec
// This mode waits for the vertical blank ("v-sync")
VkPresentModeKHR swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR;
// Find the transformation of the surface // Find the transformation of the surface
VkSurfaceTransformFlagsKHR preTransform; VkSurfaceTransformFlagsKHR preTransform;
if (surfCaps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) if (surfCaps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
@ -948,22 +990,6 @@ void Window::CreateSwapchain()
preTransform = surfCaps.currentTransform; preTransform = surfCaps.currentTransform;
} }
// Find a supported composite alpha format (not all devices support alpha opaque)
VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
// Simply select the first composite alpha format available
std::vector<VkCompositeAlphaFlagBitsKHR> compositeAlphaFlags = {
VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR,
VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
};
for (auto& compositeAlphaFlag : compositeAlphaFlags) {
if (surfCaps.supportedCompositeAlpha & compositeAlphaFlag) {
compositeAlpha = compositeAlphaFlag;
break;
};
}
VkSwapchainCreateInfoKHR swapchainCI = {}; VkSwapchainCreateInfoKHR swapchainCI = {};
swapchainCI.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; swapchainCI.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
swapchainCI.surface = vulkanSurface; swapchainCI.surface = vulkanSurface;
@ -976,7 +1002,9 @@ void Window::CreateSwapchain()
swapchainCI.imageArrayLayers = 1; swapchainCI.imageArrayLayers = 1;
swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapchainCI.queueFamilyIndexCount = 0; swapchainCI.queueFamilyIndexCount = 0;
swapchainCI.presentMode = swapchainPresentMode; // VK_PRESENT_MODE_FIFO_KHR ("v-sync") is guaranteed available per spec, so
// it is used unconditionally — no present-mode enumeration is needed.
swapchainCI.presentMode = VK_PRESENT_MODE_FIFO_KHR;
// Setting oldSwapChain to the saved handle of the previous swapchain aids in resource reuse and makes sure that we can still present already acquired images // Setting oldSwapChain to the saved handle of the previous swapchain aids in resource reuse and makes sure that we can still present already acquired images
swapchainCI.oldSwapchain = oldSwapchain; swapchainCI.oldSwapchain = oldSwapchain;
// Setting clipped to VK_TRUE allows the implementation to discard rendering outside of the surface area // Setting clipped to VK_TRUE allows the implementation to discard rendering outside of the surface area

View file

@ -164,7 +164,14 @@ export namespace Crafter {
std::chrono::nanoseconds vblank; std::chrono::nanoseconds vblank;
std::chrono::nanoseconds totalFrame; std::chrono::nanoseconds totalFrame;
std::chrono::time_point<std::chrono::high_resolution_clock> frameEnd; std::chrono::time_point<std::chrono::high_resolution_clock> frameEnd;
std::vector<std::chrono::nanoseconds> frameTimes; // Fixed-size ring buffer of the most recent frame times. LogTiming does
// order-independent sum/avg/min/max, so head position is irrelevant to
// the reported stats; this avoids the per-frame memmove a vector::erase
// at the front would incur once full.
static constexpr std::size_t frameTimeCapacity = 100;
std::array<std::chrono::nanoseconds, frameTimeCapacity> frameTimes{};
std::size_t frameTimesHead = 0;
std::size_t frameTimesCount = 0;
void LogTiming(); void LogTiming();
#endif #endif
@ -246,6 +253,11 @@ export namespace Crafter {
VkSwapchainKHR swapChain = VK_NULL_HANDLE; VkSwapchainKHR swapChain = VK_NULL_HANDLE;
VkFormat colorFormat; VkFormat colorFormat;
VkColorSpaceKHR colorSpace; VkColorSpaceKHR colorSpace;
// Supported composite-alpha mode for vulkanSurface. A surface property
// that does not change across swapchain recreation, so it is selected
// 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]; VkImage images[numFrames];
VkImageViewCreateInfo imageViews[numFrames]; VkImageViewCreateInfo imageViews[numFrames];
// Tracks whether each swapchain image has been rendered (and thus // Tracks whether each swapchain image has been rendered (and thus
@ -258,6 +270,20 @@ export namespace Crafter {
std::array<bool, numFrames> imageInitialised{}; std::array<bool, numFrames> imageInitialised{};
std::thread thread; std::thread thread;
VkCommandBuffer drawCmdBuffers[numFrames]; 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).
// presentInfo.pImageIndices and pSwapchains point at the
// currentBuffer/swapChain members, so they track value changes
// (including swapchain recreation) automatically.
VkSubmitInfo submitInfo;
VkCommandBufferBeginInfo cmdBufInfo;
VkImageMemoryBarrier acquireBarrier;
VkImageMemoryBarrier presentBarrier;
VkPresentInfoKHR presentInfo;
// ── Multi-frame-in-flight synchronisation (issue #40) ────────────── // ── Multi-frame-in-flight synchronisation (issue #40) ──────────────
// The renderer keeps up to numFrames frames in flight, so a single // The renderer keeps up to numFrames frames in flight, so a single
@ -301,6 +327,18 @@ export namespace Crafter {
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
std::vector<RenderPass*> passes; std::vector<RenderPass*> passes;
DescriptorHeapVulkan* descriptorHeap = nullptr; DescriptorHeapVulkan* descriptorHeap = nullptr;
// Cached per-frame heap-bind structs. The heap address/size per slot
// are stable once a heap is assigned; only currentBuffer varies frame
// to frame, so the structs (and the reservedRangeOffset arithmetic)
// are computed once per heap rather than every frame in Render().
// Keyed on the heap pointer: whenever descriptorHeap differs from
// cachedDescriptorHeap the cache is rebuilt. This invalidates on any
// heap (re)assignment — heaps never resize today, so a stable pointer
// means stable ranges. Not tied to onResize (the heap is independent
// of swapchain size).
VkBindHeapInfoEXT resourceHeapInfos[numFrames];
VkBindHeapInfoEXT samplerHeapInfos[numFrames];
DescriptorHeapVulkan* cachedDescriptorHeap = nullptr;
std::optional<std::array<float, 4>> clearColor; std::optional<std::array<float, 4>> clearColor;
#else #else
// DOM mode: the page IS the window. WebGPU device and canvas are // DOM mode: the page IS the window. WebGPU device and canvas are

View file

@ -36,9 +36,12 @@ void main() {
vec2 t = (sp - it.rect.xy) / it.rect.zw; vec2 t = (sp - it.rect.xy) / it.rect.zw;
vec2 uv = mix(it.uv.xy, it.uv.zw, t); vec2 uv = mix(it.uv.xy, it.uv.zw, t);
// Font slots are push constants — provably dynamically uniform, so no
// nonuniformEXT: lets the compiler hoist the descriptor load out of the
// per-pixel glyph loop and avoid the divergent-descriptor path.
float sdf = texture( float sdf = texture(
sampler2D(uiTextures[nonuniformEXT(pc.fontTextureSlot)], sampler2D(uiTextures[pc.fontTextureSlot],
uiSamplers[nonuniformEXT(pc.fontSamplerSlot)]), uiSamplers[pc.fontSamplerSlot]),
uv uv
).r; ).r;