| // Copyright 2019 The Dawn Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "dawn_native/vulkan/ResourceMemoryAllocatorVk.h" |
| |
| #include "common/Math.h" |
| #include "dawn_native/BuddyMemoryAllocator.h" |
| #include "dawn_native/ResourceHeapAllocator.h" |
| #include "dawn_native/vulkan/DeviceVk.h" |
| #include "dawn_native/vulkan/FencedDeleter.h" |
| #include "dawn_native/vulkan/ResourceHeapVk.h" |
| #include "dawn_native/vulkan/VulkanError.h" |
| |
| namespace dawn_native::vulkan { |
| |
| namespace { |
| |
| // TODO(crbug.com/dawn/849): This is a hardcoded heurstic to choose when to |
| // suballocate but it should ideally depend on the size of the memory heaps and other |
| // factors. |
| constexpr uint64_t kMaxSizeForSubAllocation = 4ull * 1024ull * 1024ull; // 4MiB |
| |
| // Have each bucket of the buddy system allocate at least some resource of the maximum |
| // size |
| constexpr uint64_t kBuddyHeapsSize = 2 * kMaxSizeForSubAllocation; |
| |
| } // anonymous namespace |
| |
| // SingleTypeAllocator is a combination of a BuddyMemoryAllocator and its client and can |
| // service suballocation requests, but for a single Vulkan memory type. |
| |
| class ResourceMemoryAllocator::SingleTypeAllocator : public ResourceHeapAllocator { |
| public: |
| SingleTypeAllocator(Device* device, size_t memoryTypeIndex, VkDeviceSize memoryHeapSize) |
| : mDevice(device), |
| mMemoryTypeIndex(memoryTypeIndex), |
| mMemoryHeapSize(memoryHeapSize), |
| mPooledMemoryAllocator(this), |
| mBuddySystem( |
| // Round down to a power of 2 that's <= mMemoryHeapSize. This will always |
| // be a multiple of kBuddyHeapsSize because kBuddyHeapsSize is a power of 2. |
| uint64_t(1) << Log2(mMemoryHeapSize), |
| // Take the min in the very unlikely case the memory heap is tiny. |
| std::min(uint64_t(1) << Log2(mMemoryHeapSize), kBuddyHeapsSize), |
| &mPooledMemoryAllocator) { |
| ASSERT(IsPowerOfTwo(kBuddyHeapsSize)); |
| } |
| ~SingleTypeAllocator() override = default; |
| |
| void DestroyPool() { |
| mPooledMemoryAllocator.DestroyPool(); |
| } |
| |
| ResultOrError<ResourceMemoryAllocation> AllocateMemory(uint64_t size, uint64_t alignment) { |
| return mBuddySystem.Allocate(size, alignment); |
| } |
| |
| void DeallocateMemory(const ResourceMemoryAllocation& allocation) { |
| mBuddySystem.Deallocate(allocation); |
| } |
| |
| // Implementation of the MemoryAllocator interface to be a client of BuddyMemoryAllocator |
| |
| ResultOrError<std::unique_ptr<ResourceHeapBase>> AllocateResourceHeap( |
| uint64_t size) override { |
| if (size > mMemoryHeapSize) { |
| return DAWN_OUT_OF_MEMORY_ERROR("Allocation size too large"); |
| } |
| |
| VkMemoryAllocateInfo allocateInfo; |
| allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; |
| allocateInfo.pNext = nullptr; |
| allocateInfo.allocationSize = size; |
| allocateInfo.memoryTypeIndex = mMemoryTypeIndex; |
| |
| VkDeviceMemory allocatedMemory = VK_NULL_HANDLE; |
| |
| // First check OOM that we want to surface to the application. |
| DAWN_TRY(CheckVkOOMThenSuccess( |
| mDevice->fn.AllocateMemory(mDevice->GetVkDevice(), &allocateInfo, nullptr, |
| &*allocatedMemory), |
| "vkAllocateMemory")); |
| |
| ASSERT(allocatedMemory != VK_NULL_HANDLE); |
| return {std::make_unique<ResourceHeap>(allocatedMemory, mMemoryTypeIndex)}; |
| } |
| |
| void DeallocateResourceHeap(std::unique_ptr<ResourceHeapBase> allocation) override { |
| mDevice->GetFencedDeleter()->DeleteWhenUnused(ToBackend(allocation.get())->GetMemory()); |
| } |
| |
| private: |
| Device* mDevice; |
| size_t mMemoryTypeIndex; |
| VkDeviceSize mMemoryHeapSize; |
| PooledResourceMemoryAllocator mPooledMemoryAllocator; |
| BuddyMemoryAllocator mBuddySystem; |
| }; |
| |
| // Implementation of ResourceMemoryAllocator |
| |
| ResourceMemoryAllocator::ResourceMemoryAllocator(Device* device) : mDevice(device) { |
| const VulkanDeviceInfo& info = mDevice->GetDeviceInfo(); |
| mAllocatorsPerType.reserve(info.memoryTypes.size()); |
| |
| for (size_t i = 0; i < info.memoryTypes.size(); i++) { |
| mAllocatorsPerType.emplace_back(std::make_unique<SingleTypeAllocator>( |
| mDevice, i, info.memoryHeaps[info.memoryTypes[i].heapIndex].size)); |
| } |
| } |
| |
| ResourceMemoryAllocator::~ResourceMemoryAllocator() = default; |
| |
| ResultOrError<ResourceMemoryAllocation> ResourceMemoryAllocator::Allocate( |
| const VkMemoryRequirements& requirements, |
| MemoryKind kind) { |
| // The Vulkan spec guarantees at least on memory type is valid. |
| int memoryType = FindBestTypeIndex(requirements, kind); |
| ASSERT(memoryType >= 0); |
| |
| VkDeviceSize size = requirements.size; |
| |
| // Sub-allocate non-mappable resources because at the moment the mapped pointer |
| // is part of the resource and not the heap, which doesn't match the Vulkan model. |
| // TODO(crbug.com/dawn/849): allow sub-allocating mappable resources, maybe. |
| if (requirements.size < kMaxSizeForSubAllocation && kind != MemoryKind::LinearMappable) { |
| // When sub-allocating, Vulkan requires that we respect bufferImageGranularity. Some |
| // hardware puts information on the memory's page table entry and allocating a linear |
| // resource in the same page as a non-linear (aka opaque) resource can cause issues. |
| // Probably because some texture compression flags are stored on the page table entry, |
| // and allocating a linear resource removes these flags. |
| // |
| // Anyway, just to be safe we ask that all sub-allocated resources are allocated with at |
| // least this alignment. TODO(crbug.com/dawn/849): this is suboptimal because multiple |
| // linear (resp. opaque) resources can coexist in the same page. In particular Nvidia |
| // GPUs often use a granularity of 64k which will lead to a lot of wasted spec. Revisit |
| // with a more efficient algorithm later. |
| uint64_t alignment = |
| std::max(requirements.alignment, |
| mDevice->GetDeviceInfo().properties.limits.bufferImageGranularity); |
| |
| ResourceMemoryAllocation subAllocation; |
| DAWN_TRY_ASSIGN(subAllocation, mAllocatorsPerType[memoryType]->AllocateMemory( |
| requirements.size, alignment)); |
| if (subAllocation.GetInfo().mMethod != AllocationMethod::kInvalid) { |
| return std::move(subAllocation); |
| } |
| } |
| |
| // If sub-allocation failed, allocate memory just for it. |
| std::unique_ptr<ResourceHeapBase> resourceHeap; |
| DAWN_TRY_ASSIGN(resourceHeap, mAllocatorsPerType[memoryType]->AllocateResourceHeap(size)); |
| |
| void* mappedPointer = nullptr; |
| if (kind == MemoryKind::LinearMappable) { |
| DAWN_TRY_WITH_CLEANUP( |
| CheckVkSuccess(mDevice->fn.MapMemory(mDevice->GetVkDevice(), |
| ToBackend(resourceHeap.get())->GetMemory(), 0, |
| size, 0, &mappedPointer), |
| "vkMapMemory"), |
| { |
| mAllocatorsPerType[memoryType]->DeallocateResourceHeap(std::move(resourceHeap)); |
| }); |
| } |
| |
| AllocationInfo info; |
| info.mMethod = AllocationMethod::kDirect; |
| return ResourceMemoryAllocation(info, /*offset*/ 0, resourceHeap.release(), |
| static_cast<uint8_t*>(mappedPointer)); |
| } |
| |
| void ResourceMemoryAllocator::Deallocate(ResourceMemoryAllocation* allocation) { |
| switch (allocation->GetInfo().mMethod) { |
| // Some memory allocation can never be initialized, for example when wrapping |
| // swapchain VkImages with a Texture. |
| case AllocationMethod::kInvalid: |
| break; |
| |
| // For direct allocation we can put the memory for deletion immediately and the fence |
| // deleter will make sure the resources are freed before the memory. |
| case AllocationMethod::kDirect: { |
| ResourceHeap* heap = ToBackend(allocation->GetResourceHeap()); |
| allocation->Invalidate(); |
| mDevice->GetFencedDeleter()->DeleteWhenUnused(heap->GetMemory()); |
| delete heap; |
| break; |
| } |
| |
| // Suballocations aren't freed immediately, otherwise another resource allocation could |
| // happen just after that aliases the old one and would require a barrier. |
| // TODO(crbug.com/dawn/851): Maybe we can produce the correct barriers to reduce the |
| // latency to reclaim memory. |
| case AllocationMethod::kSubAllocated: |
| mSubAllocationsToDelete.Enqueue(*allocation, mDevice->GetPendingCommandSerial()); |
| break; |
| |
| default: |
| UNREACHABLE(); |
| break; |
| } |
| |
| // Invalidate the underlying resource heap in case the client accidentally |
| // calls DeallocateMemory again using the same allocation. |
| allocation->Invalidate(); |
| } |
| |
| void ResourceMemoryAllocator::Tick(ExecutionSerial completedSerial) { |
| for (const ResourceMemoryAllocation& allocation : |
| mSubAllocationsToDelete.IterateUpTo(completedSerial)) { |
| ASSERT(allocation.GetInfo().mMethod == AllocationMethod::kSubAllocated); |
| size_t memoryType = ToBackend(allocation.GetResourceHeap())->GetMemoryType(); |
| |
| mAllocatorsPerType[memoryType]->DeallocateMemory(allocation); |
| } |
| |
| mSubAllocationsToDelete.ClearUpTo(completedSerial); |
| } |
| |
| int ResourceMemoryAllocator::FindBestTypeIndex(VkMemoryRequirements requirements, |
| MemoryKind kind) { |
| const VulkanDeviceInfo& info = mDevice->GetDeviceInfo(); |
| bool mappable = kind == MemoryKind::LinearMappable; |
| |
| // Find a suitable memory type for this allocation |
| int bestType = -1; |
| for (size_t i = 0; i < info.memoryTypes.size(); ++i) { |
| // Resource must support this memory type |
| if ((requirements.memoryTypeBits & (1 << i)) == 0) { |
| continue; |
| } |
| |
| // Mappable resource must be host visible |
| if (mappable && |
| (info.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) { |
| continue; |
| } |
| |
| // Mappable must also be host coherent. |
| if (mappable && |
| (info.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0) { |
| continue; |
| } |
| |
| // Found the first candidate memory type |
| if (bestType == -1) { |
| bestType = static_cast<int>(i); |
| continue; |
| } |
| |
| // For non-mappable resources, favor device local memory. |
| bool currentDeviceLocal = |
| info.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; |
| bool bestDeviceLocal = |
| info.memoryTypes[bestType].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; |
| if (!mappable && (currentDeviceLocal != bestDeviceLocal)) { |
| if (currentDeviceLocal) { |
| bestType = static_cast<int>(i); |
| } |
| continue; |
| } |
| |
| // All things equal favor the memory in the biggest heap |
| VkDeviceSize bestTypeHeapSize = |
| info.memoryHeaps[info.memoryTypes[bestType].heapIndex].size; |
| VkDeviceSize candidateHeapSize = info.memoryHeaps[info.memoryTypes[i].heapIndex].size; |
| if (candidateHeapSize > bestTypeHeapSize) { |
| bestType = static_cast<int>(i); |
| continue; |
| } |
| } |
| |
| return bestType; |
| } |
| |
| void ResourceMemoryAllocator::DestroyPool() { |
| for (auto& alloc : mAllocatorsPerType) { |
| alloc->DestroyPool(); |
| } |
| } |
| |
| } // namespace dawn_native::vulkan |