feat(window): multi-frame-in-flight frame pacing (#40) #81
3 changed files with 195 additions and 126 deletions
Merge remote-tracking branch 'origin/master' into claude/issue-40
# Conflicts: # implementations/Crafter.Graphics-Window.cpp # interfaces/Crafter.Graphics-Window.cppm
commit
2d6052ec67
|
|
@ -405,6 +405,26 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
|||
colorFormat = selectedFormat.format;
|
||||
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();
|
||||
|
||||
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]));
|
||||
}
|
||||
|
||||
// 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 = ¤tBuffer;
|
||||
presentInfo.waitSemaphoreCount = 1;
|
||||
|
||||
lastMousePos = {0,0};
|
||||
mouseDelta = {0,0};
|
||||
currentMousePos = {0,0};
|
||||
|
|
@ -659,11 +743,12 @@ void Window::Update() {
|
|||
#ifdef CRAFTER_TIMING
|
||||
frameEnd = std::chrono::high_resolution_clock::now();
|
||||
|
||||
frameTimes.push_back(totalUpdate+totalRender);
|
||||
|
||||
// Keep only the last 100 frame times
|
||||
if (frameTimes.size() > 100) {
|
||||
frameTimes.erase(frameTimes.begin());
|
||||
// Ring buffer: overwrite the oldest entry once full. Order doesn't matter
|
||||
// to LogTiming, so we never need to shift elements.
|
||||
frameTimes[frameTimesHead] = totalUpdate+totalRender;
|
||||
frameTimesHead = (frameTimesHead + 1) % frameTimeCapacity;
|
||||
if (frameTimesCount < frameTimeCapacity) {
|
||||
++frameTimesCount;
|
||||
}
|
||||
#endif
|
||||
lastFrameBegin = startTime;
|
||||
|
|
@ -675,8 +760,7 @@ void Window::Render() {
|
|||
// between Render calls), recreate and retry once.
|
||||
// Pick this frame's acquire semaphore from a free-running CPU counter: 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
|
||||
// so submitInfo below can point at the same handle.
|
||||
// acquire's signal semaphore can't be keyed by the image.
|
||||
const std::uint32_t acquireSlot = static_cast<std::uint32_t>(frameCounter % numAcquireSemaphores);
|
||||
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(vkResetFences(Device::device, 1, &waitFences[currentBuffer]));
|
||||
|
||||
VkCommandBufferBeginInfo cmdBufInfo {};
|
||||
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
|
||||
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
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in
|
||||
// PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation
|
||||
|
|
@ -722,20 +796,11 @@ void Window::Render() {
|
|||
const bool firstUse = !imageInitialised[currentBuffer];
|
||||
imageInitialised[currentBuffer] = true;
|
||||
|
||||
VkImageMemoryBarrier image_memory_barrier {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.srcAccessMask = 0,
|
||||
.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
|
||||
};
|
||||
acquireBarrier.oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED
|
||||
: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
acquireBarrier.image = images[currentBuffer];
|
||||
|
||||
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
|
||||
// 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
|
||||
// the bind earlier doesn't change anything for them.
|
||||
if (descriptorHeap) {
|
||||
VkBindHeapInfoEXT resourceHeapInfo = {
|
||||
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
|
||||
.heapRange = {
|
||||
.address = descriptorHeap->resourceHeap[currentBuffer].address,
|
||||
.size = static_cast<std::uint32_t>(descriptorHeap->resourceHeap[currentBuffer].size)
|
||||
},
|
||||
.reservedRangeOffset = (descriptorHeap->resourceHeap[currentBuffer].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1),
|
||||
.reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange
|
||||
};
|
||||
Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfo);
|
||||
|
||||
VkBindHeapInfoEXT samplerHeapInfo = {
|
||||
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
|
||||
.heapRange = {
|
||||
.address = descriptorHeap->samplerHeap[currentBuffer].address,
|
||||
.size = static_cast<std::uint32_t>(descriptorHeap->samplerHeap[currentBuffer].size)
|
||||
},
|
||||
.reservedRangeOffset = descriptorHeap->samplerHeap[currentBuffer].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange,
|
||||
.reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange
|
||||
};
|
||||
Device::vkCmdBindSamplerHeapEXT(drawCmdBuffers[currentBuffer], &samplerHeapInfo);
|
||||
// Rebuild the cached per-frame bind structs only when the heap changes.
|
||||
// The address/size of each frame's heap and the reservedRangeOffset
|
||||
// bitmask are stable for a given heap, so this runs once per assignment
|
||||
// rather than every frame.
|
||||
if (descriptorHeap != cachedDescriptorHeap) {
|
||||
for (std::uint8_t i = 0; i < numFrames; ++i) {
|
||||
resourceHeapInfos[i] = {
|
||||
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
|
||||
.heapRange = {
|
||||
.address = descriptorHeap->resourceHeap[i].address,
|
||||
.size = static_cast<std::uint32_t>(descriptorHeap->resourceHeap[i].size)
|
||||
},
|
||||
.reservedRangeOffset = (descriptorHeap->resourceHeap[i].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1),
|
||||
.reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange
|
||||
};
|
||||
samplerHeapInfos[i] = {
|
||||
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
|
||||
.heapRange = {
|
||||
.address = descriptorHeap->samplerHeap[i].address,
|
||||
.size = static_cast<std::uint32_t>(descriptorHeap->samplerHeap[i].size)
|
||||
},
|
||||
.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});
|
||||
|
|
@ -800,45 +873,25 @@ void Window::Render() {
|
|||
}
|
||||
}
|
||||
|
||||
VkImageMemoryBarrier image_memory_barrier2 {
|
||||
.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
|
||||
};
|
||||
presentBarrier.image = images[currentBuffer];
|
||||
|
||||
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]));
|
||||
|
||||
// Submit waits on this frame's acquire semaphore and signals this image's
|
||||
// render-finished semaphore; the fence (keyed by image index) lets a
|
||||
// future Render() reuse this image's command buffer + heap slot safely.
|
||||
VkSubmitInfo submitInfo{};
|
||||
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||
submitInfo.pNext = VK_NULL_HANDLE;
|
||||
submitInfo.pWaitDstStageMask = &submitPipelineStages;
|
||||
submitInfo.waitSemaphoreCount = 1;
|
||||
submitInfo.pWaitSemaphores = &imageAcquired;
|
||||
submitInfo.signalSemaphoreCount = 1;
|
||||
// Patch the per-frame fields of the cached submitInfo/presentInfo (the
|
||||
// invariant fields were set once in the constructor): submit waits on this
|
||||
// frame's acquire semaphore and signals this image's render-finished
|
||||
// semaphore, present waits on that same render-finished semaphore. The
|
||||
// fence (keyed by image index) lets a future Render() reuse this image's
|
||||
// command buffer + heap slot safely. The semaphore handles live in member
|
||||
// arrays, so taking their addresses here is stable for the call.
|
||||
submitInfo.pWaitSemaphores = &imageAcquiredSemaphores[acquireSlot];
|
||||
submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentBuffer];
|
||||
submitInfo.commandBufferCount = 1;
|
||||
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
|
||||
|
||||
submitInfo.pCommandBuffers = &drawCmdBuffers[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 = ¤tBuffer;
|
||||
|
||||
presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentBuffer];
|
||||
presentInfo.waitSemaphoreCount = 1;
|
||||
|
||||
VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo);
|
||||
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("Vblank: {}", duration_cast<std::chrono::milliseconds>(vblank)) << std::endl;
|
||||
|
||||
// Add 100-frame average and min-max timing info
|
||||
if (!frameTimes.empty()) {
|
||||
// Add 100-frame average and min-max timing info. Only the first
|
||||
// frameTimesCount entries of the ring buffer hold valid samples.
|
||||
if (frameTimesCount != 0) {
|
||||
// Calculate average
|
||||
std::chrono::nanoseconds sum(0);
|
||||
for (const auto& frameTime : frameTimes) {
|
||||
sum += frameTime;
|
||||
for (std::size_t i = 0; i < frameTimesCount; ++i) {
|
||||
sum += frameTimes[i];
|
||||
}
|
||||
auto average = sum / frameTimes.size();
|
||||
|
||||
auto average = sum / frameTimesCount;
|
||||
|
||||
// Find min and max
|
||||
auto min = frameTimes.front();
|
||||
auto max = frameTimes.front();
|
||||
for (const auto& frameTime : frameTimes) {
|
||||
if (frameTime < min) min = frameTime;
|
||||
if (frameTime > max) max = frameTime;
|
||||
auto min = frameTimes[0];
|
||||
auto max = frameTimes[0];
|
||||
for (std::size_t i = 0; i < frameTimesCount; ++i) {
|
||||
if (frameTimes[i] < min) min = frameTimes[i];
|
||||
if (frameTimes[i] > max) max = frameTimes[i];
|
||||
}
|
||||
|
||||
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
|
||||
VkSurfaceTransformFlagsKHR preTransform;
|
||||
if (surfCaps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
|
||||
|
|
@ -948,22 +990,6 @@ void Window::CreateSwapchain()
|
|||
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 = {};
|
||||
swapchainCI.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
|
||||
swapchainCI.surface = vulkanSurface;
|
||||
|
|
@ -976,7 +1002,9 @@ void Window::CreateSwapchain()
|
|||
swapchainCI.imageArrayLayers = 1;
|
||||
swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
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
|
||||
swapchainCI.oldSwapchain = oldSwapchain;
|
||||
// Setting clipped to VK_TRUE allows the implementation to discard rendering outside of the surface area
|
||||
|
|
|
|||
|
|
@ -164,7 +164,14 @@ export namespace Crafter {
|
|||
std::chrono::nanoseconds vblank;
|
||||
std::chrono::nanoseconds totalFrame;
|
||||
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();
|
||||
#endif
|
||||
|
||||
|
|
@ -246,6 +253,11 @@ export namespace Crafter {
|
|||
VkSwapchainKHR swapChain = VK_NULL_HANDLE;
|
||||
VkFormat colorFormat;
|
||||
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];
|
||||
VkImageViewCreateInfo imageViews[numFrames];
|
||||
// Tracks whether each swapchain image has been rendered (and thus
|
||||
|
|
@ -258,6 +270,20 @@ export namespace Crafter {
|
|||
std::array<bool, numFrames> imageInitialised{};
|
||||
std::thread thread;
|
||||
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) ──────────────
|
||||
// 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;
|
||||
std::vector<RenderPass*> passes;
|
||||
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;
|
||||
#else
|
||||
// DOM mode: the page IS the window. WebGPU device and canvas are
|
||||
|
|
|
|||
|
|
@ -36,9 +36,12 @@ void main() {
|
|||
vec2 t = (sp - it.rect.xy) / it.rect.zw;
|
||||
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(
|
||||
sampler2D(uiTextures[nonuniformEXT(pc.fontTextureSlot)],
|
||||
uiSamplers[nonuniformEXT(pc.fontSamplerSlot)]),
|
||||
sampler2D(uiTextures[pc.fontTextureSlot],
|
||||
uiSamplers[pc.fontSamplerSlot]),
|
||||
uv
|
||||
).r;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue