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:
commit
2d6052ec67
3 changed files with 195 additions and 126 deletions
|
|
@ -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 = ¤tBuffer;
|
||||||
|
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 = ¤tBuffer;
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue