Files
LibretroAdmin 5274546c9d vulkan: memory a smaller render size no longer needs goes back to the driver
The heap takes device memory in 64 MB blocks and gave an empty block
back only when an allocation found no room anywhere. Turn the internal
resolution up and down again and the blocks the large images were in
stayed with the core for the rest of the session, empty.

Thirty-two frames after the renderer changes size - by when the frames
that used the old images are long done with, and the images freed (two
frames, with Mesa's software Vulkan) - the blocks that are empty are
given back, all but the last of each kind (memory type; linear or not),
so that the next allocation of every kind is still an offset and not a
trip to the driver. vk_heap_trim_spare() is the new call; once, not every
frame: a block that comes empty in ordinary running is left alone.

Run in RetroArch on Mesa's software Vulkan with the test disc, the render
size taken 1x, 4x, 1x, 4x, 1x, 2x by a line of test code since removed:

  per-triangle  192 MB reserved at 4x; back at 1x, 14 MB in use and 192
                reserved before, "64 MB given back, 128 MB reserved" now,
                both times
  per-pixel     384 MB at 4x; back at 1x 320 MB, both times (its pixel
                buffer stays at the size it grew to)

and the picture at the end is the same file with the trim and without, in
both renderers.

tools/vkheap: the three empty blocks beside one in use go back; with
none in use one of each kind stays, and the next allocation does not ask
the driver.

(Throughput item 4 of the audit, the part of it that can be seen here.
Its budgets per memory heap and a smaller first reservation - 128 MB are
taken at the start for the 14 to 22 MB this disc uses - are not in this.)
2026-10-09 07:16:39 +02:00

145 lines
5.4 KiB
C++

/*
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "heap.h"
#include "vulkan_context.h"
static const VkMemoryPropertyFlags HOST_MEMORY =
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
void VulkanHeap::Init(vk::PhysicalDevice physicalDevice, vk::Device device)
{
verify(!ready);
const vk::PhysicalDeviceMemoryProperties props = physicalDevice.getMemoryProperties();
/* One block is 64 MB, or an eighth of the device's memory where that
* is less. */
VkDeviceSize largest = 0;
for (u32 i = 0; i < props.memoryHeapCount; i++)
if ((VkDeviceSize)props.memoryHeaps[i].size > largest)
largest = props.memoryHeaps[i].size;
VkDeviceSize blockSize = 64u * 1024u * 1024u;
if (largest / 8 < blockSize)
blockSize = std::max<VkDeviceSize>((largest / 8) & ~(VkDeviceSize)0xFFFFF, 4u * 1024u * 1024u);
vk_heap_fns_t fns;
fns.allocate_memory = vkAllocateMemory;
fns.free_memory = vkFreeMemory;
fns.map_memory = vkMapMemory;
fns.unmap_memory = vkUnmapMemory;
if (!vk_heap_init(&heap, (VkDevice)device, (const VkPhysicalDeviceMemoryProperties *)&props, &fns, blockSize, 0))
throwResultException(vk::Result::eErrorInitializationFailed, "vk_heap_init failed");
this->device = device;
ready = true;
trimIn = 0;
/* The first block for textures and render targets and the first one
* for the buffers the CPU fills are taken now, so that a frame gets
* its memory as an offset and not from the driver. */
vk_heap_reserve(&heap, ~0u, 0, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0, 1);
vk_heap_reserve(&heap, ~0u, HOST_MEMORY, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 1, 1);
NOTICE_LOG(RENDERER, "Vulkan heap: %u MB blocks, %u MB reserved", (u32)(blockSize >> 20), (u32)(heap.bytes_reserved >> 20));
}
void VulkanHeap::Term()
{
if (ready)
{
vk_heap_shutdown(&heap);
ready = false;
}
}
/* After the renderer has changed size: blocks that what it had before
* left empty go back to the driver, all but the last of each kind. */
void VulkanHeap::TrimSpare()
{
if (!ready)
return;
const VkDeviceSize before = heap.bytes_reserved;
if (vk_heap_trim_spare(&heap) != 0)
NOTICE_LOG(RENDERER, "Vulkan heap: %u MB given back, %u MB reserved", (u32)((before - heap.bytes_reserved) >> 20),
(u32)(heap.bytes_reserved >> 20));
}
/* Why the heap had nothing to give: what the driver said - no memory of
* the kind, or memory it would not map - where there is that to go by. */
vk::Result VulkanHeap::AllocError() const
{
return heap.last_error != VK_SUCCESS ? (vk::Result)heap.last_error : vk::Result::eErrorOutOfDeviceMemory;
}
Allocation VulkanHeap::Allocate(const vk::MemoryRequirements& requirements, VkMemoryPropertyFlags required,
VkMemoryPropertyFlags preferred, bool linear)
{
vk_heap_alloc_t alloc;
if (!vk_heap_alloc(&heap, (const VkMemoryRequirements *)&requirements, required, preferred, linear, &alloc))
throwResultException(AllocError(), "vk_heap_alloc failed");
return Allocation(&heap, alloc);
}
Allocation VulkanHeap::AllocateForImage(vk::Image image, bool linear, bool hostVisible, bool transient)
{
const vk::MemoryRequirements requirements = device.getImageMemoryRequirements(image);
vk_heap_alloc_t alloc;
if (hostVisible)
{
if (!vk_heap_alloc(&heap, (const VkMemoryRequirements *)&requirements, HOST_MEMORY,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, linear, &alloc))
throwResultException(AllocError(), "vk_heap_alloc failed");
}
/* A transient attachment takes memory the device only backs if it has
* to, where there is such a thing. */
else if (!transient
|| !vk_heap_alloc(&heap, (const VkMemoryRequirements *)&requirements,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT, 0, linear, &alloc))
{
if (!vk_heap_alloc(&heap, (const VkMemoryRequirements *)&requirements, 0,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, linear, &alloc))
throwResultException(AllocError(), "vk_heap_alloc failed");
}
Allocation allocation(&heap, alloc);
device.bindImageMemory(image, vk::DeviceMemory(alloc.memory), alloc.offset);
return allocation;
}
Allocation VulkanHeap::AllocateForBuffer(vk::Buffer buffer, bool hostVisible, bool readBack)
{
const vk::MemoryRequirements requirements = device.getBufferMemoryRequirements(buffer);
VkMemoryPropertyFlags required = 0;
VkMemoryPropertyFlags preferred = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
if (hostVisible)
{
required = HOST_MEMORY;
/* What the CPU reads back wants the host's cache between it and
* the memory; device memory seen through the bus is write-combined
* and reading it is slow. What the CPU only writes goes as close
* to the device as there is. */
if (readBack)
preferred = VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
}
Allocation allocation = Allocate(requirements, required, preferred, true);
device.bindBufferMemory(buffer, vk::DeviceMemory(allocation.alloc.memory), allocation.alloc.offset);
return allocation;
}