11 changed files with 712 additions and 218 deletions
-
2src/video_core/CMakeLists.txt
-
51src/video_core/renderer_vulkan/renderer_vulkan.cpp
-
2src/video_core/renderer_vulkan/renderer_vulkan.h
-
224src/video_core/renderer_vulkan/vk_blit_screen.cpp
-
34src/video_core/renderer_vulkan/vk_blit_screen.h
-
440src/video_core/renderer_vulkan/vk_present_manager.cpp
-
82src/video_core/renderer_vulkan/vk_present_manager.h
-
9src/video_core/renderer_vulkan/vk_scheduler.cpp
-
6src/video_core/renderer_vulkan/vk_scheduler.h
-
49src/video_core/renderer_vulkan/vk_swapchain.cpp
-
31src/video_core/renderer_vulkan/vk_swapchain.h
@ -0,0 +1,440 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include "common/microprofile.h"
|
|||
#include "common/thread.h"
|
|||
#include "video_core/renderer_vulkan/vk_present_manager.h"
|
|||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
|||
#include "video_core/renderer_vulkan/vk_swapchain.h"
|
|||
#include "video_core/vulkan_common/vulkan_device.h"
|
|||
|
|||
namespace Vulkan { |
|||
|
|||
MICROPROFILE_DEFINE(Vulkan_WaitPresent, "Vulkan", "Wait For Present", MP_RGB(128, 128, 128)); |
|||
MICROPROFILE_DEFINE(Vulkan_CopyToSwapchain, "Vulkan", "Copy to swapchain", MP_RGB(192, 255, 192)); |
|||
|
|||
namespace { |
|||
|
|||
bool CanBlitToSwapchain(const vk::PhysicalDevice& physical_device, VkFormat format) { |
|||
const VkFormatProperties props{physical_device.GetFormatProperties(format)}; |
|||
return (props.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT); |
|||
} |
|||
|
|||
[[nodiscard]] VkImageSubresourceLayers MakeImageSubresourceLayers() { |
|||
return VkImageSubresourceLayers{ |
|||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, |
|||
.mipLevel = 0, |
|||
.baseArrayLayer = 0, |
|||
.layerCount = 1, |
|||
}; |
|||
} |
|||
|
|||
[[nodiscard]] VkImageBlit MakeImageBlit(s32 frame_width, s32 frame_height, s32 swapchain_width, |
|||
s32 swapchain_height) { |
|||
return VkImageBlit{ |
|||
.srcSubresource = MakeImageSubresourceLayers(), |
|||
.srcOffsets = |
|||
{ |
|||
{ |
|||
.x = 0, |
|||
.y = 0, |
|||
.z = 0, |
|||
}, |
|||
{ |
|||
.x = frame_width, |
|||
.y = frame_height, |
|||
.z = 1, |
|||
}, |
|||
}, |
|||
.dstSubresource = MakeImageSubresourceLayers(), |
|||
.dstOffsets = |
|||
{ |
|||
{ |
|||
.x = 0, |
|||
.y = 0, |
|||
.z = 0, |
|||
}, |
|||
{ |
|||
.x = swapchain_width, |
|||
.y = swapchain_height, |
|||
.z = 1, |
|||
}, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
[[nodiscard]] VkImageCopy MakeImageCopy(u32 frame_width, u32 frame_height, u32 swapchain_width, |
|||
u32 swapchain_height) { |
|||
return VkImageCopy{ |
|||
.srcSubresource = MakeImageSubresourceLayers(), |
|||
.srcOffset = |
|||
{ |
|||
.x = 0, |
|||
.y = 0, |
|||
.z = 0, |
|||
}, |
|||
.dstSubresource = MakeImageSubresourceLayers(), |
|||
.dstOffset = |
|||
{ |
|||
.x = 0, |
|||
.y = 0, |
|||
.z = 0, |
|||
}, |
|||
.extent = |
|||
{ |
|||
.width = std::min(frame_width, swapchain_width), |
|||
.height = std::min(frame_height, swapchain_height), |
|||
.depth = 1, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
} // Anonymous namespace
|
|||
|
|||
PresentManager::PresentManager(Core::Frontend::EmuWindow& render_window_, const Device& device_, |
|||
MemoryAllocator& memory_allocator_, Scheduler& scheduler_, |
|||
Swapchain& swapchain_) |
|||
: render_window{render_window_}, device{device_}, |
|||
memory_allocator{memory_allocator_}, scheduler{scheduler_}, swapchain{swapchain_}, |
|||
blit_supported{CanBlitToSwapchain(device.GetPhysical(), swapchain.GetImageViewFormat())}, |
|||
image_count{swapchain.GetImageCount()} { |
|||
|
|||
auto& dld = device.GetLogical(); |
|||
cmdpool = dld.CreateCommandPool({ |
|||
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, |
|||
.pNext = nullptr, |
|||
.flags = |
|||
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, |
|||
.queueFamilyIndex = device.GetGraphicsFamily(), |
|||
}); |
|||
auto cmdbuffers = cmdpool.Allocate(image_count); |
|||
|
|||
frames.resize(image_count); |
|||
for (u32 i = 0; i < frames.size(); i++) { |
|||
Frame& frame = frames[i]; |
|||
frame.cmdbuf = vk::CommandBuffer{cmdbuffers[i], device.GetDispatchLoader()}; |
|||
frame.render_ready = dld.CreateSemaphore({ |
|||
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, |
|||
.pNext = nullptr, |
|||
.flags = 0, |
|||
}); |
|||
frame.present_done = dld.CreateFence({ |
|||
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, |
|||
.pNext = nullptr, |
|||
.flags = VK_FENCE_CREATE_SIGNALED_BIT, |
|||
}); |
|||
free_queue.push(&frame); |
|||
} |
|||
|
|||
present_thread = std::jthread([this](std::stop_token token) { PresentThread(token); }); |
|||
} |
|||
|
|||
PresentManager::~PresentManager() = default; |
|||
|
|||
Frame* PresentManager::GetRenderFrame() { |
|||
MICROPROFILE_SCOPE(Vulkan_WaitPresent); |
|||
|
|||
// Wait for free presentation frames
|
|||
std::unique_lock lock{free_mutex}; |
|||
free_cv.wait(lock, [this] { return !free_queue.empty(); }); |
|||
|
|||
// Take the frame from the queue
|
|||
Frame* frame = free_queue.front(); |
|||
free_queue.pop(); |
|||
|
|||
// Wait for the presentation to be finished so all frame resources are free
|
|||
frame->present_done.Wait(); |
|||
frame->present_done.Reset(); |
|||
|
|||
return frame; |
|||
} |
|||
|
|||
void PresentManager::PushFrame(Frame* frame) { |
|||
std::unique_lock lock{queue_mutex}; |
|||
present_queue.push(frame); |
|||
frame_cv.notify_one(); |
|||
} |
|||
|
|||
void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, bool is_srgb, |
|||
VkFormat image_view_format, VkRenderPass rd) { |
|||
auto& dld = device.GetLogical(); |
|||
|
|||
frame->width = width; |
|||
frame->height = height; |
|||
frame->is_srgb = is_srgb; |
|||
|
|||
frame->image = dld.CreateImage({ |
|||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, |
|||
.pNext = nullptr, |
|||
.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, |
|||
.imageType = VK_IMAGE_TYPE_2D, |
|||
.format = swapchain.GetImageFormat(), |
|||
.extent = |
|||
{ |
|||
.width = width, |
|||
.height = height, |
|||
.depth = 1, |
|||
}, |
|||
.mipLevels = 1, |
|||
.arrayLayers = 1, |
|||
.samples = VK_SAMPLE_COUNT_1_BIT, |
|||
.tiling = VK_IMAGE_TILING_OPTIMAL, |
|||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, |
|||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE, |
|||
.queueFamilyIndexCount = 0, |
|||
.pQueueFamilyIndices = nullptr, |
|||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, |
|||
}); |
|||
|
|||
frame->image_commit = memory_allocator.Commit(frame->image, MemoryUsage::DeviceLocal); |
|||
|
|||
frame->image_view = dld.CreateImageView({ |
|||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, |
|||
.pNext = nullptr, |
|||
.flags = 0, |
|||
.image = *frame->image, |
|||
.viewType = VK_IMAGE_VIEW_TYPE_2D, |
|||
.format = image_view_format, |
|||
.components = |
|||
{ |
|||
.r = VK_COMPONENT_SWIZZLE_IDENTITY, |
|||
.g = VK_COMPONENT_SWIZZLE_IDENTITY, |
|||
.b = VK_COMPONENT_SWIZZLE_IDENTITY, |
|||
.a = VK_COMPONENT_SWIZZLE_IDENTITY, |
|||
}, |
|||
.subresourceRange = |
|||
{ |
|||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, |
|||
.baseMipLevel = 0, |
|||
.levelCount = 1, |
|||
.baseArrayLayer = 0, |
|||
.layerCount = 1, |
|||
}, |
|||
}); |
|||
|
|||
const VkImageView image_view{*frame->image_view}; |
|||
frame->framebuffer = dld.CreateFramebuffer({ |
|||
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, |
|||
.pNext = nullptr, |
|||
.flags = 0, |
|||
.renderPass = rd, |
|||
.attachmentCount = 1, |
|||
.pAttachments = &image_view, |
|||
.width = width, |
|||
.height = height, |
|||
.layers = 1, |
|||
}); |
|||
} |
|||
|
|||
void PresentManager::WaitPresent() { |
|||
// Wait for the present queue to be empty
|
|||
{ |
|||
std::unique_lock queue_lock{queue_mutex}; |
|||
frame_cv.wait(queue_lock, [this] { return present_queue.empty(); }); |
|||
} |
|||
|
|||
// The above condition will be satisfied when the last frame is taken from the queue.
|
|||
// To ensure that frame has been presented as well take hold of the swapchain
|
|||
// mutex.
|
|||
std::scoped_lock swapchain_lock{swapchain_mutex}; |
|||
} |
|||
|
|||
void PresentManager::PresentThread(std::stop_token token) { |
|||
Common::SetCurrentThreadName("VulkanPresent"); |
|||
while (!token.stop_requested()) { |
|||
std::unique_lock lock{queue_mutex}; |
|||
|
|||
// Wait for presentation frames
|
|||
Common::CondvarWait(frame_cv, lock, token, [this] { return !present_queue.empty(); }); |
|||
if (token.stop_requested()) { |
|||
return; |
|||
} |
|||
|
|||
// Take the frame and notify anyone waiting
|
|||
Frame* frame = present_queue.front(); |
|||
present_queue.pop(); |
|||
frame_cv.notify_one(); |
|||
|
|||
// By exchanging the lock ownership we take the swapchain lock
|
|||
// before the queue lock goes out of scope. This way the swapchain
|
|||
// lock in WaitPresent is guaranteed to occur after here.
|
|||
std::exchange(lock, std::unique_lock{swapchain_mutex}); |
|||
|
|||
CopyToSwapchain(frame); |
|||
|
|||
// Free the frame for reuse
|
|||
std::scoped_lock fl{free_mutex}; |
|||
free_queue.push(frame); |
|||
free_cv.notify_one(); |
|||
} |
|||
} |
|||
|
|||
void PresentManager::CopyToSwapchain(Frame* frame) { |
|||
MICROPROFILE_SCOPE(Vulkan_CopyToSwapchain); |
|||
|
|||
const auto recreate_swapchain = [&] { |
|||
swapchain.Create(frame->width, frame->height, frame->is_srgb); |
|||
image_count = swapchain.GetImageCount(); |
|||
}; |
|||
|
|||
// If the size or colorspace of the incoming frames has changed, recreate the swapchain
|
|||
// to account for that.
|
|||
const bool srgb_changed = swapchain.NeedsRecreation(frame->is_srgb); |
|||
const bool size_changed = |
|||
swapchain.GetWidth() != frame->width || swapchain.GetHeight() != frame->height; |
|||
if (srgb_changed || size_changed) { |
|||
recreate_swapchain(); |
|||
} |
|||
|
|||
while (swapchain.AcquireNextImage()) { |
|||
recreate_swapchain(); |
|||
} |
|||
|
|||
const vk::CommandBuffer cmdbuf{frame->cmdbuf}; |
|||
cmdbuf.Begin({ |
|||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, |
|||
.pNext = nullptr, |
|||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, |
|||
.pInheritanceInfo = nullptr, |
|||
}); |
|||
|
|||
const VkImage image{swapchain.CurrentImage()}; |
|||
const VkExtent2D extent = swapchain.GetExtent(); |
|||
const std::array pre_barriers{ |
|||
VkImageMemoryBarrier{ |
|||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|||
.pNext = nullptr, |
|||
.srcAccessMask = 0, |
|||
.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, |
|||
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, |
|||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
|||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.image = image, |
|||
.subresourceRange{ |
|||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, |
|||
.baseMipLevel = 0, |
|||
.levelCount = 1, |
|||
.baseArrayLayer = 0, |
|||
.layerCount = VK_REMAINING_ARRAY_LAYERS, |
|||
}, |
|||
}, |
|||
VkImageMemoryBarrier{ |
|||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|||
.pNext = nullptr, |
|||
.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, |
|||
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, |
|||
.oldLayout = VK_IMAGE_LAYOUT_GENERAL, |
|||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, |
|||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.image = *frame->image, |
|||
.subresourceRange{ |
|||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, |
|||
.baseMipLevel = 0, |
|||
.levelCount = 1, |
|||
.baseArrayLayer = 0, |
|||
.layerCount = VK_REMAINING_ARRAY_LAYERS, |
|||
}, |
|||
}, |
|||
}; |
|||
const std::array post_barriers{ |
|||
VkImageMemoryBarrier{ |
|||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|||
.pNext = nullptr, |
|||
.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, |
|||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.image = image, |
|||
.subresourceRange{ |
|||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, |
|||
.baseMipLevel = 0, |
|||
.levelCount = 1, |
|||
.baseArrayLayer = 0, |
|||
.layerCount = VK_REMAINING_ARRAY_LAYERS, |
|||
}, |
|||
}, |
|||
VkImageMemoryBarrier{ |
|||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, |
|||
.pNext = nullptr, |
|||
.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT, |
|||
.dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, |
|||
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, |
|||
.newLayout = VK_IMAGE_LAYOUT_GENERAL, |
|||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, |
|||
.image = *frame->image, |
|||
.subresourceRange{ |
|||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, |
|||
.baseMipLevel = 0, |
|||
.levelCount = 1, |
|||
.baseArrayLayer = 0, |
|||
.layerCount = VK_REMAINING_ARRAY_LAYERS, |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, {}, |
|||
{}, {}, pre_barriers); |
|||
|
|||
if (blit_supported) { |
|||
cmdbuf.BlitImage(*frame->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, |
|||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
|||
MakeImageBlit(frame->width, frame->height, extent.width, extent.height), |
|||
VK_FILTER_LINEAR); |
|||
} else { |
|||
cmdbuf.CopyImage(*frame->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, |
|||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
|||
MakeImageCopy(frame->width, frame->height, extent.width, extent.height)); |
|||
} |
|||
|
|||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, {}, |
|||
{}, {}, post_barriers); |
|||
|
|||
cmdbuf.End(); |
|||
|
|||
const VkSemaphore present_semaphore = swapchain.CurrentPresentSemaphore(); |
|||
const VkSemaphore render_semaphore = swapchain.CurrentRenderSemaphore(); |
|||
const std::array wait_semaphores = {present_semaphore, *frame->render_ready}; |
|||
|
|||
static constexpr std::array<VkPipelineStageFlags, 2> wait_stage_masks{ |
|||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, |
|||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, |
|||
}; |
|||
|
|||
const VkSubmitInfo submit_info{ |
|||
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, |
|||
.pNext = nullptr, |
|||
.waitSemaphoreCount = 2U, |
|||
.pWaitSemaphores = wait_semaphores.data(), |
|||
.pWaitDstStageMask = wait_stage_masks.data(), |
|||
.commandBufferCount = 1, |
|||
.pCommandBuffers = cmdbuf.address(), |
|||
.signalSemaphoreCount = 1U, |
|||
.pSignalSemaphores = &render_semaphore, |
|||
}; |
|||
|
|||
// Submit the image copy/blit to the swapchain
|
|||
{ |
|||
std::scoped_lock lock{scheduler.submit_mutex}; |
|||
switch (const VkResult result = |
|||
device.GetGraphicsQueue().Submit(submit_info, *frame->present_done)) { |
|||
case VK_SUCCESS: |
|||
break; |
|||
case VK_ERROR_DEVICE_LOST: |
|||
device.ReportLoss(); |
|||
[[fallthrough]]; |
|||
default: |
|||
vk::Check(result); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
// Present
|
|||
swapchain.Present(render_semaphore); |
|||
} |
|||
|
|||
} // namespace Vulkan
|
|||
@ -0,0 +1,82 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <condition_variable> |
|||
#include <mutex> |
|||
#include <queue> |
|||
|
|||
#include "common/common_types.h" |
|||
#include "common/polyfill_thread.h" |
|||
#include "video_core/vulkan_common/vulkan_memory_allocator.h" |
|||
#include "video_core/vulkan_common/vulkan_wrapper.h" |
|||
|
|||
namespace Core::Frontend { |
|||
class EmuWindow; |
|||
} // namespace Core::Frontend |
|||
|
|||
namespace Vulkan { |
|||
|
|||
class Device; |
|||
class Scheduler; |
|||
class Swapchain; |
|||
|
|||
struct Frame { |
|||
u32 width; |
|||
u32 height; |
|||
bool is_srgb; |
|||
vk::Image image; |
|||
vk::ImageView image_view; |
|||
vk::Framebuffer framebuffer; |
|||
MemoryCommit image_commit; |
|||
vk::CommandBuffer cmdbuf; |
|||
vk::Semaphore render_ready; |
|||
vk::Fence present_done; |
|||
}; |
|||
|
|||
class PresentManager { |
|||
public: |
|||
PresentManager(Core::Frontend::EmuWindow& render_window, const Device& device, |
|||
MemoryAllocator& memory_allocator, Scheduler& scheduler, Swapchain& swapchain); |
|||
~PresentManager(); |
|||
|
|||
/// Returns the last used presentation frame |
|||
Frame* GetRenderFrame(); |
|||
|
|||
/// Pushes a frame for presentation |
|||
void PushFrame(Frame* frame); |
|||
|
|||
/// Recreates the present frame to match the provided parameters |
|||
void RecreateFrame(Frame* frame, u32 width, u32 height, bool is_srgb, |
|||
VkFormat image_view_format, VkRenderPass rd); |
|||
|
|||
/// Waits for the present thread to finish presenting all queued frames. |
|||
void WaitPresent(); |
|||
|
|||
private: |
|||
void PresentThread(std::stop_token token); |
|||
|
|||
void CopyToSwapchain(Frame* frame); |
|||
|
|||
private: |
|||
Core::Frontend::EmuWindow& render_window; |
|||
const Device& device; |
|||
MemoryAllocator& memory_allocator; |
|||
Scheduler& scheduler; |
|||
Swapchain& swapchain; |
|||
vk::CommandPool cmdpool; |
|||
std::vector<Frame> frames; |
|||
std::queue<Frame*> present_queue; |
|||
std::queue<Frame*> free_queue; |
|||
std::condition_variable_any frame_cv; |
|||
std::condition_variable free_cv; |
|||
std::mutex swapchain_mutex; |
|||
std::mutex queue_mutex; |
|||
std::mutex free_mutex; |
|||
std::jthread present_thread; |
|||
bool blit_supported{}; |
|||
std::size_t image_count; |
|||
}; |
|||
|
|||
} // namespace Vulkan |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue