mirror of https://git.suyu.dev/suyu/suyu
renderer_vulkan: Make unconditional use of VK_KHR_timeline_semaphore
This reworks how host<->device synchronization works on the Vulkan backend. Instead of "protecting" resources with a fence and signalling these as free when the fence is known to be signalled by the host GPU, use timeline semaphores. Vulkan timeline semaphores allow use to work on a subset of D3D12 fences. As far as we are concerned, timeline semaphores are a value set by the host or the device that can be waited by either of them. Taking advantange of this, we can have a monolithically increasing atomic value for each submission to the graphics queue. Instead of protecting resources with a fence, we simply store the current logical tick (the atomic value stored in CPU memory). When we want to know if a resource is free, it can be compared to the current GPU tick. This greatly simplifies resource management code and the free status of resources should have less false negatives. To workaround bugs in validation layers, when these are attached there's a thread waiting for timeline semaphores.merge-requests/60/head
parent
1eae35621e
commit
58b0ae84b5
@ -0,0 +1,41 @@
|
||||
// Copyright 2020 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "video_core/renderer_vulkan/vk_command_pool.h"
|
||||
#include "video_core/renderer_vulkan/vk_device.h"
|
||||
#include "video_core/renderer_vulkan/wrapper.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
constexpr size_t COMMAND_BUFFER_POOL_SIZE = 0x1000;
|
||||
|
||||
CommandPool::CommandPool(MasterSemaphore& master_semaphore, const VKDevice& device)
|
||||
: ResourcePool(master_semaphore, COMMAND_BUFFER_POOL_SIZE), device{device} {}
|
||||
|
||||
CommandPool::~CommandPool() = default;
|
||||
|
||||
void CommandPool::Allocate(size_t begin, size_t end) {
|
||||
// Command buffers are going to be commited, recorded, executed every single usage cycle.
|
||||
// They are also going to be reseted when commited.
|
||||
Pool& pool = pools.emplace_back();
|
||||
pool.handle = device.GetLogical().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(),
|
||||
});
|
||||
pool.cmdbufs = pool.handle.Allocate(COMMAND_BUFFER_POOL_SIZE);
|
||||
}
|
||||
|
||||
VkCommandBuffer CommandPool::Commit() {
|
||||
const size_t index = CommitResource();
|
||||
const auto pool_index = index / COMMAND_BUFFER_POOL_SIZE;
|
||||
const auto sub_index = index % COMMAND_BUFFER_POOL_SIZE;
|
||||
return pools[pool_index].cmdbufs[sub_index];
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
@ -0,0 +1,35 @@
|
||||
// Copyright 2020 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include "video_core/renderer_vulkan/vk_resource_pool.h"
|
||||
#include "video_core/renderer_vulkan/wrapper.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class MasterSemaphore;
|
||||
class VKDevice;
|
||||
|
||||
class CommandPool final : public ResourcePool {
|
||||
public:
|
||||
explicit CommandPool(MasterSemaphore& master_semaphore, const VKDevice& device);
|
||||
virtual ~CommandPool();
|
||||
|
||||
void Allocate(size_t begin, size_t end) override;
|
||||
|
||||
VkCommandBuffer Commit();
|
||||
|
||||
private:
|
||||
struct Pool {
|
||||
vk::CommandPool handle;
|
||||
vk::CommandBuffers cmdbufs;
|
||||
};
|
||||
|
||||
const VKDevice& device;
|
||||
std::vector<Pool> pools;
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
@ -0,0 +1,56 @@
|
||||
// Copyright 2020 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
|
||||
#include "core/settings.h"
|
||||
#include "video_core/renderer_vulkan/vk_device.h"
|
||||
#include "video_core/renderer_vulkan/vk_master_semaphore.h"
|
||||
#include "video_core/renderer_vulkan/wrapper.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
MasterSemaphore::MasterSemaphore(const VKDevice& device) {
|
||||
static constexpr VkSemaphoreTypeCreateInfoKHR semaphore_type_ci{
|
||||
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO_KHR,
|
||||
.pNext = nullptr,
|
||||
.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE_KHR,
|
||||
.initialValue = 0,
|
||||
};
|
||||
static constexpr VkSemaphoreCreateInfo semaphore_ci{
|
||||
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
|
||||
.pNext = &semaphore_type_ci,
|
||||
.flags = 0,
|
||||
};
|
||||
semaphore = device.GetLogical().CreateSemaphore(semaphore_ci);
|
||||
|
||||
if (!Settings::values.renderer_debug) {
|
||||
return;
|
||||
}
|
||||
// Validation layers have a bug where they fail to track resource usage when using timeline
|
||||
// semaphores and synchronizing with GetSemaphoreCounterValueKHR. To workaround this issue, have
|
||||
// a separate thread waiting for each timeline semaphore value.
|
||||
debug_thread = std::thread([this] {
|
||||
u64 counter = 0;
|
||||
while (!shutdown) {
|
||||
if (semaphore.Wait(counter, 10'000'000)) {
|
||||
++counter;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MasterSemaphore::~MasterSemaphore() {
|
||||
shutdown = true;
|
||||
|
||||
// This thread might not be started
|
||||
if (debug_thread.joinable()) {
|
||||
debug_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
@ -0,0 +1,70 @@
|
||||
// Copyright 2020 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "video_core/renderer_vulkan/wrapper.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class VKDevice;
|
||||
|
||||
class MasterSemaphore {
|
||||
public:
|
||||
explicit MasterSemaphore(const VKDevice& device);
|
||||
~MasterSemaphore();
|
||||
|
||||
/// Returns the current logical tick.
|
||||
[[nodiscard]] u64 CurrentTick() const noexcept {
|
||||
return current_tick;
|
||||
}
|
||||
|
||||
/// Returns the timeline semaphore handle.
|
||||
[[nodiscard]] VkSemaphore Handle() const noexcept {
|
||||
return *semaphore;
|
||||
}
|
||||
|
||||
/// Returns true when a tick has been hit by the GPU.
|
||||
[[nodiscard]] bool IsFree(u64 tick) {
|
||||
return gpu_tick >= tick;
|
||||
}
|
||||
|
||||
/// Advance to the logical tick.
|
||||
void NextTick() noexcept {
|
||||
++current_tick;
|
||||
}
|
||||
|
||||
/// Refresh the known GPU tick
|
||||
void Refresh() {
|
||||
gpu_tick = semaphore.GetCounter();
|
||||
}
|
||||
|
||||
/// Waits for a tick to be hit on the GPU
|
||||
void Wait(u64 tick) {
|
||||
// No need to wait if the GPU is ahead of the tick
|
||||
if (IsFree(tick)) {
|
||||
return;
|
||||
}
|
||||
// Update the GPU tick and try again
|
||||
Refresh();
|
||||
if (IsFree(tick)) {
|
||||
return;
|
||||
}
|
||||
// If none of the above is hit, fallback to a regular wait
|
||||
semaphore.Wait(tick);
|
||||
}
|
||||
|
||||
private:
|
||||
vk::Semaphore semaphore; ///< Timeline semaphore.
|
||||
std::atomic<u64> gpu_tick{0}; ///< Current known GPU tick.
|
||||
std::atomic<u64> current_tick{1}; ///< Current logical tick.
|
||||
std::atomic<bool> shutdown{false}; ///< True when the object is being destroyed.
|
||||
std::thread debug_thread; ///< Debug thread to workaround validation layer bugs.
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
@ -1,311 +0,0 @@
|
||||
// Copyright 2018 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "video_core/renderer_vulkan/vk_device.h"
|
||||
#include "video_core/renderer_vulkan/vk_resource_manager.h"
|
||||
#include "video_core/renderer_vulkan/wrapper.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
namespace {
|
||||
|
||||
// TODO(Rodrigo): Fine tune these numbers.
|
||||
constexpr std::size_t COMMAND_BUFFER_POOL_SIZE = 0x1000;
|
||||
constexpr std::size_t FENCES_GROW_STEP = 0x40;
|
||||
|
||||
constexpr VkFenceCreateInfo BuildFenceCreateInfo() {
|
||||
return {
|
||||
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
};
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
class CommandBufferPool final : public VKFencedPool {
|
||||
public:
|
||||
explicit CommandBufferPool(const VKDevice& device)
|
||||
: VKFencedPool(COMMAND_BUFFER_POOL_SIZE), device{device} {}
|
||||
|
||||
void Allocate(std::size_t begin, std::size_t end) override {
|
||||
// Command buffers are going to be commited, recorded, executed every single usage cycle.
|
||||
// They are also going to be reseted when commited.
|
||||
Pool& pool = pools.emplace_back();
|
||||
pool.handle = device.GetLogical().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(),
|
||||
});
|
||||
pool.cmdbufs = pool.handle.Allocate(COMMAND_BUFFER_POOL_SIZE);
|
||||
}
|
||||
|
||||
VkCommandBuffer Commit(VKFence& fence) {
|
||||
const std::size_t index = CommitResource(fence);
|
||||
const auto pool_index = index / COMMAND_BUFFER_POOL_SIZE;
|
||||
const auto sub_index = index % COMMAND_BUFFER_POOL_SIZE;
|
||||
return pools[pool_index].cmdbufs[sub_index];
|
||||
}
|
||||
|
||||
private:
|
||||
struct Pool {
|
||||
vk::CommandPool handle;
|
||||
vk::CommandBuffers cmdbufs;
|
||||
};
|
||||
|
||||
const VKDevice& device;
|
||||
std::vector<Pool> pools;
|
||||
};
|
||||
|
||||
VKResource::VKResource() = default;
|
||||
|
||||
VKResource::~VKResource() = default;
|
||||
|
||||
VKFence::VKFence(const VKDevice& device)
|
||||
: device{device}, handle{device.GetLogical().CreateFence(BuildFenceCreateInfo())} {}
|
||||
|
||||
VKFence::~VKFence() = default;
|
||||
|
||||
void VKFence::Wait() {
|
||||
switch (const VkResult result = handle.Wait()) {
|
||||
case VK_SUCCESS:
|
||||
return;
|
||||
case VK_ERROR_DEVICE_LOST:
|
||||
device.ReportLoss();
|
||||
[[fallthrough]];
|
||||
default:
|
||||
throw vk::Exception(result);
|
||||
}
|
||||
}
|
||||
|
||||
void VKFence::Release() {
|
||||
ASSERT(is_owned);
|
||||
is_owned = false;
|
||||
}
|
||||
|
||||
void VKFence::Commit() {
|
||||
is_owned = true;
|
||||
is_used = true;
|
||||
}
|
||||
|
||||
bool VKFence::Tick(bool gpu_wait, bool owner_wait) {
|
||||
if (!is_used) {
|
||||
// If a fence is not used it's always free.
|
||||
return true;
|
||||
}
|
||||
if (is_owned && !owner_wait) {
|
||||
// The fence is still being owned (Release has not been called) and ownership wait has
|
||||
// not been asked.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gpu_wait) {
|
||||
// Wait for the fence if it has been requested.
|
||||
(void)handle.Wait();
|
||||
} else {
|
||||
if (handle.GetStatus() != VK_SUCCESS) {
|
||||
// Vulkan fence is not ready, not much it can do here
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast resources their free state.
|
||||
for (auto* resource : protected_resources) {
|
||||
resource->OnFenceRemoval(this);
|
||||
}
|
||||
protected_resources.clear();
|
||||
|
||||
// Prepare fence for reusage.
|
||||
handle.Reset();
|
||||
is_used = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VKFence::Protect(VKResource* resource) {
|
||||
protected_resources.push_back(resource);
|
||||
}
|
||||
|
||||
void VKFence::Unprotect(VKResource* resource) {
|
||||
const auto it = std::find(protected_resources.begin(), protected_resources.end(), resource);
|
||||
ASSERT(it != protected_resources.end());
|
||||
|
||||
resource->OnFenceRemoval(this);
|
||||
protected_resources.erase(it);
|
||||
}
|
||||
|
||||
void VKFence::RedirectProtection(VKResource* old_resource, VKResource* new_resource) noexcept {
|
||||
std::replace(std::begin(protected_resources), std::end(protected_resources), old_resource,
|
||||
new_resource);
|
||||
}
|
||||
|
||||
VKFenceWatch::VKFenceWatch() = default;
|
||||
|
||||
VKFenceWatch::VKFenceWatch(VKFence& initial_fence) {
|
||||
Watch(initial_fence);
|
||||
}
|
||||
|
||||
VKFenceWatch::VKFenceWatch(VKFenceWatch&& rhs) noexcept {
|
||||
fence = std::exchange(rhs.fence, nullptr);
|
||||
if (fence) {
|
||||
fence->RedirectProtection(&rhs, this);
|
||||
}
|
||||
}
|
||||
|
||||
VKFenceWatch& VKFenceWatch::operator=(VKFenceWatch&& rhs) noexcept {
|
||||
fence = std::exchange(rhs.fence, nullptr);
|
||||
if (fence) {
|
||||
fence->RedirectProtection(&rhs, this);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
VKFenceWatch::~VKFenceWatch() {
|
||||
if (fence) {
|
||||
fence->Unprotect(this);
|
||||
}
|
||||
}
|
||||
|
||||
void VKFenceWatch::Wait() {
|
||||
if (fence == nullptr) {
|
||||
return;
|
||||
}
|
||||
fence->Wait();
|
||||
fence->Unprotect(this);
|
||||
}
|
||||
|
||||
void VKFenceWatch::Watch(VKFence& new_fence) {
|
||||
Wait();
|
||||
fence = &new_fence;
|
||||
fence->Protect(this);
|
||||
}
|
||||
|
||||
bool VKFenceWatch::TryWatch(VKFence& new_fence) {
|
||||
if (fence) {
|
||||
return false;
|
||||
}
|
||||
fence = &new_fence;
|
||||
fence->Protect(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void VKFenceWatch::OnFenceRemoval(VKFence* signaling_fence) {
|
||||
ASSERT_MSG(signaling_fence == fence, "Removing the wrong fence");
|
||||
fence = nullptr;
|
||||
}
|
||||
|
||||
VKFencedPool::VKFencedPool(std::size_t grow_step) : grow_step{grow_step} {}
|
||||
|
||||
VKFencedPool::~VKFencedPool() = default;
|
||||
|
||||
std::size_t VKFencedPool::CommitResource(VKFence& fence) {
|
||||
const auto Search = [&](std::size_t begin, std::size_t end) -> std::optional<std::size_t> {
|
||||
for (std::size_t iterator = begin; iterator < end; ++iterator) {
|
||||
if (watches[iterator]->TryWatch(fence)) {
|
||||
// The resource is now being watched, a free resource was successfully found.
|
||||
return iterator;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
// Try to find a free resource from the hinted position to the end.
|
||||
auto found = Search(free_iterator, watches.size());
|
||||
if (!found) {
|
||||
// Search from beginning to the hinted position.
|
||||
found = Search(0, free_iterator);
|
||||
if (!found) {
|
||||
// Both searches failed, the pool is full; handle it.
|
||||
const std::size_t free_resource = ManageOverflow();
|
||||
|
||||
// Watch will wait for the resource to be free.
|
||||
watches[free_resource]->Watch(fence);
|
||||
found = free_resource;
|
||||
}
|
||||
}
|
||||
// Free iterator is hinted to the resource after the one that's been commited.
|
||||
free_iterator = (*found + 1) % watches.size();
|
||||
return *found;
|
||||
}
|
||||
|
||||
std::size_t VKFencedPool::ManageOverflow() {
|
||||
const std::size_t old_capacity = watches.size();
|
||||
Grow();
|
||||
|
||||
// The last entry is guaranted to be free, since it's the first element of the freshly
|
||||
// allocated resources.
|
||||
return old_capacity;
|
||||
}
|
||||
|
||||
void VKFencedPool::Grow() {
|
||||
const std::size_t old_capacity = watches.size();
|
||||
watches.resize(old_capacity + grow_step);
|
||||
std::generate(watches.begin() + old_capacity, watches.end(),
|
||||
[]() { return std::make_unique<VKFenceWatch>(); });
|
||||
Allocate(old_capacity, old_capacity + grow_step);
|
||||
}
|
||||
|
||||
VKResourceManager::VKResourceManager(const VKDevice& device) : device{device} {
|
||||
GrowFences(FENCES_GROW_STEP);
|
||||
command_buffer_pool = std::make_unique<CommandBufferPool>(device);
|
||||
}
|
||||
|
||||
VKResourceManager::~VKResourceManager() = default;
|
||||
|
||||
VKFence& VKResourceManager::CommitFence() {
|
||||
const auto StepFences = [&](bool gpu_wait, bool owner_wait) -> VKFence* {
|
||||
const auto Tick = [=](auto& fence) { return fence->Tick(gpu_wait, owner_wait); };
|
||||
const auto hinted = fences.begin() + fences_iterator;
|
||||
|
||||
auto it = std::find_if(hinted, fences.end(), Tick);
|
||||
if (it == fences.end()) {
|
||||
it = std::find_if(fences.begin(), hinted, Tick);
|
||||
if (it == hinted) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
fences_iterator = std::distance(fences.begin(), it) + 1;
|
||||
if (fences_iterator >= fences.size())
|
||||
fences_iterator = 0;
|
||||
|
||||
auto& fence = *it;
|
||||
fence->Commit();
|
||||
return fence.get();
|
||||
};
|
||||
|
||||
VKFence* found_fence = StepFences(false, false);
|
||||
if (!found_fence) {
|
||||
// Try again, this time waiting.
|
||||
found_fence = StepFences(true, false);
|
||||
|
||||
if (!found_fence) {
|
||||
// Allocate new fences and try again.
|
||||
LOG_INFO(Render_Vulkan, "Allocating new fences {} -> {}", fences.size(),
|
||||
fences.size() + FENCES_GROW_STEP);
|
||||
|
||||
GrowFences(FENCES_GROW_STEP);
|
||||
found_fence = StepFences(true, false);
|
||||
ASSERT(found_fence != nullptr);
|
||||
}
|
||||
}
|
||||
return *found_fence;
|
||||
}
|
||||
|
||||
VkCommandBuffer VKResourceManager::CommitCommandBuffer(VKFence& fence) {
|
||||
return command_buffer_pool->Commit(fence);
|
||||
}
|
||||
|
||||
void VKResourceManager::GrowFences(std::size_t new_fences_count) {
|
||||
const std::size_t previous_size = fences.size();
|
||||
fences.resize(previous_size + new_fences_count);
|
||||
|
||||
std::generate(fences.begin() + previous_size, fences.end(),
|
||||
[this] { return std::make_unique<VKFence>(device); });
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
@ -1,196 +0,0 @@
|
||||
// Copyright 2018 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "video_core/renderer_vulkan/wrapper.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class VKDevice;
|
||||
class VKFence;
|
||||
class VKResourceManager;
|
||||
|
||||
class CommandBufferPool;
|
||||
|
||||
/// Interface for a Vulkan resource
|
||||
class VKResource {
|
||||
public:
|
||||
explicit VKResource();
|
||||
virtual ~VKResource();
|
||||
|
||||
/**
|
||||
* Signals the object that an owning fence has been signaled.
|
||||
* @param signaling_fence Fence that signals its usage end.
|
||||
*/
|
||||
virtual void OnFenceRemoval(VKFence* signaling_fence) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fences take ownership of objects, protecting them from GPU-side or driver-side concurrent access.
|
||||
* They must be commited from the resource manager. Their usage flow is: commit the fence from the
|
||||
* resource manager, protect resources with it and use them, send the fence to an execution queue
|
||||
* and Wait for it if needed and then call Release. Used resources will automatically be signaled
|
||||
* when they are free to be reused.
|
||||
* @brief Protects resources for concurrent usage and signals its release.
|
||||
*/
|
||||
class VKFence {
|
||||
friend class VKResourceManager;
|
||||
|
||||
public:
|
||||
explicit VKFence(const VKDevice& device);
|
||||
~VKFence();
|
||||
|
||||
/**
|
||||
* Waits for the fence to be signaled.
|
||||
* @warning You must have ownership of the fence and it has to be previously sent to a queue to
|
||||
* call this function.
|
||||
*/
|
||||
void Wait();
|
||||
|
||||
/**
|
||||
* Releases ownership of the fence. Pass after it has been sent to an execution queue.
|
||||
* Unmanaged usage of the fence after the call will result in undefined behavior because it may
|
||||
* be being used for something else.
|
||||
*/
|
||||
void Release();
|
||||
|
||||
/// Protects a resource with this fence.
|
||||
void Protect(VKResource* resource);
|
||||
|
||||
/// Removes protection for a resource.
|
||||
void Unprotect(VKResource* resource);
|
||||
|
||||
/// Redirects one protected resource to a new address.
|
||||
void RedirectProtection(VKResource* old_resource, VKResource* new_resource) noexcept;
|
||||
|
||||
/// Retreives the fence.
|
||||
operator VkFence() const {
|
||||
return *handle;
|
||||
}
|
||||
|
||||
private:
|
||||
/// Take ownership of the fence.
|
||||
void Commit();
|
||||
|
||||
/**
|
||||
* Updates the fence status.
|
||||
* @warning Waiting for the owner might soft lock the execution.
|
||||
* @param gpu_wait Wait for the fence to be signaled by the driver.
|
||||
* @param owner_wait Wait for the owner to signal its freedom.
|
||||
* @returns True if the fence is free. Waiting for gpu and owner will always return true.
|
||||
*/
|
||||
bool Tick(bool gpu_wait, bool owner_wait);
|
||||
|
||||
const VKDevice& device; ///< Device handler
|
||||
vk::Fence handle; ///< Vulkan fence
|
||||
std::vector<VKResource*> protected_resources; ///< List of resources protected by this fence
|
||||
bool is_owned = false; ///< The fence has been commited but not released yet.
|
||||
bool is_used = false; ///< The fence has been commited but it has not been checked to be free.
|
||||
};
|
||||
|
||||
/**
|
||||
* A fence watch is used to keep track of the usage of a fence and protect a resource or set of
|
||||
* resources without having to inherit VKResource from their handlers.
|
||||
*/
|
||||
class VKFenceWatch final : public VKResource {
|
||||
public:
|
||||
explicit VKFenceWatch();
|
||||
VKFenceWatch(VKFence& initial_fence);
|
||||
VKFenceWatch(VKFenceWatch&&) noexcept;
|
||||
VKFenceWatch(const VKFenceWatch&) = delete;
|
||||
~VKFenceWatch() override;
|
||||
|
||||
VKFenceWatch& operator=(VKFenceWatch&&) noexcept;
|
||||
|
||||
/// Waits for the fence to be released.
|
||||
void Wait();
|
||||
|
||||
/**
|
||||
* Waits for a previous fence and watches a new one.
|
||||
* @param new_fence New fence to wait to.
|
||||
*/
|
||||
void Watch(VKFence& new_fence);
|
||||
|
||||
/**
|
||||
* Checks if it's currently being watched and starts watching it if it's available.
|
||||
* @returns True if a watch has started, false if it's being watched.
|
||||
*/
|
||||
bool TryWatch(VKFence& new_fence);
|
||||
|
||||
void OnFenceRemoval(VKFence* signaling_fence) override;
|
||||
|
||||
/**
|
||||
* Do not use it paired with Watch. Use TryWatch instead.
|
||||
* Returns true when the watch is free.
|
||||
*/
|
||||
bool IsUsed() const {
|
||||
return fence != nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
VKFence* fence{}; ///< Fence watching this resource. nullptr when the watch is free.
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a pool of resources protected by fences. Manages resource overflow allocating more
|
||||
* resources.
|
||||
*/
|
||||
class VKFencedPool {
|
||||
public:
|
||||
explicit VKFencedPool(std::size_t grow_step);
|
||||
virtual ~VKFencedPool();
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Commits a free resource and protects it with a fence. It may allocate new resources.
|
||||
* @param fence Fence that protects the commited resource.
|
||||
* @returns Index of the resource commited.
|
||||
*/
|
||||
std::size_t CommitResource(VKFence& fence);
|
||||
|
||||
/// Called when a chunk of resources have to be allocated.
|
||||
virtual void Allocate(std::size_t begin, std::size_t end) = 0;
|
||||
|
||||
private:
|
||||
/// Manages pool overflow allocating new resources.
|
||||
std::size_t ManageOverflow();
|
||||
|
||||
/// Allocates a new page of resources.
|
||||
void Grow();
|
||||
|
||||
std::size_t grow_step = 0; ///< Number of new resources created after an overflow
|
||||
std::size_t free_iterator = 0; ///< Hint to where the next free resources is likely to be found
|
||||
std::vector<std::unique_ptr<VKFenceWatch>> watches; ///< Set of watched resources
|
||||
};
|
||||
|
||||
/**
|
||||
* The resource manager handles all resources that can be protected with a fence avoiding
|
||||
* driver-side or GPU-side concurrent usage. Usage is documented in VKFence.
|
||||
*/
|
||||
class VKResourceManager final {
|
||||
public:
|
||||
explicit VKResourceManager(const VKDevice& device);
|
||||
~VKResourceManager();
|
||||
|
||||
/// Commits a fence. It has to be sent to a queue and released.
|
||||
VKFence& CommitFence();
|
||||
|
||||
/// Commits an unused command buffer and protects it with a fence.
|
||||
VkCommandBuffer CommitCommandBuffer(VKFence& fence);
|
||||
|
||||
private:
|
||||
/// Allocates new fences.
|
||||
void GrowFences(std::size_t new_fences_count);
|
||||
|
||||
const VKDevice& device; ///< Device handler.
|
||||
std::size_t fences_iterator = 0; ///< Index where a free fence is likely to be found.
|
||||
std::vector<std::unique_ptr<VKFence>> fences; ///< Pool of fences.
|
||||
std::unique_ptr<CommandBufferPool> command_buffer_pool; ///< Pool of command buffers.
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
@ -0,0 +1,63 @@
|
||||
// Copyright 2020 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "video_core/renderer_vulkan/vk_master_semaphore.h"
|
||||
#include "video_core/renderer_vulkan/vk_resource_pool.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
ResourcePool::ResourcePool(MasterSemaphore& master_semaphore_, size_t grow_step_)
|
||||
: master_semaphore{master_semaphore_}, grow_step{grow_step_} {}
|
||||
|
||||
ResourcePool::~ResourcePool() = default;
|
||||
|
||||
size_t ResourcePool::CommitResource() {
|
||||
// Refresh semaphore to query updated results
|
||||
master_semaphore.Refresh();
|
||||
|
||||
const auto search = [this](size_t begin, size_t end) -> std::optional<size_t> {
|
||||
for (size_t iterator = begin; iterator < end; ++iterator) {
|
||||
if (master_semaphore.IsFree(ticks[iterator])) {
|
||||
ticks[iterator] = master_semaphore.CurrentTick();
|
||||
return iterator;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
// Try to find a free resource from the hinted position to the end.
|
||||
auto found = search(free_iterator, ticks.size());
|
||||
if (!found) {
|
||||
// Search from beginning to the hinted position.
|
||||
found = search(0, free_iterator);
|
||||
if (!found) {
|
||||
// Both searches failed, the pool is full; handle it.
|
||||
const size_t free_resource = ManageOverflow();
|
||||
|
||||
ticks[free_resource] = master_semaphore.CurrentTick();
|
||||
found = free_resource;
|
||||
}
|
||||
}
|
||||
// Free iterator is hinted to the resource after the one that's been commited.
|
||||
free_iterator = (*found + 1) % ticks.size();
|
||||
return *found;
|
||||
}
|
||||
|
||||
size_t ResourcePool::ManageOverflow() {
|
||||
const size_t old_capacity = ticks.size();
|
||||
Grow();
|
||||
|
||||
// The last entry is guaranted to be free, since it's the first element of the freshly
|
||||
// allocated resources.
|
||||
return old_capacity;
|
||||
}
|
||||
|
||||
void ResourcePool::Grow() {
|
||||
const size_t old_capacity = ticks.size();
|
||||
ticks.resize(old_capacity + grow_step);
|
||||
Allocate(old_capacity, old_capacity + grow_step);
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
@ -0,0 +1,43 @@
|
||||
// Copyright 2020 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class MasterSemaphore;
|
||||
|
||||
/**
|
||||
* Handles a pool of resources protected by fences. Manages resource overflow allocating more
|
||||
* resources.
|
||||
*/
|
||||
class ResourcePool {
|
||||
public:
|
||||
explicit ResourcePool(MasterSemaphore& master_semaphore, size_t grow_step);
|
||||
virtual ~ResourcePool();
|
||||
|
||||
protected:
|
||||
size_t CommitResource();
|
||||
|
||||
/// Called when a chunk of resources have to be allocated.
|
||||
virtual void Allocate(size_t begin, size_t end) = 0;
|
||||
|
||||
private:
|
||||
/// Manages pool overflow allocating new resources.
|
||||
size_t ManageOverflow();
|
||||
|
||||
/// Allocates a new page of resources.
|
||||
void Grow();
|
||||
|
||||
MasterSemaphore& master_semaphore;
|
||||
size_t grow_step = 0; ///< Number of new resources created after an overflow
|
||||
size_t free_iterator = 0; ///< Hint to where the next free resources is likely to be found
|
||||
std::vector<u64> ticks; ///< Ticks for each resource
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
Loading…
Reference in New Issue