Skip to content

Commit 924ffbf

Browse files
committed
Fix Vulkan stutter with buffer recycling pool and reduced budgets
1 parent 9671292 commit 924ffbf

2 files changed

Lines changed: 103 additions & 8 deletions

File tree

Engine/include/private/Graphics/Vulkan/VulkanBuffer.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class ENGINE_API VulkanBuffer : public BufferBase {
6767

6868
VkBuffer m_buffer = VK_NULL_HANDLE;
6969
VkDeviceMemory m_memory = VK_NULL_HANDLE;
70+
VkDeviceSize m_allocSize = 0;
71+
VkBufferUsageFlags m_usage = 0;
72+
uint32_t m_memoryTypeIndex = 0;
7073

7174
VkBuffer m_stagingBuffer = VK_NULL_HANDLE;
7275
VkDeviceMemory m_stagingMemory = VK_NULL_HANDLE;
@@ -91,11 +94,30 @@ class ENGINE_API VulkanBuffer : public BufferBase {
9194
VkBuffer buffer;
9295
VkDeviceMemory memory;
9396
VkDevice device;
97+
VkDeviceSize allocSize;
98+
VkBufferUsageFlags usage;
99+
uint32_t memoryTypeIndex;
94100
uint64_t frameNumber;
95101
};
96102
static std::vector<DeferredBufferDelete> s_deferredDeletions;
97103
static uint64_t s_frameNumber;
98104

105+
// --- Buffer recycling pool ---
106+
struct PooledBuffer {
107+
VkBuffer buffer;
108+
VkDeviceMemory memory;
109+
VkDevice device;
110+
VkDeviceSize allocSize;
111+
VkBufferUsageFlags usage;
112+
uint32_t memoryTypeIndex;
113+
};
114+
static std::vector<PooledBuffer> s_bufferPool;
115+
static constexpr size_t MAX_POOL_SIZE = 64;
116+
117+
bool TryRecycleBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
118+
VkMemoryPropertyFlags properties,
119+
VkBuffer& buffer, VkDeviceMemory& memory);
120+
99121
public:
100122
// Called by the renderer each frame after fence wait to safely
101123
// destroy buffers that are no longer referenced by the GPU.

Engine/src/VulkanBuffer.cpp

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ std::vector<VulkanBuffer::PendingStagingCleanup> VulkanBuffer::s_pendingCleanup;
1919
std::vector<VulkanBuffer::DeferredBufferDelete> VulkanBuffer::s_deferredDeletions;
2020
uint64_t VulkanBuffer::s_frameNumber = 0;
2121

22+
// Buffer recycling pool
23+
std::vector<VulkanBuffer::PooledBuffer> VulkanBuffer::s_bufferPool;
24+
2225
VulkanBuffer::VulkanBuffer(VkDevice device, VkPhysicalDevice physicalDevice,
2326
uint32_t size, BufferType type,
2427
VkCommandPool commandPool, VkQueue graphicsQueue)
@@ -221,11 +224,14 @@ void VulkanBuffer::Cleanup() {
221224
m_stagingMemory = VK_NULL_HANDLE;
222225
}
223226
// Defer GPU buffer destruction — may still be referenced by in-flight
224-
// command buffers. Will be cleaned up after fence wait.
227+
// command buffers. Will be recycled or cleaned up after fence wait.
225228
if (m_buffer != VK_NULL_HANDLE) {
226-
s_deferredDeletions.push_back({m_buffer, m_memory, m_device, s_frameNumber});
229+
s_deferredDeletions.push_back({m_buffer, m_memory, m_device,
230+
m_allocSize, m_usage, m_memoryTypeIndex,
231+
s_frameNumber});
227232
m_buffer = VK_NULL_HANDLE;
228233
m_memory = VK_NULL_HANDLE;
234+
m_allocSize = 0;
229235
}
230236

231237
bIsInitialized = false;
@@ -265,6 +271,16 @@ void VulkanBuffer::CreateBuffer(VkDeviceSize size,
265271
VkMemoryPropertyFlags properties,
266272
VkBuffer& buffer,
267273
VkDeviceMemory& memory) {
274+
// Try to recycle a pooled buffer to avoid expensive vkAllocateMemory
275+
if (TryRecycleBuffer(size, usage, properties, buffer, memory)) {
276+
// Track the recycled buffer's info (keep existing alloc metadata)
277+
if (&buffer == &m_buffer) {
278+
m_usage = usage;
279+
// m_allocSize and m_memoryTypeIndex stay from pool
280+
}
281+
return;
282+
}
283+
268284
VkBufferCreateInfo bufferInfo{};
269285
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
270286
bufferInfo.size = size;
@@ -280,11 +296,12 @@ void VulkanBuffer::CreateBuffer(VkDeviceSize size,
280296
VkMemoryRequirements memRequirements;
281297
vkGetBufferMemoryRequirements(m_device, buffer, &memRequirements);
282298

299+
uint32_t memTypeIdx = FindMemoryType(memRequirements.memoryTypeBits, properties);
300+
283301
VkMemoryAllocateInfo allocInfo{};
284302
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
285303
allocInfo.allocationSize = memRequirements.size;
286-
allocInfo.memoryTypeIndex =
287-
FindMemoryType(memRequirements.memoryTypeBits, properties);
304+
allocInfo.memoryTypeIndex = memTypeIdx;
288305

289306
if (vkAllocateMemory(m_device, &allocInfo, nullptr, &memory) !=
290307
VK_SUCCESS) {
@@ -293,6 +310,13 @@ void VulkanBuffer::CreateBuffer(VkDeviceSize size,
293310
}
294311

295312
vkBindBufferMemory(m_device, buffer, memory, 0);
313+
314+
// Track allocation metadata for recycling
315+
if (&buffer == &m_buffer) {
316+
m_allocSize = memRequirements.size;
317+
m_usage = usage;
318+
m_memoryTypeIndex = memTypeIdx;
319+
}
296320
}
297321

298322
void VulkanBuffer::CopyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer,
@@ -463,26 +487,75 @@ uint32_t VulkanBuffer::FindMemoryType(uint32_t typeFilter,
463487
}
464488

465489
void VulkanBuffer::ProcessDeferredDeletions(uint32_t maxFramesInFlight) {
490+
// Rate-limit processing to avoid vkFreeMemory spikes
491+
constexpr int MAX_PER_FRAME = 8;
492+
int processed = 0;
466493
auto it = s_deferredDeletions.begin();
467-
while (it != s_deferredDeletions.end()) {
494+
while (it != s_deferredDeletions.end() && processed < MAX_PER_FRAME) {
468495
if (s_frameNumber - it->frameNumber >= maxFramesInFlight) {
469-
vkDestroyBuffer(it->device, it->buffer, nullptr);
470-
if (it->memory != VK_NULL_HANDLE)
471-
vkFreeMemory(it->device, it->memory, nullptr);
496+
// Recycle into pool if there's room, otherwise destroy
497+
if (s_bufferPool.size() < MAX_POOL_SIZE && it->allocSize > 0) {
498+
s_bufferPool.push_back({it->buffer, it->memory, it->device,
499+
it->allocSize, it->usage, it->memoryTypeIndex});
500+
} else {
501+
vkDestroyBuffer(it->device, it->buffer, nullptr);
502+
if (it->memory != VK_NULL_HANDLE)
503+
vkFreeMemory(it->device, it->memory, nullptr);
504+
}
472505
it = s_deferredDeletions.erase(it);
506+
++processed;
473507
} else {
474508
++it;
475509
}
476510
}
477511
}
478512

513+
bool VulkanBuffer::TryRecycleBuffer(VkDeviceSize size,
514+
VkBufferUsageFlags usage,
515+
VkMemoryPropertyFlags properties,
516+
VkBuffer& buffer,
517+
VkDeviceMemory& memory) {
518+
// Find a pooled buffer with matching usage and sufficient size.
519+
// Prefer smallest fitting buffer to minimize waste.
520+
int bestIdx = -1;
521+
VkDeviceSize bestSize = UINT64_MAX;
522+
for (size_t i = 0; i < s_bufferPool.size(); ++i) {
523+
auto& p = s_bufferPool[i];
524+
if (p.device == m_device && p.usage == usage &&
525+
p.allocSize >= size && p.allocSize < bestSize) {
526+
// Don't recycle if way too large (>4x waste)
527+
if (p.allocSize <= size * 4) {
528+
bestIdx = static_cast<int>(i);
529+
bestSize = p.allocSize;
530+
}
531+
}
532+
}
533+
if (bestIdx >= 0) {
534+
buffer = s_bufferPool[bestIdx].buffer;
535+
memory = s_bufferPool[bestIdx].memory;
536+
// Remove from pool (swap with last for O(1))
537+
s_bufferPool[bestIdx] = s_bufferPool.back();
538+
s_bufferPool.pop_back();
539+
return true;
540+
}
541+
return false;
542+
}
543+
479544
void VulkanBuffer::FlushAllDeferredDeletions() {
480545
for (auto& entry : s_deferredDeletions) {
481546
vkDestroyBuffer(entry.device, entry.buffer, nullptr);
482547
if (entry.memory != VK_NULL_HANDLE)
483548
vkFreeMemory(entry.device, entry.memory, nullptr);
484549
}
485550
s_deferredDeletions.clear();
551+
552+
// Also drain the recycling pool
553+
for (auto& entry : s_bufferPool) {
554+
vkDestroyBuffer(entry.device, entry.buffer, nullptr);
555+
if (entry.memory != VK_NULL_HANDLE)
556+
vkFreeMemory(entry.device, entry.memory, nullptr);
557+
}
558+
s_bufferPool.clear();
486559
}
487560

488561
} // namespace RenderEngine

0 commit comments

Comments
 (0)