feat: 显存分配器迁移到 VMA(P1 路线第一项)

- vendored VulkanMemoryAllocator 子模块,唯一实现编译单元 VmaImpl.cpp
- MemoryAllocator 重写为 VMA 后端:接口从"Allocate(reqs)+手绑"升级为
  "创建即分配即绑定"(CreateBuffer/CreateImage/Destroy/Flush),
  VMA 要求资源与内存成对释放,约束成单一出口
- Buffer/Texture/RenderGraph 三处调用方迁移;staging 持久映射 + 按分配
  精确 flush(原来按整块 flush)
- 获得:TLSF 通用分配、DedicatedAllocation 自动启用、memory budget 感知、
  内存别名基础(TAA/复用前置)
- 编译零错误、validation 零警告、8 秒冒烟通过
This commit is contained in:
2026-09-23 12:51:11 +08:00
parent 6f10fd624d
commit 0de8498350
12 changed files with 200 additions and 295 deletions
+6 -9
View File
@@ -45,8 +45,7 @@ namespace Engine::RG {
for (auto& img : images) {
if (!img->valid) continue;
vkDevice.destroyImageView(img->view);
vkDevice.destroyImage(img->image);
m_Allocator.Free(img->allocation);
m_Allocator.Destroy(img->allocation); // image 资源与显存成对释放
}
}
m_Pool.clear();
@@ -530,8 +529,7 @@ namespace Engine::RG {
for (auto& img : it->second) {
if (!img->valid) continue;
vkDevice.destroyImageView(img->view);
vkDevice.destroyImage(img->image);
m_Allocator.Free(img->allocation);
m_Allocator.Destroy(img->allocation);
}
it = m_Pool.erase(it);
} else {
@@ -576,11 +574,10 @@ namespace Engine::RG {
.setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined);
img->image = vkDevice.createImage(imageInfo);
auto reqs = vkDevice.getImageMemoryRequirements(img->image);
img->allocation = m_Allocator.Allocate(reqs, vk::MemoryPropertyFlagBits::eDeviceLocal);
vkDevice.bindImageMemory(img->image, img->allocation.Memory, img->allocation.Offset);
// VMA:创建 + 分配 + 绑定一步完成
img->allocation = m_Allocator.CreateImage(imageInfo,
vk::MemoryPropertyFlagBits::eDeviceLocal);
img->image = img->allocation.Image;
vk::ImageViewCreateInfo viewInfo;
viewInfo.setImage(img->image)
+1 -1
View File
@@ -367,7 +367,7 @@ namespace Engine
.setWaitSemaphores(presentWait);
try {
auto result = m_Device->GetPresentQueue().presentKHR(presentInfo);
const auto result = m_Device->GetPresentQueue().presentKHR(presentInfo);
if (result == vk::Result::eSuboptimalKHR) {
HandleResize();
}
+9 -27
View File
@@ -18,31 +18,18 @@ namespace Engine {
vk::MemoryPropertyFlags memProps)
: m_Device(device), m_Allocator(allocator) , m_Size(size)
{
vk::BufferCreateInfo bufferInfo;
bufferInfo.setSize(size)
.setUsage(usage)
.setSharingMode(vk::SharingMode::eExclusive);
m_Buffer = m_Device.GetDevice().createBuffer(bufferInfo);
auto reqs = m_Device.GetDevice().getBufferMemoryRequirements(m_Buffer);
auto alloc = allocator.Allocate(reqs, memProps);
m_Memory = alloc.Memory;
m_Offset = alloc.Offset;
m_MappedPtr = alloc.MappedPtr;
m_Device.GetDevice().bindBufferMemory(m_Buffer, m_Memory, m_Offset);
m_Allocation = alloc; // 成员变量,保存起来
// VMA 惯用法:创建 + 分配 + 绑定一步完成;
// HOST_VISIBLE 时持久映射,MappedPtr 直接可用
m_Allocation = allocator.CreateBuffer(size, usage, memProps);
m_Buffer = m_Allocation.Buffer;
m_MappedPtr = m_Allocation.MappedPtr;
STATS.AddBuffer(m_Size);
}
Buffer::~Buffer()
{
m_Device.GetDevice().destroyBuffer(m_Buffer);
m_Allocator.Free(m_Allocation);
m_Allocator.Destroy(m_Allocation);
STATS.RemoveBuffer(m_Size);
}
@@ -54,12 +41,7 @@ namespace Engine {
}
memcpy(static_cast<char*>(m_MappedPtr) + offset, data, size);
// flush 整个内存块,避免对齐问题
vk::MappedMemoryRange range;
range.setMemory(m_Memory)
.setOffset(0)
.setSize(VK_WHOLE_SIZE);
m_Device.GetDevice().flushMappedMemoryRanges(range);
// 非 coherent 内存写后冲刷(coherent 时 VMA 内部自动跳过)
m_Allocator.Flush(m_Allocation);
}
} // namespace Engine
} // namespace Engine
+118 -191
View File
@@ -6,220 +6,147 @@
#include <Engine/Vulkan/Device.h>
#include <Engine/Core/Log.h>
#include <algorithm>
#include <vk_mem_alloc.h>
#include <stdexcept>
#include "Engine/Core/Stats.h"
namespace Engine {
MemoryAllocator::MemoryAllocator(Device& device)
: m_Device(device)
{
ENGINE_INFO("MemoryAllocator created");
}
MemoryAllocator::~MemoryAllocator()
{
const auto& vkDevice = m_Device.GetDevice();
for (auto& blocks : m_Blocks) {
for (auto& block : blocks) {
if (block.IsMapped) {
vkDevice.unmapMemory(block.Memory);
}
vkDevice.freeMemory(block.Memory);
STATS.RemoveMemoryBlock(block.Size);
}
blocks.clear();
}
}
vk::DeviceSize MemoryAllocator::GetBlockSize(vk::MemoryPropertyFlags props) const
{
if (props & vk::MemoryPropertyFlagBits::eHostVisible) {
return 16ull * 1024 * 1024; // 16 MB
}
return 64ull * 1024 * 1024; // 64 MB
}
MemoryAllocator::Allocation MemoryAllocator::Allocate(
vk::MemoryRequirements reqs, vk::MemoryPropertyFlags props)
{
u32 typeIndex = m_Device.FindMemoryType(reqs.memoryTypeBits, props);
auto& blocks = m_Blocks[typeIndex];
// 1. 扫描已有块的 FreeList,找能放下的区域(first-fit)
for (u32 i = 0; i < blocks.size(); ++i) {
auto& block = blocks[i];
for (auto it = block.FreeList.begin(); it != block.FreeList.end(); ++it) {
vk::DeviceSize alignedOffset =
(it->Offset + reqs.alignment - 1) & ~(reqs.alignment - 1);
vk::DeviceSize end = alignedOffset + reqs.size;
if (end <= it->Offset + it->Size) {
// 找到了
Allocation alloc;
alloc.Memory = block.Memory;
alloc.Offset = alignedOffset;
alloc.Size = reqs.size;
alloc.MappedPtr = block.IsMapped
? static_cast<char*>(block.MappedPtr) + alignedOffset
: nullptr;
alloc.BlockIndex = i;
alloc.TypeIndex = typeIndex;
alloc.Valid = true;
// 更新 FreeList
if (alignedOffset == it->Offset && reqs.size == it->Size) {
// 整个区域被用了,移除
block.FreeList.erase(it);
} else if (alignedOffset == it->Offset) {
// 从头部切
it->Offset += reqs.size;
it->Size -= reqs.size;
} else if (end == it->Offset + it->Size) {
// 从尾部切
it->Size = alignedOffset - it->Offset;
} else {
// 从中间切,拆成两段
FreeRange tail;
tail.Offset = end;
tail.Size = (it->Offset + it->Size) - end;
it->Size = alignedOffset - it->Offset;
block.FreeList.insert(it + 1, tail);
}
return alloc;
namespace
{
// props → VMA 创建参数:
// HOST_VISIBLE → AUTO + 持久映射 + 顺序写访问(VMA 自动挑 host 内存类型)
// 其余(DeviceLocal)→ AUTO_PREFER_DEVICE(优先显存,允许溢出到 BAR)
VmaAllocationCreateInfo MakeCreateInfo(vk::MemoryPropertyFlags props)
{
VmaAllocationCreateInfo ci{};
if (props & vk::MemoryPropertyFlagBits::eHostVisible) {
ci.usage = VMA_MEMORY_USAGE_AUTO;
ci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT
| VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
} else {
ci.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
}
// requiredFlags 兜底:调用方明确要求的属性 VMA 一定满足
ci.requiredFlags = static_cast<VkMemoryPropertyFlags>(props);
return ci;
}
}
// 2. 在已有块的尾部 bump 分配
for (u32 i = 0; i < blocks.size(); ++i) {
auto& block = blocks[i];
vk::DeviceSize alignedOffset =
(block.Used + reqs.alignment - 1) & ~(reqs.alignment - 1);
MemoryAllocator::MemoryAllocator(Device& device)
: m_Device(device)
{
VmaAllocatorCreateInfo ci{};
ci.instance = static_cast<VkInstance>(device.GetInstance());
ci.physicalDevice = static_cast<VkPhysicalDevice>(device.GetPhysicalDevice());
ci.device = static_cast<VkDevice>(device.GetDevice());
// 与 Device 创建实例时的 API 版本一致(启用 1.1+ 核心化的
// dedicated allocation / memory requirements 控制等路径)
ci.vulkanApiVersion = VK_API_VERSION_1_4;
// VMA_STATIC_VULKAN_FUNCTIONS=1:直接用链接的 Vulkan loader 函数表
if (alignedOffset + reqs.size <= block.Size) {
Allocation alloc;
alloc.Memory = block.Memory;
alloc.Offset = alignedOffset;
alloc.Size = reqs.size;
alloc.MappedPtr = block.IsMapped
? static_cast<char*>(block.MappedPtr) + alignedOffset
: nullptr;
alloc.BlockIndex = i;
alloc.TypeIndex = typeIndex;
alloc.Valid = true;
if (vmaCreateAllocator(&ci, &m_Vma) != VK_SUCCESS) {
throw std::runtime_error("MemoryAllocator: vmaCreateAllocator failed");
}
block.Used = alignedOffset + reqs.size;
return alloc;
ENGINE_INFO("MemoryAllocator created (VMA backend)");
}
MemoryAllocator::~MemoryAllocator()
{
if (m_Vma) {
vmaDestroyAllocator(m_Vma);
}
}
// 3. 创建新块
vk::DeviceSize blockSize = std::max(GetBlockSize(props), reqs.size);
MemoryAllocator::Allocation MemoryAllocator::CreateBuffer(
vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags props)
{
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bi.size = size;
bi.usage = static_cast<VkBufferUsageFlags>(usage);
bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
vk::MemoryAllocateInfo allocInfo;
allocInfo.setAllocationSize(blockSize)
.setMemoryTypeIndex(typeIndex);
VmaAllocationCreateInfo aci = MakeCreateInfo(props);
Block block;
block.Memory = m_Device.GetDevice().allocateMemory(allocInfo);
block.Size = blockSize;
block.Used = 0;
if (props & vk::MemoryPropertyFlagBits::eHostVisible) {
block.MappedPtr = m_Device.GetDevice().mapMemory(
block.Memory, 0, VK_WHOLE_SIZE);
block.IsMapped = true;
}
blocks.push_back(std::move(block));
m_TotalAllocated[typeIndex] += blockSize;
u32 newIndex = static_cast<u32>(blocks.size() - 1);
auto& newBlock = blocks[newIndex];
vk::DeviceSize alignedOffset = (reqs.alignment - 1) & ~(reqs.alignment - 1);
Allocation alloc;
alloc.Memory = newBlock.Memory;
alloc.Offset = alignedOffset;
alloc.Size = reqs.size;
alloc.MappedPtr = newBlock.IsMapped
? static_cast<char*>(newBlock.MappedPtr) + alignedOffset
: nullptr;
alloc.BlockIndex = newIndex;
alloc.TypeIndex = typeIndex;
alloc.Valid = true;
newBlock.Used = alignedOffset + reqs.size;
ENGINE_INFO("MemoryAllocator: new block of {} bytes (type {})",
blockSize, typeIndex);
STATS.AddMemoryBlock(blockSize);
STATS.AddMemoryUsed(reqs.size);
return alloc;
}
void MemoryAllocator::Free(const Allocation& alloc)
{
if (!alloc.Valid) return;
STATS.RemoveMemoryUsed(alloc.Size);
auto& block = m_Blocks[alloc.TypeIndex][alloc.BlockIndex];
FreeRange range;
range.Offset = alloc.Offset;
range.Size = alloc.Size;
// 插入 FreeList,保持按 offset 排序,方便合并
auto it = std::lower_bound(
block.FreeList.begin(), block.FreeList.end(), range,
[](const FreeRange& a, const FreeRange& b) {
return a.Offset < b.Offset;
});
it = block.FreeList.insert(it, range);
// 向前合并
if (it != block.FreeList.begin()) {
auto prev = it - 1;
if (prev->Offset + prev->Size == it->Offset) {
prev->Size += it->Size;
block.FreeList.erase(it);
it = prev;
VkBuffer buffer = nullptr;
VmaAllocation handle = nullptr;
if (vmaCreateBuffer(m_Vma, &bi, &aci, &buffer, &handle, nullptr) != VK_SUCCESS) {
ENGINE_ERROR("MemoryAllocator: CreateBuffer({} bytes) failed", size);
return {};
}
Allocation out;
out.Buffer = buffer;
out.Size = size;
out.Handle = handle;
out.Valid = true;
VmaAllocationInfo info{};
vmaGetAllocationInfo(m_Vma, handle, &info);
out.MappedPtr = info.pMappedData; // 非 MAPPED 标记时为 null
STATS.AddMemoryUsed(size);
return out;
}
// 向后合并
auto next = it + 1;
if (next != block.FreeList.end()) {
if (it->Offset + it->Size == next->Offset) {
it->Size += next->Size;
block.FreeList.erase(next);
MemoryAllocator::Allocation MemoryAllocator::CreateImage(
const vk::ImageCreateInfo& imageInfo, vk::MemoryPropertyFlags props)
{
VkImageCreateInfo ii = imageInfo; // 同布局,隐式转换
VmaAllocationCreateInfo aci = MakeCreateInfo(props);
VkImage image = nullptr;
VmaAllocation handle = nullptr;
if (vmaCreateImage(m_Vma, &ii, &aci, &image, &handle, nullptr) != VK_SUCCESS) {
ENGINE_ERROR("MemoryAllocator: CreateImage({}x{}) failed",
imageInfo.extent.width, imageInfo.extent.height);
return {};
}
Allocation out;
out.Image = image;
out.Size = ii.extent.depth * ii.extent.height * ii.extent.width; // 近似值(统计用)
out.Handle = handle;
out.Valid = true;
STATS.AddMemoryUsed(out.Size);
return out;
}
}
void MemoryAllocator::PrintStats() const
{
for (u32 type = 0; type < VK_MAX_MEMORY_TYPES; ++type) {
if (m_Blocks[type].empty()) continue;
void MemoryAllocator::Destroy(Allocation& alloc)
{
if (!alloc.Valid) return;
vk::DeviceSize totalUsed = 0;
for (auto& b : m_Blocks[type]) totalUsed += b.Used;
// VMA 要求资源与分配成对销毁,顺序不限但句柄必须配对
if (alloc.Buffer) {
vmaDestroyBuffer(m_Vma, alloc.Buffer, alloc.Handle);
} else if (alloc.Image) {
vmaDestroyImage(m_Vma, alloc.Image, alloc.Handle);
}
ENGINE_INFO(" Type {}: {} blocks, total allocated {} MB, used {} MB",
type,
m_Blocks[type].size(),
m_TotalAllocated[type] / (1024 * 1024),
totalUsed / (1024 * 1024));
STATS.RemoveMemoryUsed(alloc.Size);
alloc = {};
}
}
} // namespace Engine
void MemoryAllocator::Flush(const Allocation& alloc)
{
if (!alloc.Valid || !alloc.Handle) return;
// HOST_COHERENT 时 VMA 内部自动跳过
vmaFlushAllocation(m_Vma, alloc.Handle, 0, VK_WHOLE_SIZE);
}
void MemoryAllocator::PrintStats() const
{
VmaTotalStatistics stats;
vmaCalculateStatistics(m_Vma, &stats);
ENGINE_INFO(" VMA: {} allocations in {} blocks, total {} MB (used {} MB)",
stats.total.statistics.allocationCount,
stats.total.statistics.blockCount,
stats.total.statistics.blockBytes / (1024 * 1024),
stats.total.statistics.allocationBytes / (1024 * 1024));
}
} // namespace Engine
+12 -30
View File
@@ -56,22 +56,13 @@ namespace Engine
vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) *
static_cast<vk::DeviceSize>(height) * 4; // RGBA8
// 2. 创建 Staging Buffer(HOST_VISIBLE)
vk::BufferCreateInfo stagingInfo;
stagingInfo.setSize(imageSize)
.setUsage(vk::BufferUsageFlagBits::eTransferSrc)
.setSharingMode(vk::SharingMode::eExclusive);
vk::Buffer stagingBuffer = m_Device.GetDevice().createBuffer(stagingInfo);
auto stagingReqs = m_Device.GetDevice().getBufferMemoryRequirements(stagingBuffer);
auto stagingAlloc = m_Allocator.Allocate(
stagingReqs,
// 2. 创建 Staging Buffer(HOST_VISIBLE,VMA 持久映射)
auto stagingAlloc = m_Allocator.CreateBuffer(
imageSize,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
m_Device.GetDevice().bindBufferMemory(stagingBuffer, stagingAlloc.Memory, stagingAlloc.Offset);
// 3. 拷贝像素到 Staging Buffer
std::memcpy(stagingAlloc.MappedPtr, pixels, static_cast<size_t>(imageSize));
@@ -88,15 +79,9 @@ namespace Engine
.setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined);
m_Image = m_Device.GetDevice().createImage(imageInfo);
auto imageReqs = m_Device.GetDevice().getImageMemoryRequirements(m_Image);
auto imageAlloc = m_Allocator.Allocate(imageReqs, vk::MemoryPropertyFlagBits::eDeviceLocal);
m_ImageAllocation = imageAlloc;
m_Memory = imageAlloc.Memory;
m_Device.GetDevice().bindImageMemory(m_Image, m_Memory, imageAlloc.Offset);
m_ImageAllocation = m_Allocator.CreateImage(imageInfo,
vk::MemoryPropertyFlagBits::eDeviceLocal);
m_Image = m_ImageAllocation.Image;
// 5. 创建 ImageView
vk::ImageViewCreateInfo viewInfo;
@@ -168,7 +153,7 @@ namespace Engine
.setImageOffset({0, 0, 0})
.setImageExtent({m_Width, m_Height, 1});
cmd.copyBufferToImage(stagingBuffer, m_Image,
cmd.copyBufferToImage(stagingAlloc.Buffer, m_Image,
vk::ImageLayout::eTransferDstOptimal, region);
// 6.3 布局:TRANSFER_DST → SHADER_READ_ONLY
@@ -202,12 +187,10 @@ namespace Engine
ENGINE_ERROR("Texture upload: waitForFences failed");
}
// 8. 清理临时对象
// 8. 清理临时对象(staging 资源与分配经 VMA 成对释放)
m_Device.GetDevice().destroyFence(fence);
m_Device.GetDevice().destroyCommandPool(pool);
m_Device.GetDevice().destroyBuffer(stagingBuffer);
m_Allocator.Free(stagingAlloc);
// staging 内存由 MemoryAllocator 统一释放
m_Allocator.Destroy(stagingAlloc);
// 显存占用 = 宽 * 高 * 4 字节(RGBA8)
m_TextureBytes = static_cast<u64>(m_Width) * m_Height * 4;
@@ -220,10 +203,9 @@ namespace Engine
vkDevice.destroySampler(m_Sampler);
vkDevice.destroyImageView(m_View);
vkDevice.destroyImage(m_Image);
// 归还 image 的显存
m_Allocator.Free(m_ImageAllocation);
// image 资源与显存经 VMA 成对释放
m_Allocator.Destroy(m_ImageAllocation);
STATS.RemoveTexture(m_TextureBytes);
}
+12
View File
@@ -0,0 +1,12 @@
//
// Created by atdunbg on 2026/9/23.
//
// VMA 实现编译单元:整个引擎唯一一份 vk_mem_alloc 实现。
//
// 静态链接 Vulkan loader(项目已链接 Vulkan::Vulkan),与 vulkan-hpp 的
// dynamic loader 互不干扰(各自持有函数表)。
//
#define VMA_STATIC_VULKAN_FUNCTIONS 1
#define VMA_IMPLEMENTATION
#include <vk_mem_alloc.h>