From c1743aa42d6cf5f510556e691a935f6f553a87f9 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 11 Jul 2026 13:17:31 -0400 Subject: [PATCH] [Refactor] (MG_State, MG_Backend): PipeResource storage layer + zero-copy coherent persistent maps Introduce a Mesa pipe_resource-style PipeResource that owns a GL buffer's bytes and its backend GPU resource, abstracting WHERE the authoritative bytes live: - Shadow mode (non-persistent buffers): a CPU Vector; the backend keeps its own GPU copy in sync via BufferBackendOps, exactly as before. - Persistent mode (coherent GL_MAP_PERSISTENT maps): the backend's host-visible, COHERENT, persistently-mapped GPU memory is the single source of truth. The app writes into it directly, every reader resolves against it, and NO per-write backend transfer happens. The CPU shadow is released. BufferObject no longer owns a raw shadow Vector; it holds a PipeResource and exposes one accessor, MappedData(), that all readers go through. Every buffer-data consumer (UBO payload, PBO texture upload, indirect draws, resident/streamed uploads, both backends) was migrated from GetDataReadOnly()->data() to MappedData(), so a persistent buffer's readers see GPU memory - not a stale shadow. That stale-shadow inconsistency is what corrupted rendering (wrong UBOs -> misplaced/"lost" vertices) in the first zero-copy attempt (625c8a6, reverted in 896cafc); routing every consumer through one accessor makes it structurally impossible. Backends provide the map via BufferBackendOps::AcquirePersistentMap: - DirectVulkan: a HOST_VISIBLE|HOST_COHERENT (required, not just requested), persistently mapped resident VkBuffer carrying every usage, seeded from the shadow, never recreated; AcquireResidentSlice binds it directly. - DirectGLES: EXT_buffer_storage immutable persistent+coherent glMapBufferRange, falling back to the shadow when the extension is absent. Fixes the ~7GB GpuMemory OOM + 100%-CPU/ANR running modern Blaze3D Minecraft on both Magma and Espryt (per-draw whole-buffer re-upload of the coherent persistent ring buffer), without the coherency/stale-read hazards of the reverted attempt. BufferTest: zero-copy stress guard (15,360 draws -> 0 per-draw transfers, and every reader resolves to GPU memory) + a shadow-fallback test. Host suite: 203/203 pass. Device verification pending. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 15 +- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 89 ++++++++- MobileGL/MG_Backend/DirectGLES/Managers.h | 7 + .../MG_Backend/DirectVulkan/DirectVulkan.cpp | 10 +- .../DirectVulkan/Renderer/UniformManager.cpp | 5 +- .../DirectVulkan/Renderer/VkBufferManager.cpp | 86 ++++++++- .../DirectVulkan/Renderer/VkBufferManager.h | 13 +- .../DirectVulkan/Renderer/VkBufferObject.cpp | 7 +- .../DirectVulkan/Renderer/VkBufferObject.h | 6 +- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 22 +-- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 14 +- .../GLState/BufferState/BufferObject.cpp | 106 +++++++---- .../GLState/BufferState/BufferObject.h | 43 ++++- .../GLState/BufferState/PipeResource.h | 83 +++++++++ MobileGL/MG_Test/Buffer/BufferTest.cpp | 172 +++++++++++++++++- 15 files changed, 577 insertions(+), 101 deletions(-) create mode 100644 MobileGL/MG_State/GLState/BufferState/PipeResource.h diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 5ce0daa8..e88c32b5 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -126,13 +126,12 @@ namespace MobileGL::MG_Backend::DirectGLES { auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); if (drawBuffer) { drawBuffer->SyncPersistentMappedRange(); - const auto drawData = drawBuffer->GetDataReadOnly(); const SizeT commandOffset = reinterpret_cast(indirect); - if (!drawData || commandOffset + requiredBytes > drawData->size()) { + if (commandOffset + requiredBytes > drawBuffer->GetSize()) { MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label); return nullptr; } - return drawData->data() + commandOffset; + return drawBuffer->MappedData() + commandOffset; } if (!indirect) { @@ -1561,25 +1560,23 @@ namespace MobileGL::MG_Backend::DirectGLES { drawBuffer->SyncPersistentMappedRange(); parameterBuffer->SyncPersistentMappedRange(); - const auto drawData = drawBuffer->GetDataReadOnly(); - const auto parameterData = parameterBuffer->GetDataReadOnly(); const SizeT commandOffset = reinterpret_cast(indirect); const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(maxdrawcount - 1) + sizeof(DrawElementsIndirectCommand); - if (!drawData || commandBytes > drawData->size()) { + if (commandBytes > drawBuffer->GetSize()) { MGLOG_E("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); return; } - if (!parameterData || drawcount < 0 || static_cast(drawcount) + sizeof(Uint32) > parameterData->size()) { + if (drawcount < 0 || static_cast(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) { MGLOG_E("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); return; } Uint32 actualDrawCount = 0; - std::memcpy(&actualDrawCount, parameterData->data() + drawcount, sizeof(actualDrawCount)); + std::memcpy(&actualDrawCount, parameterBuffer->MappedData() + drawcount, sizeof(actualDrawCount)); actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); - ExecuteIndexedIndirectCommands(mode, type, indexSize, drawData->data() + commandOffset, commandOffset, + ExecuteIndexedIndirectCommands(mode, type, indexSize, drawBuffer->MappedData() + commandOffset, commandOffset, drawBuffer, static_cast(actualDrawCount), stride, "MultiDrawElementsIndirectCount"); } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 5132414b..0f1a7b9f 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -293,7 +293,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(bufferObject.GetUsage()); BindBufferId(TempBufferTarget, resource.id); g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, - size > 0 ? bufferObject.GetDataReadOnly()->data() : nullptr, usage); + size > 0 ? bufferObject.MappedData() : nullptr, usage); resource.storageSize = size; resource.storageInitialized = true; resource.pendingRespecify = false; @@ -315,12 +315,83 @@ namespace MobileGL::MG_Backend::DirectGLES { if (start >= end) return; BindBufferId(TempBufferTarget, resource.id); g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start), - bufferObject.GetDataReadOnly()->data() + start); + bufferObject.MappedData() + start); + } + + // EXT_buffer_storage bit values (same numeric values as the desktop ARB + // tokens); defined locally so this compiles regardless of which GLES headers + // expose the EXT tokens. + constexpr GLbitfield kMapPersistentBit = 0x0040; + constexpr GLbitfield kMapCoherentBit = 0x0080; + constexpr GLbitfield kDynamicStorageBit = 0x0100; + + // Zero-copy persistent map: back the buffer with real immutable, + // persistently+coherently mapped GL storage (EXT_buffer_storage) and hand the + // app that mapped pointer (adopted by the frontend PipeResource). Returns + // nullptr when the extension is unavailable or the context is not current, in + // which case the frontend keeps its CPU-shadow model. Idempotent. + void* Ops_AcquirePersistentMap(BufferObject& bufferObject) { + if (!CanTouchGLNow() || !g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || + !g_GLESFuncs.glGenBuffers) { + return nullptr; + } + const SizeT size = bufferObject.GetSize(); + if (size == 0) return nullptr; + + auto* resource = static_cast(bufferObject.GetBackendResource().get()); + if (!resource) { + auto created = MakeShared(); + resource = created.get(); + bufferObject.SetBackendResource(std::move(created)); + } + resource->contextGeneration = g_bufferContextGeneration; + + if (resource->persistentMapped && resource->persistentPtr && resource->storageSize == size) { + return resource->persistentPtr; // idempotent + } + + // Need a fresh id: glBufferStorage fails on a buffer that already has + // immutable storage, and any prior mutable store is replaced anyway. + if (resource->id != 0) { + g_GLESFuncs.glDeleteBuffers(1, &resource->id); + resource->id = 0; + } + g_GLESFuncs.glGenBuffers(1, &resource->id); + if (resource->id == 0) return nullptr; + + // Seed from the shadow (MappedData() is still the shadow: the frontend + // adopts and drops it only after this returns). + BindBufferId(TempBufferTarget, resource->id); + const void* initial = bufferObject.MappedData(); + g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast(size), initial, + GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit | + kDynamicStorageBit); + void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), + GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); + if (!ptr) { + MGLOG_E("Ops_AcquirePersistentMap: glMapBufferRange(persistent) failed for buffer %u", + resource->id); + resource->persistentMapped = false; + resource->persistentPtr = nullptr; + return nullptr; + } + resource->persistentPtr = ptr; + resource->persistentMapped = true; + resource->storageSize = size; + resource->storageInitialized = true; + resource->pendingRespecify = false; + { + const std::lock_guard lock(resource->pendingMutex); + resource->pendingRanges.clear(); + } + resource->syncedChangeSerial = bufferObject.GetChangeSerial(); + return ptr; } void Ops_Respecify(BufferObject& bufferObject) { auto* resource = ResourceOf(bufferObject); if (!resource) return; // lazy: EnsureBufferResource full-uploads on creation + if (resource->persistentMapped) return; // immutable persistent storage is never respecified if (!CanTouchGLNow() || resource->id == 0 || resource->contextGeneration != g_bufferContextGeneration) { resource->pendingRespecify = true; @@ -380,7 +451,7 @@ namespace MobileGL::MG_Backend::DirectGLES { GL_MAP_WRITE_BIT | (invalidate ? GL_MAP_INVALIDATE_RANGE_BIT : 0) | (unsynchronized ? GL_MAP_UNSYNCHRONIZED_BIT : 0)); if (mappedData) { - Memcpy(mappedData, bufferObject.GetDataReadOnly()->data() + range.start, + Memcpy(mappedData, bufferObject.MappedData() + range.start, range.end - range.start); g_GLESFuncs.glUnmapBuffer(TempBufferTarget); resource->syncedChangeSerial = bufferObject.GetChangeSerial(); @@ -419,6 +490,7 @@ namespace MobileGL::MG_Backend::DirectGLES { .SubData = Ops_SubData, .FlushMappedRange = Ops_FlushMappedRange, .OnDestroy = Ops_OnDestroy, + .AcquirePersistentMap = Ops_AcquirePersistentMap, }; } // namespace @@ -491,6 +563,17 @@ namespace MobileGL::MG_Backend::DirectGLES { resource->pendingRespecify = true; resource->pendingRanges.clear(); resource->contextGeneration = g_bufferContextGeneration; + // The persistent map (and its pointer) died with the old context; the + // frontend re-acquires a fresh one on its next map. + resource->persistentMapped = false; + resource->persistentPtr = nullptr; + } + + // Zero-copy coherent persistent buffer: the app writes straight into the + // persistently mapped immutable store, so there is nothing to (re)upload at + // draw time. This is where the per-draw whole-buffer glBufferSubData used to run. + if (resource->persistentMapped && resource->persistentPtr && resource->id != 0) { + return resource; } if (resource->id == 0) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index b5bba17b..bb949a32 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -144,6 +144,13 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool pendingRespecify = false; VecRange1D pendingRanges; std::mutex pendingMutex; + // Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is + // immutable, persistently+coherently mapped, and persistentPtr is what the app + // (and the frontend PipeResource) write into directly. While set, draw-time + // sync is a no-op and no per-draw glBufferSubData is issued. Cleared on ES + // context loss. + Bool persistentMapped = false; + void* persistentPtr = nullptr; }; // Registered as the frontend's BufferBackendOps at backend init and on diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index ce664e7f..8c6d5457 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -253,13 +253,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); if (drawBuffer) { drawBuffer->SyncPersistentMappedRange(); - const auto drawData = drawBuffer->GetDataReadOnly(); const SizeT commandOffset = reinterpret_cast(indirect); - if (!drawData || commandOffset + requiredBytes > drawData->size()) { + if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) { MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label); return nullptr; } - return drawData->data() + commandOffset; + return drawBuffer->MappedData() + commandOffset; } if (!indirect) { @@ -514,14 +513,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { } parameterBuffer->SyncPersistentMappedRange(); - const auto parameterData = parameterBuffer->GetDataReadOnly(); - if (!parameterData) { + if (parameterBuffer->MappedData() == nullptr) { MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer"); return; } Uint32 actualDrawCount = 0; - std::memcpy(&actualDrawCount, parameterData->data() + drawcount, sizeof(actualDrawCount)); + std::memcpy(&actualDrawCount, parameterBuffer->MappedData() + drawcount, sizeof(actualDrawCount)); actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); MultiDrawArraysIndirect(mode, indirect, static_cast(actualDrawCount), stride); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 4c42dae9..8779c7ce 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -621,8 +621,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { frontendBinding, program.GetUniformBlockName(static_cast(blockIndex)).c_str()); bufferObject->SyncPersistentMappedRange(); - const auto bufferData = bufferObject->GetDataReadOnly(); - MOBILEGL_ASSERT(bufferData != nullptr && !bufferData->empty(), + MOBILEGL_ASSERT(bufferObject->MappedData() != nullptr && bufferObject->GetSize() != 0, "ResolveUniformBufferPayload: bound UBO data is empty for block '%s'", program.GetUniformBlockName(static_cast(blockIndex)).c_str()); @@ -650,7 +649,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const VkDeviceSize available = rangeEnd - rangeStart; outSize = blockSize; - outData = bufferData->data() + static_cast(rangeStart); + outData = bufferObject->MappedData() + static_cast(rangeStart); if (available < blockSize) { static thread_local Vector paddedUbo; paddedUbo.assign(static_cast(blockSize), 0); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index e5ab5d53..98e3d2a6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -14,6 +14,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; constexpr SizeT kLiveResourcePruneThreshold = 256; + // A zero-copy persistent buffer is created once and never recreated (the app holds + // its mapped pointer), and may be bound to any role, so it carries every usage. + // TRANSFER_DST is added by CreateResidentStorage. + constexpr VkBufferUsageFlags kPersistentBackedUsage = + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | + VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | + VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + // The app writes into the persistent map with no explicit flush, so its memory must + // be host-coherent (Adreno host-visible memory is; requiring it keeps us portable). + constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags = + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + using MG_State::GLState::BackendBufferResource; using MG_State::GLState::BufferBackendOps; using MG_State::GLState::BufferObject; @@ -40,6 +53,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + void* Ops_AcquirePersistentMap(BufferObject& bufferObject) { + if (g_activeBufferManager) { + return g_activeBufferManager->AcquirePersistentMap(bufferObject); + } + return nullptr; + } + void Ops_OnDestroy(SharedPtr&& resource) { if (g_activeBufferManager) { g_activeBufferManager->OnResourceDestroyed(std::move(resource)); @@ -55,6 +75,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { .SubData = Ops_SubData, .FlushMappedRange = Ops_FlushMappedRange, .OnDestroy = Ops_OnDestroy, + .AcquirePersistentMap = Ops_AcquirePersistentMap, }; } // namespace @@ -211,7 +232,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, - VkBufferUsageFlags usage) { + VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) { // Staged range copies write resident storage with vkCmdCopyBuffer. usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT; const Bool created = resource.buffer.Create({ @@ -220,6 +241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { .usage = usage, .memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = kResidentBufferAllocationFlags, + .requiredFlags = requiredFlags, }); if (!created || resource.buffer.Map() == nullptr) { MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)", @@ -243,7 +265,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource.pendingFullUpload = true; return false; } - if (!resource.buffer.Upload(bufferObject.GetDataReadOnly()->data(), size, 0)) { + if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) { MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed"); resource.pendingFullUpload = true; return false; @@ -258,7 +280,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } BufferSlice staging{}; - if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject.GetDataReadOnly()->data() + offset, + if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject.MappedData() + offset, static_cast(size), 16, staging)) { return false; } @@ -318,7 +340,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return; } - if (!resource->buffer.Upload(bufferObject.GetDataReadOnly()->data(), size, 0)) { + if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) { MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed"); resource->pendingFullUpload = true; } @@ -339,7 +361,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } if (!IsResourceBusy(*resource)) { - if (!resource->buffer.Upload(bufferObject.GetDataReadOnly()->data() + offset, + if (!resource->buffer.Upload(bufferObject.MappedData() + offset, static_cast(size), static_cast(offset))) { MGLOG_E("VkBufferManager::OnSubData: host upload failed"); resource->pendingFullUpload = true; @@ -375,7 +397,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // GL_MAP_UNSYNCHRONIZED_BIT: the app guarantees it does not overwrite // data the GPU is still reading; honour it with a direct host write. if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) { - if (!resource->buffer.Upload(bufferObject.GetDataReadOnly()->data() + offset, + if (!resource->buffer.Upload(bufferObject.MappedData() + offset, static_cast(size), static_cast(offset))) { MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed"); resource->pendingFullUpload = true; @@ -407,6 +429,46 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_deferredResourceReleases[m_currentFrameIndex].push_back(std::move(vkResource)); } + void* VkBufferManager::AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject) { + const VkDeviceSize size = static_cast(bufferObject.GetSize()); + if (size == 0) { + return nullptr; + } + + auto resource = std::static_pointer_cast(bufferObject.GetBackendResource()); + if (!resource) { + resource = MakeShared(); + bufferObject.SetBackendResource(resource); + TrackLiveResource(resource); + } + + // Idempotent: an already-backed buffer returns the same mapped base. + if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) { + return resource->buffer.GetMappedData(); + } + + // One-time creation of HOST_VISIBLE + HOST_COHERENT, persistently mapped storage + // carrying every usage (never recreated, so the app's pointer never dangles). Seed + // it from the current shadow - MappedData() is still the shadow here because the + // frontend adopts (and drops) the shadow only after this returns. + DeferRelease(std::move(resource->buffer)); + if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) { + resource->persistentMapped = false; + resource->storageSize = 0; + resource->usageFlags = 0; + return nullptr; + } + const Uint8* seed = bufferObject.MappedData(); + if (seed != nullptr) { + resource->buffer.Upload(seed, size, 0); + } + resource->persistentMapped = true; + resource->pendingFullUpload = false; + resource->storageSize = size; + resource->lastUseSerial = 0; + return resource->buffer.GetMappedData(); + } + Bool VkBufferManager::AcquireResidentSlice(BufferKind kind, const SharedPtr& bufferObject, BufferSlice& outSlice) { @@ -423,6 +485,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } + // Zero-copy persistent buffers already hold the app's live coherent writes in + // host-visible storage carrying every usage; bind directly, no re-upload/staging. + if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) { + resource->lastUseSerial = m_frameSerial; + outSlice = resource->buffer.GetSlice(0, size); + return outSlice.IsValid(); + } + const Bool needsRecreate = !resource->buffer.IsValid() || resource->storageSize != size || ((resource->usageFlags & requiredUsage) != requiredUsage) || resource->pendingFullUpload; @@ -432,7 +502,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (!CreateResidentStorage(*resource, size, usage)) { return false; } - if (!resource->buffer.Upload(bufferObject->GetDataReadOnly()->data(), size, 0)) { + if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) { MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed"); resource->buffer.Destroy(); resource->storageSize = 0; @@ -469,7 +539,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->GetDataReadOnly()->data(), size, 16, + if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16, outSlice)) { return false; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 93661aa5..0e346080 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -49,6 +49,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Set when an immediate op could not be applied; forces a full re-upload // on the next AcquireResidentSlice. Bool pendingFullUpload = false; + // Backs a zero-copy coherent persistent map (PipeResource GPU residency): the + // buffer is HOST_VISIBLE+COHERENT, persistently mapped, carries every usage and is + // never orphaned or recreated. Draw-time acquire binds it directly, no re-upload. + Bool persistentMapped = false; // Cached transient (streaming) slice for the current frame. BufferSlice transientSlice{}; @@ -95,6 +99,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool AcquireStreamedSlice(BufferKind kind, const SharedPtr& bufferObject, BufferSlice& outSlice); + // Zero-copy persistent map (PipeResource GPU residency): create (once) a + // HOST_VISIBLE+COHERENT, persistently mapped resident buffer carrying every usage, + // seed it from the shadow, and return its mapped base for the app to write into + // directly. Idempotent. Returns nullptr on failure (frontend keeps its shadow). + void* AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject); + // Immediate ops, dispatched from the frontend BufferBackendOps table. void OnRespecify(MG_State::GLState::BufferObject& bufferObject); void OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size); @@ -116,7 +126,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind); SharedPtr GetOrCreateResource(const SharedPtr& bufferObject); static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject); - Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage); + Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage, + VkMemoryPropertyFlags requiredFlags = 0); // Swap storage (conditional orphan) and refill it from the shadow copy. Bool SwapStorageAndUploadAll(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject); // Record a staging-slice copy into the resident storage, ordered against diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp index bfd3a1a8..1393cbe2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp @@ -49,11 +49,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Bool VkBufferObject::Create(const VkBufferObjectDesc& desc) { - return Create(desc.allocator, desc.size, desc.usage, desc.memoryUsage, desc.allocationFlags); + return Create(desc.allocator, desc.size, desc.usage, desc.memoryUsage, desc.allocationFlags, + desc.requiredFlags); } Bool VkBufferObject::Create(VmaAllocator allocator, VkDeviceSize size, VkBufferUsageFlags usage, - VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags) { + VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags, + VkMemoryPropertyFlags requiredFlags) { MOBILEGL_ASSERT(allocator != nullptr, "VkBufferObject::Create requires valid VMA allocator"); MOBILEGL_ASSERT(size > 0, "VkBufferObject::Create requires non-zero size"); @@ -69,6 +71,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VmaAllocationCreateInfo allocationInfo{}; allocationInfo.usage = memoryUsage; allocationInfo.flags = allocationFlags; + allocationInfo.requiredFlags = requiredFlags; const VkResult result = vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h index 80be2981..24314562 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h @@ -20,6 +20,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkBufferUsageFlags usage = 0; VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_AUTO; VmaAllocationCreateFlags allocationFlags = 0; + // Memory property bits the allocation MUST satisfy (e.g. HOST_VISIBLE|HOST_COHERENT + // for a persistently-mapped buffer the app writes into without explicit flushes). + VkMemoryPropertyFlags requiredFlags = 0; }; class VkBufferObject { @@ -34,7 +37,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool Create(const VkBufferObjectDesc& desc); Bool Create(VmaAllocator allocator, VkDeviceSize size, VkBufferUsageFlags usage, - VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags = 0); + VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags = 0, + VkMemoryPropertyFlags requiredFlags = 0); void Destroy(); void* Map(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 1baae4d0..2639a263 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -673,12 +673,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Uint8* base = nullptr; SizeT available = 0; if (attr.Buffer) { - const auto& data = attr.Buffer->GetDataReadOnly(); - if (!data || attr.Offset >= data->size()) { + if (attr.Offset >= attr.Buffer->GetSize()) { return; } - base = data->data() + attr.Offset; - available = data->size() - attr.Offset; + base = attr.Buffer->MappedData() + attr.Offset; + available = attr.Buffer->GetSize() - attr.Offset; } else { base = reinterpret_cast(attr.Offset); available = static_cast(firstVertex + vertexCount) * stride; @@ -2313,8 +2312,7 @@ void main() { auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex()); MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO"); if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) { - const auto indexData = indexBufferShared->GetDataReadOnly(); - MOBILEGL_ASSERT(indexData != nullptr && !indexData->empty(), "DrawElements requires non-empty EBO data"); + MOBILEGL_ASSERT(indexBufferShared->GetSize() != 0, "DrawElements requires non-empty EBO data"); if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Index, indexBufferShared, slice)) { MOBILEGL_ASSERT(false, "DrawElements skipped: failed to prepare transient index buffer"); return false; @@ -5414,18 +5412,14 @@ void main() { return; } - const auto parameterData = parameterBuffer->GetDataReadOnly(); - const auto drawData = drawBuffer->GetDataReadOnly(); - if (!parameterData || !drawData) { - MGLOG_E("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read buffers"); - return; - } + const Uint8* parameterData = parameterBuffer->MappedData(); + const Uint8* drawData = drawBuffer->MappedData(); Uint32 actualDrawCount = 0; - std::memcpy(&actualDrawCount, parameterData->data() + drawcount, sizeof(actualDrawCount)); + std::memcpy(&actualDrawCount, parameterData + drawcount, sizeof(actualDrawCount)); actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); for (Uint32 idraw = 0; idraw < actualDrawCount; ++idraw) { DrawIndexedCmdParam cmd{}; - std::memcpy(&cmd, drawData->data() + commandOffset + static_cast(idraw) * stride, sizeof(cmd)); + std::memcpy(&cmd, drawData + commandOffset + static_cast(idraw) * stride, sizeof(cmd)); vkCmdDrawIndexed(frame.commandBuffer, cmd.indexCount, cmd.instanceCount, cmd.firstIndex, cmd.vertexOffset, cmd.firstInstance); } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 2a146de3..ecb9b2f1 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -779,7 +779,7 @@ namespace MobileGL::MG_Impl::GLImpl { const auto& pixelUnpackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject(); if (pixelUnpackBufferObject) { - originalPixels = reinterpret_cast(pixelUnpackBufferObject->GetDataReadOnly()->data()) + + originalPixels = reinterpret_cast(pixelUnpackBufferObject->MappedData()) + reinterpret_cast(pixels); } if (!originalPixels) { @@ -896,7 +896,7 @@ namespace MobileGL::MG_Impl::GLImpl { if (pixelUnpackBufferObject) { MGLOG_D("TexSubImage2D_State: Using Pixel Unpack Buffer Object ID: %u", pixelUnpackBufferObject->GetExternalIndex()); - originalPixels = reinterpret_cast(pixelUnpackBufferObject->GetDataReadOnly()->data()) + + originalPixels = reinterpret_cast(pixelUnpackBufferObject->MappedData()) + reinterpret_cast(pixels); } @@ -982,7 +982,7 @@ namespace MobileGL::MG_Impl::GLImpl { const auto& pixelUnpackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject(); if (pixelUnpackBufferObject) { - originalPixels = reinterpret_cast(pixelUnpackBufferObject->GetDataReadOnly()->data()) + + originalPixels = reinterpret_cast(pixelUnpackBufferObject->MappedData()) + reinterpret_cast(pixels); } if (!originalPixels) { @@ -1393,7 +1393,7 @@ namespace MobileGL::MG_Impl::GLImpl { if (pixelUnpackBufferObject) { MGLOG_D("%s: Using Pixel Unpack Buffer Object ID: %u", __func__, pixelUnpackBufferObject->GetExternalIndex()); - originalPixels = reinterpret_cast(pixelUnpackBufferObject->GetDataReadOnly()->data()) + + originalPixels = reinterpret_cast(pixelUnpackBufferObject->MappedData()) + reinterpret_cast(pixels); } @@ -1512,7 +1512,7 @@ namespace MobileGL::MG_Impl::GLImpl { if (pixelUnpackBufferObject) { MGLOG_D("%s: Using Pixel Unpack Buffer Object ID: %u", __func__, pixelUnpackBufferObject->GetExternalIndex()); - originalPixels = reinterpret_cast(pixelUnpackBufferObject->GetDataReadOnly()->data()) + + originalPixels = reinterpret_cast(pixelUnpackBufferObject->MappedData()) + reinterpret_cast(pixels); } @@ -1603,7 +1603,7 @@ namespace MobileGL::MG_Impl::GLImpl { const auto& pixelUnpackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject(); if (pixelUnpackBufferObject) { - originalPixels = reinterpret_cast(pixelUnpackBufferObject->GetDataReadOnly()->data()) + + originalPixels = reinterpret_cast(pixelUnpackBufferObject->MappedData()) + reinterpret_cast(pixels); } @@ -3020,7 +3020,7 @@ namespace MobileGL::MG_Impl::GLImpl { const auto& pixelUnpackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject(); if (pixelUnpackBufferObject) { - originalPixels = reinterpret_cast(pixelUnpackBufferObject->GetDataReadOnly()->data()) + + originalPixels = reinterpret_cast(pixelUnpackBufferObject->MappedData()) + reinterpret_cast(pixels); } if (!originalPixels) { diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index 7937cf69..7729cf02 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -24,12 +24,11 @@ namespace MobileGL::MG_State::GLState { BufferObject::BufferObject(Uint externalIndex) : m_externalIndex(externalIndex), m_size(0), m_usage(BufferUsage::StaticDraw), m_isMapped(false), - m_mappingAccess(BufferMappingAccessBit::Null), m_mappedRange({0, 0}), m_dataPtr(MakeShared()), - m_ownsStagingData{} {} + m_mappingAccess(BufferMappingAccessBit::Null), m_mappedRange({0, 0}), m_ownsStagingData{} {} BufferObject::~BufferObject() { - if (m_backendResource && g_bufferBackendOps && g_bufferBackendOps->OnDestroy) { - g_bufferBackendOps->OnDestroy(std::move(m_backendResource)); + if (m_resource.Backend() && g_bufferBackendOps && g_bufferBackendOps->OnDestroy) { + g_bufferBackendOps->OnDestroy(m_resource.ReleaseBackend()); } } @@ -56,13 +55,22 @@ namespace MobileGL::MG_State::GLState { } } + void BufferObject::NotifyContentWrite(SizeT offset, SizeT size) { + if (m_resource.IsGpuResident()) { + // The write already landed in coherent GPU memory; the backend has no separate + // copy to sync. Only bump the serial so cached transient slices invalidate. + ++m_changeSerial; + return; + } + NotifySubData(offset, size); + } + void BufferObject::Respecify(SizeT size, const void* data) { ReleaseMemory(); m_size = size; - m_dataPtr->reserve(std::bit_ceil(size)); // power-of-2 reserve - m_dataPtr->resize(size); + m_resource.ResizeShadow(size); if (data && size > 0) { - Memcpy(m_dataPtr->data(), data, size); + Memcpy(m_resource.Bytes(), data, size); } m_isImmutableStorage = false; m_storageFlags = 0; @@ -76,12 +84,11 @@ namespace MobileGL::MG_State::GLState { void BufferObject::AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags) { ReleaseMemory(); m_size = size; - m_dataPtr->reserve(std::bit_ceil(size)); - m_dataPtr->resize(size); + m_resource.ResizeShadow(size); if (data) { - Memcpy(m_dataPtr->data(), data, size); + Memcpy(m_resource.Bytes(), data, size); } else if (size > 0) { - Memset(m_dataPtr->data(), 0, size); + Memset(m_resource.Bytes(), 0, size); } m_isImmutableStorage = true; m_storageFlags = storageFlags; @@ -94,8 +101,8 @@ namespace MobileGL::MG_State::GLState { data.size, m_size); MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent), "Cannot upload data while buffer is non-persistently mapped."); - Memcpy(m_dataPtr->data() + atOffset, data.data, data.size); - NotifySubData(atOffset, data.size); + Memcpy(m_resource.Bytes() + atOffset, data.data, data.size); + NotifyContentWrite(atOffset, data.size); } void BufferObject::SetUsage(BufferUsage usage) { @@ -105,10 +112,13 @@ namespace MobileGL::MG_State::GLState { void BufferObject::ReleaseMemory() { if (!m_isMapped) return; - if (m_mappingAccess & BufferMappingAccessBit::Write) { // if we wrote to the buffer - if (!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly + if (m_mappingAccess & BufferMappingAccessBit::Write) { // if we wrote to the buffer + // A persistent GPU-resident map wrote straight into coherent GPU memory, so + // there is nothing to copy back and no range to push down on unmap. + if (!m_resource.IsGpuResident() && + !(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) { - Memcpy(m_dataPtr->data() + m_mappedRange.start, m_stagingData.data(), + Memcpy(m_resource.Bytes() + m_mappedRange.start, m_stagingData.data(), m_mappedRange.end - m_mappedRange.start); } NotifyFlushMappedRange(m_mappedRange, m_mappingAccess); @@ -135,14 +145,20 @@ namespace MobileGL::MG_State::GLState { MOBILEGL_ASSERT(end <= m_mappedRange.end, "Flush range out of bounds: mappedRange.end (%zu) < end (%zu)", m_mappedRange.end, end); + // FLUSH_EXPLICIT maps are never GPU-resident (only coherent maps are adopted), so + // the staged bytes must be copied into the shadow before the backend reads them. if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) { - Memcpy(m_dataPtr->data() + start, m_stagingData.data() + offset, length); + Memcpy(m_resource.Bytes() + start, m_stagingData.data() + offset, length); } NotifyFlushMappedRange({start, end}, m_mappingAccess); } void BufferObject::SyncPersistentMappedRange() { if (!m_isMapped) return; + // GPU-resident: the app already wrote directly into coherent GPU memory. This is + // the whole point of the persistent-map path - the per-draw whole-buffer re-upload + // that used to run here is gone. + if (m_resource.IsGpuResident()) return; if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) return; if (!(m_mappingAccess & BufferMappingAccessBit::Write)) return; if (m_mappingAccess & BufferMappingAccessBit::FlushExplicit) return; @@ -153,6 +169,8 @@ namespace MobileGL::MG_State::GLState { void BufferObject::SyncMappedRangeForGpuRead(Range1D range) { if (!m_isMapped) return; + // GPU-resident maps already alias GPU-visible memory; nothing to push. + if (m_resource.IsGpuResident()) return; if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) return; if (!(m_mappingAccess & BufferMappingAccessBit::Write)) return; // Non-FLUSH_EXPLICIT persistent maps are already covered wholesale by @@ -170,7 +188,7 @@ namespace MobileGL::MG_State::GLState { MOBILEGL_ASSERT(atOffset + data.size <= m_size, "WritebackFromBackend out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset, data.size, m_size); - Memcpy(m_dataPtr->data() + atOffset, data.data, data.size); + Memcpy(m_resource.Bytes() + atOffset, data.data, data.size); ++m_changeSerial; } @@ -181,15 +199,15 @@ namespace MobileGL::MG_State::GLState { "UploadSubData out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset, data.size, m_size); - Memcpy(m_dataPtr->data() + atOffset, data.data, data.size); - NotifySubData(atOffset, data.size); + Memcpy(m_resource.Bytes() + atOffset, data.data, data.size); + NotifyContentWrite(atOffset, data.size); } void BufferObject::DownloadSubData(void* dst, SizeT atOffset, SizeT size) const { MOBILEGL_ASSERT(atOffset + size <= m_size, "DownloadSubData out of bounds: atOffset (%zu) + size (%zu) > m_size (%zu)", atOffset, size, m_size); - Memcpy(dst, m_dataPtr->data() + atOffset, size); + Memcpy(dst, m_resource.Bytes() + atOffset, size); } void BufferObject::CopyDataFrom(const SharedPtr& src, SizeT srcOffset, SizeT dstOffset, SizeT size) { @@ -204,9 +222,8 @@ namespace MobileGL::MG_State::GLState { "Destination buffer copy out of bounds: dstOffset (%zu) + size (%zu) > m_size (%zu)", dstOffset, size, m_size); - const Uint8* srcData = src->m_dataPtr->data() + srcOffset; - Memcpy(m_dataPtr->data() + dstOffset, srcData, size); - NotifySubData(dstOffset, size); + Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size); + NotifyContentWrite(dstOffset, size); } void* BufferObject::AcquireMemory(Bool markMapped, Bool read, Bool write) { @@ -222,14 +239,14 @@ namespace MobileGL::MG_State::GLState { if (!(m_mappingAccess & (BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer))) { - Memcpy(m_stagingData.data(), m_dataPtr->data(), m_size); + Memcpy(m_stagingData.data(), m_resource.Bytes(), m_size); } return m_stagingData.data(); } } - return m_dataPtr->data(); + return m_resource.Bytes(); } void* BufferObject::AcquireMemoryRange(Range1D range, Flags access) { @@ -242,7 +259,20 @@ namespace MobileGL::MG_State::GLState { if (access & BufferMappingAccessBit::Persistent) { m_ownsStagingData = false; - return m_dataPtr->data() + range.start; + // Zero-copy: for a coherent (non-FLUSH_EXPLICIT) persistent write map, ask the + // active backend for host-visible, coherent GPU storage and adopt it as the + // single source of truth. The backend seeds it from the current shadow before + // returning; AdoptPersistentMap then releases the shadow. Falls back to the + // shadow when the backend declines (returns null). Only attempted once - the + // storage is immutable and outlives unmap/remap. + if (!m_resource.IsGpuResident() && (access & BufferMappingAccessBit::Write) && + !(access & BufferMappingAccessBit::FlushExplicit) && g_bufferBackendOps && + g_bufferBackendOps->AcquirePersistentMap) { + if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) { + m_resource.AdoptPersistentMap(base); + } + } + return m_resource.Bytes() + range.start; } if (access & BufferMappingAccessBit::Write) { @@ -250,18 +280,22 @@ namespace MobileGL::MG_State::GLState { m_ownsStagingData = true; if (!(access & (BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer))) { - Memcpy(m_stagingData.data(), m_dataPtr->data() + range.start, m_stagingData.size()); + Memcpy(m_stagingData.data(), m_resource.Bytes() + range.start, m_stagingData.size()); } return m_stagingData.data(); } else { m_ownsStagingData = false; - return m_dataPtr->data() + range.start; + return m_resource.Bytes() + range.start; } } - const SharedPtr& BufferObject::GetDataReadOnly() const { - return m_dataPtr; + const Uint8* BufferObject::MappedData() const { + return m_resource.Bytes(); + } + + Bool BufferObject::IsBackendPersistentMapped() const { + return m_resource.IsGpuResident(); } SizeT BufferObject::GetSize() const { @@ -281,11 +315,11 @@ namespace MobileGL::MG_State::GLState { } const SharedPtr& BufferObject::GetBackendResource() const { - return m_backendResource; + return m_resource.Backend(); } void BufferObject::SetBackendResource(SharedPtr resource) { - m_backendResource = std::move(resource); + m_resource.SetBackend(std::move(resource)); } Bool BufferObject::IsMapped() const { @@ -299,12 +333,14 @@ namespace MobileGL::MG_State::GLState { void* BufferObject::GetMappedPointer() const { if (!m_isMapped) return nullptr; if (m_mappingAccess & BufferMappingAccessBit::Persistent) { - return const_cast(m_dataPtr->data()) + m_mappedRange.start; + // GPU-resident maps return the coherent GPU pointer; shadow-backed persistent + // maps return the shadow. m_resource.Bytes() resolves both. + return const_cast(m_resource.Bytes()) + m_mappedRange.start; } if (m_ownsStagingData) { return const_cast(m_stagingData.data()); } - return const_cast(m_dataPtr->data()) + m_mappedRange.start; + return const_cast(m_resource.Bytes()) + m_mappedRange.start; } Flags BufferObject::GetMappingAccess() const { diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.h b/MobileGL/MG_State/GLState/BufferState/BufferObject.h index e7ff5848..cf9ab1b3 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.h @@ -9,6 +9,7 @@ #pragma once #include #include +#include "PipeResource.h" namespace MobileGL { enum class BufferTarget { @@ -59,13 +60,9 @@ namespace MobileGL { namespace MG_State::GLState { class BufferObject; - // Opaque, refcounted handle to the backend's storage for one buffer - // (the pipe_resource analogue). The frontend owns the reference; the - // active backend derives from it and attaches its own payload. - class BackendBufferResource { - public: - virtual ~BackendBufferResource() = default; - }; + // BackendBufferResource and PipeResource (the storage abstraction that holds + // either the CPU shadow or the backend's persistently-mapped GPU memory) live + // in PipeResource.h. // Immediate buffer transfer interface implemented by the active backend // (the pipe_context buffer-op analogue). Ops are invoked at GL call time, @@ -91,6 +88,17 @@ namespace MobileGL { // Final release of the backend resource (called from ~BufferObject). // The backend defers actual destruction until the GPU is done with it. void (*OnDestroy)(SharedPtr&& resource) = nullptr; + // Zero-copy persistent mapping. For a coherent (non-FLUSH_EXPLICIT) persistent + // write map, the backend may hand back a host-visible, COHERENT, persistently + // mapped pointer into its own GPU storage for the whole buffer [0, size), + // created with every buffer usage and seeded from the shadow. From that point + // the GPU buffer is the single source of truth: the app writes into it + // directly, all reads/writes resolve against it (HostData()), and NO further + // backend transfer ops are dispatched for this buffer. Returns nullptr when the + // backend cannot back the map; the frontend then keeps the CPU-shadow model. + // Must be idempotent: a second call for an already-backed buffer returns the + // same base pointer. + void* (*AcquirePersistentMap)(BufferObject& bufferObject) = nullptr; }; // Registered by the active backend at init, cleared at shutdown. @@ -148,7 +156,16 @@ namespace MobileGL { BufferUsage GetUsage() const; Range1D GetMappedRange() const; void* GetMappedPointer() const; - const SharedPtr& GetDataReadOnly() const; + // Host-visible base pointer to the buffer's authoritative bytes for + // [0, GetSize()): the coherent persistent GPU map when the buffer is + // persistent-resident, otherwise the CPU shadow. Every reader goes through + // this so no consumer branches on where the bytes live (the class of bug + // that a partial persistent-map redirect would reintroduce). + const Uint8* MappedData() const; + // True once the buffer's bytes were adopted into backend GPU memory (a + // coherent persistent map): reads/writes hit GPU memory and no per-write + // backend transfer op is dispatched. + Bool IsBackendPersistentMapped() const; Flags GetMappingAccess() const; GLbitfield GetStorageFlags() const; Uint GetExternalIndex() const; @@ -163,11 +180,18 @@ namespace MobileGL { void NotifyRespecify(); void NotifySubData(SizeT offset, SizeT size); void NotifyFlushMappedRange(Range1D range, Flags appAccess); + // A content write of [offset, offset+size) just landed in m_resource. For a + // persistent GPU-resident buffer the bytes are already in coherent GPU memory, + // so this only bumps the change serial; otherwise it dispatches a backend + // SubData transfer to sync the backend's separate GPU copy. + void NotifyContentWrite(SizeT offset, SizeT size); const Uint m_externalIndex = 0; SizeT m_size = 0; BufferUsage m_usage = BufferUsage::StaticDraw; - SharedPtr m_dataPtr; + // Owns the buffer's bytes (CPU shadow or backend persistent GPU map) and + // the backend GPU resource. All data access goes through it. + PipeResource m_resource; Bool m_isMapped; Flags m_mappingAccess; Bool m_isImmutableStorage = false; @@ -176,7 +200,6 @@ namespace MobileGL { Range1D m_mappedRange; Vector m_stagingData; Bool m_ownsStagingData; - SharedPtr m_backendResource; }; } // namespace MG_State::GLState } // namespace MobileGL diff --git a/MobileGL/MG_State/GLState/BufferState/PipeResource.h b/MobileGL/MG_State/GLState/BufferState/PipeResource.h new file mode 100644 index 00000000..235efc0a --- /dev/null +++ b/MobileGL/MG_State/GLState/BufferState/PipeResource.h @@ -0,0 +1,83 @@ +// MobileGL - MobileGL/MG_State/GLState/BufferState/PipeResource.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include +#include +#include + +namespace MobileGL::MG_State::GLState { + // Opaque, refcounted handle to the backend's GPU storage for one buffer + // (the driver-side resource). The active backend derives from it and attaches + // its own payload (VkBufferResource / GLESBufferResource). Held by PipeResource. + class BackendBufferResource { + public: + virtual ~BackendBufferResource() = default; + }; + + // Mesa pipe_resource analogue for a GL buffer's storage. It owns the buffer's + // bytes and its backend GPU resource, and abstracts WHERE the authoritative + // bytes live so no caller has to branch on the mode: + // + // - Shadow mode (default, non-persistent buffers): the bytes live in a CPU + // Vector (the shadow). GL writes mutate the shadow; the active backend keeps + // its own GPU copy in sync via BufferBackendOps (glBufferData/SubData/...). + // + // - Persistent mode (coherent GL_MAP_PERSISTENT maps): the bytes live in the + // backend's host-visible, COHERENT, persistently-mapped GPU memory. That GPU + // buffer is the single source of truth - the app writes into it directly, + // every read/write resolves against it, and NO per-write backend transfer + // happens. The CPU shadow is released on adoption. + // + // Bytes() always returns a host-visible base pointer valid for [0, size) in both + // modes, so readers/writers just call Bytes() (the size lives on the owning + // BufferObject). (Named Bytes(), not Data(), to avoid colliding with the type + // alias Data = Vector used for the shadow.) + class PipeResource { + public: + Uint8* Bytes() { return m_gpuMapped != nullptr ? static_cast(m_gpuMapped) : m_shadow->data(); } + const Uint8* Bytes() const { + return m_gpuMapped != nullptr ? static_cast(m_gpuMapped) : m_shadow->data(); + } + + // True once the buffer's bytes have been adopted into backend GPU memory. + Bool IsGpuResident() const { return m_gpuMapped != nullptr; } + + // Shadow (re)allocation for non-persistent storage (glBufferData / + // glBufferStorage before any persistent map). Mirrors the previous + // power-of-two reserve + exact resize of the old m_dataPtr. + void ResizeShadow(SizeT size) { + m_shadow->reserve(std::bit_ceil(size == 0 ? SizeT{1} : size)); + m_shadow->resize(size); + } + // Direct shadow access, used only by the backend's upload-from-shadow path, + // which never runs for a GPU-resident (persistent) buffer. + Data& Shadow() { return *m_shadow; } + const Data& Shadow() const { return *m_shadow; } + + // Transition to persistent GPU residency: adopt the backend's coherent + // mapped base as the source of truth and drop the CPU shadow. The caller + // must have already seeded the GPU memory from the shadow (via the backend + // AcquirePersistentMap op) before calling this. + void AdoptPersistentMap(void* mappedBase) { + m_gpuMapped = mappedBase; + m_shadow->clear(); + m_shadow->shrink_to_fit(); + } + + // Backend GPU resource, owned here in both modes. + const SharedPtr& Backend() const { return m_backend; } + void SetBackend(SharedPtr backend) { m_backend = std::move(backend); } + SharedPtr ReleaseBackend() { return std::move(m_backend); } + + private: + SharedPtr m_shadow = MakeShared(); + void* m_gpuMapped = nullptr; + SharedPtr m_backend; + }; +} // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index f2038004..77d0bb95 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -691,8 +691,7 @@ TEST_F(GeneralBufferTest, General_PersistentCoherentWriteDirtyWithoutUnmap) { bufferObject->SyncPersistentMappedRange(); EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial); - const auto data = bufferObject->GetDataReadOnly(); - EXPECT_EQ(reinterpret_cast(data->data())[2], 1234); + EXPECT_EQ(reinterpret_cast(bufferObject->MappedData())[2], 1234); EXPECT_TRUE(UnmapBuffer(GL_ARRAY_BUFFER)); EXPECT_EQ(GetError(), GL_NO_ERROR); } @@ -841,3 +840,172 @@ TEST_F(GeneralBufferTest, General_GeneralTest_1) { EXPECT_EQ(GetError(), GL_NO_ERROR); } + +// --------------------------------------------------------------------------- +// Zero-copy persistent-coherent mapping via the PipeResource layer (regression +// guard for the GpuMemory OOM / rendering-corruption bug). A fake backend hands +// out a block of "GPU" memory from AcquirePersistentMap; the frontend adopts it +// as the buffer's storage. The test asserts (a) the app maps straight onto that +// GPU memory, (b) EVERY reader (MappedData(), the accessor all backend consumers +// now use) resolves to that same GPU memory rather than a stale shadow - the bug +// that corrupted UBO/vertex data - and (c) a long map/write/draw loop drives ZERO +// per-draw backend transfer ops. +namespace { + struct ZeroCopyMockBackend { + Vector gpu; // stand-in for host-visible coherent GPU storage + int acquireMapCalls = 0; + int subDataCalls = 0; + int respecifyCalls = 0; + int flushCalls = 0; + Bool provideMap = true; // false => backend declines, exercising the shadow fallback + }; + + ZeroCopyMockBackend* g_zeroCopyMock = nullptr; + + void* ZeroCopyMock_AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject) { + if (!g_zeroCopyMock || !g_zeroCopyMock->provideMap) return nullptr; + if (g_zeroCopyMock->gpu.size() != bufferObject.GetSize()) { + g_zeroCopyMock->gpu.assign(bufferObject.GetSize(), 0); + // Seed from the shadow (still current: the frontend adopts only after we return). + const Uint8* shadow = bufferObject.MappedData(); + if (shadow != nullptr && bufferObject.GetSize() > 0) { + Memcpy(g_zeroCopyMock->gpu.data(), shadow, bufferObject.GetSize()); + } + } + ++g_zeroCopyMock->acquireMapCalls; + return g_zeroCopyMock->gpu.data(); + } + + void ZeroCopyMock_Respecify(MG_State::GLState::BufferObject&) { + if (g_zeroCopyMock) ++g_zeroCopyMock->respecifyCalls; + } + void ZeroCopyMock_SubData(MG_State::GLState::BufferObject&, SizeT, SizeT) { + if (g_zeroCopyMock) ++g_zeroCopyMock->subDataCalls; + } + void ZeroCopyMock_Flush(MG_State::GLState::BufferObject&, Range1D, Flags) { + if (g_zeroCopyMock) ++g_zeroCopyMock->flushCalls; + } + void ZeroCopyMock_OnDestroy(SharedPtr&&) {} + + const MG_State::GLState::BufferBackendOps kZeroCopyMockOps = { + .Respecify = ZeroCopyMock_Respecify, + .SubData = ZeroCopyMock_SubData, + .FlushMappedRange = ZeroCopyMock_Flush, + .OnDestroy = ZeroCopyMock_OnDestroy, + .AcquirePersistentMap = ZeroCopyMock_AcquirePersistentMap, + }; + + struct ScopedBackendOps { + explicit ScopedBackendOps(const MG_State::GLState::BufferBackendOps* ops) { + MG_State::GLState::SetBufferBackendOps(ops); + } + ~ScopedBackendOps() { MG_State::GLState::SetBufferBackendOps(nullptr); } + }; +} // namespace + +TEST_F(GeneralBufferTest, General_PersistentCoherentZeroCopyStressNoPerDrawReupload) { + ZeroCopyMockBackend mock; + g_zeroCopyMock = &mock; + ScopedBackendOps scopedOps(&kZeroCopyMockOps); + + GLuint buffer = 0; + GenBuffers(1, &buffer); + BindBuffer(GL_ARRAY_BUFFER, buffer); + + constexpr SizeT kCount = 4096; // 16 KiB of GLint - a "large" dynamic ring buffer + Vector initial(kCount, 0); + BufferStorage(GL_ARRAY_BUFFER, static_cast(kCount * sizeof(GLint)), initial.data(), + GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + auto* mapped = static_cast( + MapBufferRange(GL_ARRAY_BUFFER, 0, static_cast(kCount * sizeof(GLint)), + GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT)); + ASSERT_NE(mapped, nullptr); + EXPECT_EQ(mock.acquireMapCalls, 1); + + auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer); + ASSERT_NE(bufferObject, nullptr); + EXPECT_TRUE(bufferObject->IsBackendPersistentMapped()); + // The app maps straight onto the backend's GPU storage... + EXPECT_EQ(static_cast(mapped), static_cast(mock.gpu.data())); + // ...and EVERY consumer (they all read MappedData() now) resolves to that same GPU + // memory, not a stale shadow. This is the invariant whose violation corrupted UBOs. + EXPECT_EQ(static_cast(bufferObject->MappedData()), static_cast(mock.gpu.data())); + + // Isolate the per-draw behavior: storage creation legitimately issued one Respecify. + mock.subDataCalls = 0; + mock.flushCalls = 0; + mock.respecifyCalls = 0; + + constexpr int kFrames = 240; + constexpr int kDrawsPerFrame = 64; // 15,360 draws total + for (int frame = 0; frame < kFrames; ++frame) { + for (int draw = 0; draw < kDrawsPerFrame; ++draw) { + mapped[draw] = frame * 1000 + draw; // MC writes through the coherent map + bufferObject->SyncPersistentMappedRange(); // draw-time hook + } + } + + // The crux: across 15,360 draws, NOT ONE per-draw backend transfer. + EXPECT_EQ(mock.acquireMapCalls, 1); + EXPECT_EQ(mock.subDataCalls, 0); + EXPECT_EQ(mock.flushCalls, 0); + EXPECT_EQ(mock.respecifyCalls, 0); + + // The app's writes are coherently visible in the backend storage (no copy), and a + // reader going through MappedData() sees them too. + const auto* gpuInts = reinterpret_cast(mock.gpu.data()); + const auto* viaMapped = reinterpret_cast(bufferObject->MappedData()); + for (int draw = 0; draw < kDrawsPerFrame; ++draw) { + EXPECT_EQ(mapped[draw], (kFrames - 1) * 1000 + draw); + EXPECT_EQ(gpuInts[draw], (kFrames - 1) * 1000 + draw); + EXPECT_EQ(viaMapped[draw], (kFrames - 1) * 1000 + draw); + } + + EXPECT_TRUE(UnmapBuffer(GL_ARRAY_BUFFER)); + EXPECT_EQ(mock.flushCalls, 0); + EXPECT_EQ(mock.subDataCalls, 0); + g_zeroCopyMock = nullptr; +} + +TEST_F(GeneralBufferTest, General_PersistentCoherentFallbackSyncsPerDrawWhenBackendDeclines) { + // Backend cannot back the map => the legacy CPU-shadow path must still be correct, + // and this documents the behavior the fix removed (one whole-range transfer per draw), + // proving the harness above would catch a regression (non-zero per-draw count). + ZeroCopyMockBackend mock; + mock.provideMap = false; + g_zeroCopyMock = &mock; + ScopedBackendOps scopedOps(&kZeroCopyMockOps); + + GLuint buffer = 0; + GenBuffers(1, &buffer); + BindBuffer(GL_ARRAY_BUFFER, buffer); + + constexpr SizeT kCount = 256; + Vector initial(kCount, 0); + BufferStorage(GL_ARRAY_BUFFER, static_cast(kCount * sizeof(GLint)), initial.data(), + GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + auto* mapped = static_cast( + MapBufferRange(GL_ARRAY_BUFFER, 0, static_cast(kCount * sizeof(GLint)), + GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT)); + ASSERT_NE(mapped, nullptr); + EXPECT_EQ(mock.acquireMapCalls, 0); // backend declined => shadow-backed map + + auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer); + ASSERT_NE(bufferObject, nullptr); + EXPECT_FALSE(bufferObject->IsBackendPersistentMapped()); + + constexpr int kDraws = 100; + for (int draw = 0; draw < kDraws; ++draw) { + mapped[draw % kCount] = draw; + bufferObject->SyncPersistentMappedRange(); + } + // Legacy behavior: every draw pushed the whole range -> one SubData per draw. + EXPECT_EQ(mock.subDataCalls, kDraws); + + EXPECT_TRUE(UnmapBuffer(GL_ARRAY_BUFFER)); + g_zeroCopyMock = nullptr; +}