Vulkan Schnee 0.0.1
High-performance rendering engine
Loading...
Searching...
No Matches
VulkanStagedBuffer.cpp
Go to the documentation of this file.
2
3#include <cstring>
4#include <optional>
5#include <stdexcept>
6#include <vulkan/vulkan.h>
7
9
10namespace EngineCore
11{
12
13#if !defined(HEADLESS) && !defined(COMPUTE_DEBUG)
14 constexpr VkPipelineStageFlags2 STAGED_BUFFER_DST_STAGES =
15 VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_MESH_SHADER_BIT_EXT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT;
16#else
17 constexpr VkPipelineStageFlags2 STAGED_BUFFER_DST_STAGES =
18 VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT;
19#endif
20
21constexpr VkDeviceSize MIN_BUFFER_SIZE = 64;
22
24
25void VulkanStagedBuffer::create(ApplicationContext* context, const VkDeviceSize size, const VkBufferUsageFlags usage, const bool persistentMapping)
26{
27 context_ = context;
28 usePersistentMapping_ = persistentMapping;
29 buffer.emplace(context);
30 stagingBuffer.emplace(context);
31 buffer->create(size, usage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
32 stagingBuffer->create(size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
33
35 // Persistently map staging buffer - avoids expensive map/unmap on every upload
36 // HOST_COHERENT memory can stay mapped indefinitely per Vulkan spec
37 stagingBuffer->map();
38 }
39}
40
42{
43 if (buffer) buffer->destroy();
44 if (stagingBuffer) stagingBuffer->destroy();
45}
46
47VulkanStagedBufferSyncObjects VulkanStagedBuffer::upload(const ApplicationContext* context, const void* data, const VkDeviceSize size)
48{
49 // Full upload starting at offset 0
50 return uploadPartial(context, data, size, 0);
51}
52
53VulkanStagedBufferSyncObjects VulkanStagedBuffer::uploadPartial(const ApplicationContext* context, const void* data, const VkDeviceSize size, const VkDeviceSize offset)
54{
56 // Direct memcpy to persistently mapped staging buffer - no map/unmap overhead
57 // Staging buffer uses HOST_COHERENT memory, so no flush needed
58 void* mappedMemory = stagingBuffer->getMappedMemory();
59 memcpy(mappedMemory, data, size);
60 } else {
61 // Traditional map/copy/unmap approach
62 stagingBuffer->uploadData(data, size);
63 }
64
65 VulkanStagedBufferSyncObjects stagedBufferSyncObjects{};
66
67 // Release barrier: Graphics queue releases ownership to Transfer queue
68 // This barrier is recorded in the graphics command buffer before transfer begins
69 VkBufferMemoryBarrier2 releaseBarrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2};
70 releaseBarrier.srcAccessMask = VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT;
71 releaseBarrier.srcStageMask = VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT;
72 releaseBarrier.dstAccessMask = VK_ACCESS_2_NONE; // Release doesn't need dst access
73 releaseBarrier.dstStageMask = VK_PIPELINE_STAGE_2_NONE; // Release doesn't need dst stage
74 releaseBarrier.srcQueueFamilyIndex = context->getVkGraphicsQueueFamilyIndex();
75 releaseBarrier.dstQueueFamilyIndex = context->getVkTransferQueueFamilyIndex();
76 releaseBarrier.buffer = buffer->getBuffer();
77 releaseBarrier.offset = offset; // Only barrier the region being updated
78 releaseBarrier.size = size;
79
80 // Acquire barrier: Graphics queue acquires ownership from Transfer queue
81 // This barrier is recorded in the graphics command buffer after transfer completes
82 VkBufferMemoryBarrier2 acquireBarrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2};
83 acquireBarrier.srcAccessMask = VK_ACCESS_2_NONE; // Acquire doesn't need src access
84 acquireBarrier.srcStageMask = VK_PIPELINE_STAGE_2_NONE; // Acquire doesn't need src stage
85 acquireBarrier.dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT;
86 acquireBarrier.dstStageMask = STAGED_BUFFER_DST_STAGES;
87 acquireBarrier.srcQueueFamilyIndex = context->getVkTransferQueueFamilyIndex();
88 acquireBarrier.dstQueueFamilyIndex = context->getVkGraphicsQueueFamilyIndex();
89 acquireBarrier.buffer = buffer->getBuffer();
90 acquireBarrier.offset = offset; // Only barrier the region being updated
91 acquireBarrier.size = size;
92
93 stagedBufferSyncObjects.releaseBarrier = releaseBarrier;
94 stagedBufferSyncObjects.acquireBarrier = acquireBarrier;
95
96 BufferCopyObject copyObject{};
97 copyObject.sourceBuffer = stagingBuffer->getBuffer();
98 copyObject.destinationBuffer = buffer->getBuffer();
99 copyObject.size = size;
100 copyObject.dstOffset = offset; // Copy to offset in destination buffer
101 stagedBufferSyncObjects.copyObject = copyObject;
102
103 return stagedBufferSyncObjects;
104}
105
106void VulkanStagedBuffer::setDebugName(const std::string& name, const std::optional<uint32_t> index ) {
107 debugName_ = name + (index.has_value() ? std::to_string(index.value()) : "");
108 buffer->setDebugName(name + " buffer" + (index.has_value() ? std::to_string(index.value()) : ""));
109 stagingBuffer->setDebugName(name + " staging" + (index.has_value() ? std::to_string(index.value()) : ""));
110}
111
112bool VulkanStagedBuffer::ensureSize(VkDeviceSize requiredSize, VkBufferUsageFlags usage)
113{
114 // Enforce minimum size
115 if (requiredSize < MIN_BUFFER_SIZE) {
116 requiredSize = MIN_BUFFER_SIZE;
117 }
118
119 // Check if current buffer is sufficient
120 if (buffer.has_value() && buffer->getBufferSize() >= requiredSize) {
121 return false;
122 }
123
124 // Calculate new size with 2x growth strategy for amortized O(1) resizing
125 // This avoids expensive buffer recreation on every small growth
126 VkDeviceSize currentSize = buffer.has_value() ? buffer->getBufferSize() : 0;
127 VkDeviceSize newSize = requiredSize;
128
129 // If we have an existing buffer, grow by at least 2x to avoid frequent resizes
130 if (currentSize > 0) {
131 VkDeviceSize doubledSize = currentSize * 2;
132 if (doubledSize > newSize) {
133 newSize = doubledSize;
134 }
135 }
136
137 // Cap at a reasonable maximum to avoid over-allocation (512 MB per buffer)
138 // If required size exceeds cap, allocate exactly what's needed
139 constexpr VkDeviceSize MAX_BUFFER_SIZE = 512 * 1024 * 1024;
140 if (newSize > MAX_BUFFER_SIZE && requiredSize <= MAX_BUFFER_SIZE) {
141 newSize = MAX_BUFFER_SIZE;
142 }
143
144 // Need to recreate - move old buffers to pending deletion
145 if (buffer.has_value() && stagingBuffer.has_value()) {
146 // Use aggregate initialization to avoid default-constructing VulkanBuffers
147 // (VulkanBuffer default constructor requires Engine which may not be available in tests)
149 std::move(buffer.value()),
150 std::move(stagingBuffer.value()),
152 });
153 buffer.reset();
154 stagingBuffer.reset();
155 }
156
157 // Create new buffers with grown size
158 buffer.emplace(context_);
159 stagingBuffer.emplace(context_);
160 buffer->create(newSize, usage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
161 stagingBuffer->create(newSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
162
164 // Persistently map staging buffer for fast uploads
165 stagingBuffer->map();
166 }
167
168 // Restore debug name if set
169 if (!debugName_.empty()) {
170 buffer->setDebugName(debugName_ + " buffer");
171 stagingBuffer->setDebugName(debugName_ + " staging");
172 }
173
174 return true;
175}
176
178{
179 // Decrement frame counter and delete buffers that have waited long enough
180 auto it = pendingDeletions.begin();
181 while (it != pendingDeletions.end()) {
182 if (it->framesRemaining == 0) {
183 it->deviceBuffer.destroy();
184 it->stagingBuffer.destroy();
185 it = pendingDeletions.erase(it);
186 } else {
187 it->framesRemaining--;
188 ++it;
189 }
190 }
191}
192
193VkDeviceSize VulkanStagedBuffer::getSize() const
194{
195 if (!buffer.has_value()) {
196 return 0;
197 }
198 return buffer->getBufferSize();
199}
200
201} // namespace EngineCore
The application context is the core class which stores the basic openxr and vulkan objects.
uint32_t getVkTransferQueueFamilyIndex() const
gets the transfer queue family index
uint32_t getVkGraphicsQueueFamilyIndex() const
Gets the zero-based index of the vulkan draw queue family.
VulkanStagedBufferSyncObjects uploadPartial(const ApplicationContext *context, const void *data, VkDeviceSize size, VkDeviceSize offset)
Upload data to a specific offset in the buffer (for incremental updates)
VkDeviceSize getSize() const
Gets the current size of the device buffer.
std::vector< PendingDeletion > pendingDeletions
bool ensureSize(VkDeviceSize requiredSize, VkBufferUsageFlags usage)
Ensures the buffer is at least the required size. If resizing is needed, the old buffers are moved to...
static constexpr uint32_t DELETION_DELAY_FRAMES
std::optional< VulkanBuffer > stagingBuffer
std::optional< VulkanBuffer > buffer
void setDebugName(const std::string &name, std::optional< uint32_t > index=std::nullopt)
VulkanStagedBufferSyncObjects upload(const ApplicationContext *context, const void *data, VkDeviceSize size)
void processPendingDeletions()
Destroys all buffers that were pending deletion. Call this after all frames that might reference old ...
void create(ApplicationContext *context, VkDeviceSize size, VkBufferUsageFlags usage, bool persistentMapping=false)
Creates the staged buffer with device and staging buffers.
Log category system implementation.
constexpr VkPipelineStageFlags2 STAGED_BUFFER_DST_STAGES
constexpr size_t MIN_BUFFER_SIZE
VkBuffer sourceBuffer
VkDeviceSize size
VkDeviceSize dstOffset
Offset in destination buffer (for partial uploads)
VkBuffer destinationBuffer
Old buffers waiting to be deleted after all frames complete.