SleakEngine 1.0.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
VulkanBuffer.cpp
Go to the documentation of this file.
1// VMA backs all buffer allocations (suballocation from large device blocks).
2#include <vulkan/vulkan.h>
3#define VMA_STATIC_VULKAN_FUNCTIONS 1
4#define VMA_DYNAMIC_VULKAN_FUNCTIONS 0
5#define VMA_IMPLEMENTATION
6#include <vma/vk_mem_alloc.h>
7
9#include <Core/Logger.hpp>
10#include <cstring>
11#include <stdexcept>
12
13namespace Sleak {
14namespace RenderEngine {
15
16VmaAllocator VulkanBuffer::s_allocator = VK_NULL_HANDLE;
17
18void VulkanBuffer::InitAllocator(VkInstance instance,
19 VkPhysicalDevice physicalDevice,
20 VkDevice device) {
21 if (s_allocator != VK_NULL_HANDLE) return;
22 VmaAllocatorCreateInfo aci{};
23 aci.instance = instance;
24 aci.physicalDevice = physicalDevice;
25 aci.device = device;
26 aci.vulkanApiVersion = VK_API_VERSION_1_1;
27 if (vmaCreateAllocator(&aci, &s_allocator) != VK_SUCCESS) {
28 SLEAK_ERROR("Failed to create VMA allocator!");
29 s_allocator = VK_NULL_HANDLE;
30 }
31}
32
34 if (s_allocator == VK_NULL_HANDLE) return;
35 vmaDestroyAllocator(s_allocator);
36 s_allocator = VK_NULL_HANDLE;
37}
38
39// Static batch state
40bool VulkanBuffer::s_batchingEnabled = false;
41bool VulkanBuffer::s_batchActive = false;
42VkCommandBuffer VulkanBuffer::s_batchCommandBuffer = VK_NULL_HANDLE;
43VkDevice VulkanBuffer::s_batchDevice = VK_NULL_HANDLE;
44VkCommandPool VulkanBuffer::s_batchCommandPool = VK_NULL_HANDLE;
45VkQueue VulkanBuffer::s_batchQueue = VK_NULL_HANDLE;
46std::vector<VulkanBuffer::PendingStagingCleanup> VulkanBuffer::s_pendingCleanup;
47
48// Static deferred deletion state
49std::vector<VulkanBuffer::DeferredBufferDelete> VulkanBuffer::s_deferredDeletions;
50uint64_t VulkanBuffer::s_frameNumber = 0;
51
52// OOM fallback state
53static uint32_t g_maxFramesInFlight = 3; // updated each frame by ProcessDeferredDeletions
54static uint64_t g_lastPoolWarnFrame = UINT64_MAX;
55static uint64_t g_lastReclaimWarnFrame = UINT64_MAX;
56
57// Recycling bucket granularity, per-frame pool-insertion cap, idle-trim rate.
58static constexpr VkDeviceSize kPoolBucket = 64 * 1024;
59static constexpr size_t kMaxDeletesPerFrame = 64;
60static constexpr size_t kIdleTrimPerFrame = 8;
61// Only drain the pool after this many consecutive streaming-idle frames, so
62// brief gaps while moving don't starve the pool of its reuse benefit.
63static constexpr uint32_t kIdleFramesBeforeTrim = 120;
64static uint32_t g_idleFrames = 0;
65
66// Recycling helpers
67/// Rounds a size up to the nearest pool bucket so similar-sized buffers can share slots.
68static VkDeviceSize BucketSize(VkDeviceSize s) {
69 return ((s + kPoolBucket - 1) / kPoolBucket) * kPoolBucket;
70}
71/// True for vertex/index usages, the only kinds worth pooling for reuse.
72static bool UsageIsRecyclable(VkBufferUsageFlags usage) {
73 return (usage & (VK_BUFFER_USAGE_VERTEX_BUFFER_BIT |
74 VK_BUFFER_USAGE_INDEX_BUFFER_BIT)) != 0;
75}
76/// True when a buffer is both device-local and a recyclable usage, the pool's eligibility gate.
77static bool IsRecyclable(VkBufferUsageFlags usage, VkMemoryPropertyFlags props) {
78 return (props & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) &&
79 UsageIsRecyclable(usage);
80}
81
82// Buffer recycling pool
83std::vector<VulkanBuffer::PooledBuffer> VulkanBuffer::s_bufferPool;
84VkDeviceSize VulkanBuffer::s_poolBytes = 0;
85
86// VRAM tracking
87VkDeviceSize VulkanBuffer::s_totalAllocatedBytes = 0;
88VkPhysicalDevice VulkanBuffer::s_physicalDeviceGlobal = VK_NULL_HANDLE;
89VkDeviceSize VulkanBuffer::s_perTypeBytes[VK_MAX_MEMORY_TYPES] = {};
90bool VulkanBuffer::s_memTypeIsDeviceLocal[VK_MAX_MEMORY_TYPES] = {};
91uint32_t VulkanBuffer::s_memTypeCount = 0;
92
93
94VulkanBuffer::VulkanBuffer(VkDevice device, VkPhysicalDevice physicalDevice,
95 uint32_t size, BufferType type,
96 VkCommandPool commandPool, VkQueue graphicsQueue)
97 : m_device(device),
98 m_physicalDevice(physicalDevice),
99 m_commandPool(commandPool),
100 m_graphicsQueue(graphicsQueue) {
101 Size = size;
102 Type = type;
103}
104
108
109bool VulkanBuffer::Initialize(void* data) {
110 if (Size == 0) return false;
111
112 switch (Type) {
113 case BufferType::Vertex: {
114 VkDeviceSize stagingAllocSize = 0;
115 uint32_t stagingMemTypeIdx = 0;
116 CreateBuffer(
117 Size,
118 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
119 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
120 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
121 m_stagingBuffer, m_stagingMemory, &stagingAllocSize,
122 &stagingMemTypeIdx);
123 if (m_stagingBuffer == VK_NULL_HANDLE) return false;
124
125 // Copy data to staging buffer
126 if (data) {
127 void* mapped;
128 vmaMapMemory(s_allocator, m_stagingMemory, &mapped);
129 memcpy(mapped, data, Size);
130 vmaUnmapMemory(s_allocator, m_stagingMemory);
131 }
132
133 // Create device-local buffer
134 CreateBuffer(
135 Size,
136 VK_BUFFER_USAGE_TRANSFER_DST_BIT |
137 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
138 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
139 m_buffer, m_memory);
140
141 if (m_buffer == VK_NULL_HANDLE) {
142 if (m_stagingBuffer != VK_NULL_HANDLE) {
143 vmaDestroyBuffer(s_allocator, m_stagingBuffer,
144 m_stagingMemory);
145 m_stagingBuffer = VK_NULL_HANDLE;
146 m_stagingMemory = VK_NULL_HANDLE;
147 s_totalAllocatedBytes -= stagingAllocSize;
148 if (stagingMemTypeIdx < s_memTypeCount)
149 s_perTypeBytes[stagingMemTypeIdx] -= stagingAllocSize;
150 }
151 return false;
152 }
153
154 if (data) {
155 CopyBuffer(m_stagingBuffer, m_buffer, Size);
156 }
157
158 if (s_batchActive) {
159 s_pendingCleanup.push_back({m_stagingBuffer, m_stagingMemory, stagingAllocSize, stagingMemTypeIdx});
160 } else {
161 vmaDestroyBuffer(s_allocator, m_stagingBuffer, m_stagingMemory);
162 s_totalAllocatedBytes -= stagingAllocSize;
163 if (stagingMemTypeIdx < s_memTypeCount)
164 s_perTypeBytes[stagingMemTypeIdx] -= stagingAllocSize;
165 }
166 m_stagingBuffer = VK_NULL_HANDLE;
167 m_stagingMemory = VK_NULL_HANDLE;
168 break;
169 }
170 case BufferType::Index: {
171 VkDeviceSize stagingAllocSize = 0;
172 uint32_t stagingMemTypeIdx = 0;
173 CreateBuffer(
174 Size,
175 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
176 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
177 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
178 m_stagingBuffer, m_stagingMemory, &stagingAllocSize,
179 &stagingMemTypeIdx);
180 if (m_stagingBuffer == VK_NULL_HANDLE) return false;
181
182 if (data) {
183 void* mapped;
184 vmaMapMemory(s_allocator, m_stagingMemory, &mapped);
185 memcpy(mapped, data, Size);
186 vmaUnmapMemory(s_allocator, m_stagingMemory);
187 }
188
189 CreateBuffer(
190 Size,
191 VK_BUFFER_USAGE_TRANSFER_DST_BIT |
192 VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
193 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
194 m_buffer, m_memory);
195
196 if (m_buffer == VK_NULL_HANDLE) {
197 if (m_stagingBuffer != VK_NULL_HANDLE) {
198 vmaDestroyBuffer(s_allocator, m_stagingBuffer,
199 m_stagingMemory);
200 m_stagingBuffer = VK_NULL_HANDLE;
201 m_stagingMemory = VK_NULL_HANDLE;
202 s_totalAllocatedBytes -= stagingAllocSize;
203 if (stagingMemTypeIdx < s_memTypeCount)
204 s_perTypeBytes[stagingMemTypeIdx] -= stagingAllocSize;
205 }
206 return false;
207 }
208
209 if (data) {
210 CopyBuffer(m_stagingBuffer, m_buffer, Size);
211 }
212
213 if (s_batchActive) {
214 s_pendingCleanup.push_back({m_stagingBuffer, m_stagingMemory, stagingAllocSize, stagingMemTypeIdx});
215 } else {
216 vmaDestroyBuffer(s_allocator, m_stagingBuffer, m_stagingMemory);
217 s_totalAllocatedBytes -= stagingAllocSize;
218 if (stagingMemTypeIdx < s_memTypeCount)
219 s_perTypeBytes[stagingMemTypeIdx] -= stagingAllocSize;
220 }
221 m_stagingBuffer = VK_NULL_HANDLE;
222 m_stagingMemory = VK_NULL_HANDLE;
223 break;
224 }
226 // Constant buffers are host-visible for frequent updates
227 CreateBuffer(
228 Size,
229 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
230 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
231 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
232 m_buffer, m_memory);
233
234 if (m_buffer == VK_NULL_HANDLE) return false;
235
236 // Persistently map
237 vmaMapMemory(s_allocator, m_memory, &m_mappedData);
238
239 if (data) {
240 memcpy(m_mappedData, data, Size);
241 }
242 break;
243 }
244 default: {
245 // Generic buffer
246 CreateBuffer(
247 Size,
248 VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
249 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
250 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
251 m_buffer, m_memory);
252
253 if (data) {
254 void* mapped;
255 vmaMapMemory(s_allocator, m_memory, &mapped);
256 memcpy(mapped, data, Size);
257 vmaUnmapMemory(s_allocator, m_memory);
258 }
259 break;
260 }
261 }
262
263 bIsInitialized = true;
264 return true;
265}
266
268 // For constant buffers with persistent mapping, nothing extra needed
269 // Data is already written to mapped memory
270}
271
272void VulkanBuffer::Update(void* data, size_t size) {
273 if (!data || size == 0) return;
274
275 if (Type == BufferType::Constant && m_mappedData) {
276 // Constant buffer is persistently mapped
277 memcpy(m_mappedData, data, size);
278 } else if (Type == BufferType::Vertex || Type == BufferType::Index) {
279 VkBuffer staging;
280 VmaAllocation stagingMem;
281 VkDeviceSize stagingAllocSize = 0;
282 uint32_t stagingMemTypeIdx = 0;
283 CreateBuffer(
284 size,
285 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
286 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
287 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
288 staging, stagingMem, &stagingAllocSize, &stagingMemTypeIdx);
289
290 void* mapped;
291 vmaMapMemory(s_allocator, stagingMem, &mapped);
292 memcpy(mapped, data, size);
293 vmaUnmapMemory(s_allocator, stagingMem);
294
295 CopyBuffer(staging, m_buffer, size);
296
297 if (s_batchActive) {
298 s_pendingCleanup.push_back({staging, stagingMem, stagingAllocSize, stagingMemTypeIdx});
299 } else {
300 vmaDestroyBuffer(s_allocator, staging, stagingMem);
301 s_totalAllocatedBytes -= stagingAllocSize;
302 if (stagingMemTypeIdx < s_memTypeCount)
303 s_perTypeBytes[stagingMemTypeIdx] -= stagingAllocSize;
304 }
305 }
306}
307
309 if (m_device == VK_NULL_HANDLE) return;
310
311 // Only flush if THIS buffer has a pending copy in the active batch.
312 // Normally old buffers are destroyed before the batch starts (two-pass
313 // column rebuild), so this only triggers during scene transitions.
314 if (m_pendingInBatch && s_batchActive) {
316 m_pendingInBatch = false;
317 }
318
319 if (Type == BufferType::Constant && m_mappedData) {
320 vmaUnmapMemory(s_allocator, m_memory);
321 m_mappedData = nullptr;
322 }
323
324 // Staging buffers can be destroyed immediately (not GPU-visible after copy)
325 if (m_stagingBuffer != VK_NULL_HANDLE) {
326 vmaDestroyBuffer(s_allocator, m_stagingBuffer, m_stagingMemory);
327 m_stagingBuffer = VK_NULL_HANDLE;
328 m_stagingMemory = VK_NULL_HANDLE;
329 }
330 // Defer GPU buffer destruction — may still be referenced by in-flight
331 // command buffers. Will be recycled or cleaned up after fence wait.
332 if (m_buffer != VK_NULL_HANDLE) {
333 s_deferredDeletions.push_back({m_buffer, m_memory, m_device,
334 m_allocSize, m_usage, m_memoryTypeIndex,
335 s_frameNumber, m_bufferSize});
336 m_buffer = VK_NULL_HANDLE;
337 m_memory = VK_NULL_HANDLE;
338 m_allocSize = 0;
339 m_bufferSize = 0;
340 }
341
342 bIsInitialized = false;
343}
344
346 if (bIsMapped) return true;
347 if (Type == BufferType::Constant && m_mappedData) {
348 Data = m_mappedData;
349 bIsMapped = true;
350 return true;
351 }
352 VkResult result = vmaMapMemory(s_allocator, m_memory, &Data);
353 if (result != VK_SUCCESS) return false;
354 bIsMapped = true;
355 return true;
356}
357
359 if (!bIsMapped) return;
360 // Don't unmap persistent constant buffer mappings
361 if (Type == BufferType::Constant && m_mappedData) {
362 bIsMapped = false;
363 return;
364 }
365 vmaUnmapMemory(s_allocator, m_memory);
366 Data = nullptr;
367 bIsMapped = false;
368}
369
371 return m_mappedData ? m_mappedData : Data;
372}
373
374void VulkanBuffer::CreateBuffer(VkDeviceSize size,
375 VkBufferUsageFlags usage,
376 VkMemoryPropertyFlags properties,
377 VkBuffer& buffer,
378 VmaAllocation& memory,
379 VkDeviceSize* outAllocSize,
380 uint32_t* outMemTypeIdx) {
381 // Recyclable device-local vertex/index buffers are bucketed so similar
382 // meshes can reuse a freed allocation instead of hitting vkAllocateMemory.
383 const bool recyclable = IsRecyclable(usage, properties);
384 const VkDeviceSize createSize = recyclable ? BucketSize(size) : size;
385
386 if (recyclable &&
387 TryRecycleBuffer(createSize, usage, properties, buffer, memory,
388 outAllocSize)) {
389 if (outMemTypeIdx && &buffer == &m_buffer) *outMemTypeIdx = m_memoryTypeIndex;
390 return;
391 }
392
393 VkBufferCreateInfo bufferInfo{};
394 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
395 bufferInfo.size = createSize;
396 bufferInfo.usage = usage;
397 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
398
399 VmaAllocationCreateInfo allocCI{};
400 allocCI.usage = VMA_MEMORY_USAGE_AUTO;
401 allocCI.requiredFlags = properties; // preserve exact memory-property semantics
402 if (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)
403 allocCI.flags |= VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
404
405 VmaAllocationInfo info{};
406 VkResult r = vmaCreateBuffer(s_allocator, &bufferInfo, &allocCI,
407 &buffer, &memory, &info);
408
409 if (r != VK_SUCCESS && !s_bufferPool.empty()) {
410 // OOM step 1: evict whole recycling pool (pool buffers are not GPU-referenced)
411 if (g_lastPoolWarnFrame != s_frameNumber) {
412 SLEAK_WARN("Vulkan alloc failed, evicting {} pooled buffers ({:.1f} MB)",
413 s_bufferPool.size(),
414 static_cast<double>(s_poolBytes) / (1024.0 * 1024.0));
415 g_lastPoolWarnFrame = s_frameNumber;
416 }
417 for (auto& e : s_bufferPool) {
418 vmaDestroyBuffer(s_allocator, e.buffer, e.memory);
419 s_totalAllocatedBytes -= e.allocSize;
420 if (e.memoryTypeIndex < s_memTypeCount)
421 s_perTypeBytes[e.memoryTypeIndex] -= e.allocSize;
422 }
423 s_bufferPool.clear();
424 s_poolBytes = 0;
425 r = vmaCreateBuffer(s_allocator, &bufferInfo, &allocCI,
426 &buffer, &memory, &info);
427 }
428
429 if (r != VK_SUCCESS) {
430 // OOM step 2: reclaim fence-safe deferred deletions (no device-wide idle)
431 if (g_lastReclaimWarnFrame != s_frameNumber) {
432 SLEAK_WARN("Vulkan alloc still failing, reclaiming fence-safe deferred deletions");
433 g_lastReclaimWarnFrame = s_frameNumber;
434 }
436 r = vmaCreateBuffer(s_allocator, &bufferInfo, &allocCI,
437 &buffer, &memory, &info);
438 }
439
440 if (r != VK_SUCCESS) {
441 SLEAK_ERROR("Failed to allocate Vulkan buffer memory!");
442 buffer = VK_NULL_HANDLE;
443 memory = VK_NULL_HANDLE;
444 return;
445 }
446
447 s_totalAllocatedBytes += info.size;
448 if (info.memoryType < s_memTypeCount)
449 s_perTypeBytes[info.memoryType] += info.size;
450
451 if (outAllocSize) *outAllocSize = info.size;
452 if (outMemTypeIdx) *outMemTypeIdx = info.memoryType;
453
454 if (&buffer == &m_buffer) {
455 m_allocSize = info.size;
456 m_usage = usage;
457 m_memoryTypeIndex = info.memoryType;
458 m_bufferSize = createSize;
459 }
460}
461
462void VulkanBuffer::CopyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer,
463 VkDeviceSize size) {
464 // If a batch is active, record into the shared command buffer
465 EnsureBatchStarted(m_device, m_commandPool, m_graphicsQueue);
466 if (s_batchActive) {
467 VkBufferCopy copyRegion{};
468 copyRegion.size = size;
469 vkCmdCopyBuffer(s_batchCommandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);
470 m_pendingInBatch = true;
471 return;
472 }
473
474 // Fallback: immediate copy (used when no batch is active, e.g. texture uploads)
475 VkCommandBufferAllocateInfo allocInfo{};
476 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
477 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
478 allocInfo.commandPool = m_commandPool;
479 allocInfo.commandBufferCount = 1;
480
481 VkCommandBuffer commandBuffer;
482 vkAllocateCommandBuffers(m_device, &allocInfo, &commandBuffer);
483
484 VkCommandBufferBeginInfo beginInfo{};
485 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
486 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
487
488 vkBeginCommandBuffer(commandBuffer, &beginInfo);
489
490 VkBufferCopy copyRegion{};
491 copyRegion.size = size;
492 vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);
493
494 vkEndCommandBuffer(commandBuffer);
495
496 VkFenceCreateInfo fenceInfo{};
497 fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
498 VkFence copyFence;
499 vkCreateFence(m_device, &fenceInfo, nullptr, &copyFence);
500
501 VkSubmitInfo submitInfo{};
502 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
503 submitInfo.commandBufferCount = 1;
504 submitInfo.pCommandBuffers = &commandBuffer;
505
506 vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, copyFence);
507 vkWaitForFences(m_device, 1, &copyFence, VK_TRUE, UINT64_MAX);
508
509 vkDestroyFence(m_device, copyFence, nullptr);
510 vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
511}
512
513void VulkanBuffer::EnsureBatchStarted(VkDevice device, VkCommandPool pool,
514 VkQueue queue) {
515 if (s_batchActive || !s_batchingEnabled) return;
516
517 s_batchDevice = device;
518 s_batchCommandPool = pool;
519 s_batchQueue = queue;
520
521 VkCommandBufferAllocateInfo allocInfo{};
522 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
523 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
524 allocInfo.commandPool = pool;
525 allocInfo.commandBufferCount = 1;
526
527 if (vkAllocateCommandBuffers(device, &allocInfo, &s_batchCommandBuffer) != VK_SUCCESS) {
528 SLEAK_ERROR("Failed to allocate batch transfer command buffer!");
529 return;
530 }
531
532 VkCommandBufferBeginInfo beginInfo{};
533 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
534 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
535
536 if (vkBeginCommandBuffer(s_batchCommandBuffer, &beginInfo) != VK_SUCCESS) {
537 SLEAK_ERROR("Failed to begin batch transfer command buffer!");
538 vkFreeCommandBuffers(device, pool, 1, &s_batchCommandBuffer);
539 s_batchCommandBuffer = VK_NULL_HANDLE;
540 return;
541 }
542
543 s_batchActive = true;
544}
545
547 if (!s_batchActive) return;
548
549 vkEndCommandBuffer(s_batchCommandBuffer);
550
551 // Single submit + single fence for ALL batched copies
552 VkFenceCreateInfo fenceInfo{};
553 fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
554 VkFence batchFence;
555 vkCreateFence(s_batchDevice, &fenceInfo, nullptr, &batchFence);
556
557 VkSubmitInfo submitInfo{};
558 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
559 submitInfo.commandBufferCount = 1;
560 submitInfo.pCommandBuffers = &s_batchCommandBuffer;
561
562 vkQueueSubmit(s_batchQueue, 1, &submitInfo, batchFence);
563 vkWaitForFences(s_batchDevice, 1, &batchFence, VK_TRUE, UINT64_MAX);
564
565 // Cleanup
566 vkDestroyFence(s_batchDevice, batchFence, nullptr);
567 vkFreeCommandBuffers(s_batchDevice, s_batchCommandPool, 1, &s_batchCommandBuffer);
568
569 for (auto& pending : s_pendingCleanup) {
570 vmaDestroyBuffer(s_allocator, pending.buffer, pending.memory);
571 s_totalAllocatedBytes -= pending.allocSize;
572 if (pending.memoryTypeIndex < s_memTypeCount)
573 s_perTypeBytes[pending.memoryTypeIndex] -= pending.allocSize;
574 }
575 s_pendingCleanup.clear();
576
577 s_batchCommandBuffer = VK_NULL_HANDLE;
578 s_batchActive = false;
579}
580
582VulkanBuffer::FlushPendingCopiesAsync(VkSemaphore signalSemaphore) {
583 AsyncFlushResult result;
584
585 if (!s_batchActive) return result;
586
587 vkEndCommandBuffer(s_batchCommandBuffer);
588
589 // Submit with semaphore signal — NO fence wait (zero CPU blocking)
590 VkSubmitInfo submitInfo{};
591 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
592 submitInfo.commandBufferCount = 1;
593 submitInfo.pCommandBuffers = &s_batchCommandBuffer;
594 submitInfo.signalSemaphoreCount = 1;
595 submitInfo.pSignalSemaphores = &signalSemaphore;
596
597 vkQueueSubmit(s_batchQueue, 1, &submitInfo, VK_NULL_HANDLE);
598
599 // Return everything the caller needs for per-frame deferred cleanup
600 result.submitted = true;
601 result.stagingBuffers = std::move(s_pendingCleanup);
602 result.commandBuffer = s_batchCommandBuffer;
603 result.commandPool = s_batchCommandPool;
604 result.device = s_batchDevice;
605
606 s_pendingCleanup.clear();
607 s_batchCommandBuffer = VK_NULL_HANDLE;
608 s_batchActive = false;
609
610 return result;
611}
612
613uint32_t VulkanBuffer::FindMemoryType(uint32_t typeFilter,
614 VkMemoryPropertyFlags properties) {
615 VkPhysicalDeviceMemoryProperties memProperties;
616 vkGetPhysicalDeviceMemoryProperties(m_physicalDevice, &memProperties);
617
618 for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
619 if ((typeFilter & (1 << i)) &&
620 (memProperties.memoryTypes[i].propertyFlags & properties) ==
621 properties) {
622 return i;
623 }
624 }
625
626 SLEAK_ERROR("Failed to find suitable memory type!");
627 return 0;
628}
629
630void VulkanBuffer::ProcessDeferredDeletions(uint32_t maxFramesInFlight) {
631 g_maxFramesInFlight = maxFramesInFlight;
632
633 // Reclaim EVERY buffer whose fence chain confirms the GPU is done (aged
634 // past frames-in-flight). Destroys are uncapped: vmaDestroyBuffer only
635 // returns a suballocation to its VMA block, so even large unload bursts
636 // are cheap — and the deferred queue never backlogs (a backlog would keep
637 // freed buffers allocated and grow VRAM monotonically while moving). Only
638 // pool INSERTIONS are throttled per frame; excess eligible buffers are
639 // simply destroyed this frame instead of pooled.
640 size_t pooled = 0;
641 for (size_t i = 0; i < s_deferredDeletions.size(); ) {
642 auto& entry = s_deferredDeletions[i];
643 if (s_frameNumber - entry.frameNumber >= maxFramesInFlight) {
644 const bool deviceLocal =
645 entry.memoryTypeIndex < s_memTypeCount &&
646 s_memTypeIsDeviceLocal[entry.memoryTypeIndex];
647 const bool poolable =
648 deviceLocal && UsageIsRecyclable(entry.usage) &&
649 entry.bufferSize > 0 &&
650 pooled < kMaxDeletesPerFrame &&
651 s_bufferPool.size() < MAX_POOL_SIZE &&
652 s_poolBytes + entry.allocSize <= MAX_POOL_BYTES;
653
654 if (poolable) {
655 // Keep allocated (still counted in s_totalAllocatedBytes).
656 s_bufferPool.push_back({entry.buffer, entry.memory, entry.device,
657 entry.allocSize, entry.usage,
658 entry.memoryTypeIndex, entry.bufferSize,
659 s_frameNumber});
660 s_poolBytes += entry.allocSize;
661 ++pooled;
662 } else {
663 vmaDestroyBuffer(s_allocator, entry.buffer, entry.memory);
664 s_totalAllocatedBytes -= entry.allocSize;
665 if (entry.memoryTypeIndex < s_memTypeCount)
666 s_perTypeBytes[entry.memoryTypeIndex] -= entry.allocSize;
667 }
668 entry = std::move(s_deferredDeletions.back());
669 s_deferredDeletions.pop_back();
670 } else {
671 ++i;
672 }
673 }
674
675 EvictPoolOverBudget();
676
677 // Idle drain: only after SUSTAINED inactivity (empty queue for many
678 // consecutive frames). Brief gaps while streaming keep the pool intact so
679 // it provides reuse during movement; once the player truly stops, the pool
680 // gently drains and idle VRAM returns toward baseline. Pool entries are
681 // already GPU-safe (aged past frames-in-flight before being pooled).
682 if (s_deferredDeletions.empty()) {
684 ++g_idleFrames;
685 } else {
686 for (size_t n = 0; n < kIdleTrimPerFrame && !s_bufferPool.empty(); ++n) {
687 auto& e = s_bufferPool.back();
688 vmaDestroyBuffer(s_allocator, e.buffer, e.memory);
689 s_totalAllocatedBytes -= e.allocSize;
690 s_poolBytes -= e.allocSize;
691 if (e.memoryTypeIndex < s_memTypeCount)
692 s_perTypeBytes[e.memoryTypeIndex] -= e.allocSize;
693 s_bufferPool.pop_back();
694 }
695 }
696 } else {
697 g_idleFrames = 0;
698 }
699
700#ifdef DEBUG
701 // Rate-limited VMA VRAM observability: logical live vs driver block bytes.
702 if (s_allocator && (s_frameNumber % 600) == 0) {
703 VmaTotalStatistics vs{};
704 vmaCalculateStatistics(s_allocator, &vs);
705 SLEAK_LOG(
706 "VMA: live {} MB / blocks {} MB | pool {} entries {} MB | deferred {}",
707 vs.total.statistics.allocationBytes / (1024 * 1024),
708 vs.total.statistics.blockBytes / (1024 * 1024),
709 s_bufferPool.size(), s_poolBytes / (1024 * 1024),
710 s_deferredDeletions.size());
711 }
712#endif
713}
714
715bool VulkanBuffer::TryRecycleBuffer(VkDeviceSize size,
716 VkBufferUsageFlags usage,
717 VkMemoryPropertyFlags properties,
718 VkBuffer& buffer,
719 VmaAllocation& memory,
720 VkDeviceSize* outAllocSize) {
721 if (!IsRecyclable(usage, properties) || s_bufferPool.empty()) return false;
722
723 // Best-fit: smallest pooled buffer with exact usage that is big enough.
724 size_t best = s_bufferPool.size();
725 VkDeviceSize bestSize = 0;
726 for (size_t i = 0; i < s_bufferPool.size(); ++i) {
727 const auto& e = s_bufferPool[i];
728 if (e.usage == usage && e.device == m_device && e.bufferSize >= size) {
729 if (best == s_bufferPool.size() || e.bufferSize < bestSize) {
730 best = i;
731 bestSize = e.bufferSize;
732 }
733 }
734 }
735 if (best == s_bufferPool.size()) return false;
736
737 PooledBuffer e = s_bufferPool[best];
738 s_bufferPool[best] = s_bufferPool.back();
739 s_bufferPool.pop_back();
740 s_poolBytes -= e.allocSize;
741
742 // Buffer memory binding is permanent — reuse as-is, do NOT rebind.
743 buffer = e.buffer;
744 memory = e.memory;
745 if (outAllocSize) *outAllocSize = e.allocSize;
746
747 if (&buffer == &m_buffer) {
748 m_allocSize = e.allocSize;
749 m_usage = e.usage;
750 m_memoryTypeIndex = e.memoryTypeIndex;
751 m_bufferSize = e.bufferSize;
752 }
753 return true;
754}
755
756void VulkanBuffer::EvictPoolOverBudget() {
757 // Evict oldest entries first until under both byte and count budget.
758 while ((s_poolBytes > MAX_POOL_BYTES || s_bufferPool.size() > MAX_POOL_SIZE) &&
759 !s_bufferPool.empty()) {
760 size_t oldestIdx = 0;
761 uint64_t oldestFrame = s_bufferPool[0].insertFrame;
762 for (size_t i = 1; i < s_bufferPool.size(); ++i) {
763 if (s_bufferPool[i].insertFrame < oldestFrame) {
764 oldestFrame = s_bufferPool[i].insertFrame;
765 oldestIdx = i;
766 }
767 }
768 auto& e = s_bufferPool[oldestIdx];
769 vmaDestroyBuffer(s_allocator, e.buffer, e.memory);
770 s_totalAllocatedBytes -= e.allocSize;
771 s_poolBytes -= e.allocSize;
772 if (e.memoryTypeIndex < s_memTypeCount)
773 s_perTypeBytes[e.memoryTypeIndex] -= e.allocSize;
774 s_bufferPool[oldestIdx] = s_bufferPool.back();
775 s_bufferPool.pop_back();
776 }
777}
778
780 for (auto& entry : s_deferredDeletions) {
781 vmaDestroyBuffer(s_allocator, entry.buffer, entry.memory);
782 s_totalAllocatedBytes -= entry.allocSize;
783 if (entry.memoryTypeIndex < s_memTypeCount)
784 s_perTypeBytes[entry.memoryTypeIndex] -= entry.allocSize;
785 }
786 s_deferredDeletions.clear();
787
788 // Also drain the recycling pool
789 for (auto& entry : s_bufferPool) {
790 vmaDestroyBuffer(s_allocator, entry.buffer, entry.memory);
791 s_totalAllocatedBytes -= entry.allocSize;
792 if (entry.memoryTypeIndex < s_memTypeCount)
793 s_perTypeBytes[entry.memoryTypeIndex] -= entry.allocSize;
794 }
795 s_bufferPool.clear();
796 s_poolBytes = 0;
797}
798
800 return s_totalAllocatedBytes;
801}
802
804
806 if (s_physicalDeviceGlobal == VK_NULL_HANDLE) return 0;
807
808 VkPhysicalDeviceMemoryProperties memProps;
809 vkGetPhysicalDeviceMemoryProperties(s_physicalDeviceGlobal, &memProps);
810
811 VkDeviceSize totalDeviceLocal = 0;
812 for (uint32_t i = 0; i < memProps.memoryHeapCount; i++) {
813 if (memProps.memoryHeaps[i].flags &
814 VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) {
815 totalDeviceLocal += memProps.memoryHeaps[i].size;
816 }
817 }
818 return totalDeviceLocal;
819}
820
821void VulkanBuffer::SetPhysicalDevice(VkPhysicalDevice device) {
822 s_physicalDeviceGlobal = device;
823 if (device == VK_NULL_HANDLE) return;
824 VkPhysicalDeviceMemoryProperties memProps;
825 vkGetPhysicalDeviceMemoryProperties(device, &memProps);
826 s_memTypeCount = memProps.memoryTypeCount;
827 for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) {
828 uint32_t heapIdx = memProps.memoryTypes[i].heapIndex;
829 s_memTypeIsDeviceLocal[i] = (memProps.memoryHeaps[heapIdx].flags &
830 VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0;
831 s_perTypeBytes[i] = 0;
832 }
833}
834
836 VkDeviceSize total = 0;
837 for (uint32_t i = 0; i < s_memTypeCount; ++i) {
838 if (s_memTypeIsDeviceLocal[i]) total += s_perTypeBytes[i];
839 }
840 return total;
841}
842
843void VulkanBuffer::EvictPoolForMemType(uint32_t memTypeIdx) {
844 for (size_t i = 0; i < s_bufferPool.size(); ) {
845 auto& e = s_bufferPool[i];
846 if (e.memoryTypeIndex == memTypeIdx) {
847 vmaDestroyBuffer(s_allocator, e.buffer, e.memory);
848 s_totalAllocatedBytes -= e.allocSize;
849 s_poolBytes -= e.allocSize;
850 if (memTypeIdx < s_memTypeCount)
851 s_perTypeBytes[memTypeIdx] -= e.allocSize;
852 s_bufferPool[i] = s_bufferPool.back();
853 s_bufferPool.pop_back();
854 } else {
855 ++i;
856 }
857 }
858}
859
860} // namespace RenderEngine
861} // namespace Sleak
#define SLEAK_ERROR(...)
Definition Logger.hpp:22
#define SLEAK_WARN(...)
Definition Logger.hpp:21
#define SLEAK_LOG(...)
Definition Logger.hpp:19
static void SetPhysicalDevice(VkPhysicalDevice device)
static void ProcessDeferredDeletions(uint32_t maxFramesInFlight)
bool Map() override
Maps host-visible memory for direct CPU writes.
static VkDeviceSize GetTotalAllocatedBytes()
bool Initialize(void *data) override
Allocates the buffer (recycling a pooled one if a match exists) and uploads initial data,...
static void InitAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device)
VulkanBuffer(VkDevice device, VkPhysicalDevice physicalDevice, uint32_t size, BufferType type, VkCommandPool commandPool, VkQueue graphicsQueue)
static VkDeviceSize GetDeviceLocalAllocatedBytes()
static AsyncFlushResult FlushPendingCopiesAsync(VkSemaphore signalSemaphore)
static VkDeviceSize GetDeviceLocalHeapSize()
static uint32_t g_maxFramesInFlight
static constexpr size_t kIdleTrimPerFrame
static uint64_t g_lastReclaimWarnFrame
static constexpr uint32_t kIdleFramesBeforeTrim
BufferType
GPU buffer usage kind, drives backend binding flags and layout.
static constexpr size_t kMaxDeletesPerFrame
static bool UsageIsRecyclable(VkBufferUsageFlags usage)
True for vertex/index usages, the only kinds worth pooling for reuse.
static constexpr VkDeviceSize kPoolBucket
static uint32_t g_idleFrames
static uint64_t g_lastPoolWarnFrame
static VkDeviceSize BucketSize(VkDeviceSize s)
Rounds a size up to the nearest pool bucket so similar-sized buffers can share slots.
static bool IsRecyclable(VkBufferUsageFlags usage, VkMemoryPropertyFlags props)
True when a buffer is both device-local and a recyclable usage, the pool's eligibility gate.
Root namespace for everything the engine exposes.
Definition Camera.hpp:10
Result of an async batched flush: the recorded command buffer plus staging resources to free later.
std::vector< PendingStagingCleanup > stagingBuffers