[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.
This commit is contained in:
2026-07-11 20:18:38 -04:00
parent 0f99d93300
commit c1743aa42d
15 changed files with 577 additions and 101 deletions
@@ -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<SizeT>(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<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
MultiDrawArraysIndirect(mode, indirect, static_cast<GLsizei>(actualDrawCount), stride);
}