Files
fs2open.github.com/code/graphics/vulkan/VulkanBuffer.cpp
T
the-e 110c0a18c1 Resolve stale uniform bindings and improve Vulkan streaming buffer allocation handling.
Unified Vulkan buffer management with `getVkBufferForBinding` to handle outlived streaming bindings safely. Eliminated `clearPendingUniformBindings` to prevent premature state cleanup. Introduced `isFrameAllocCurrent` tracking to avoid stale memory access across frames, ensuring consistent fallback to placeholder buffers for deleted or recycled allocations. Updated descriptor writes and shadow cascades to rely on these safeguards. Fixed various out-of-lifecycle issues in deferred rendering paths and external consumers like qtFRED.
2026-11-11 08:16:38 +01:00

956 lines
31 KiB
C++

#include "VulkanBuffer.h"
#include "VulkanDeletionQueue.h"
#include "VulkanDraw.h"
#include "globalincs/pstypes.h"
namespace graphics::vulkan {
namespace {
VulkanBufferManager* g_bufferManager = nullptr;
}
VulkanBufferManager* getBufferManager()
{
Assertion(g_bufferManager != nullptr, "Vulkan BufferManager not initialized!");
return g_bufferManager;
}
void setBufferManager(VulkanBufferManager* manager)
{
g_bufferManager = manager;
}
VulkanBufferManager::VulkanBufferManager() = default;
VulkanBufferManager::~VulkanBufferManager()
{
if (m_initialized) {
shutdown();
}
}
bool VulkanBufferManager::createOneShotBuffer(vk::Flags<vk::BufferUsageFlagBits> usage, const void* data, size_t size, vk::Buffer& buf, VulkanAllocation& alloc) const
{
vk::BufferCreateInfo bufferInfo;
bufferInfo.size = size;
bufferInfo.usage = usage;
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
try {
buf = m_device.createBuffer(bufferInfo);
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "Failed to create buffer: %s\n", e.what()));
return false;
}
if (!m_memoryManager->allocateBufferMemory(buf, MemoryUsage::CpuToGpu, alloc)) {
m_device.destroyBuffer(buf);
buf = nullptr;
nprintf(("vulkan", "Failed to allocate buffer memory!\n"));
return false;
}
void* mapped = m_memoryManager->mapMemory(alloc);
if (mapped) {
memcpy(mapped, data, size);
m_memoryManager->flushMemory(alloc, 0, size);
m_memoryManager->unmapMemory(alloc);
} else {
m_memoryManager->freeAllocation(alloc);
m_device.destroyBuffer(buf);
buf = nullptr;
nprintf(("vulkan", "Failed to map buffer memory!\n"));
return false;
}
return true;
}
// ========== Frame bump allocator ==========
bool VulkanBufferManager::createFrameAllocBuffer(FrameBumpAllocator& alloc, size_t size)
{
vk::BufferCreateInfo bufferInfo;
bufferInfo.size = size;
bufferInfo.usage = vk::BufferUsageFlagBits::eVertexBuffer
| vk::BufferUsageFlagBits::eIndexBuffer
| vk::BufferUsageFlagBits::eUniformBuffer
| vk::BufferUsageFlagBits::eStorageBuffer
| vk::BufferUsageFlagBits::eTransferDst;
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
try {
alloc.buffer = m_device.createBuffer(bufferInfo);
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "Failed to create frame allocator buffer: %s\n", e.what()));
return false;
}
if (!m_memoryManager->allocateBufferMemory(alloc.buffer, MemoryUsage::CpuToGpu, alloc.allocation)) {
m_device.destroyBuffer(alloc.buffer);
alloc.buffer = nullptr;
nprintf(("vulkan", "Failed to allocate frame allocator buffer memory!\n"));
return false;
}
alloc.mappedPtr = m_memoryManager->mapMemory(alloc.allocation);
if (!alloc.mappedPtr) {
m_memoryManager->freeAllocation(alloc.allocation);
m_device.destroyBuffer(alloc.buffer);
alloc.buffer = nullptr;
alloc.allocation = {};
nprintf(("vulkan", "Failed to map frame allocator buffer!\n"));
return false;
}
alloc.capacity = size;
alloc.cursor = 0;
return true;
}
void VulkanBufferManager::initFrameAllocators()
{
for (auto & m_frameAlloc : m_frameAllocs) {
Verification(createFrameAllocBuffer(m_frameAlloc, FRAME_ALLOC_INITIAL_SIZE),
"Failed to create Vulkan frame-allocator buffer during initialization");
}
nprintf(("vulkan", "Frame bump allocators initialized: %u x %zuKB\n",
MAX_FRAMES_IN_FLIGHT, FRAME_ALLOC_INITIAL_SIZE / 1024));
}
void VulkanBufferManager::shutdownFrameAllocators()
{
for (auto & alloc : m_frameAllocs) {
if (alloc.mappedPtr) {
m_memoryManager->unmapMemory(alloc.allocation);
alloc.mappedPtr = nullptr;
}
if (alloc.buffer) {
m_device.destroyBuffer(alloc.buffer);
alloc.buffer = nullptr;
}
if (alloc.allocation.isValid()) {
m_memoryManager->freeAllocation(alloc.allocation);
alloc.allocation = {};
}
alloc.capacity = 0;
alloc.cursor = 0;
}
}
size_t VulkanBufferManager::bumpAllocate(size_t size)
{
auto& alloc = m_frameAllocs[m_currentFrame];
// Align cursor up to UBO alignment (satisfies UBO/SSBO/vertex alignment)
size_t alignedOffset = (alloc.cursor + m_uboAlignment - 1) & ~(static_cast<size_t>(m_uboAlignment) - 1);
if (alignedOffset + size > alloc.capacity) {
// Growth preserves the cursor and copies the live contents across, so every offset handed
// out before the growth still addresses the same bytes -- alignedOffset stays valid.
growFrameAllocator(alignedOffset + size);
Assertion(alignedOffset + size <= alloc.capacity,
"Frame allocator growth failed to provide enough capacity");
}
alloc.cursor = alignedOffset + size;
return alignedOffset;
}
void VulkanBufferManager::growFrameAllocator(size_t requiredEnd)
{
auto& alloc = m_frameAllocs[m_currentFrame];
// Double capacity until sufficient. requiredEnd covers the allocation that triggered the
// growth; the cursor is carried over, so the new buffer has to fit both.
size_t newCapacity = alloc.capacity > 0 ? alloc.capacity * 2 : FRAME_ALLOC_INITIAL_SIZE;
while (newCapacity < requiredEnd) {
newCapacity *= 2;
}
nprintf(("vulkan", "Growing frame allocator %u: %zuKB -> %zuKB\n",
m_currentFrame, alloc.capacity / 1024, newCapacity / 1024));
// Growth must not disturb allocations already handed out this frame. Callers keep a
// (frameAllocBuffer, frameAllocOffset) pair and go on writing through the *current*
// allocator's mapping while binding the buffer recorded at allocation time -- so if growth
// swapped the buffer out from under them, their writes would land in the new buffer while
// their draws kept reading the old one. Normally the next frame's index change forces a
// realloc before that can be observed, which is why it only shows up where the frame index
// stands still (qtFRED's briefing map renders outside flip()).
//
// So: carry the cursor over, copy the live bytes to the same offsets in the new buffer, and
// repoint every handle still referencing the old buffer. Offsets stay valid, contents stay
// intact, and writes and reads agree again.
const vk::Buffer oldBuffer = alloc.buffer;
VulkanAllocation oldAllocation = alloc.allocation; // unmapMemory takes a non-const reference
void* oldMapped = alloc.mappedPtr;
const size_t liveBytes = alloc.cursor;
FrameBumpAllocator grown = {};
Verification(createFrameAllocBuffer(grown, newCapacity), "Failed to grow Vulkan frame-allocator buffer");
if (liveBytes > 0 && oldMapped != nullptr && grown.mappedPtr != nullptr) {
memcpy(grown.mappedPtr, oldMapped, liveBytes);
m_memoryManager->flushMemory(grown.allocation, 0, liveBytes);
}
grown.cursor = liveBytes;
// Growth is not a rewind -- offsets survive it -- so the generation has to carry over too, or
// handles allocated before it would compare equal to a later generation and look fresh.
grown.generation = alloc.generation;
// The old buffer stays alive through the deletion queue so draws already recorded against it
// keep reading intact data until it retires.
auto* deletionQueue = getDeletionQueue();
if (oldMapped) {
m_memoryManager->unmapMemory(oldAllocation);
}
if (oldBuffer) {
deletionQueue->queueBuffer(oldBuffer, oldAllocation);
}
alloc = grown;
if (oldBuffer) {
for (auto& bufferObj : m_buffers) {
if (bufferObj.valid && bufferObj.isStreaming() && bufferObj.frameAllocBuffer == oldBuffer) {
bufferObj.frameAllocBuffer = alloc.buffer;
}
}
}
}
// ========== Init / Shutdown ==========
bool VulkanBufferManager::init(vk::Device device,
VulkanMemoryManager* memoryManager,
uint32_t graphicsQueueFamily,
uint32_t minUboAlignment)
{
if (m_initialized) {
nprintf(("vulkan", "VulkanBufferManager::init called when already initialized!\n"));
return false;
}
if (!device || !memoryManager) {
nprintf(("vulkan", "VulkanBufferManager::init called with null device or memory manager!\n"));
return false;
}
m_device = device;
m_memoryManager = memoryManager;
m_graphicsQueueFamily = graphicsQueueFamily;
m_currentFrame = 0;
m_uboAlignment = minUboAlignment > 0 ? minUboAlignment : 256;
// Create fallback color buffer with white (1,1,1,1) for shaders expecting vertColor
std::array<float, 4> whiteColor = { 1.0f, 1.0f, 1.0f, 1.0f };
if (!createOneShotBuffer(vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst, whiteColor.data(), sizeof(whiteColor), m_fallbackColorBuffer, m_fallbackColorAllocation)) {
nprintf(("vulkan", "VulkanBufferManager::init could not create fallback color buffer\n"));
return false;
}
std::array<float, 4> zeroTexCoord = { 0.0f, 0.0f, 0.0f, 0.0f };
if (!createOneShotBuffer(vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst, zeroTexCoord.data(), sizeof(zeroTexCoord), m_fallbackTexCoordBuffer, m_fallbackTexCoordAllocation)) {
nprintf(("vulkan", "VulkanBufferManager::init could not create fallback texcoord buffer\n"));
return false;
}
// Create fallback uniform buffer (zeros) for uninitialized descriptor set bindings
// Without this, descriptor set UBO bindings left unwritten after pool reset
// contain undefined data, causing intermittent rendering failures
std::array<float, FALLBACK_UNIFORM_BUFFER_SIZE> dummy_ubo = {};
if (!createOneShotBuffer(vk::BufferUsageFlagBits::eUniformBuffer | vk::BufferUsageFlagBits::eStorageBuffer, dummy_ubo.data(), sizeof(dummy_ubo), m_fallbackUniformBuffer, m_fallbackUniformAllocation)) {
nprintf(("vulkan", "VulkanBufferManager::init could not create fallback uniform buffer\n"));
return false;
}
initFrameAllocators();
m_initialized = true;
nprintf(("vulkan", "Vulkan Buffer Manager initialized (frame bump allocator, UBO alignment=%u, %u frames)\n",
m_uboAlignment, MAX_FRAMES_IN_FLIGHT));
return true;
}
void VulkanBufferManager::shutdown()
{
if (!m_initialized) {
return;
}
// Destroy fallback color buffer
if (m_fallbackColorBuffer) {
m_device.destroyBuffer(m_fallbackColorBuffer);
m_fallbackColorBuffer = nullptr;
}
if (m_fallbackColorAllocation.isValid()) {
m_memoryManager->freeAllocation(m_fallbackColorAllocation);
m_fallbackColorAllocation = {};
}
// Destroy fallback texcoord buffer
if (m_fallbackTexCoordBuffer) {
m_device.destroyBuffer(m_fallbackTexCoordBuffer);
m_fallbackTexCoordBuffer = nullptr;
}
if (m_fallbackTexCoordAllocation.isValid()) {
m_memoryManager->freeAllocation(m_fallbackTexCoordAllocation);
m_fallbackTexCoordAllocation = {};
}
// Destroy fallback uniform buffer
if (m_fallbackUniformBuffer) {
m_device.destroyBuffer(m_fallbackUniformBuffer);
m_fallbackUniformBuffer = nullptr;
}
if (m_fallbackUniformAllocation.isValid()) {
m_memoryManager->freeAllocation(m_fallbackUniformAllocation);
m_fallbackUniformAllocation = {};
}
// Free all remaining static buffers
for (auto& bufferObj : m_buffers) {
if (bufferObj.valid) {
if (!bufferObj.isStreaming() && bufferObj.buffer) {
m_device.destroyBuffer(bufferObj.buffer);
}
if (!bufferObj.isStreaming() && bufferObj.allocation.isValid()) {
m_memoryManager->freeAllocation(bufferObj.allocation);
}
bufferObj.valid = false;
}
}
shutdownFrameAllocators();
m_buffers.clear();
m_freeIndices.clear();
m_activeBufferCount = 0;
m_totalBufferMemory = 0;
m_initialized = false;
nprintf(("vulkan", "Vulkan Buffer Manager shutdown\n"));
}
void VulkanBufferManager::setCurrentFrame(uint32_t frameIndex, uint64_t frameNumber)
{
m_currentFrame = frameIndex % MAX_FRAMES_IN_FLIGHT;
m_currentFrameNumber = frameNumber;
// Reset bump cursor — safe because the GPU fence for this frame-in-flight
// was already waited on before setCurrentFrame is called.
m_frameAllocs[m_currentFrame].cursor = 0;
++m_frameAllocs[m_currentFrame].generation;
}
// ========== Buffer usage / memory helpers ==========
vk::BufferUsageFlags VulkanBufferManager::getVkUsageFlags(BufferType type, bool rtCapable)
{
vk::BufferUsageFlags flags = vk::BufferUsageFlagBits::eTransferDst;
switch (type) {
case BufferType::Vertex:
flags |= vk::BufferUsageFlagBits::eVertexBuffer;
break;
case BufferType::Index:
flags |= vk::BufferUsageFlagBits::eIndexBuffer;
break;
case BufferType::Uniform:
flags |= vk::BufferUsageFlagBits::eUniformBuffer;
break;
}
if (rtCapable) {
// Required to use this buffer as acceleration structure geometry input
// and to query its device address. Only valid if the VMA allocator was
// created with VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT (see
// VulkanMemoryManager::init), which is only true when
// CAPABILITY_RAYTRACED_SHADOWS was negotiated as supported.
flags |= vk::BufferUsageFlagBits::eShaderDeviceAddress |
vk::BufferUsageFlagBits::eAccelerationStructureBuildInputReadOnlyKHR;
}
return flags;
}
namespace {
MemoryPurpose bufferTypeToPurpose(BufferType type)
{
switch (type) {
case BufferType::Vertex:
case BufferType::Index:
return MemoryPurpose::Geometry;
case BufferType::Uniform:
default:
// Uniform buffers are already tracked separately via UniformBufferManager/gr_debug_stats.
return MemoryPurpose::Unknown;
}
}
}
MemoryUsage VulkanBufferManager::getMemoryUsage(BufferUsageHint hint)
{
switch (hint) {
case BufferUsageHint::Static:
// Static data goes to device-local memory for best GPU performance
// For simplicity, we use CpuToGpu which allows host writes
// A more optimized path would use staging buffers for truly static data
return MemoryUsage::CpuToGpu;
case BufferUsageHint::Dynamic:
case BufferUsageHint::Streaming:
// Frequently updated data needs to be host visible
return MemoryUsage::CpuToGpu;
case BufferUsageHint::PersistentMapping:
// Persistent mapping requires host visible memory
return MemoryUsage::CpuOnly;
default:
return MemoryUsage::CpuToGpu;
}
}
// ========== Buffer create / delete ==========
gr_buffer_handle VulkanBufferManager::createBuffer(BufferType type, BufferUsageHint usage, bool rtCapable)
{
Assert(m_initialized);
VulkanBufferObject bufferObj;
bufferObj.type = type;
bufferObj.usage = usage;
bufferObj.rtCapable = rtCapable;
bufferObj.valid = true;
// Note: actual buffer creation is deferred until data is uploaded
int index;
if (!m_freeIndices.empty()) {
// Reuse a freed slot
index = m_freeIndices.back();
m_freeIndices.pop_back();
m_buffers[index] = bufferObj;
} else {
// Add new slot
index = static_cast<int>(m_buffers.size());
m_buffers.push_back(bufferObj);
}
++m_activeBufferCount;
return gr_buffer_handle(index);
}
void VulkanBufferManager::deleteBuffer(gr_buffer_handle handle)
{
Assert(m_initialized && isValidHandle(handle));
VulkanBufferObject& bufferObj = m_buffers[handle.value()];
Assert(bufferObj.valid);
if (!bufferObj.isStreaming()) {
// Queue static buffer for deferred destruction
auto* deletionQueue = getDeletionQueue();
if (bufferObj.buffer) {
deletionQueue->queueBuffer(bufferObj.buffer, bufferObj.allocation);
m_totalBufferMemory -= bufferObj.dataSize;
}
bufferObj.buffer = nullptr;
bufferObj.allocation = {};
bufferObj.dataSize = 0;
} else {
// Streaming buffers have no per-buffer resources — just mark invalid
}
--m_activeBufferCount;
bufferObj.valid = false;
// Add to free list for reuse
m_freeIndices.push_back(handle.value());
}
// ========== createOrResizeBuffer (static only) ==========
bool VulkanBufferManager::createOrResizeBuffer(VulkanBufferObject& bufferObj, size_t size)
{
Assertion(!bufferObj.isStreaming(), "createOrResizeBuffer called on streaming buffer!");
// If buffer exists and is large enough, no-op
if (bufferObj.buffer && bufferObj.dataSize >= size) {
return true;
}
// Save old buffer info for data copy
vk::Buffer oldBuffer = bufferObj.buffer;
VulkanAllocation oldAllocation = bufferObj.allocation;
size_t oldDataSize = bufferObj.dataSize;
// Create new buffer
vk::BufferCreateInfo bufferInfo;
bufferInfo.size = size;
bufferInfo.usage = getVkUsageFlags(bufferObj.type, bufferObj.rtCapable);
// Exclusive sharing: all buffer access happens on the graphics queue (there
// is no separate transfer queue). Reintroducing async transfer would require
// eConcurrent here (or explicit queue-family ownership transfers).
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
try {
bufferObj.buffer = m_device.createBuffer(bufferInfo);
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "Failed to create Vulkan buffer: %s\n", e.what()));
bufferObj.buffer = oldBuffer;
return false;
}
// Allocate memory
MemoryUsage memUsage = getMemoryUsage(bufferObj.usage);
if (!m_memoryManager->allocateBufferMemory(
bufferObj.buffer, memUsage, bufferObj.allocation, bufferTypeToPurpose(bufferObj.type))) {
m_device.destroyBuffer(bufferObj.buffer);
bufferObj.buffer = oldBuffer;
bufferObj.allocation = oldAllocation;
return false;
}
// Preserve existing contents across the grow.
//
// This is intentionally a CPU-side copy across two host mappings, not a
// GPU-side vkCmdCopyBuffer. It is correct here because everything reaching
// createOrResizeBuffer is a static buffer, and getMemoryUsage() maps static
// usage to CpuToGpu (host-visible) memory — so both allocations are always
// mappable (the Asserts below cannot trip on this path) and their contents
// are CPU-authored, meaning the CPU already holds the authoritative bytes.
// Resize is grow-only (see the size check above), so this one-time copy is
// amortized. A device-local-only (GpuOnly) buffer could not be mapped and
// would require a vkCmdCopyBuffer with command-buffer synchronization; if
// such a usage is ever routed here, the Assert(oldMapped) will fire loudly
// rather than silently corrupting.
if (oldBuffer && oldDataSize > 0) {
void* oldMapped = m_memoryManager->mapMemory(oldAllocation);
void* newMapped = m_memoryManager->mapMemory(bufferObj.allocation);
Assert(oldMapped);
Assert(newMapped);
size_t copySize = std::min(oldDataSize, size);
// Mirror the post-write flush below: on non-coherent memory the source
// must be invalidated before the CPU reads it. This is a no-op on the
// coherent, CPU-authored buffers this path handles today, kept for
// correctness-by-construction if a non-coherent usage is ever added.
m_memoryManager->invalidateMemory(oldAllocation, 0, copySize);
memcpy(newMapped, oldMapped, copySize);
m_memoryManager->flushMemory(bufferObj.allocation, 0, copySize);
m_memoryManager->unmapMemory(oldAllocation);
m_memoryManager->unmapMemory(bufferObj.allocation);
}
// Queue old buffer for deferred destruction
if (oldBuffer) {
auto* deletionQueue = getDeletionQueue();
deletionQueue->queueBuffer(oldBuffer, oldAllocation);
m_totalBufferMemory -= oldDataSize;
}
bufferObj.dataSize = size;
m_totalBufferMemory += size;
// A new VkBuffer was created above (and thus a new device address, if
// rtCapable) -- bump the generation so cached-address consumers can
// detect staleness.
++bufferObj.generation;
return true;
}
// ========== Buffer data updates ==========
void VulkanBufferManager::updateBufferData(gr_buffer_handle handle, size_t size, const void* data)
{
Assert(m_initialized && isValidHandle(handle));
if (size == 0) {
nprintf(("vulkan", "WARNING: updateBufferData called with size 0\n"));
return;
}
VulkanBufferObject& bufferObj = m_buffers[handle.value()];
Assert(bufferObj.valid);
if (bufferObj.isStreaming()) {
auto& alloc = m_frameAllocs[m_currentFrame];
if (data) {
// Pattern A: full replacement — allocate and copy
size_t offset = bumpAllocate(size);
memcpy(static_cast<uint8_t*>(alloc.mappedPtr) + offset, data, size);
m_memoryManager->flushMemory(alloc.allocation, offset, size);
bufferObj.frameAllocBuffer = alloc.buffer;
bufferObj.frameAllocOffset = offset;
bufferObj.dataSize = size;
bufferObj.frameAllocFrame = m_currentFrame;
bufferObj.frameAllocGeneration = alloc.generation;
} else {
// Pattern B: pre-alloc for offset writes (null data)
if (bufferObj.frameAllocFrame != m_currentFrame ||
bufferObj.frameAllocGeneration != alloc.generation || size > bufferObj.dataSize) {
// First allocation this frame, the cursor was rewound under us, or need more space
size_t offset = bumpAllocate(size);
bufferObj.frameAllocBuffer = alloc.buffer;
bufferObj.frameAllocOffset = offset;
bufferObj.dataSize = size;
bufferObj.frameAllocFrame = m_currentFrame;
bufferObj.frameAllocGeneration = alloc.generation;
}
// Otherwise: same frame, same generation and size fits — keep current allocation
}
} else {
// Static / PersistentMapping path.
//
// A full-content rewrite of a buffer the GPU may still be reading from an
// in-flight frame must not write the host-visible memory in place -- host
// writes are not synchronized against already-queued GPU reads. Orphan
// instead: defer the old VkBuffer to the deletion queue and write into a
// fresh one. Draws recorded afterwards pick up the new handle via
// getVkBuffer(); frames already using the old handle keep reading intact
// data until the deletion queue retires it.
//
// Offset writes (updateBufferDataOffset) intentionally do NOT orphan:
// their dominant caller is the GPU heap appending freshly-allocated,
// never-yet-drawn regions at model page-in, which is safe in place.
//
// PersistentMapping buffers are also exempt: the engine holds their
// mapped pointer long-term (mapBuffer) and synchronizes reuse itself
// via gr_sync fences, so swapping the VkBuffer underneath would leave
// the caller writing into the orphaned allocation.
if (data != nullptr && bufferObj.buffer && bufferObj.usage == BufferUsageHint::Static &&
m_currentFrameNumber < bufferObj.lastUsedFrameNumber + MAX_FRAMES_IN_FLIGHT) {
auto* deletionQueue = getDeletionQueue();
deletionQueue->queueBuffer(bufferObj.buffer, bufferObj.allocation);
m_totalBufferMemory -= bufferObj.dataSize;
bufferObj.buffer = nullptr;
bufferObj.allocation = {};
bufferObj.dataSize = 0;
}
Verification(createOrResizeBuffer(bufferObj, size), "Failed to create or resize Vulkan buffer");
// A null data pointer just allocates/resizes the buffer without writing
if (data) {
void* mapped = m_memoryManager->mapMemory(bufferObj.allocation);
Assert(mapped);
memcpy(mapped, data, size);
m_memoryManager->flushMemory(bufferObj.allocation, 0, size);
m_memoryManager->unmapMemory(bufferObj.allocation);
}
}
}
void VulkanBufferManager::updateBufferDataOffset(gr_buffer_handle handle, size_t offset, size_t size, const void* data)
{
Assert(m_initialized && isValidHandle(handle));
VulkanBufferObject& bufferObj = m_buffers[handle.value()];
Assert(bufferObj.valid);
if (bufferObj.isStreaming()) {
// Auto-allocate if not yet allocated this frame. This happens when
// the caller skips updateBufferData (e.g. gr_add_to_immediate_buffer
// when the data fits the existing buffer size).
auto& fa = m_frameAllocs[m_currentFrame];
if (bufferObj.frameAllocFrame != m_currentFrame || bufferObj.frameAllocGeneration != fa.generation) {
size_t allocSize = std::max(bufferObj.dataSize, offset + size);
Assert(allocSize > 0);
size_t allocOffset = bumpAllocate(allocSize);
bufferObj.frameAllocBuffer = fa.buffer;
bufferObj.frameAllocOffset = allocOffset;
bufferObj.dataSize = allocSize;
bufferObj.frameAllocFrame = m_currentFrame;
bufferObj.frameAllocGeneration = fa.generation;
}
Assert(offset + size <= bufferObj.dataSize);
auto& alloc = m_frameAllocs[m_currentFrame];
size_t totalOffset = bufferObj.frameAllocOffset + offset;
memcpy(static_cast<uint8_t*>(alloc.mappedPtr) + totalOffset, data, size);
m_memoryManager->flushMemory(alloc.allocation, totalOffset, size);
} else {
// Static path
Assert(bufferObj.buffer);
Assert(offset + size <= bufferObj.dataSize);
// Map, update region, and unmap
void* mapped = m_memoryManager->mapMemory(bufferObj.allocation);
Assert(mapped);
memcpy(static_cast<uint8_t*>(mapped) + offset, data, size);
m_memoryManager->flushMemory(bufferObj.allocation, offset, size);
m_memoryManager->unmapMemory(bufferObj.allocation);
}
}
// ========== Map / Flush ==========
void* VulkanBufferManager::mapBuffer(gr_buffer_handle handle)
{
if (!m_initialized || !isValidHandle(handle)) {
return nullptr;
}
VulkanBufferObject& bufferObj = m_buffers[handle.value()];
if (!bufferObj.valid) {
return nullptr;
}
if (bufferObj.isStreaming()) {
Assert(isFrameAllocCurrent(bufferObj));
auto& alloc = m_frameAllocs[m_currentFrame];
return static_cast<uint8_t*>(alloc.mappedPtr) + bufferObj.frameAllocOffset;
}
// Static / PersistentMapping
if (!bufferObj.buffer) {
return nullptr;
}
// Only persistent mapping buffers should stay mapped
if (bufferObj.usage != BufferUsageHint::PersistentMapping) {
nprintf(("vulkan", "WARNING: mapBuffer called on non-persistent buffer\n"));
}
// Map the entire buffer
void* mapped = m_memoryManager->mapMemory(bufferObj.allocation);
return mapped;
}
void VulkanBufferManager::flushMappedBuffer(gr_buffer_handle handle, size_t offset, size_t size)
{
Assert(m_initialized && isValidHandle(handle));
VulkanBufferObject const& bufferObj = m_buffers[handle.value()];
Assert(bufferObj.valid);
if (bufferObj.isStreaming()) {
// Adjust offset for current frame's allocation
Assert(isFrameAllocCurrent(bufferObj));
auto& alloc = m_frameAllocs[m_currentFrame];
m_memoryManager->flushMemory(alloc.allocation, bufferObj.frameAllocOffset + offset, size);
} else {
m_memoryManager->flushMemory(bufferObj.allocation, offset, size);
}
}
// ========== Uniform buffer binding ==========
void VulkanBufferManager::bindUniformBuffer(uniform_block_type blockType, size_t offset, size_t size, gr_buffer_handle buffer) const
{
// Resolve the full offset NOW (frame base + caller offset) so the binding
// captures the correct allocation. The vk::Buffer is still looked up at
// draw time (via handle) to survive buffer recreation.
size_t resolvedOffset = getFrameBaseOffset(buffer) + offset;
auto* drawManager = getDrawManager();
drawManager->setPendingUniformBinding(blockType, buffer,
static_cast<vk::DeviceSize>(resolvedOffset),
static_cast<vk::DeviceSize>(size));
}
// ========== Buffer queries ==========
bool VulkanBufferManager::isFrameAllocCurrent(const VulkanBufferObject& bufferObj) const
{
if (!bufferObj.isStreaming()) {
return true;
}
return bufferObj.frameAllocFrame == m_currentFrame &&
bufferObj.frameAllocGeneration == m_frameAllocs[m_currentFrame].generation;
}
bool VulkanBufferManager::isFrameAllocCurrent(gr_buffer_handle handle) const
{
if (!isValidHandle(handle)) {
return false;
}
const VulkanBufferObject& bufferObj = m_buffers[handle.value()];
if (!bufferObj.valid) {
return false;
}
return isFrameAllocCurrent(bufferObj);
}
vk::Buffer VulkanBufferManager::getVkBuffer(gr_buffer_handle handle) const
{
if (!isValidHandle(handle)) {
return nullptr;
}
const VulkanBufferObject& bufferObj = m_buffers[handle.value()];
if (!bufferObj.valid) {
return nullptr;
}
if (bufferObj.isStreaming()) {
// Streaming buffers return the frame allocator buffer they were uploaded to
Assertion(isFrameAllocCurrent(bufferObj),
"Streaming buffer %d was allocated in frame %u (generation %u) but fetched in frame %u "
"(generation %u) -- it must be uploaded again before it can be drawn with. A binding kept "
"from an earlier frame belongs on getVkBufferForBinding().",
handle.value(), bufferObj.frameAllocFrame, bufferObj.frameAllocGeneration, m_currentFrame,
m_frameAllocs[m_currentFrame].generation);
return bufferObj.frameAllocBuffer;
} else {
// Record that this frame (potentially) references the buffer -- consulted
// by updateBufferData() to decide whether a rewrite must orphan.
bufferObj.lastUsedFrameNumber = m_currentFrameNumber;
return bufferObj.buffer;
}
}
vk::Buffer VulkanBufferManager::getVkBufferForBinding(gr_buffer_handle handle) const
{
if (!isFrameAllocCurrent(handle)) {
// Either the handle no longer resolves at all, or it is a streaming buffer whose
// sub-allocation belongs to a frame that has since been recycled. Both mean the caller
// has to fall back; only getVkBuffer() treats the latter as a bug.
return nullptr;
}
// Delegated rather than inlined: getVkBuffer()'s non-streaming branch stamps
// lastUsedFrameNumber, which is what updateBufferData() consults to decide whether a rewrite
// has to orphan. Duplicating the lookup here would mean remembering to keep that stamp.
return getVkBuffer(handle);
}
size_t VulkanBufferManager::getBufferSize(gr_buffer_handle handle) const
{
if (!isValidHandle(handle)) {
return 0;
}
const VulkanBufferObject& bufferObj = m_buffers[handle.value()];
if (!bufferObj.valid) {
return 0;
}
return bufferObj.dataSize;
}
uint64_t VulkanBufferManager::getBufferGeneration(gr_buffer_handle handle) const
{
if (!isValidHandle(handle)) {
return 0;
}
const VulkanBufferObject& bufferObj = m_buffers[handle.value()];
if (!bufferObj.valid) {
return 0;
}
return bufferObj.generation;
}
size_t VulkanBufferManager::getFrameBaseOffset(gr_buffer_handle handle) const
{
if (!isValidHandle(handle)) {
return 0;
}
const VulkanBufferObject& bufferObj = m_buffers[handle.value()];
if (!bufferObj.valid) {
return 0;
}
if (bufferObj.isStreaming()) {
// Return the bump allocator offset for the most recent upload this frame.
// Stale handle detection: a sub-allocation that is not the live one has a meaningless
// offset (the bump allocator has rewound since), which means a buffer marked
// Streaming/Dynamic is being bound for rendering without being uploaded first.
Assert(isFrameAllocCurrent(bufferObj));
return bufferObj.frameAllocOffset;
} else {
return 0;
}
}
bool VulkanBufferManager::isValidHandle(gr_buffer_handle handle) const
{
if (!handle.isValid()) {
return false;
}
if (static_cast<size_t>(handle.value()) >= m_buffers.size()) {
return false;
}
return m_buffers[handle.value()].valid;
}
VulkanBufferObject* VulkanBufferManager::getBufferObject(gr_buffer_handle handle)
{
if (!isValidHandle(handle)) {
return nullptr;
}
return &m_buffers[handle.value()];
}
const VulkanBufferObject* VulkanBufferManager::getBufferObject(gr_buffer_handle handle) const
{
if (!isValidHandle(handle)) {
return nullptr;
}
return &m_buffers[handle.value()];
}
// ========== gr_screen function pointer implementations ==========
gr_buffer_handle vulkan_create_buffer(BufferType type, BufferUsageHint usage, bool rt_capable)
{
auto* bufferManager = getBufferManager();
return bufferManager->createBuffer(type, usage, rt_capable);
}
void vulkan_delete_buffer(gr_buffer_handle handle)
{
auto* bufferManager = getBufferManager();
bufferManager->deleteBuffer(handle);
}
void vulkan_update_buffer_data(gr_buffer_handle handle, size_t size, const void* data)
{
auto* bufferManager = getBufferManager();
bufferManager->updateBufferData(handle, size, data);
}
void vulkan_update_buffer_data_offset(gr_buffer_handle handle, size_t offset, size_t size, const void* data)
{
auto* bufferManager = getBufferManager();
bufferManager->updateBufferDataOffset(handle, offset, size, data);
}
void* vulkan_map_buffer(gr_buffer_handle handle)
{
auto* bufferManager = getBufferManager();
void* result = bufferManager->mapBuffer(handle);
Assert(result);
return result;
}
void vulkan_flush_mapped_buffer(gr_buffer_handle handle, size_t offset, size_t size)
{
auto* bufferManager = getBufferManager();
bufferManager->flushMappedBuffer(handle, offset, size);
}
void vulkan_bind_uniform_buffer(uniform_block_type blockType, size_t offset, size_t size, gr_buffer_handle buffer)
{
auto* bufferManager = getBufferManager();
bufferManager->bindUniformBuffer(blockType, offset, size, buffer);
}
} // namespace graphics::vulkan