Merge pull request #2122 from ReinUsesLisp/vulkan-resource-manager
vk_resource_manager: Implement fence and command buffer allocator
This commit is contained in:
		| @@ -105,7 +105,9 @@ if (ENABLE_VULKAN) | ||||
|     target_sources(video_core PRIVATE | ||||
|         renderer_vulkan/declarations.h | ||||
|         renderer_vulkan/vk_device.cpp | ||||
|         renderer_vulkan/vk_device.h) | ||||
|         renderer_vulkan/vk_device.h | ||||
|         renderer_vulkan/vk_resource_manager.cpp | ||||
|         renderer_vulkan/vk_resource_manager.h) | ||||
|  | ||||
|     target_include_directories(video_core PRIVATE ../../externals/Vulkan-Headers/include) | ||||
|     target_compile_definitions(video_core PRIVATE HAS_VULKAN) | ||||
|   | ||||
							
								
								
									
										285
									
								
								src/video_core/renderer_vulkan/vk_resource_manager.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								src/video_core/renderer_vulkan/vk_resource_manager.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,285 @@ | ||||
| // 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/declarations.h" | ||||
| #include "video_core/renderer_vulkan/vk_device.h" | ||||
| #include "video_core/renderer_vulkan/vk_resource_manager.h" | ||||
|  | ||||
| namespace Vulkan { | ||||
|  | ||||
| // TODO(Rodrigo): Fine tune these numbers. | ||||
| constexpr std::size_t COMMAND_BUFFER_POOL_SIZE = 0x1000; | ||||
| constexpr std::size_t FENCES_GROW_STEP = 0x40; | ||||
|  | ||||
| class CommandBufferPool final : public VKFencedPool { | ||||
| public: | ||||
|     CommandBufferPool(const VKDevice& device) | ||||
|         : VKFencedPool(COMMAND_BUFFER_POOL_SIZE), device{device} {} | ||||
|  | ||||
|     void Allocate(std::size_t begin, std::size_t end) { | ||||
|         const auto dev = device.GetLogical(); | ||||
|         const auto& dld = device.GetDispatchLoader(); | ||||
|         const u32 graphics_family = device.GetGraphicsFamily(); | ||||
|  | ||||
|         auto pool = std::make_unique<Pool>(); | ||||
|  | ||||
|         // Command buffers are going to be commited, recorded, executed every single usage cycle. | ||||
|         // They are also going to be reseted when commited. | ||||
|         const auto pool_flags = vk::CommandPoolCreateFlagBits::eTransient | | ||||
|                                 vk::CommandPoolCreateFlagBits::eResetCommandBuffer; | ||||
|         const vk::CommandPoolCreateInfo cmdbuf_pool_ci(pool_flags, graphics_family); | ||||
|         pool->handle = dev.createCommandPoolUnique(cmdbuf_pool_ci, nullptr, dld); | ||||
|  | ||||
|         const vk::CommandBufferAllocateInfo cmdbuf_ai(*pool->handle, | ||||
|                                                       vk::CommandBufferLevel::ePrimary, | ||||
|                                                       static_cast<u32>(COMMAND_BUFFER_POOL_SIZE)); | ||||
|         pool->cmdbufs = | ||||
|             dev.allocateCommandBuffersUnique<std::allocator<UniqueCommandBuffer>>(cmdbuf_ai, dld); | ||||
|  | ||||
|         pools.push_back(std::move(pool)); | ||||
|     } | ||||
|  | ||||
|     vk::CommandBuffer 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 { | ||||
|         UniqueCommandPool handle; | ||||
|         std::vector<UniqueCommandBuffer> cmdbufs; | ||||
|     }; | ||||
|  | ||||
|     const VKDevice& device; | ||||
|  | ||||
|     std::vector<std::unique_ptr<Pool>> pools; | ||||
| }; | ||||
|  | ||||
| VKResource::VKResource() = default; | ||||
|  | ||||
| VKResource::~VKResource() = default; | ||||
|  | ||||
| VKFence::VKFence(const VKDevice& device, UniqueFence handle) | ||||
|     : device{device}, handle{std::move(handle)} {} | ||||
|  | ||||
| VKFence::~VKFence() = default; | ||||
|  | ||||
| void VKFence::Wait() { | ||||
|     const auto dev = device.GetLogical(); | ||||
|     const auto& dld = device.GetDispatchLoader(); | ||||
|     dev.waitForFences({*handle}, true, std::numeric_limits<u64>::max(), dld); | ||||
| } | ||||
|  | ||||
| void VKFence::Release() { | ||||
|     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; | ||||
|     } | ||||
|  | ||||
|     const auto dev = device.GetLogical(); | ||||
|     const auto& dld = device.GetDispatchLoader(); | ||||
|     if (gpu_wait) { | ||||
|         // Wait for the fence if it has been requested. | ||||
|         dev.waitForFences({*handle}, true, std::numeric_limits<u64>::max(), dld); | ||||
|     } else { | ||||
|         if (dev.getFenceStatus(*handle, dld) != vk::Result::eSuccess) { | ||||
|             // 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. | ||||
|     dev.resetFences({*handle}, dld); | ||||
|     is_used = false; | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| void VKFence::Protect(VKResource* resource) { | ||||
|     protected_resources.push_back(resource); | ||||
| } | ||||
|  | ||||
| void VKFence::Unprotect(const VKResource* resource) { | ||||
|     const auto it = std::find(protected_resources.begin(), protected_resources.end(), resource); | ||||
|     if (it != protected_resources.end()) { | ||||
|         protected_resources.erase(it); | ||||
|     } | ||||
| } | ||||
|  | ||||
| VKFenceWatch::VKFenceWatch() = default; | ||||
|  | ||||
| VKFenceWatch::~VKFenceWatch() { | ||||
|     if (fence) { | ||||
|         fence->Unprotect(this); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void VKFenceWatch::Wait() { | ||||
|     if (!fence) { | ||||
|         return; | ||||
|     } | ||||
|     fence->Wait(); | ||||
|     fence->Unprotect(this); | ||||
|     fence = nullptr; | ||||
| } | ||||
|  | ||||
| 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; | ||||
| } | ||||
|  | ||||
| vk::CommandBuffer VKResourceManager::CommitCommandBuffer(VKFence& fence) { | ||||
|     return command_buffer_pool->Commit(fence); | ||||
| } | ||||
|  | ||||
| void VKResourceManager::GrowFences(std::size_t new_fences_count) { | ||||
|     const auto dev = device.GetLogical(); | ||||
|     const auto& dld = device.GetDispatchLoader(); | ||||
|     const vk::FenceCreateInfo fence_ci; | ||||
|  | ||||
|     const std::size_t previous_size = fences.size(); | ||||
|     fences.resize(previous_size + new_fences_count); | ||||
|  | ||||
|     std::generate(fences.begin() + previous_size, fences.end(), [&]() { | ||||
|         return std::make_unique<VKFence>(device, dev.createFenceUnique(fence_ci, nullptr, dld)); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| } // namespace Vulkan | ||||
							
								
								
									
										180
									
								
								src/video_core/renderer_vulkan/vk_resource_manager.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/video_core/renderer_vulkan/vk_resource_manager.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| // 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/declarations.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, UniqueFence handle); | ||||
|     ~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(const VKResource* resource); | ||||
|  | ||||
|     /// Retreives the fence. | ||||
|     operator vk::Fence() 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 | ||||
|     UniqueFence 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(); | ||||
|  | ||||
|     /// 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; | ||||
|  | ||||
| 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. | ||||
|     vk::CommandBuffer 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 | ||||
		Reference in New Issue
	
	Block a user