Browse Source
vk_memory_manager: Implement memory manager
vk_memory_manager: Implement memory manager
A memory manager object handles the memory allocations for a device. It allocates chunks of Vulkan memory objects and then suballocates.nce_cpp
4 changed files with 343 additions and 1 deletions
-
2externals/Vulkan-Headers
-
2src/video_core/CMakeLists.txt
-
253src/video_core/renderer_vulkan/vk_memory_manager.cpp
-
87src/video_core/renderer_vulkan/vk_memory_manager.h
@ -1 +1 @@ |
|||||
Subproject commit 7f02d9bb810f371de0fe833c80004c34f7ff8c57 |
|
||||
|
Subproject commit 15e5c4db7500b936ae758236f2e72fc1aec22020 |
||||
@ -0,0 +1,253 @@ |
|||||
|
// Copyright 2018 yuzu Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
#include <algorithm>
|
||||
|
#include <optional>
|
||||
|
#include <tuple>
|
||||
|
#include <vector>
|
||||
|
#include "common/alignment.h"
|
||||
|
#include "common/assert.h"
|
||||
|
#include "common/common_types.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_memory_manager.h"
|
||||
|
|
||||
|
namespace Vulkan { |
||||
|
|
||||
|
// TODO(Rodrigo): Fine tune this number
|
||||
|
constexpr u64 ALLOC_CHUNK_SIZE = 64 * 1024 * 1024; |
||||
|
|
||||
|
class VKMemoryAllocation final { |
||||
|
public: |
||||
|
explicit VKMemoryAllocation(const VKDevice& device, vk::DeviceMemory memory, |
||||
|
vk::MemoryPropertyFlags properties, u64 alloc_size, u32 type) |
||||
|
: device{device}, memory{memory}, properties{properties}, alloc_size{alloc_size}, |
||||
|
shifted_type{ShiftType(type)}, is_mappable{properties & |
||||
|
vk::MemoryPropertyFlagBits::eHostVisible} { |
||||
|
if (is_mappable) { |
||||
|
const auto dev = device.GetLogical(); |
||||
|
const auto& dld = device.GetDispatchLoader(); |
||||
|
base_address = static_cast<u8*>(dev.mapMemory(memory, 0, alloc_size, {}, dld)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
~VKMemoryAllocation() { |
||||
|
const auto dev = device.GetLogical(); |
||||
|
const auto& dld = device.GetDispatchLoader(); |
||||
|
if (is_mappable) |
||||
|
dev.unmapMemory(memory, dld); |
||||
|
dev.free(memory, nullptr, dld); |
||||
|
} |
||||
|
|
||||
|
VKMemoryCommit Commit(vk::DeviceSize commit_size, vk::DeviceSize alignment) { |
||||
|
auto found = TryFindFreeSection(free_iterator, alloc_size, static_cast<u64>(commit_size), |
||||
|
static_cast<u64>(alignment)); |
||||
|
if (!found) { |
||||
|
found = TryFindFreeSection(0, free_iterator, static_cast<u64>(commit_size), |
||||
|
static_cast<u64>(alignment)); |
||||
|
if (!found) { |
||||
|
// Signal out of memory, it'll try to do more allocations.
|
||||
|
return nullptr; |
||||
|
} |
||||
|
} |
||||
|
u8* address = is_mappable ? base_address + *found : nullptr; |
||||
|
auto commit = std::make_unique<VKMemoryCommitImpl>(this, memory, address, *found, |
||||
|
*found + commit_size); |
||||
|
commits.push_back(commit.get()); |
||||
|
|
||||
|
// Last commit's address is highly probable to be free.
|
||||
|
free_iterator = *found + commit_size; |
||||
|
|
||||
|
return commit; |
||||
|
} |
||||
|
|
||||
|
void Free(const VKMemoryCommitImpl* commit) { |
||||
|
ASSERT(commit); |
||||
|
const auto it = |
||||
|
std::find_if(commits.begin(), commits.end(), |
||||
|
[&](const auto& stored_commit) { return stored_commit == commit; }); |
||||
|
if (it == commits.end()) { |
||||
|
LOG_CRITICAL(Render_Vulkan, "Freeing unallocated commit!"); |
||||
|
UNREACHABLE(); |
||||
|
return; |
||||
|
} |
||||
|
commits.erase(it); |
||||
|
} |
||||
|
|
||||
|
/// Returns whether this allocation is compatible with the arguments.
|
||||
|
bool IsCompatible(vk::MemoryPropertyFlags wanted_properties, u32 type_mask) const { |
||||
|
return (wanted_properties & properties) != vk::MemoryPropertyFlagBits(0) && |
||||
|
(type_mask & shifted_type) != 0; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
static constexpr u32 ShiftType(u32 type) { |
||||
|
return 1U << type; |
||||
|
} |
||||
|
|
||||
|
/// A memory allocator, it may return a free region between "start" and "end" with the solicited
|
||||
|
/// requeriments.
|
||||
|
std::optional<u64> TryFindFreeSection(u64 start, u64 end, u64 size, u64 alignment) const { |
||||
|
u64 iterator = start; |
||||
|
while (iterator + size < end) { |
||||
|
const u64 try_left = Common::AlignUp(iterator, alignment); |
||||
|
const u64 try_right = try_left + size; |
||||
|
|
||||
|
bool overlap = false; |
||||
|
for (const auto& commit : commits) { |
||||
|
const auto [commit_left, commit_right] = commit->interval; |
||||
|
if (try_left < commit_right && commit_left < try_right) { |
||||
|
// There's an overlap, continue the search where the overlapping commit ends.
|
||||
|
iterator = commit_right; |
||||
|
overlap = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (!overlap) { |
||||
|
// A free address has been found.
|
||||
|
return try_left; |
||||
|
} |
||||
|
} |
||||
|
// No free regions where found, return an empty optional.
|
||||
|
return std::nullopt; |
||||
|
} |
||||
|
|
||||
|
const VKDevice& device; ///< Vulkan device.
|
||||
|
const vk::DeviceMemory memory; ///< Vulkan memory allocation handler.
|
||||
|
const vk::MemoryPropertyFlags properties; ///< Vulkan properties.
|
||||
|
const u64 alloc_size; ///< Size of this allocation.
|
||||
|
const u32 shifted_type; ///< Stored Vulkan type of this allocation, shifted.
|
||||
|
const bool is_mappable; ///< Whether the allocation is mappable.
|
||||
|
|
||||
|
/// Base address of the mapped pointer.
|
||||
|
u8* base_address{}; |
||||
|
|
||||
|
/// Hints where the next free region is likely going to be.
|
||||
|
u64 free_iterator{}; |
||||
|
|
||||
|
/// Stores all commits done from this allocation.
|
||||
|
std::vector<const VKMemoryCommitImpl*> commits; |
||||
|
}; |
||||
|
|
||||
|
VKMemoryManager::VKMemoryManager(const VKDevice& device) |
||||
|
: device{device}, props{device.GetPhysical().getMemoryProperties(device.GetDispatchLoader())}, |
||||
|
is_memory_unified{GetMemoryUnified(props)} {} |
||||
|
|
||||
|
VKMemoryManager::~VKMemoryManager() = default; |
||||
|
|
||||
|
VKMemoryCommit VKMemoryManager::Commit(const vk::MemoryRequirements& reqs, bool host_visible) { |
||||
|
ASSERT(reqs.size < ALLOC_CHUNK_SIZE); |
||||
|
|
||||
|
// When a host visible commit is asked, search for host visible and coherent, otherwise search
|
||||
|
// for a fast device local type.
|
||||
|
const vk::MemoryPropertyFlags wanted_properties = |
||||
|
host_visible |
||||
|
? vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent |
||||
|
: vk::MemoryPropertyFlagBits::eDeviceLocal; |
||||
|
|
||||
|
const auto TryCommit = [&]() -> VKMemoryCommit { |
||||
|
for (auto& alloc : allocs) { |
||||
|
if (!alloc->IsCompatible(wanted_properties, reqs.memoryTypeBits)) |
||||
|
continue; |
||||
|
|
||||
|
if (auto commit = alloc->Commit(reqs.size, reqs.alignment); commit) { |
||||
|
return commit; |
||||
|
} |
||||
|
} |
||||
|
return {}; |
||||
|
}; |
||||
|
|
||||
|
if (auto commit = TryCommit(); commit) { |
||||
|
return commit; |
||||
|
} |
||||
|
|
||||
|
// Commit has failed, allocate more memory.
|
||||
|
if (!AllocMemory(wanted_properties, reqs.memoryTypeBits, ALLOC_CHUNK_SIZE)) { |
||||
|
// TODO(Rodrigo): Try to use host memory.
|
||||
|
LOG_CRITICAL(Render_Vulkan, "Ran out of memory!"); |
||||
|
UNREACHABLE(); |
||||
|
} |
||||
|
|
||||
|
// Commit again, this time it won't fail since there's a fresh allocation above. If it does,
|
||||
|
// there's a bug.
|
||||
|
auto commit = TryCommit(); |
||||
|
ASSERT(commit); |
||||
|
return commit; |
||||
|
} |
||||
|
|
||||
|
VKMemoryCommit VKMemoryManager::Commit(vk::Buffer buffer, bool host_visible) { |
||||
|
const auto dev = device.GetLogical(); |
||||
|
const auto& dld = device.GetDispatchLoader(); |
||||
|
const auto requeriments = dev.getBufferMemoryRequirements(buffer, dld); |
||||
|
auto commit = Commit(requeriments, host_visible); |
||||
|
dev.bindBufferMemory(buffer, commit->GetMemory(), commit->GetOffset(), dld); |
||||
|
return commit; |
||||
|
} |
||||
|
|
||||
|
VKMemoryCommit VKMemoryManager::Commit(vk::Image image, bool host_visible) { |
||||
|
const auto dev = device.GetLogical(); |
||||
|
const auto& dld = device.GetDispatchLoader(); |
||||
|
const auto requeriments = dev.getImageMemoryRequirements(image, dld); |
||||
|
auto commit = Commit(requeriments, host_visible); |
||||
|
dev.bindImageMemory(image, commit->GetMemory(), commit->GetOffset(), dld); |
||||
|
return commit; |
||||
|
} |
||||
|
|
||||
|
bool VKMemoryManager::AllocMemory(vk::MemoryPropertyFlags wanted_properties, u32 type_mask, |
||||
|
u64 size) { |
||||
|
const u32 type = [&]() { |
||||
|
for (u32 type_index = 0; type_index < props.memoryTypeCount; ++type_index) { |
||||
|
const auto flags = props.memoryTypes[type_index].propertyFlags; |
||||
|
if ((type_mask & (1U << type_index)) && (flags & wanted_properties)) { |
||||
|
// The type matches in type and in the wanted properties.
|
||||
|
return type_index; |
||||
|
} |
||||
|
} |
||||
|
LOG_CRITICAL(Render_Vulkan, "Couldn't find a compatible memory type!"); |
||||
|
UNREACHABLE(); |
||||
|
return 0u; |
||||
|
}(); |
||||
|
|
||||
|
const auto dev = device.GetLogical(); |
||||
|
const auto& dld = device.GetDispatchLoader(); |
||||
|
|
||||
|
// Try to allocate found type.
|
||||
|
const vk::MemoryAllocateInfo memory_ai(size, type); |
||||
|
vk::DeviceMemory memory; |
||||
|
if (const vk::Result res = dev.allocateMemory(&memory_ai, nullptr, &memory, dld); |
||||
|
res != vk::Result::eSuccess) { |
||||
|
LOG_CRITICAL(Render_Vulkan, "Device allocation failed with code {}!", vk::to_string(res)); |
||||
|
return false; |
||||
|
} |
||||
|
allocs.push_back( |
||||
|
std::make_unique<VKMemoryAllocation>(device, memory, wanted_properties, size, type)); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/*static*/ bool VKMemoryManager::GetMemoryUnified(const vk::PhysicalDeviceMemoryProperties& props) { |
||||
|
for (u32 heap_index = 0; heap_index < props.memoryHeapCount; ++heap_index) { |
||||
|
if (!(props.memoryHeaps[heap_index].flags & vk::MemoryHeapFlagBits::eDeviceLocal)) { |
||||
|
// Memory is considered unified when heaps are device local only.
|
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
VKMemoryCommitImpl::VKMemoryCommitImpl(VKMemoryAllocation* allocation, vk::DeviceMemory memory, |
||||
|
u8* data, u64 begin, u64 end) |
||||
|
: allocation{allocation}, memory{memory}, data{data}, |
||||
|
interval(std::make_pair(begin, begin + end)) {} |
||||
|
|
||||
|
VKMemoryCommitImpl::~VKMemoryCommitImpl() { |
||||
|
allocation->Free(this); |
||||
|
} |
||||
|
|
||||
|
u8* VKMemoryCommitImpl::GetData() const { |
||||
|
ASSERT_MSG(data != nullptr, "Trying to access an unmapped commit."); |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
} // namespace Vulkan
|
||||
@ -0,0 +1,87 @@ |
|||||
|
// Copyright 2019 yuzu Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <memory> |
||||
|
#include <utility> |
||||
|
#include <vector> |
||||
|
#include "common/common_types.h" |
||||
|
#include "video_core/renderer_vulkan/declarations.h" |
||||
|
|
||||
|
namespace Vulkan { |
||||
|
|
||||
|
class VKDevice; |
||||
|
class VKMemoryAllocation; |
||||
|
class VKMemoryCommitImpl; |
||||
|
|
||||
|
using VKMemoryCommit = std::unique_ptr<VKMemoryCommitImpl>; |
||||
|
|
||||
|
class VKMemoryManager final { |
||||
|
public: |
||||
|
explicit VKMemoryManager(const VKDevice& device); |
||||
|
~VKMemoryManager(); |
||||
|
|
||||
|
/** |
||||
|
* Commits a memory with the specified requeriments. |
||||
|
* @param reqs Requeriments returned from a Vulkan call. |
||||
|
* @param host_visible Signals the allocator that it *must* use host visible and coherent |
||||
|
* memory. When passing false, it will try to allocate device local memory. |
||||
|
* @returns A memory commit. |
||||
|
*/ |
||||
|
VKMemoryCommit Commit(const vk::MemoryRequirements& reqs, bool host_visible); |
||||
|
|
||||
|
/// Commits memory required by the buffer and binds it. |
||||
|
VKMemoryCommit Commit(vk::Buffer buffer, bool host_visible); |
||||
|
|
||||
|
/// Commits memory required by the image and binds it. |
||||
|
VKMemoryCommit Commit(vk::Image image, bool host_visible); |
||||
|
|
||||
|
/// Returns true if the memory allocations are done always in host visible and coherent memory. |
||||
|
bool IsMemoryUnified() const { |
||||
|
return is_memory_unified; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
/// Allocates a chunk of memory. |
||||
|
bool AllocMemory(vk::MemoryPropertyFlags wanted_properties, u32 type_mask, u64 size); |
||||
|
|
||||
|
/// Returns true if the device uses an unified memory model. |
||||
|
static bool GetMemoryUnified(const vk::PhysicalDeviceMemoryProperties& props); |
||||
|
|
||||
|
const VKDevice& device; ///< Device handler. |
||||
|
const vk::PhysicalDeviceMemoryProperties props; ///< Physical device properties. |
||||
|
const bool is_memory_unified; ///< True if memory model is unified. |
||||
|
std::vector<std::unique_ptr<VKMemoryAllocation>> allocs; ///< Current allocations. |
||||
|
}; |
||||
|
|
||||
|
class VKMemoryCommitImpl final { |
||||
|
friend VKMemoryAllocation; |
||||
|
|
||||
|
public: |
||||
|
explicit VKMemoryCommitImpl(VKMemoryAllocation* allocation, vk::DeviceMemory memory, u8* data, |
||||
|
u64 begin, u64 end); |
||||
|
~VKMemoryCommitImpl(); |
||||
|
|
||||
|
/// Returns the writeable memory map. The commit has to be mappable. |
||||
|
u8* GetData() const; |
||||
|
|
||||
|
/// Returns the Vulkan memory handler. |
||||
|
vk::DeviceMemory GetMemory() const { |
||||
|
return memory; |
||||
|
} |
||||
|
|
||||
|
/// Returns the start position of the commit relative to the allocation. |
||||
|
vk::DeviceSize GetOffset() const { |
||||
|
return static_cast<vk::DeviceSize>(interval.first); |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
std::pair<u64, u64> interval{}; ///< Interval where the commit exists. |
||||
|
vk::DeviceMemory memory; ///< Vulkan device memory handler. |
||||
|
VKMemoryAllocation* allocation{}; ///< Pointer to the large memory allocation. |
||||
|
u8* data{}; ///< Pointer to the host mapped memory, it has the commit offset included. |
||||
|
}; |
||||
|
|
||||
|
} // namespace Vulkan |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue