Browse Source
Merge pull request #5297 from ReinUsesLisp/vulkan-allocator-common
Merge pull request #5297 from ReinUsesLisp/vulkan-allocator-common
vulkan_memory_allocator: Improvements to the memory allocatornce_cpp
committed by
GitHub
19 changed files with 609 additions and 554 deletions
-
4src/video_core/CMakeLists.txt
-
10src/video_core/renderer_vulkan/renderer_vulkan.cpp
-
4src/video_core/renderer_vulkan/renderer_vulkan.h
-
22src/video_core/renderer_vulkan/vk_blit_screen.cpp
-
10src/video_core/renderer_vulkan/vk_blit_screen.h
-
40src/video_core/renderer_vulkan/vk_buffer_cache.cpp
-
22src/video_core/renderer_vulkan/vk_buffer_cache.h
-
32src/video_core/renderer_vulkan/vk_compute_pass.cpp
-
15src/video_core/renderer_vulkan/vk_compute_pass.h
-
230src/video_core/renderer_vulkan/vk_memory_manager.cpp
-
132src/video_core/renderer_vulkan/vk_memory_manager.h
-
14src/video_core/renderer_vulkan/vk_rasterizer.cpp
-
10src/video_core/renderer_vulkan/vk_rasterizer.h
-
126src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp
-
62src/video_core/renderer_vulkan/vk_staging_buffer_pool.h
-
22src/video_core/renderer_vulkan/vk_texture_cache.cpp
-
22src/video_core/renderer_vulkan/vk_texture_cache.h
-
268src/video_core/vulkan_common/vulkan_memory_allocator.cpp
-
118src/video_core/vulkan_common/vulkan_memory_allocator.h
@ -1,230 +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 <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/vk_memory_manager.h"
|
|||
#include "video_core/vulkan_common/vulkan_device.h"
|
|||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
|||
|
|||
namespace Vulkan { |
|||
|
|||
namespace { |
|||
|
|||
u64 GetAllocationChunkSize(u64 required_size) { |
|||
static constexpr u64 sizes[] = {16ULL << 20, 32ULL << 20, 64ULL << 20, 128ULL << 20}; |
|||
auto it = std::lower_bound(std::begin(sizes), std::end(sizes), required_size); |
|||
return it != std::end(sizes) ? *it : Common::AlignUp(required_size, 256ULL << 20); |
|||
} |
|||
|
|||
} // Anonymous namespace
|
|||
|
|||
class VKMemoryAllocation final { |
|||
public: |
|||
explicit VKMemoryAllocation(const Device& device_, vk::DeviceMemory memory_, |
|||
VkMemoryPropertyFlags properties_, u64 allocation_size_, u32 type_) |
|||
: device{device_}, memory{std::move(memory_)}, properties{properties_}, |
|||
allocation_size{allocation_size_}, shifted_type{ShiftType(type_)} {} |
|||
|
|||
VKMemoryCommit Commit(VkDeviceSize commit_size, VkDeviceSize alignment) { |
|||
auto found = TryFindFreeSection(free_iterator, allocation_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; |
|||
} |
|||
} |
|||
auto commit = std::make_unique<VKMemoryCommitImpl>(device, this, memory, *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(std::begin(commits), std::end(commits), commit); |
|||
if (it == commits.end()) { |
|||
UNREACHABLE_MSG("Freeing unallocated commit!"); |
|||
return; |
|||
} |
|||
commits.erase(it); |
|||
} |
|||
|
|||
/// Returns whether this allocation is compatible with the arguments.
|
|||
bool IsCompatible(VkMemoryPropertyFlags wanted_properties, u32 type_mask) const { |
|||
return (wanted_properties & properties) && (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
|
|||
/// requirements.
|
|||
std::optional<u64> TryFindFreeSection(u64 start, u64 end, u64 size, u64 alignment) const { |
|||
u64 iterator = Common::AlignUp(start, alignment); |
|||
while (iterator + size <= end) { |
|||
const u64 try_left = iterator; |
|||
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 = Common::AlignUp(commit_right, alignment); |
|||
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 Device& device; ///< Vulkan device.
|
|||
const vk::DeviceMemory memory; ///< Vulkan memory allocation handler.
|
|||
const VkMemoryPropertyFlags properties; ///< Vulkan properties.
|
|||
const u64 allocation_size; ///< Size of this allocation.
|
|||
const u32 shifted_type; ///< Stored Vulkan type of this allocation, shifted.
|
|||
|
|||
/// 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 Device& device_) |
|||
: device{device_}, properties{device_.GetPhysical().GetMemoryProperties()} {} |
|||
|
|||
VKMemoryManager::~VKMemoryManager() = default; |
|||
|
|||
VKMemoryCommit VKMemoryManager::Commit(const VkMemoryRequirements& requirements, |
|||
bool host_visible) { |
|||
const u64 chunk_size = GetAllocationChunkSize(requirements.size); |
|||
|
|||
// When a host visible commit is asked, search for host visible and coherent, otherwise search
|
|||
// for a fast device local type.
|
|||
const VkMemoryPropertyFlags wanted_properties = |
|||
host_visible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT |
|||
: VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; |
|||
|
|||
if (auto commit = TryAllocCommit(requirements, wanted_properties)) { |
|||
return commit; |
|||
} |
|||
|
|||
// Commit has failed, allocate more memory.
|
|||
if (!AllocMemory(wanted_properties, requirements.memoryTypeBits, chunk_size)) { |
|||
// TODO(Rodrigo): Handle these situations in some way like flushing to guest memory.
|
|||
// Allocation has failed, panic.
|
|||
UNREACHABLE_MSG("Ran out of VRAM!"); |
|||
return {}; |
|||
} |
|||
|
|||
// Commit again, this time it won't fail since there's a fresh allocation above. If it does,
|
|||
// there's a bug.
|
|||
auto commit = TryAllocCommit(requirements, wanted_properties); |
|||
ASSERT(commit); |
|||
return commit; |
|||
} |
|||
|
|||
VKMemoryCommit VKMemoryManager::Commit(const vk::Buffer& buffer, bool host_visible) { |
|||
auto commit = Commit(device.GetLogical().GetBufferMemoryRequirements(*buffer), host_visible); |
|||
buffer.BindMemory(commit->GetMemory(), commit->GetOffset()); |
|||
return commit; |
|||
} |
|||
|
|||
VKMemoryCommit VKMemoryManager::Commit(const vk::Image& image, bool host_visible) { |
|||
auto commit = Commit(device.GetLogical().GetImageMemoryRequirements(*image), host_visible); |
|||
image.BindMemory(commit->GetMemory(), commit->GetOffset()); |
|||
return commit; |
|||
} |
|||
|
|||
bool VKMemoryManager::AllocMemory(VkMemoryPropertyFlags wanted_properties, u32 type_mask, |
|||
u64 size) { |
|||
const u32 type = [&] { |
|||
for (u32 type_index = 0; type_index < properties.memoryTypeCount; ++type_index) { |
|||
const auto flags = properties.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; |
|||
} |
|||
} |
|||
UNREACHABLE_MSG("Couldn't find a compatible memory type!"); |
|||
return 0U; |
|||
}(); |
|||
|
|||
// Try to allocate found type.
|
|||
vk::DeviceMemory memory = device.GetLogical().TryAllocateMemory({ |
|||
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, |
|||
.pNext = nullptr, |
|||
.allocationSize = size, |
|||
.memoryTypeIndex = type, |
|||
}); |
|||
if (!memory) { |
|||
LOG_CRITICAL(Render_Vulkan, "Device allocation failed!"); |
|||
return false; |
|||
} |
|||
|
|||
allocations.push_back(std::make_unique<VKMemoryAllocation>(device, std::move(memory), |
|||
wanted_properties, size, type)); |
|||
return true; |
|||
} |
|||
|
|||
VKMemoryCommit VKMemoryManager::TryAllocCommit(const VkMemoryRequirements& requirements, |
|||
VkMemoryPropertyFlags wanted_properties) { |
|||
for (auto& allocation : allocations) { |
|||
if (!allocation->IsCompatible(wanted_properties, requirements.memoryTypeBits)) { |
|||
continue; |
|||
} |
|||
if (auto commit = allocation->Commit(requirements.size, requirements.alignment)) { |
|||
return commit; |
|||
} |
|||
} |
|||
return {}; |
|||
} |
|||
|
|||
VKMemoryCommitImpl::VKMemoryCommitImpl(const Device& device_, VKMemoryAllocation* allocation_, |
|||
const vk::DeviceMemory& memory_, u64 begin_, u64 end_) |
|||
: device{device_}, memory{memory_}, interval{begin_, end_}, allocation{allocation_} {} |
|||
|
|||
VKMemoryCommitImpl::~VKMemoryCommitImpl() { |
|||
allocation->Free(this); |
|||
} |
|||
|
|||
MemoryMap VKMemoryCommitImpl::Map(u64 size, u64 offset_) const { |
|||
return MemoryMap(this, std::span<u8>(memory.Map(interval.first + offset_, size), size)); |
|||
} |
|||
|
|||
void VKMemoryCommitImpl::Unmap() const { |
|||
memory.Unmap(); |
|||
} |
|||
|
|||
MemoryMap VKMemoryCommitImpl::Map() const { |
|||
return Map(interval.second - interval.first); |
|||
} |
|||
|
|||
} // namespace Vulkan
|
|||
@ -1,132 +0,0 @@ |
|||
// Copyright 2019 yuzu Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include <memory> |
|||
#include <span> |
|||
#include <utility> |
|||
#include <vector> |
|||
#include "common/common_types.h" |
|||
#include "video_core/vulkan_common/vulkan_wrapper.h" |
|||
|
|||
namespace Vulkan { |
|||
|
|||
class Device; |
|||
class MemoryMap; |
|||
class VKMemoryAllocation; |
|||
class VKMemoryCommitImpl; |
|||
|
|||
using VKMemoryCommit = std::unique_ptr<VKMemoryCommitImpl>; |
|||
|
|||
class VKMemoryManager final { |
|||
public: |
|||
explicit VKMemoryManager(const Device& device_); |
|||
VKMemoryManager(const VKMemoryManager&) = delete; |
|||
~VKMemoryManager(); |
|||
|
|||
/** |
|||
* Commits a memory with the specified requeriments. |
|||
* @param requirements Requirements 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 VkMemoryRequirements& requirements, bool host_visible); |
|||
|
|||
/// Commits memory required by the buffer and binds it. |
|||
VKMemoryCommit Commit(const vk::Buffer& buffer, bool host_visible); |
|||
|
|||
/// Commits memory required by the image and binds it. |
|||
VKMemoryCommit Commit(const vk::Image& image, bool host_visible); |
|||
|
|||
private: |
|||
/// Allocates a chunk of memory. |
|||
bool AllocMemory(VkMemoryPropertyFlags wanted_properties, u32 type_mask, u64 size); |
|||
|
|||
/// Tries to allocate a memory commit. |
|||
VKMemoryCommit TryAllocCommit(const VkMemoryRequirements& requirements, |
|||
VkMemoryPropertyFlags wanted_properties); |
|||
|
|||
const Device& device; ///< Device handler. |
|||
const VkPhysicalDeviceMemoryProperties properties; ///< Physical device properties. |
|||
std::vector<std::unique_ptr<VKMemoryAllocation>> allocations; ///< Current allocations. |
|||
}; |
|||
|
|||
class VKMemoryCommitImpl final { |
|||
friend VKMemoryAllocation; |
|||
friend MemoryMap; |
|||
|
|||
public: |
|||
explicit VKMemoryCommitImpl(const Device& device_, VKMemoryAllocation* allocation_, |
|||
const vk::DeviceMemory& memory_, u64 begin_, u64 end_); |
|||
~VKMemoryCommitImpl(); |
|||
|
|||
/// Maps a memory region and returns a pointer to it. |
|||
/// It's illegal to have more than one memory map at the same time. |
|||
MemoryMap Map(u64 size, u64 offset = 0) const; |
|||
|
|||
/// Maps the whole commit and returns a pointer to it. |
|||
/// It's illegal to have more than one memory map at the same time. |
|||
MemoryMap Map() const; |
|||
|
|||
/// Returns the Vulkan memory handler. |
|||
VkDeviceMemory GetMemory() const { |
|||
return *memory; |
|||
} |
|||
|
|||
/// Returns the start position of the commit relative to the allocation. |
|||
VkDeviceSize GetOffset() const { |
|||
return static_cast<VkDeviceSize>(interval.first); |
|||
} |
|||
|
|||
private: |
|||
/// Unmaps memory. |
|||
void Unmap() const; |
|||
|
|||
const Device& device; ///< Vulkan device. |
|||
const vk::DeviceMemory& memory; ///< Vulkan device memory handler. |
|||
std::pair<u64, u64> interval{}; ///< Interval where the commit exists. |
|||
VKMemoryAllocation* allocation{}; ///< Pointer to the large memory allocation. |
|||
}; |
|||
|
|||
/// Holds ownership of a memory map. |
|||
class MemoryMap final { |
|||
public: |
|||
explicit MemoryMap(const VKMemoryCommitImpl* commit_, std::span<u8> span_) |
|||
: commit{commit_}, span{span_} {} |
|||
|
|||
~MemoryMap() { |
|||
if (commit) { |
|||
commit->Unmap(); |
|||
} |
|||
} |
|||
|
|||
/// Prematurely releases the memory map. |
|||
void Release() { |
|||
commit->Unmap(); |
|||
commit = nullptr; |
|||
} |
|||
|
|||
/// Returns a span to the memory map. |
|||
[[nodiscard]] std::span<u8> Span() const noexcept { |
|||
return span; |
|||
} |
|||
|
|||
/// Returns the address of the memory map. |
|||
[[nodiscard]] u8* Address() const noexcept { |
|||
return span.data(); |
|||
} |
|||
|
|||
/// Returns the address of the memory map; |
|||
[[nodiscard]] operator u8*() const noexcept { |
|||
return span.data(); |
|||
} |
|||
|
|||
private: |
|||
const VKMemoryCommitImpl* commit{}; ///< Mapped memory commit. |
|||
std::span<u8> span; ///< Address to the mapped memory. |
|||
}; |
|||
|
|||
} // namespace Vulkan |
|||
@ -0,0 +1,268 @@ |
|||
// Copyright 2018 yuzu Emulator Project
|
|||
// Licensed under GPLv2 or any later version
|
|||
// Refer to the license.txt file included.
|
|||
|
|||
#include <algorithm>
|
|||
#include <bit>
|
|||
#include <optional>
|
|||
#include <vector>
|
|||
|
|||
#include "common/alignment.h"
|
|||
#include "common/assert.h"
|
|||
#include "common/common_types.h"
|
|||
#include "common/logging/log.h"
|
|||
#include "video_core/vulkan_common/vulkan_device.h"
|
|||
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
|||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
|||
|
|||
namespace Vulkan { |
|||
namespace { |
|||
struct Range { |
|||
u64 begin; |
|||
u64 end; |
|||
|
|||
[[nodiscard]] bool Contains(u64 iterator, u64 size) const noexcept { |
|||
return iterator < end && begin < iterator + size; |
|||
} |
|||
}; |
|||
|
|||
[[nodiscard]] u64 AllocationChunkSize(u64 required_size) { |
|||
static constexpr std::array sizes{ |
|||
0x1000ULL << 10, 0x1400ULL << 10, 0x1800ULL << 10, 0x1c00ULL << 10, 0x2000ULL << 10, |
|||
0x3200ULL << 10, 0x4000ULL << 10, 0x6000ULL << 10, 0x8000ULL << 10, 0xA000ULL << 10, |
|||
0x10000ULL << 10, 0x18000ULL << 10, 0x20000ULL << 10, |
|||
}; |
|||
static_assert(std::is_sorted(sizes.begin(), sizes.end())); |
|||
|
|||
const auto it = std::ranges::lower_bound(sizes, required_size); |
|||
return it != sizes.end() ? *it : Common::AlignUp(required_size, 4ULL << 20); |
|||
} |
|||
|
|||
[[nodiscard]] VkMemoryPropertyFlags MemoryUsagePropertyFlags(MemoryUsage usage) { |
|||
switch (usage) { |
|||
case MemoryUsage::DeviceLocal: |
|||
return VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; |
|||
case MemoryUsage::Upload: |
|||
return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; |
|||
case MemoryUsage::Download: |
|||
return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | |
|||
VK_MEMORY_PROPERTY_HOST_CACHED_BIT; |
|||
} |
|||
UNREACHABLE_MSG("Invalid memory usage={}", usage); |
|||
return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; |
|||
} |
|||
} // Anonymous namespace
|
|||
|
|||
class MemoryAllocation { |
|||
public: |
|||
explicit MemoryAllocation(const Device& device_, vk::DeviceMemory memory_, |
|||
VkMemoryPropertyFlags properties, u64 allocation_size_, u32 type) |
|||
: device{device_}, memory{std::move(memory_)}, allocation_size{allocation_size_}, |
|||
property_flags{properties}, shifted_memory_type{1U << type} {} |
|||
|
|||
[[nodiscard]] std::optional<MemoryCommit> Commit(VkDeviceSize size, VkDeviceSize alignment) { |
|||
const std::optional<u64> alloc = FindFreeRegion(size, alignment); |
|||
if (!alloc) { |
|||
// Signal out of memory, it'll try to do more allocations.
|
|||
return std::nullopt; |
|||
} |
|||
const Range range{ |
|||
.begin = *alloc, |
|||
.end = *alloc + size, |
|||
}; |
|||
commits.insert(std::ranges::upper_bound(commits, *alloc, {}, &Range::begin), range); |
|||
return std::make_optional<MemoryCommit>(this, *memory, *alloc, *alloc + size); |
|||
} |
|||
|
|||
void Free(u64 begin) { |
|||
const auto it = std::ranges::find(commits, begin, &Range::begin); |
|||
ASSERT_MSG(it != commits.end(), "Invalid commit"); |
|||
commits.erase(it); |
|||
} |
|||
|
|||
[[nodiscard]] std::span<u8> Map() { |
|||
if (memory_mapped_span.empty()) { |
|||
u8* const raw_pointer = memory.Map(0, allocation_size); |
|||
memory_mapped_span = std::span<u8>(raw_pointer, allocation_size); |
|||
} |
|||
return memory_mapped_span; |
|||
} |
|||
|
|||
/// Returns whether this allocation is compatible with the arguments.
|
|||
[[nodiscard]] bool IsCompatible(VkMemoryPropertyFlags flags, u32 type_mask) const { |
|||
return (flags & property_flags) && (type_mask & shifted_memory_type) != 0; |
|||
} |
|||
|
|||
private: |
|||
[[nodiscard]] static constexpr u32 ShiftType(u32 type) { |
|||
return 1U << type; |
|||
} |
|||
|
|||
[[nodiscard]] std::optional<u64> FindFreeRegion(u64 size, u64 alignment) noexcept { |
|||
ASSERT(std::has_single_bit(alignment)); |
|||
const u64 alignment_log2 = std::countr_zero(alignment); |
|||
std::optional<u64> candidate; |
|||
u64 iterator = 0; |
|||
auto commit = commits.begin(); |
|||
while (iterator + size <= allocation_size) { |
|||
candidate = candidate.value_or(iterator); |
|||
if (commit == commits.end()) { |
|||
break; |
|||
} |
|||
if (commit->Contains(*candidate, size)) { |
|||
candidate = std::nullopt; |
|||
} |
|||
iterator = Common::AlignUpLog2(commit->end, alignment_log2); |
|||
++commit; |
|||
} |
|||
return candidate; |
|||
} |
|||
|
|||
const Device& device; ///< Vulkan device.
|
|||
const vk::DeviceMemory memory; ///< Vulkan memory allocation handler.
|
|||
const u64 allocation_size; ///< Size of this allocation.
|
|||
const VkMemoryPropertyFlags property_flags; ///< Vulkan memory property flags.
|
|||
const u32 shifted_memory_type; ///< Shifted Vulkan memory type.
|
|||
std::vector<Range> commits; ///< All commit ranges done from this allocation.
|
|||
std::span<u8> memory_mapped_span; ///< Memory mapped span. Empty if not queried before.
|
|||
}; |
|||
|
|||
MemoryCommit::MemoryCommit(MemoryAllocation* allocation_, VkDeviceMemory memory_, u64 begin_, |
|||
u64 end_) noexcept |
|||
: allocation{allocation_}, memory{memory_}, begin{begin_}, end{end_} {} |
|||
|
|||
MemoryCommit::~MemoryCommit() { |
|||
Release(); |
|||
} |
|||
|
|||
MemoryCommit& MemoryCommit::operator=(MemoryCommit&& rhs) noexcept { |
|||
Release(); |
|||
allocation = std::exchange(rhs.allocation, nullptr); |
|||
memory = rhs.memory; |
|||
begin = rhs.begin; |
|||
end = rhs.end; |
|||
span = std::exchange(rhs.span, std::span<u8>{}); |
|||
return *this; |
|||
} |
|||
|
|||
MemoryCommit::MemoryCommit(MemoryCommit&& rhs) noexcept |
|||
: allocation{std::exchange(rhs.allocation, nullptr)}, memory{rhs.memory}, begin{rhs.begin}, |
|||
end{rhs.end}, span{std::exchange(rhs.span, std::span<u8>{})} {} |
|||
|
|||
std::span<u8> MemoryCommit::Map() { |
|||
if (span.empty()) { |
|||
span = allocation->Map().subspan(begin, end - begin); |
|||
} |
|||
return span; |
|||
} |
|||
|
|||
void MemoryCommit::Release() { |
|||
if (allocation) { |
|||
allocation->Free(begin); |
|||
} |
|||
} |
|||
|
|||
MemoryAllocator::MemoryAllocator(const Device& device_) |
|||
: device{device_}, properties{device_.GetPhysical().GetMemoryProperties()} {} |
|||
|
|||
MemoryAllocator::~MemoryAllocator() = default; |
|||
|
|||
MemoryCommit MemoryAllocator::Commit(const VkMemoryRequirements& requirements, MemoryUsage usage) { |
|||
// Find the fastest memory flags we can afford with the current requirements
|
|||
const VkMemoryPropertyFlags flags = MemoryPropertyFlags(requirements.memoryTypeBits, usage); |
|||
if (std::optional<MemoryCommit> commit = TryCommit(requirements, flags)) { |
|||
return std::move(*commit); |
|||
} |
|||
// Commit has failed, allocate more memory.
|
|||
// TODO(Rodrigo): Handle out of memory situations in some way like flushing to guest memory.
|
|||
AllocMemory(flags, requirements.memoryTypeBits, AllocationChunkSize(requirements.size)); |
|||
|
|||
// Commit again, this time it won't fail since there's a fresh allocation above.
|
|||
// If it does, there's a bug.
|
|||
return TryCommit(requirements, flags).value(); |
|||
} |
|||
|
|||
MemoryCommit MemoryAllocator::Commit(const vk::Buffer& buffer, MemoryUsage usage) { |
|||
auto commit = Commit(device.GetLogical().GetBufferMemoryRequirements(*buffer), usage); |
|||
buffer.BindMemory(commit.Memory(), commit.Offset()); |
|||
return commit; |
|||
} |
|||
|
|||
MemoryCommit MemoryAllocator::Commit(const vk::Image& image, MemoryUsage usage) { |
|||
auto commit = Commit(device.GetLogical().GetImageMemoryRequirements(*image), usage); |
|||
image.BindMemory(commit.Memory(), commit.Offset()); |
|||
return commit; |
|||
} |
|||
|
|||
void MemoryAllocator::AllocMemory(VkMemoryPropertyFlags flags, u32 type_mask, u64 size) { |
|||
const u32 type = FindType(flags, type_mask).value(); |
|||
vk::DeviceMemory memory = device.GetLogical().AllocateMemory({ |
|||
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, |
|||
.pNext = nullptr, |
|||
.allocationSize = size, |
|||
.memoryTypeIndex = type, |
|||
}); |
|||
allocations.push_back( |
|||
std::make_unique<MemoryAllocation>(device, std::move(memory), flags, size, type)); |
|||
} |
|||
|
|||
std::optional<MemoryCommit> MemoryAllocator::TryCommit(const VkMemoryRequirements& requirements, |
|||
VkMemoryPropertyFlags flags) { |
|||
for (auto& allocation : allocations) { |
|||
if (!allocation->IsCompatible(flags, requirements.memoryTypeBits)) { |
|||
continue; |
|||
} |
|||
if (auto commit = allocation->Commit(requirements.size, requirements.alignment)) { |
|||
return commit; |
|||
} |
|||
} |
|||
return std::nullopt; |
|||
} |
|||
|
|||
VkMemoryPropertyFlags MemoryAllocator::MemoryPropertyFlags(u32 type_mask, MemoryUsage usage) const { |
|||
return MemoryPropertyFlags(type_mask, MemoryUsagePropertyFlags(usage)); |
|||
} |
|||
|
|||
VkMemoryPropertyFlags MemoryAllocator::MemoryPropertyFlags(u32 type_mask, |
|||
VkMemoryPropertyFlags flags) const { |
|||
if (FindType(flags, type_mask)) { |
|||
// Found a memory type with those requirements
|
|||
return flags; |
|||
} |
|||
if (flags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) { |
|||
// Remove host cached bit in case it's not supported
|
|||
return MemoryPropertyFlags(type_mask, flags & ~VK_MEMORY_PROPERTY_HOST_CACHED_BIT); |
|||
} |
|||
if (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) { |
|||
// Remove device local, if it's not supported by the requested resource
|
|||
return MemoryPropertyFlags(type_mask, flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); |
|||
} |
|||
UNREACHABLE_MSG("No compatible memory types found"); |
|||
return 0; |
|||
} |
|||
|
|||
std::optional<u32> MemoryAllocator::FindType(VkMemoryPropertyFlags flags, u32 type_mask) const { |
|||
for (u32 type_index = 0; type_index < properties.memoryTypeCount; ++type_index) { |
|||
const VkMemoryPropertyFlags type_flags = properties.memoryTypes[type_index].propertyFlags; |
|||
if ((type_mask & (1U << type_index)) && (type_flags & flags)) { |
|||
// The type matches in type and in the wanted properties.
|
|||
return type_index; |
|||
} |
|||
} |
|||
// Failed to find index
|
|||
return std::nullopt; |
|||
} |
|||
|
|||
bool IsHostVisible(MemoryUsage usage) noexcept { |
|||
switch (usage) { |
|||
case MemoryUsage::DeviceLocal: |
|||
return false; |
|||
case MemoryUsage::Upload: |
|||
case MemoryUsage::Download: |
|||
return true; |
|||
} |
|||
UNREACHABLE_MSG("Invalid memory usage={}", usage); |
|||
return false; |
|||
} |
|||
|
|||
} // namespace Vulkan
|
|||
@ -0,0 +1,118 @@ |
|||
// Copyright 2019 yuzu Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include <memory> |
|||
#include <span> |
|||
#include <utility> |
|||
#include <vector> |
|||
#include "common/common_types.h" |
|||
#include "video_core/vulkan_common/vulkan_wrapper.h" |
|||
|
|||
namespace Vulkan { |
|||
|
|||
class Device; |
|||
class MemoryMap; |
|||
class MemoryAllocation; |
|||
|
|||
/// Hints and requirements for the backing memory type of a commit |
|||
enum class MemoryUsage { |
|||
DeviceLocal, ///< Hints device local usages, fastest memory type to read and write from the GPU |
|||
Upload, ///< Requires a host visible memory type optimized for CPU to GPU uploads |
|||
Download, ///< Requires a host visible memory type optimized for GPU to CPU readbacks |
|||
}; |
|||
|
|||
/// Ownership handle of a memory commitment. |
|||
/// Points to a subregion of a memory allocation. |
|||
class MemoryCommit { |
|||
public: |
|||
explicit MemoryCommit() noexcept = default; |
|||
explicit MemoryCommit(MemoryAllocation* allocation_, VkDeviceMemory memory_, u64 begin_, |
|||
u64 end_) noexcept; |
|||
~MemoryCommit(); |
|||
|
|||
MemoryCommit& operator=(MemoryCommit&&) noexcept; |
|||
MemoryCommit(MemoryCommit&&) noexcept; |
|||
|
|||
MemoryCommit& operator=(const MemoryCommit&) = delete; |
|||
MemoryCommit(const MemoryCommit&) = delete; |
|||
|
|||
/// Returns a host visible memory map. |
|||
/// It will map the backing allocation if it hasn't been mapped before. |
|||
std::span<u8> Map(); |
|||
|
|||
/// Returns the Vulkan memory handler. |
|||
VkDeviceMemory Memory() const { |
|||
return memory; |
|||
} |
|||
|
|||
/// Returns the start position of the commit relative to the allocation. |
|||
VkDeviceSize Offset() const { |
|||
return static_cast<VkDeviceSize>(begin); |
|||
} |
|||
|
|||
private: |
|||
void Release(); |
|||
|
|||
MemoryAllocation* allocation{}; ///< Pointer to the large memory allocation. |
|||
VkDeviceMemory memory{}; ///< Vulkan device memory handler. |
|||
u64 begin{}; ///< Beginning offset in bytes to where the commit exists. |
|||
u64 end{}; ///< Offset in bytes where the commit ends. |
|||
std::span<u8> span; ///< Host visible memory span. Empty if not queried before. |
|||
}; |
|||
|
|||
/// Memory allocator container. |
|||
/// Allocates and releases memory allocations on demand. |
|||
class MemoryAllocator { |
|||
public: |
|||
explicit MemoryAllocator(const Device& device_); |
|||
~MemoryAllocator(); |
|||
|
|||
MemoryAllocator& operator=(const MemoryAllocator&) = delete; |
|||
MemoryAllocator(const MemoryAllocator&) = delete; |
|||
|
|||
/** |
|||
* Commits a memory with the specified requeriments. |
|||
* |
|||
* @param requirements Requirements 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. |
|||
*/ |
|||
MemoryCommit Commit(const VkMemoryRequirements& requirements, MemoryUsage usage); |
|||
|
|||
/// Commits memory required by the buffer and binds it. |
|||
MemoryCommit Commit(const vk::Buffer& buffer, MemoryUsage usage); |
|||
|
|||
/// Commits memory required by the image and binds it. |
|||
MemoryCommit Commit(const vk::Image& image, MemoryUsage usage); |
|||
|
|||
private: |
|||
/// Allocates a chunk of memory. |
|||
void AllocMemory(VkMemoryPropertyFlags flags, u32 type_mask, u64 size); |
|||
|
|||
/// Tries to allocate a memory commit. |
|||
std::optional<MemoryCommit> TryCommit(const VkMemoryRequirements& requirements, |
|||
VkMemoryPropertyFlags flags); |
|||
|
|||
/// Returns the fastest compatible memory property flags from a wanted usage. |
|||
VkMemoryPropertyFlags MemoryPropertyFlags(u32 type_mask, MemoryUsage usage) const; |
|||
|
|||
/// Returns the fastest compatible memory property flags from the wanted flags. |
|||
VkMemoryPropertyFlags MemoryPropertyFlags(u32 type_mask, VkMemoryPropertyFlags flags) const; |
|||
|
|||
/// Returns index to the fastest memory type compatible with the passed requirements. |
|||
std::optional<u32> FindType(VkMemoryPropertyFlags flags, u32 type_mask) const; |
|||
|
|||
const Device& device; ///< Device handle. |
|||
const VkPhysicalDeviceMemoryProperties properties; ///< Physical device properties. |
|||
std::vector<std::unique_ptr<MemoryAllocation>> allocations; ///< Current allocations. |
|||
}; |
|||
|
|||
/// Returns true when a memory usage is guaranteed to be host visible. |
|||
bool IsHostVisible(MemoryUsage usage) noexcept; |
|||
|
|||
} // namespace Vulkan |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue