From 111295af8cec873c5a4f28b414dc8e4ede3eb165 Mon Sep 17 00:00:00 2001 From: PancakeTAS Date: Fri, 2 Jan 2026 16:58:17 +0100 Subject: [PATCH] feat(frame-pacing): implement basic virtual swapchain --- .../lsfg-vk-common/helpers/pointers.hpp | 32 +- .../include/lsfg-vk-common/vulkan/image.hpp | 5 +- .../include/lsfg-vk-common/vulkan/vulkan.hpp | 4 + lsfg-vk-common/src/vulkan/image.cpp | 12 +- lsfg-vk-common/src/vulkan/vulkan.cpp | 7 +- lsfg-vk-layer/.clang-tidy | 2 + lsfg-vk-layer/src/entrypoint.cpp | 191 +++++-- lsfg-vk-layer/src/hooks/swapchain.cpp | 536 +++++++++++++----- lsfg-vk-layer/src/hooks/swapchain.hpp | 97 ++-- 9 files changed, 649 insertions(+), 237 deletions(-) diff --git a/lsfg-vk-common/include/lsfg-vk-common/helpers/pointers.hpp b/lsfg-vk-common/include/lsfg-vk-common/helpers/pointers.hpp index afa703f..abc37b5 100644 --- a/lsfg-vk-common/include/lsfg-vk-common/helpers/pointers.hpp +++ b/lsfg-vk-common/include/lsfg-vk-common/helpers/pointers.hpp @@ -5,7 +5,8 @@ #include #include #include -#include + +#include namespace ls { /// helper alias for std::reference_wrapper @@ -25,8 +26,7 @@ namespace ls { /// @throws std::logic_error if value already present template T& emplace(Args&&... args) { - if (this->opt.has_value()) - throw std::logic_error("lazy: value already present"); + assert(!this->opt.has_value() && "lazy: value already present"); this->opt.emplace(std::forward(args)...); return *this->opt; @@ -40,8 +40,7 @@ namespace ls { /// @return reference to value /// @throws std::logic_error if no value present const T& operator*() const { - if (!this->opt.has_value()) - throw std::logic_error("lazy: no value present"); + assert(this->opt.has_value() && "lazy: no value present"); return *this->opt; } @@ -49,8 +48,7 @@ namespace ls { /// @return pointer to value /// @throws std::logic_error if no value present const T* operator->() const { - if (!this->opt.has_value()) - throw std::logic_error("lazy: no value present"); + assert(this->opt.has_value() && "lazy: no value present"); return &(*this->opt); } @@ -58,8 +56,7 @@ namespace ls { /// @return mutable reference to value /// @throws std::logic_error if no value present T& mut() { - if (!this->opt.has_value()) - throw std::logic_error("lazy: no value present"); + assert(this->opt.has_value() && "lazy: no value present"); return *this->opt; } private: @@ -130,4 +127,21 @@ namespace ls { T* ptr{}; std::function deleter{}; }; + + /// find a structure in a pNext chain + /// @param type the structure type to find + /// @param chain the pNext chain + /// @return pointer to the found structure, or nullptr + template + T* find_structure(VkStructureType type, const void* chain) noexcept { + auto* next = reinterpret_cast(const_cast(chain)); + while (next) { + if (next->sType == type) + return reinterpret_cast(next); + + next = const_cast(next->pNext); + } + + return nullptr; + } } diff --git a/lsfg-vk-common/include/lsfg-vk-common/vulkan/image.hpp b/lsfg-vk-common/include/lsfg-vk-common/vulkan/image.hpp index db38f01..604c6ad 100644 --- a/lsfg-vk-common/include/lsfg-vk-common/vulkan/image.hpp +++ b/lsfg-vk-common/include/lsfg-vk-common/vulkan/image.hpp @@ -20,13 +20,16 @@ namespace vk { /// @param usage usage flags /// @param importFd optional file descriptor for shared memory /// @param exportFd optional pointer to an integer where the file descriptor will be stored + /// @param flags additional flags for image creation + /// @param chain optional pNext chain for image creation /// @throws ls::vulkan_error on failure Image(const vk::Vulkan& vk, VkExtent2D extent, VkFormat format = VK_FORMAT_R8G8B8A8_UNORM, VkImageUsageFlags usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, std::optional importFd = std::nullopt, - std::optional exportFd = std::nullopt); + std::optional exportFd = std::nullopt, + VkImageCreateFlags flags = 0, const void* chain = nullptr); /// get the image handle /// @return the image handle diff --git a/lsfg-vk-common/include/lsfg-vk-common/vulkan/vulkan.hpp b/lsfg-vk-common/include/lsfg-vk-common/vulkan/vulkan.hpp index bb4dcf5..40510f2 100644 --- a/lsfg-vk-common/include/lsfg-vk-common/vulkan/vulkan.hpp +++ b/lsfg-vk-common/include/lsfg-vk-common/vulkan/vulkan.hpp @@ -117,6 +117,10 @@ namespace vk { PFN_vkAcquireNextImageKHR AcquireNextImageKHR; PFN_vkQueuePresentKHR QueuePresentKHR; PFN_vkDestroySwapchainKHR DestroySwapchainKHR; + + // optional extension functions + PFN_vkWaitForPresentKHR WaitForPresentKHR; + PFN_vkWaitForPresent2KHR WaitForPresent2KHR; }; /// initialize vulkan device function pointers diff --git a/lsfg-vk-common/src/vulkan/image.cpp b/lsfg-vk-common/src/vulkan/image.cpp index 6eb45c4..be32c84 100644 --- a/lsfg-vk-common/src/vulkan/image.cpp +++ b/lsfg-vk-common/src/vulkan/image.cpp @@ -16,16 +16,18 @@ namespace { /// create a image ls::owned_ptr createImage(const vk::Vulkan& vk, VkExtent2D extent, VkFormat format, VkImageUsageFlags usage, - bool external) { + bool external, VkImageCreateFlags flags, const void* chain) { VkImage handle{}; const VkExternalMemoryImageCreateInfo externalInfo{ .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, + .pNext = chain, .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR }; const VkImageCreateInfo imageInfo{ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - .pNext = external ? &externalInfo : nullptr, + .pNext = external ? &externalInfo : chain, + .flags = flags, .imageType = VK_IMAGE_TYPE_2D, .format = format, .extent = { @@ -155,10 +157,12 @@ Image::Image(const vk::Vulkan& vk, VkFormat format, VkImageUsageFlags usage, std::optional importFd, - std::optional exportFd) : + std::optional exportFd, + VkImageCreateFlags flags, const void* chain) : image(createImage(vk, extent, format, usage, - importFd.has_value() || exportFd.has_value() + importFd.has_value() || exportFd.has_value(), + flags, chain )), memory(allocateMemory(vk, *this->image, diff --git a/lsfg-vk-common/src/vulkan/vulkan.cpp b/lsfg-vk-common/src/vulkan/vulkan.cpp index 9d1bfec..2751626 100644 --- a/lsfg-vk-common/src/vulkan/vulkan.cpp +++ b/lsfg-vk-common/src/vulkan/vulkan.cpp @@ -388,7 +388,12 @@ VulkanDeviceFuncs vk::initVulkanDeviceFuncs(PFN_vkGetDeviceProcAddr f, VkDevice .QueuePresentKHR = graphical ? dpa(f, d, "vkQueuePresentKHR") : nullptr, .DestroySwapchainKHR = graphical ? - dpa(f, d, "vkDestroySwapchainKHR") : nullptr + dpa(f, d, "vkDestroySwapchainKHR") : nullptr, + + .WaitForPresentKHR = reinterpret_cast( + f(d, "vkWaitForPresentKHR")), + .WaitForPresent2KHR = reinterpret_cast( + f(d, "vkWaitForPresent2KHR")) }; } diff --git a/lsfg-vk-layer/.clang-tidy b/lsfg-vk-layer/.clang-tidy index 0939c9a..172e158 100644 --- a/lsfg-vk-layer/.clang-tidy +++ b/lsfg-vk-layer/.clang-tidy @@ -28,3 +28,5 @@ Checks: - -cppcoreguidelines-pro-bounds-pointer-arithmetic - -cppcoreguidelines-pro-type-union-access - -clang-diagnostic-unsafe-buffer-usage +# Vulkan layers override methods, where not all parameters are needed +- -readability-named-parameter diff --git a/lsfg-vk-layer/src/entrypoint.cpp b/lsfg-vk-layer/src/entrypoint.cpp index 80eb7f6..feb856d 100644 --- a/lsfg-vk-layer/src/entrypoint.cpp +++ b/lsfg-vk-layer/src/entrypoint.cpp @@ -5,12 +5,14 @@ #include "hooks/layer.hpp" #include "hooks/swapchain.hpp" #include "lsfg-vk-common/helpers/errors.hpp" +#include "lsfg-vk-common/helpers/pointers.hpp" #include #include #include #include #include +#include #include #include #include @@ -318,11 +320,78 @@ namespace { } } + VkResult myvkGetSwapchainImagesKHR( + VkDevice, + VkSwapchainKHR swapchain, + uint32_t* count, + VkImage* images) { + const auto& it = layer_info->swapchains.find(swapchain); + if (it == layer_info->swapchains.end()) + return VK_ERROR_SURFACE_LOST_KHR; + + return it->second->GetSwapchainImagesKHR(count, images); + } + + VkResult myvkAcquireNextImageKHR( + VkDevice, + VkSwapchainKHR swapchain, + uint64_t timeout, + VkSemaphore semaphore, + VkFence fence, + uint32_t* idx) { + const auto& it = layer_info->swapchains.find(swapchain); + if (it == layer_info->swapchains.end()) + return VK_ERROR_SURFACE_LOST_KHR; + + return it->second->AcquireNextImageKHR(timeout, semaphore, fence, idx); + } + + VkResult myvkAcquireNextImage2KHR( + VkDevice, + const VkAcquireNextImageInfoKHR* info, + uint32_t* idx) { + const auto& it = layer_info->swapchains.find(info->swapchain); + if (it == layer_info->swapchains.end()) + return VK_ERROR_SURFACE_LOST_KHR; + + return it->second->AcquireNextImage2KHR(info, idx); + } + + VkResult myvkReleaseSwapchainImagesKHR( + VkDevice, + const VkReleaseSwapchainImagesInfoKHR* info) { + const auto& it = layer_info->swapchains.find(info->swapchain); + if (it == layer_info->swapchains.end()) + return VK_ERROR_SURFACE_LOST_KHR; + + return it->second->ReleaseSwapchainImagesKHR(info); + } + + VkResult myvkWaitForPresentKHR( + VkDevice, + VkSwapchainKHR swapchain, + uint64_t id, + uint64_t timeout) { + const auto& it = layer_info->swapchains.find(swapchain); + if (it == layer_info->swapchains.end()) + return VK_ERROR_SURFACE_LOST_KHR; + + return it->second->WaitForPresentKHR(id, timeout); + } + + VkResult myvkWaitForPresent2KHR( + VkDevice, + VkSwapchainKHR swapchain, + const VkPresentWait2InfoKHR* info) { + const auto& it = layer_info->swapchains.find(swapchain); + if (it == layer_info->swapchains.end()) + return VK_ERROR_SURFACE_LOST_KHR; + + return it->second->WaitForPresent2KHR(info); + } + VkResult myvkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* info) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunknown-warning-option" -#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" VkResult result = VK_SUCCESS; // re-create out-of-date managed swapchains @@ -337,6 +406,49 @@ namespace { // ignore error: return VK_ERROR_UNKNOWN; } + // check for VK_KHR_swapchain_maintenance1 + std::vector fences(info->swapchainCount, VK_NULL_HANDLE); + std::vector> modes(info->swapchainCount, std::nullopt); + + const auto* fenceInfo = ls::find_structure( + VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_FENCE_INFO_KHR, + info->pNext + ); + if (fenceInfo) { + for (uint32_t i = 0; i < fenceInfo->swapchainCount; i++) + fences[i] = fenceInfo->pFences[i]; + } + + const auto* presentModeInfo = ls::find_structure( + VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODE_INFO_KHR, + info->pNext + ); + if (presentModeInfo) { + for (uint32_t i = 0; i < presentModeInfo->swapchainCount; i++) + modes[i] = presentModeInfo->pPresentModes[i]; + } + + // check for VK_KHR_present_id(2) + std::vector> presentIds(info->swapchainCount, std::nullopt); + + const auto* presentIdInfo = ls::find_structure( + VK_STRUCTURE_TYPE_PRESENT_ID_KHR, + info->pNext + ); + if (presentIdInfo) { + for (uint32_t i = 0; i < presentIdInfo->swapchainCount; i++) + presentIds[i] = presentIdInfo->pPresentIds[i]; + } + + const auto* presentId2Info = ls::find_structure( + VK_STRUCTURE_TYPE_PRESENT_ID_2_KHR, + info->pNext + ); + if (presentId2Info) { + for (uint32_t i = 0; i < presentId2Info->swapchainCount; i++) + presentIds[i] = presentId2Info->pPresentIds[i]; + } + // collect semaphores and values std::vector waitValues(info->waitSemaphoreCount); std::vector signalSemaphores; @@ -354,6 +466,42 @@ namespace { signalValues.push_back(sync.second); } + // present all managed swapchains + for (uint32_t i = 0; i < info->swapchainCount; i++) { + const auto& handle = info->pSwapchains[i]; + + const auto& it = layer_info->swapchains.find(handle); + if (it == layer_info->swapchains.end()) + return VK_ERROR_SURFACE_LOST_KHR; + + try { + const auto& fence = fences[i]; + const auto& present_mode = modes[i]; + const auto& present_id = presentIds[i]; + + const MyVkPresentInfo mypresentinfo{ + .idx = info->pImageIndices[i], + .fence = fence, + .present_mode = present_mode, + .id = present_id + }; + result = it->second->partial_QueuePresentKHR(mypresentinfo); + } catch (const ls::vulkan_error& e) { + if (e.error() != VK_ERROR_OUT_OF_DATE_KHR) { // swallow out-of-date errors + std::cerr << "lsfg-vk: something went wrong during lsfg-vk swapchain presentation:\n" + "- " << e.what() << '\n'; + } + result = e.error(); + } catch (const std::exception& e) { + std::cerr << "lsfg-vk: something went wrong during lsfg-vk swapchain presentation:\n" + "- " << e.what() << '\n'; + result = VK_ERROR_UNKNOWN; + } + + if (result != VK_SUCCESS && info->pResults) + info->pResults[i] = result; + } + // submit the present operation const VkTimelineSemaphoreSubmitInfo timelineInfo{ .sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO, @@ -380,37 +528,7 @@ namespace { return res; } - // present all managed swapchains - for (uint32_t i = 0; i < info->swapchainCount; i++) { - const auto& handle = info->pSwapchains[i]; - - const auto& it = layer_info->swapchains.find(handle); - if (it == layer_info->swapchains.end()) - return VK_ERROR_SURFACE_LOST_KHR; - - try { - result = it->second->present(queue, - const_cast(info->pNext), - info->pImageIndices[i] - ); - } catch (const ls::vulkan_error& e) { - if (e.error() != VK_ERROR_OUT_OF_DATE_KHR) { // swallow out-of-date errors - std::cerr << "lsfg-vk: something went wrong during lsfg-vk swapchain presentation:\n" - "- " << e.what() << '\n'; - } - result = e.error(); - } catch (const std::exception& e) { - std::cerr << "lsfg-vk: something went wrong during lsfg-vk swapchain presentation:\n" - "- " << e.what() << '\n'; - result = VK_ERROR_UNKNOWN; - } - - if (result != VK_SUCCESS && info->pResults) - info->pResults[i] = result; - } - return result; -#pragma clang diagnostic pop } void myvkDestroySwapchainKHR( @@ -461,6 +579,13 @@ VkResult vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface* pVers { "vkDestroyDevice", VKPTR(myvkDestroyDevice) }, { "vkDestroyInstance", VKPTR(myvkDestroyInstance) }, { "vkCreateSwapchainKHR", VKPTR(myvkCreateSwapchainKHR) }, + { "vkGetSwapchainImagesKHR", VKPTR(myvkGetSwapchainImagesKHR) }, + { "vkAcquireNextImageKHR", VKPTR(myvkAcquireNextImageKHR) }, + { "vkAcquireNextImage2KHR", VKPTR(myvkAcquireNextImage2KHR) }, + { "vkReleaseSwapchainImagesKHR", VKPTR(myvkReleaseSwapchainImagesKHR) }, + { "vkReleaseSwapchainImagesEXT", VKPTR(myvkReleaseSwapchainImagesKHR) }, + { "vkWaitForPresentKHR", VKPTR(myvkWaitForPresentKHR) }, + { "vkWaitForPresent2KHR", VKPTR(myvkWaitForPresent2KHR) }, { "vkQueuePresentKHR", VKPTR(myvkQueuePresentKHR) }, { "vkDestroySwapchainKHR", VKPTR(myvkDestroySwapchainKHR) } #undef VKPTR diff --git a/lsfg-vk-layer/src/hooks/swapchain.cpp b/lsfg-vk-layer/src/hooks/swapchain.cpp index 12c58ad..cbc4205 100644 --- a/lsfg-vk-layer/src/hooks/swapchain.cpp +++ b/lsfg-vk-layer/src/hooks/swapchain.cpp @@ -4,35 +4,33 @@ #include "instance.hpp" #include "layer.hpp" #include "lsfg-vk-common/helpers/errors.hpp" +#include "lsfg-vk-common/helpers/pointers.hpp" #include "lsfg-vk-common/vulkan/command_buffer.hpp" +#include "lsfg-vk-common/vulkan/fence.hpp" #include "lsfg-vk-common/vulkan/semaphore.hpp" #include "lsfg-vk-common/vulkan/timeline_semaphore.hpp" #include "lsfg-vk-common/vulkan/vulkan.hpp" #include "swapchain.hpp" -#include #include #include +#include #include -#include +#include +#include +#include #include #include +#include +#include +#include #include using namespace lsfgvk; using namespace lsfgvk::layer; namespace { - /// helper to acquire the max number of images - uint32_t getMaxImages(const vk::Vulkan& vk, VkSurfaceKHR surface) { - VkSurfaceCapabilitiesKHR caps{}; - auto res = vk.fi().GetPhysicalDeviceSurfaceCapabilitiesKHR(vk.physdev(), surface, &caps); - if (res != VK_SUCCESS) - throw ls::vulkan_error(res, "vkGetPhysicalDeviceSurfaceCapabilitiesKHR() failed"); - - return caps.maxImageCount; - } /// helper to get swapchain images std::vector getSwapchainImages(const vk::Vulkan& vk, VkSwapchainKHR swapchain) { uint32_t imageCount = 0; @@ -53,167 +51,409 @@ MyVkSwapchain::MyVkSwapchain(MyVkLayer& layer, MyVkInstance& instance, MyVkDevic VkSwapchainCreateInfoKHR info, const std::function& createFunc) : layer(std::ref(layer)), instance(std::ref(instance)), device(std::ref(device)), - extent(info.imageExtent), format(info.imageFormat) { - // modify create info - const uint32_t maxImageCount = getMaxImages(this->device.get().vkd(), info.surface); - info.minImageCount += layer.profile().multiplier; - if (maxImageCount && info.minImageCount > maxImageCount) - info.minImageCount = maxImageCount; + presentSemaphore(device.vkd(), 0), presentIndex(1) { + const auto& vk = device.vkd(); - info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + // ensure underlying swapchain supports transfer operations + info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; - // create swapchain - this->handle = createFunc(&info); - this->images = getSwapchainImages(this->device.get().vkd(), this->handle); - - this->reinitialize(); -} - -void MyVkSwapchain::reinitialize() { - const auto& vk = this->device.get().vkd(); - this->generator.emplace(this->layer, this->device, - this->extent, - this->format + // check for VK_KHR_swapchain_mutable_format + VkImageCreateFlags flags{}; + if (info.flags & VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR) + flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + const auto* chain = ls::find_structure( + VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, + info.pNext ); - this->waitSemaphore.emplace(vk, 0); - this->waitValue = 1; + // create underlying swapchain + this->handle = createFunc(&info); + this->swapchainImages = getSwapchainImages(vk, this->handle); - this->commandBuffer.emplace(vk); - this->fence.emplace(vk); + // create virtual swapchain images + this->images.reserve(this->swapchainImages.size()); + this->availableImages = std::vector(this->swapchainImages.size(), true); + for (size_t i = 0; i < this->swapchainImages.size(); i++) { + this->images.emplace_back(vk, + info.imageExtent, + info.imageFormat, + info.imageUsage, + std::nullopt, std::nullopt, + flags, + reinterpret_cast(chain) + ); + } - const size_t max_flight = std::max(this->images.size(), this->passes.size() + 2); + // create thread + this->doneSemaphore.emplace(vk, 0); + this->thread = std::thread(&MyVkSwapchain::thread_main, this); - this->passes.clear(); - for (size_t i = 0; i < max_flight; i++) { - this->passes.emplace_back(RenderPass { - .commandBuffer = vk::CommandBuffer(vk), + // this->reinitialize(); +} + +// void MyVkSwapchain::reinitialize() { +// // ... +// } + +MyVkSwapchain::~MyVkSwapchain() noexcept { + this->running.store(false); + if (this->thread.joinable()) + this->thread.join(); +} + +/* Virtual swapchain logic */ + +std::pair MyVkSwapchain::sync() { + return { this->presentSemaphore.handle(), this->presentIndex++ }; +} + +#define FILL_BARRIER(handle) \ + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, \ + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, \ + .image = (handle), \ + .subresourceRange = { \ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, \ + .levelCount = 1, \ + .layerCount = 1 \ + } + +void MyVkSwapchain::thread_main() noexcept { + const auto& vk = this->device.get().vkd(); + auto& offload = this->device.get().offload(); + + struct Pass { + vk::Semaphore acquireSemaphore; + vk::CommandBuffer commandBuffer; + vk::Fence copyFence; + vk::Semaphore presentSemaphore; + }; + + std::vector passes; + passes.reserve(this->swapchainImages.size() + 1); + for (size_t i = 0; i < this->swapchainImages.size() + 1; i++) { + passes.emplace_back(Pass { .acquireSemaphore = vk::Semaphore(vk), - .pcs = { - vk::Semaphore(vk), - vk::Semaphore(vk) - } + .commandBuffer = vk::CommandBuffer(vk), + .copyFence = vk::Fence(vk), + .presentSemaphore = vk::Semaphore(vk) }); } - this->idx = 0; + uint64_t counter{1}; + while (this->running.load()) { + // wait for present signal and fetch the image index + if (!this->presentSemaphore.wait(vk, counter, 100'1000)) + continue; // timeout after 100us + counter++; + + if (this->presents.empty()) { + // NOTE: the timeline semaphore is only hooked up + // after the partial present call, which means this + // queue must be filled. no mutex is necessary. + + std::cerr << "lsfg-vk: virtual swapchain encountered an impossible state\n"; + break; + } + + const auto ppi = this->presents.front(); + this->presents.pop(); + + // acquire a real swapchain image + const auto& pass = passes[counter % passes.size()]; + + uint32_t real_idx{}; + { + const std::scoped_lock lock(this->swapchainMutex); + + auto res = vk.df().AcquireNextImageKHR(vk.dev(), + this->handle, UINT64_MAX, + pass.acquireSemaphore.handle(), VK_NULL_HANDLE, + &real_idx + ); + if (res != VK_SUCCESS) { + status.store(res); + + if (res != VK_SUBOPTIMAL_KHR) + break; + } + } + + // copy virtual image into real swapchain image + const auto& cmdbuf = pass.commandBuffer; + cmdbuf.begin(vk); + + auto& virtualImage = this->images.at(ppi.idx); + auto& swapchainImage = this->swapchainImages.at(real_idx); + + cmdbuf.blitImage(vk, + { + { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_NONE, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + FILL_BARRIER(virtualImage.handle()) + }, + { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_NONE, + .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + FILL_BARRIER(swapchainImage) + }, + }, + { virtualImage.handle(), swapchainImage }, + virtualImage.getExtent(), + { + { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + FILL_BARRIER(swapchainImage) + } + } + ); + + cmdbuf.end(vk); + + { + const std::scoped_lock lock(offload.mutex); + cmdbuf.submit(vk, + { pass.acquireSemaphore.handle() }, VK_NULL_HANDLE, 0, + { pass.presentSemaphore.handle() }, VK_NULL_HANDLE, 0, + pass.copyFence.handle(), offload.queue + ); + } + + // present the real swapchain image + const uint64_t presentId = ppi.id.value_or(0); + const VkPresentIdKHR presentIdInfo{ + .sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR, + .swapchainCount = 1, + .pPresentIds = &presentId + }; + const auto mode = ppi.present_mode.value_or(VK_PRESENT_MODE_FIFO_KHR); + const VkSwapchainPresentModeInfoKHR presentModeInfo{ + .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODE_INFO_KHR, + .pNext = ppi.id.has_value() ? &presentIdInfo : nullptr, + .swapchainCount = 1, + .pPresentModes = &mode + }; + + const void* chain{}; + if (ppi.present_mode.has_value()) + chain = &presentModeInfo; + else if (ppi.id.has_value()) + chain = &presentIdInfo; + const VkPresentInfoKHR presentInfo{ + .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + .pNext = chain, + .waitSemaphoreCount = 1, + .pWaitSemaphores = &pass.presentSemaphore.handle(), + .swapchainCount = 1, + .pSwapchains = &this->handle, + .pImageIndices = &real_idx, + }; + { + const std::scoped_lock lock(offload.mutex); + const std::scoped_lock lock2(this->swapchainMutex); + auto res = vk.df().QueuePresentKHR(offload.queue, &presentInfo); + if (res != VK_SUCCESS) { + status.store(res); + + if (res != VK_SUBOPTIMAL_KHR) + break; + } + } + + if (ppi.id.has_value()) { + this->doneSemaphore->signal(vk, presentId + 1); + } + + // wait for the copy to finish + if (!pass.copyFence.wait(vk, UINT64_MAX)) { + std::cerr << "lsfg-vk: virtual swapchain encountered an impossible timeout\n"; + break; + } + pass.copyFence.reset(vk); + + // mark image as available again + { + const std::scoped_lock lock(this->availabilityMutex); + this->availableImages.at(ppi.idx) = true; + } + + // signal the fence + if (ppi.fence != VK_NULL_HANDLE) { + auto res = vk.df().QueueSubmit(offload.queue, 0, nullptr, ppi.fence); + if (res != VK_SUCCESS) + status.store(res); + } + } + + if (offload.mutex.try_lock()) { // must be in vkDeviceWaitIdle if locked + vk.df().QueueWaitIdle(offload.queue); + offload.mutex.unlock(); + } +} + +/* Reimplementations of Vulkan functions */ + +VkResult MyVkSwapchain::GetSwapchainImagesKHR(uint32_t *count, VkImage *images) noexcept { + if (images == nullptr) { + *count = static_cast(this->images.size()); + return VK_SUCCESS; + } + + VkResult res = VK_SUCCESS; + auto limit = static_cast(this->images.size()); + + if (*count < limit) { + limit = *count; + res = VK_INCOMPLETE; + } + + for (uint32_t i = 0; i < limit; i++) + images[i] = this->images[i].handle(); + + *count = limit; + return res; } namespace { - /// helper to acquire the next swapchain image - uint32_t acquireNextImageKHR(const vk::Vulkan& vk, VkSwapchainKHR swapchain, - const vk::Semaphore& semaphore) { - uint32_t imageIdx{}; + /// find and mark an available image + std::optional mark_available(std::vector& avail, std::mutex& mutex) noexcept { + const std::scoped_lock lock(mutex); + for (size_t i = 0; i < avail.size(); i++) { + if (!avail[i]) + continue; - auto res = vk.df().AcquireNextImageKHR(vk.dev(), swapchain, UINT64_MAX, - semaphore.handle(), VK_NULL_HANDLE, &imageIdx); - if (res != VK_SUCCESS && res != VK_SUBOPTIMAL_KHR) - throw ls::vulkan_error(res, "vkAcquireNextImageKHR() failed"); - - return imageIdx; + avail[i] = false; + return static_cast(i); + } + return std::nullopt; } - /// helper to present a swapchain image - VkResult queuePresentKHR(const vk::Vulkan& vk, VkQueue queue, - VkSwapchainKHR swapchain, uint32_t imageIdx, - const vk::Semaphore& waitSemaphore, void* next) { - const VkPresentInfoKHR presentInfo{ - .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, - .pNext = next, - .waitSemaphoreCount = 1, - .pWaitSemaphores = &waitSemaphore.handle(), - .swapchainCount = 1, - .pSwapchains = &swapchain, - .pImageIndices = &imageIdx, +} + +namespace { + /// find now in microseconds + uint64_t nowInUs() noexcept { + timespec ts{}; + clock_gettime(CLOCK_MONOTONIC, &ts); + + return static_cast(ts.tv_sec) * 1'000'000 + + static_cast(ts.tv_nsec) / 1'000; + } + /// signal a semaphore and fence + void signalSemaphoreAndFence(const vk::Vulkan& vk, + VkSemaphore semaphore, VkFence fence) noexcept { + const VkSubmitInfo info{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .signalSemaphoreCount = semaphore != VK_NULL_HANDLE, + .pSignalSemaphores = semaphore != VK_NULL_HANDLE ? &semaphore : nullptr }; - auto res = vk.df().QueuePresentKHR(queue, &presentInfo); - if (res != VK_SUCCESS && res != VK_SUBOPTIMAL_KHR) - throw ls::vulkan_error(res, "vkQueuePresentKHR() failed"); - - return res; + vk.df().QueueSubmit(vk.queue(), 1, &info, fence); } } -std::pair MyVkSwapchain::sync() { - return { this->waitSemaphore->handle(), this->waitValue }; -} - -VkResult MyVkSwapchain::present(VkQueue queue, void* next, uint32_t imageIdx) { +VkResult MyVkSwapchain::AcquireNextImageKHR(uint64_t timeout, + VkSemaphore semaphore, VkFence fence, uint32_t *idx) noexcept { const auto& vk = this->device.get().vkd(); - // wait for completion of copy - if (this->waitValue > 1 && !this->fence->wait(vk, 150ULL * 1000 * 1000)) - throw ls::vulkan_error(VK_TIMEOUT, "vkWaitForFences() failed"); - this->fence->reset(vk); + // fast path: try to get an available image without blocking + if (auto optIdx = mark_available(this->availableImages, this->availabilityMutex)) { + *idx = *optIdx; - // copy swapchain image into backend source image - auto& cmdbuf = this->commandBuffer.mut(); - cmdbuf.begin(vk); - - const auto& sync = this->generator.mut().prepare(cmdbuf, - this->images.at(imageIdx) - ); - - cmdbuf.end(vk); - cmdbuf.submit(vk, - {}, this->waitSemaphore->handle(), this->waitValue++, - {}, sync.first, sync.second, - this->fence->handle() - ); - - // schedule frame generation - this->generator.mut().schedule(); - - // present generated frames - for (size_t i = 0; i < this->generator->count(); i++) { - auto& pass = this->passes.at(this->idx % this->passes.size()); - - // acquire swapchain image - const uint32_t passImageIdx = acquireNextImageKHR(vk, this->handle, - pass.acquireSemaphore - ); - - // copy backend destination image into swapchain image - auto& passCmdbuf = pass.commandBuffer; - passCmdbuf.begin(vk); - - const auto& sync = this->generator.mut().obtain(passCmdbuf, - this->images.at(passImageIdx) - ); - - passCmdbuf.end(vk); - - std::vector waitSemaphores{ pass.acquireSemaphore.handle() }; - if (i > 0) { // non-first pass - const auto& prev_pass = this->passes.at((this->idx - 1) % this->passes.size()); - waitSemaphores.push_back(prev_pass.pcs.second.handle()); - } - - const std::vector signalSemaphores{ - pass.pcs.first.handle(), - pass.pcs.second.handle() - }; - - passCmdbuf.submit(vk, - waitSemaphores, sync.first, sync.second, - signalSemaphores, VK_NULL_HANDLE, 0 - ); - - // present swapchain image - queuePresentKHR(vk, queue, this->handle, - passImageIdx, - pass.pcs.first, - next - ); - - next = nullptr; // should only be set for first present - this->idx++; + signalSemaphoreAndFence(vk, semaphore, fence); + return this->status.load(); } - // present original swapchain image - const auto& prev_pass = this->passes.at((this->idx - 1) % this->passes.size()); - return queuePresentKHR(vk, queue, this->handle, - imageIdx, - prev_pass.pcs.second, - next + // if call is non-blocking: return not ready + if (timeout == 0) + return VK_NOT_READY; + + // otherwise: repeat fetch and delay 100us + const uint64_t start = nowInUs(); + timeout *= 1'000; // to microseconds + + while (nowInUs() - start < timeout) { + auto res = this->status.load(); + if (res != VK_SUCCESS && res != VK_SUBOPTIMAL_KHR) + return res; + + if (auto optIdx = mark_available(this->availableImages, this->availabilityMutex)) { + *idx = *optIdx; + + signalSemaphoreAndFence(vk, semaphore, fence); + return res; + } + + usleep(100); + } + + return VK_TIMEOUT; +} + +VkResult MyVkSwapchain::AcquireNextImage2KHR(const VkAcquireNextImageInfoKHR* info, + uint32_t* idx) noexcept { + return this->AcquireNextImageKHR( + info->timeout, + info->semaphore, + info->fence, + idx + ); +} + +VkResult MyVkSwapchain::partial_QueuePresentKHR(const MyVkPresentInfo& info) noexcept { + this->presents.emplace(info); + return this->status.load(); +} + +VkResult MyVkSwapchain::ReleaseSwapchainImagesKHR( + const VkReleaseSwapchainImagesInfoKHR* info) noexcept { + for (uint32_t i = 0; i < info->imageIndexCount; i++) { + const uint32_t idx = info->pImageIndices[i]; + + const std::scoped_lock lock(this->availabilityMutex); + this->availableImages.at(idx) = true; + } + return VK_SUCCESS; +} + +VkResult MyVkSwapchain::WaitForPresentKHR(uint64_t id, uint64_t timeout) noexcept { + const auto& vk = this->device.get().vkd(); + + if (!this->doneSemaphore->wait(vk, id + 1, timeout)) + return VK_TIMEOUT; + + const std::scoped_lock lock(this->swapchainMutex); + const auto& df = vk.df(); + + if (df.WaitForPresentKHR) { + return vk.df().WaitForPresentKHR(vk.dev(), this->handle, id, timeout); + } + + if (df.WaitForPresent2KHR) { + const VkPresentWait2InfoKHR info{ + .sType = VK_STRUCTURE_TYPE_PRESENT_WAIT_2_INFO_KHR, + .presentId = id, + .timeout = timeout + }; + return vk.df().WaitForPresent2KHR(vk.dev(), this->handle, &info); + } + + return VK_ERROR_EXTENSION_NOT_PRESENT; // should never happen +} + +VkResult MyVkSwapchain::WaitForPresent2KHR( + const VkPresentWait2InfoKHR* info) noexcept { + return this->WaitForPresentKHR( + info->presentId, + info->timeout ); } diff --git a/lsfg-vk-layer/src/hooks/swapchain.hpp b/lsfg-vk-layer/src/hooks/swapchain.hpp index 5facd1d..e042797 100644 --- a/lsfg-vk-layer/src/hooks/swapchain.hpp +++ b/lsfg-vk-layer/src/hooks/swapchain.hpp @@ -4,15 +4,17 @@ #include "device.hpp" #include "instance.hpp" -#include "../generator.hpp" #include "lsfg-vk-common/helpers/pointers.hpp" -#include "lsfg-vk-common/vulkan/command_buffer.hpp" -#include "lsfg-vk-common/vulkan/fence.hpp" -#include "lsfg-vk-common/vulkan/semaphore.hpp" +#include "lsfg-vk-common/vulkan/image.hpp" #include "lsfg-vk-common/vulkan/timeline_semaphore.hpp" +#include #include #include +#include +#include +#include +#include #include #include @@ -20,7 +22,17 @@ namespace lsfgvk::layer { - /// swapchain wrapper class + /// helper struct for partial present information + struct MyVkPresentInfo { + uint32_t idx{}; + // VK_KHR_swapchain_maintenance1 + VkFence fence{}; + std::optional present_mode; + // VK_KHR_present_id(2) + std::optional id; + }; + + /// swapchain wrapper (and virtual) class class MyVkSwapchain { public: /// create a vulkan swapchain wrapper @@ -33,61 +45,64 @@ namespace lsfgvk::layer { MyVkSwapchain(MyVkLayer& layer, MyVkInstance& instance, MyVkDevice& device, VkSwapchainCreateInfoKHR info, const std::function& createFunc); - - /// reinitialize the swapchain resources + /// reinitialize the contained swapchain resources /// @throws ls::vulkan_error on vulkan errors void reinitialize(); - /// get the preset synchronization info - /// @return pair of wait and signal semaphores - std::pair sync(); + /// get the synchronization pair for presentation + /// @return semaphore and value pair + [[nodiscard]] std::pair sync(); - /// present a frame - /// @param queue presentation queue - /// @param next pNext chain pointer - /// @param imageIdx swapchain image index to present to - /// @throws ls::vulkan_error on vulkan errors - VkResult present(VkQueue queue, void* next, uint32_t imageIdx); + /// reimplement vkGetSwapchainImagesKHR + VkResult GetSwapchainImagesKHR(uint32_t* count, VkImage* images) noexcept; + /// reimplement vkAcquireNextImageKHR + VkResult AcquireNextImageKHR(uint64_t timeout, + VkSemaphore semaphore, VkFence fence, uint32_t* idx) noexcept; + /// reimplement vkAcquireNextImage2KHR + VkResult AcquireNextImage2KHR(const VkAcquireNextImageInfoKHR* info, + uint32_t* idx) noexcept; + /// reimplement vkQueuePresentKHR + VkResult partial_QueuePresentKHR(const MyVkPresentInfo& info) noexcept; + /// reimplement vkReleaseSwapchainImagesKHR + VkResult ReleaseSwapchainImagesKHR(const VkReleaseSwapchainImagesInfoKHR* info) noexcept; + /// reimplement vkWaitForPresentKHR + VkResult WaitForPresentKHR(uint64_t id, uint64_t timeout) noexcept; + /// reimplement vkWaitForPresent2KHR + VkResult WaitForPresent2KHR(const VkPresentWait2InfoKHR* info) noexcept; - /// get swapchain extent - /// @return swapchain extent - [[nodiscard]] VkExtent2D swapchainExtent() const { return this->extent; } - /// get swapchain format - /// @return swapchain format - [[nodiscard]] VkFormat swapchainFormat() const { return this->format; } + /// get the underlying real swapchain handle + /// @return real swapchain handle + [[nodiscard]] auto swapchain() const noexcept { return this->handle; } // non-moveable, non-copyable MyVkSwapchain(const MyVkSwapchain&) = delete; MyVkSwapchain& operator=(const MyVkSwapchain&) = delete; MyVkSwapchain(MyVkSwapchain&&) = delete; MyVkSwapchain& operator=(MyVkSwapchain&&) = delete; - ~MyVkSwapchain() = default; + ~MyVkSwapchain() noexcept; private: ls::R layer; ls::R instance; ls::R device; - VkSwapchainKHR handle; - VkExtent2D extent; - VkFormat format; - std::vector images; + vk::TimelineSemaphore presentSemaphore; + uint64_t presentIndex; - ls::lazy generator; + std::mutex swapchainMutex; + VkSwapchainKHR handle; // from real swapchain + std::vector swapchainImages; - ls::lazy waitSemaphore; - uint64_t waitValue{1}; + std::vector images; // virtual swapchain images + std::mutex availabilityMutex; + std::vector availableImages; + //std::mutex presentationMutex; -- should not be necessary! + std::queue presents; + ls::lazy doneSemaphore; - ls::lazy commandBuffer; - ls::lazy fence; - - struct RenderPass { - vk::CommandBuffer commandBuffer; - vk::Semaphore acquireSemaphore; - - std::pair pcs; - }; - std::vector passes; - size_t idx{0}; + std::atomic_bool running{true}; + std::atomic status{VK_SUCCESS}; + std::thread thread; + void thread_main() noexcept; }; }