feat(frame-pacing): implement basic virtual swapchain

This commit is contained in:
PancakeTAS 2026-01-02 16:58:17 +01:00
parent 35933799c9
commit 111295af8c
No known key found for this signature in database
9 changed files with 649 additions and 237 deletions

View file

@ -5,7 +5,8 @@
#include <cassert>
#include <functional>
#include <optional>
#include <stdexcept>
#include <vulkan/vulkan_core.h>
namespace ls {
/// helper alias for std::reference_wrapper
@ -25,8 +26,7 @@ namespace ls {
/// @throws std::logic_error if value already present
template<typename... Args>
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>(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<void(T&)> 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<typename T>
T* find_structure(VkStructureType type, const void* chain) noexcept {
auto* next = reinterpret_cast<VkBaseInStructure*>(const_cast<void*>(chain));
while (next) {
if (next->sType == type)
return reinterpret_cast<T*>(next);
next = const_cast<VkBaseInStructure*>(next->pNext);
}
return nullptr;
}
}

View file

@ -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<int> importFd = std::nullopt,
std::optional<int*> exportFd = std::nullopt);
std::optional<int*> exportFd = std::nullopt,
VkImageCreateFlags flags = 0, const void* chain = nullptr);
/// get the image handle
/// @return the image handle

View file

@ -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

View file

@ -16,16 +16,18 @@ namespace {
/// create a image
ls::owned_ptr<VkImage> 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<int> importFd,
std::optional<int*> exportFd) :
std::optional<int*> 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,

View file

@ -388,7 +388,12 @@ VulkanDeviceFuncs vk::initVulkanDeviceFuncs(PFN_vkGetDeviceProcAddr f, VkDevice
.QueuePresentKHR = graphical ?
dpa<PFN_vkQueuePresentKHR>(f, d, "vkQueuePresentKHR") : nullptr,
.DestroySwapchainKHR = graphical ?
dpa<PFN_vkDestroySwapchainKHR>(f, d, "vkDestroySwapchainKHR") : nullptr
dpa<PFN_vkDestroySwapchainKHR>(f, d, "vkDestroySwapchainKHR") : nullptr,
.WaitForPresentKHR = reinterpret_cast<PFN_vkWaitForPresentKHR>(
f(d, "vkWaitForPresentKHR")),
.WaitForPresent2KHR = reinterpret_cast<PFN_vkWaitForPresent2KHR>(
f(d, "vkWaitForPresent2KHR"))
};
}

View file

@ -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

View file

@ -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 <cstdint>
#include <exception>
#include <iostream>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
@ -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<VkFence> fences(info->swapchainCount, VK_NULL_HANDLE);
std::vector<std::optional<VkPresentModeKHR>> modes(info->swapchainCount, std::nullopt);
const auto* fenceInfo = ls::find_structure<VkSwapchainPresentFenceInfoKHR>(
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<VkSwapchainPresentModeInfoKHR>(
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<std::optional<uint64_t>> presentIds(info->swapchainCount, std::nullopt);
const auto* presentIdInfo = ls::find_structure<VkPresentIdKHR>(
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<VkPresentId2KHR>(
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<uint64_t> waitValues(info->waitSemaphoreCount);
std::vector<VkSemaphore> 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<void*>(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

View file

@ -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 <algorithm>
#include <cstddef>
#include <cstdint>
#include <ctime>
#include <functional>
#include <string>
#include <iostream>
#include <mutex>
#include <optional>
#include <utility>
#include <vector>
#include <time.h>
#include <unistd.h>
#include <bits/time.h>
#include <vulkan/vulkan_core.h>
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<VkImage> 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<VkSwapchainKHR(VkSwapchainCreateInfoKHR*)>& 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<VkImageFormatListCreateInfo>(
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<bool>(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<const void*>(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<VkSemaphore, uint64_t> 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<Pass> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(offload.mutex);
const std::scoped_lock<std::mutex> 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<std::mutex> 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<uint32_t>(this->images.size());
return VK_SUCCESS;
}
VkResult res = VK_SUCCESS;
auto limit = static_cast<uint32_t>(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<uint32_t> mark_available(std::vector<bool>& avail, std::mutex& mutex) noexcept {
const std::scoped_lock<std::mutex> 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<uint32_t>(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<uint64_t>(ts.tv_sec) * 1'000'000
+ static_cast<uint64_t>(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<VkSemaphore, uint64_t> 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<VkSemaphore> 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<VkSemaphore> 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<std::mutex> 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<std::mutex> 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
);
}

View file

@ -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 <atomic>
#include <cstdint>
#include <functional>
#include <mutex>
#include <optional>
#include <queue>
#include <thread>
#include <utility>
#include <vector>
@ -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<VkPresentModeKHR> present_mode;
// VK_KHR_present_id(2)
std::optional<uint64_t> 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<VkSwapchainKHR(VkSwapchainCreateInfoKHR*)>& 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<VkSemaphore, uint64_t> sync();
/// get the synchronization pair for presentation
/// @return semaphore and value pair
[[nodiscard]] std::pair<VkSemaphore, uint64_t> 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<MyVkLayer> layer;
ls::R<MyVkInstance> instance;
ls::R<MyVkDevice> device;
VkSwapchainKHR handle;
VkExtent2D extent;
VkFormat format;
std::vector<VkImage> images;
vk::TimelineSemaphore presentSemaphore;
uint64_t presentIndex;
ls::lazy<Generator> generator;
std::mutex swapchainMutex;
VkSwapchainKHR handle; // from real swapchain
std::vector<VkImage> swapchainImages;
ls::lazy<vk::TimelineSemaphore> waitSemaphore;
uint64_t waitValue{1};
std::vector<vk::Image> images; // virtual swapchain images
std::mutex availabilityMutex;
std::vector<bool> availableImages;
//std::mutex presentationMutex; -- should not be necessary!
std::queue<MyVkPresentInfo> presents;
ls::lazy<vk::TimelineSemaphore> doneSemaphore;
ls::lazy<vk::CommandBuffer> commandBuffer;
ls::lazy<vk::Fence> fence;
struct RenderPass {
vk::CommandBuffer commandBuffer;
vk::Semaphore acquireSemaphore;
std::pair<vk::Semaphore, vk::Semaphore> pcs;
};
std::vector<RenderPass> passes;
size_t idx{0};
std::atomic_bool running{true};
std::atomic<VkResult> status{VK_SUCCESS};
std::thread thread;
void thread_main() noexcept;
};
}