mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
[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:
@@ -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<GLESBufferResource*>(bufferObject.GetBackendResource().get());
|
||||
if (!resource) {
|
||||
auto created = MakeShared<GLESBufferResource>();
|
||||
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<GLsizeiptr>(size), initial,
|
||||
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit |
|
||||
kDynamicStorageBit);
|
||||
void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(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<std::mutex> 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) {
|
||||
|
||||
Reference in New Issue
Block a user