diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 22ae0114..d41aa01b 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -9,6 +9,7 @@ #include "BackendObject_DirectGLES.h" #include "MG_Backend/BackendObject.h" #include +#include #include #include #include @@ -632,6 +633,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_Util::BackendLoader::AcquireGLESFunctions(m_GLESFunctions, m_EGLFunctions.eglGetProcAddress); DirectGLES::SetEGLFuncsTable(m_EGLFunctions); DirectGLES::SetGLESFuncsTable(m_GLESFunctions); + BufferImpl::RegisterBufferBackendOps(); m_initialized = true; } diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index e5278a92..05f64ef5 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -109,7 +109,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) { auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); if (drawBuffer) { - drawBuffer->MarkPersistentMappedRangeDirty(); + drawBuffer->SyncPersistentMappedRange(); const auto drawData = drawBuffer->GetDataReadOnly(); const SizeT commandOffset = reinterpret_cast(indirect); if (!drawData || commandOffset + requiredBytes > drawData->size()) { @@ -177,16 +177,9 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace BufferImpl { void CreateAndSyncBufferObject(const SharedPtr& bufferObject) { - bufferObject->MarkPersistentMappedRangeDirty(); - if (!(bufferObject->GetChangeBits() & BufferChangeBits::DirtyBit)) return; - - const auto& backendBufferIt = g_backendBufferObjects.find(bufferObject.get()); - Bool exist = (backendBufferIt != g_backendBufferObjects.end()); - auto& backendObj = exist ? backendBufferIt->second : g_backendBufferObjects.GetOrCreate(bufferObject); - if (!exist) { - backendObj = MakeShared(); - } - backendObj->SyncToBackend(bufferObject); + // Immediate BufferBackendOps keep existing storage current; this only + // needs to materialize the resource (and replay pending ops). + EnsureBufferResource(bufferObject); } void SyncBufferBindingPoints(BufferTarget target, GLenum glTarget) { @@ -202,16 +195,15 @@ namespace MobileGL::MG_Backend::DirectGLES { continue; } - CreateAndSyncBufferObject(obj); - const auto& backendBufferIt = g_backendBufferObjects.find(obj.get()); - if (backendBufferIt == g_backendBufferObjects.end()) { + auto* backendResource = EnsureBufferResource(obj); + if (!backendResource || backendResource->id == 0) { MGLOG_E("No backend buffer found for %s binding point %zu.", MG_Util::ConvertGLEnumToString(glTarget).c_str(), i); continue; } const auto& range = point.GetRange(); - auto backendBufferId = backendBufferIt->second->GetBackendBufferId(); + auto backendBufferId = backendResource->id; if (range.start == 0 && range.end >= obj->GetSize()) { g_GLESFuncs.glBindBufferBase(glTarget, static_cast(i), backendBufferId); } else { @@ -233,20 +225,19 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - CreateAndSyncBufferObject(bufferObject); - const auto& backendBufferIt = g_backendBufferObjects.find(bufferObject.get()); - if (backendBufferIt == g_backendBufferObjects.end()) { + auto* backendResource = EnsureBufferResource(bufferObject); + if (!backendResource || backendResource->id == 0) { MGLOG_E("No backend buffer found for %s.", MG_Util::ConvertGLEnumToString(glTarget).c_str()); return; } - backendBufferIt->second->Bind(glTarget); + BindBufferId(glTarget, backendResource->id); } void SyncNeccessaryBuffers(Bool includeIBO = false, Bool includeIndirectBuffer = false) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - g_backendBufferObjects.CollectGarbageIfNeeded(); + ProcessDeferredBufferReleases(); // All buffers we need are: // 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed) @@ -291,7 +282,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - g_backendBufferObjects.CollectGarbageIfNeeded(); + ProcessDeferredBufferReleases(); SyncBufferBindingPoints(BufferTarget::Uniform, GL_UNIFORM_BUFFER); SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER); if (includeDispatchIndirectBuffer) { @@ -945,16 +936,15 @@ namespace MobileGL::MG_Backend::DirectGLES { auto range = point.GetRange(); if (bufferObj) { - const auto& backendBufferIt = BufferImpl::g_backendBufferObjects.find(bufferObj.get()); - if (backendBufferIt != BufferImpl::g_backendBufferObjects.end()) { - const auto& backendBufferObject = backendBufferIt->second; - backendBufferObject->Bind(GL_UNIFORM_BUFFER); + auto* backendResource = BufferImpl::EnsureBufferResource(bufferObj); + if (backendResource && backendResource->id != 0) { + BufferImpl::BindBufferId(GL_UNIFORM_BUFFER, backendResource->id); if (range.end == 0) { g_GLESFuncs.glBindBufferBase(GL_UNIFORM_BUFFER, lastUBOBinding, - backendBufferObject->GetBackendBufferId()); + backendResource->id); } else { g_GLESFuncs.glBindBufferRange( - GL_UNIFORM_BUFFER, lastUBOBinding, backendBufferObject->GetBackendBufferId(), + GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id, (GLintptr)range.start, (GLintptr)(range.end - range.start)); } } else { @@ -1232,8 +1222,8 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - drawBuffer->MarkPersistentMappedRangeDirty(); - parameterBuffer->MarkPersistentMappedRangeDirty(); + drawBuffer->SyncPersistentMappedRange(); + parameterBuffer->SyncPersistentMappedRange(); const auto drawData = drawBuffer->GetDataReadOnly(); const auto parameterData = parameterBuffer->GetDataReadOnly(); @@ -2802,8 +2792,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("ReadPixels: GL_FLOAT fallback PBO is too small"); return true; } - pixelPackBufferObject->UploadSubData({packed.data(), packed.size()}, pboOffset); - pixelPackBufferObject->ClearDirty(); + pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); } else if (pixels != nullptr && !packed.empty()) { Memcpy(pixels, packed.data(), packed.size()); } @@ -2860,8 +2849,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("ReadPixels: depth GL_FLOAT fallback PBO is too small"); return true; } - pixelPackBufferObject->UploadSubData({packed.data(), packed.size()}, pboOffset); - pixelPackBufferObject->ClearDirty(); + pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); } else if (pixels != nullptr && !packed.empty()) { Memcpy(pixels, packed.data(), packed.size()); } @@ -2918,8 +2906,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("ReadPixels: stencil GL_UNSIGNED_INT fallback PBO is too small"); return true; } - pixelPackBufferObject->UploadSubData({packed.data(), packed.size()}, pboOffset); - pixelPackBufferObject->ClearDirty(); + pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); } else if (pixels != nullptr && !packed.empty()) { Memcpy(pixels, packed.data(), packed.size()); } @@ -2982,18 +2969,16 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool usePBO; GLuint prevPixelPackBuffer = 0; if (pixelPackBufferObject) { - BufferImpl::CreateAndSyncBufferObject(pixelPackBufferObject); + auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject); MGLOG_D("ReadPixels: Using PBO %u", pixelPackBufferObject->GetExternalIndex()); usePBO = true; - const auto& backendBufferIt = BufferImpl::g_backendBufferObjects.find(pixelPackBufferObject.get()); - if (backendBufferIt == BufferImpl::g_backendBufferObjects.end()) { + if (!backendResource || backendResource->id == 0) { MGLOG_E("ReadPixels: No backend buffer found for PBO %u.", pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0); return; } - const auto& backendBufferObject = backendBufferIt->second; - backendBufferObject->Bind(GL_PIXEL_PACK_BUFFER); + BufferImpl::BindBufferId(GL_PIXEL_PACK_BUFFER, backendResource->id); g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, (GLint*)&prevPixelPackBuffer); } else { usePBO = false; @@ -3010,13 +2995,11 @@ namespace MobileGL::MG_Backend::DirectGLES { if (pboMappedPtr) { MGLOG_D("ReadPixels: Copying data from PBO to client memory"); SizeT size = pixelPackBufferObject->GetSize(); - pixelPackBufferObject->UploadSubData({pboMappedPtr, size}, 0); - pixelPackBufferObject->ClearDirty(); + pixelPackBufferObject->WritebackFromBackend({pboMappedPtr, size}, 0); MGLOG_D("ReadPixels: Unmapping PBO"); g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER); } else { MGLOG_E("ReadPixels: glMapBufferRange returned nullptr"); - MGLOG_E("ReadPixels: glMapBufferRange returned nullptr"); } MGLOG_D("ReadPixels: Restoring previous pixel pack buffer binding %u", prevPixelPackBuffer); g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, prevPixelPackBuffer); @@ -3132,17 +3115,15 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool usePBO; GLuint prevPixelPackBuffer = 0; if (pixelPackBufferObject) { - BufferImpl::CreateAndSyncBufferObject(pixelPackBufferObject); + auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject); MGLOG_D("GetTexImage: Using PBO %u", pixelPackBufferObject->GetExternalIndex()); usePBO = true; - const auto& backendBufferIt = BufferImpl::g_backendBufferObjects.find(pixelPackBufferObject.get()); - if (backendBufferIt == BufferImpl::g_backendBufferObjects.end()) { + if (!backendResource || backendResource->id == 0) { MGLOG_E("GetTexImage: No backend buffer found for PBO %u.", pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0); return; } - const auto& backendBufferObject = backendBufferIt->second; - backendBufferObject->Bind(GL_PIXEL_PACK_BUFFER); + BufferImpl::BindBufferId(GL_PIXEL_PACK_BUFFER, backendResource->id); g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, (GLint*)&prevPixelPackBuffer); } else { usePBO = false; @@ -3168,8 +3149,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (pboMappedPtr) { MGLOG_D("ReadPixels: Copying data from PBO to client memory"); SizeT size = pixelPackBufferObject->GetSize(); - pixelPackBufferObject->UploadSubData({pboMappedPtr, size}, 0); - pixelPackBufferObject->ClearDirty(); + pixelPackBufferObject->WritebackFromBackend({pboMappedPtr, size}, 0); MGLOG_D("ReadPixels: Unmapping PBO"); g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER); } else { @@ -3489,6 +3469,10 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } + namespace { + thread_local Bool t_backendContextCurrent = false; + } + Bool MakeCurrent() { if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE || g_Context == EGL_NO_CONTEXT) { @@ -3500,11 +3484,13 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error); return false; } + t_backendContextCurrent = true; return true; } Bool ReleaseCurrent() { if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY) { + t_backendContextCurrent = false; return true; } if (!g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { @@ -3512,14 +3498,21 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error); return false; } + t_backendContextCurrent = false; return true; } + Bool IsBackendContextCurrentOnThisThread() { + return t_backendContextCurrent && g_Context != EGL_NO_CONTEXT; + } + void Present() { g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface); } void DestroyEGLContext() { + BufferImpl::UnregisterBufferBackendOps(); + t_backendContextCurrent = false; if (g_Display != EGL_NO_DISPLAY) { g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (g_Context != EGL_NO_CONTEXT) { diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 053a0376..8d4fe20d 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -95,6 +95,9 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool InitPbufferSurface(EGLint width, EGLint height); Bool MakeCurrent(); Bool ReleaseCurrent(); + // True when the backend ES context is current on the calling thread, i.e. + // immediate buffer ops may issue GL calls right now. + Bool IsBackendContextCurrentOnThisThread(); void Present(); void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs); void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 401e3877..4bb02d43 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -114,171 +114,254 @@ namespace MobileGL::MG_Backend::DirectGLES { } namespace BufferImpl { - BackendBufferObject::BackendBufferObject() { + namespace { + using MG_State::GLState::BackendBufferResource; + using MG_State::GLState::BufferBackendOps; + using MG_State::GLState::BufferObject; + + // GL_ARRAY_BUFFER redundant-bind cache (id 0 = unknown/none). + Uint g_boundArrayBufferId = 0; + Bool g_boundArrayBufferKnown = false; + + // Resources whose owning BufferObject died; ids deleted at the next + // sync point with a current ES context. + Vector> g_deferredBufferReleases; + std::mutex g_deferredBufferReleasesMutex; + + GLESBufferResource* ResourceOf(BufferObject& bufferObject) { + return static_cast(bufferObject.GetBackendResource().get()); + } + + Bool CanTouchGLNow() { + return false && DirectGLES::IsBackendContextCurrentOnThisThread(); // BISECT-EXPERIMENT + } + + // (Re)specify backend storage from the shadow copy: glBufferData. + // The orphaning point - the ES driver performs the actual rename. + void RespecifyStorageNow(GLESBufferResource& resource, BufferObject& bufferObject) { #ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - g_GLESFuncs.glGenBuffers(1, &m_backendBufferId); - if (m_backendBufferId == 0) { - MGLOG_E("Failed to generate buffer object."); - MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - } else { - MGLOG_D("Generated buffer object with ID: %u.", m_backendBufferId); + const SizeT size = bufferObject.GetSize(); + 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); + resource.storageSize = size; + resource.storageInitialized = true; + resource.pendingRespecify = false; + resource.pendingRanges.clear(); } - } - void BackendBufferObject::SyncToBackend(const SharedPtr& stateBufferObject) { + Bool StorageMatches(const GLESBufferResource& resource, const BufferObject& bufferObject) { + return resource.storageInitialized && !resource.pendingRespecify && + resource.storageSize == bufferObject.GetSize(); + } + + void UploadRangeNow(GLESBufferResource& resource, BufferObject& bufferObject, SizeT start, SizeT end) { #ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - if (!stateBufferObject) { - MGLOG_E("State buffer object is null, cannot sync to backend."); - return; + if (start >= end) return; + BindBufferId(TempBufferTarget, resource.id); + g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start), + bufferObject.GetDataReadOnly()->data() + start); } - SizeT bufferSize = stateBufferObject->GetSize(); - if (bufferSize == 0) { - MGLOG_W("Buffer size is zero, skipping sync for object with ID: %u", m_backendBufferId); - return; - } - - MGLOG_D("Syncing buffer object with backend ID %u to backend for state ID %u", m_backendBufferId, - stateBufferObject->GetExternalIndex()); - - // Decide sync method - // glBufferData - Bool needsRegeneration = - !m_isInitialized || (stateBufferObject->GetChangeBits() & BufferChangeBits::PreferReallocationBit); - - if (needsRegeneration) { - MGLOG_D("Buffer size changed significantly or not initialized, regenerating buffer with ID: %u", - m_backendBufferId); - SyncToBackend_glBufferData(stateBufferObject); - m_isInitialized = true; - m_prevBufferSize = bufferSize; - stateBufferObject->ClearDirty(); - return; - } - - // glMapBufferRange or glBufferSubData - Bool useInvalidationMap = !(stateBufferObject->GetChangeBits() & BufferChangeBits::ForbidInvalidationBit); - // TODO: MAY AFFECT PERFORMANCE - Bool useUnsynchronizedMap = - !(stateBufferObject->GetChangeBits() & BufferChangeBits::ForbidUnsynchronizationBit); - Bool useMapBufferRange = PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC && - (useInvalidationMap || useUnsynchronizedMap); - - if (!useMapBufferRange && PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC) { - auto usage = stateBufferObject->GetUsage(); - if (usage == BufferUsage::DynamicDraw || usage == BufferUsage::StreamDraw || - usage == BufferUsage::StreamCopy || usage == BufferUsage::DynamicCopy) { - useMapBufferRange = true; - } - } - - if (useMapBufferRange) { - MGLOG_D("Using glMapBufferRange to sync buffer with ID: %u", m_backendBufferId); - SyncToBackend_glMapBufferRange(stateBufferObject, useInvalidationMap, useUnsynchronizedMap); - } else { - MGLOG_D("Using glBufferSubData to sync buffer with ID: %u", m_backendBufferId); - SyncToBackend_glBufferSubData(stateBufferObject); - } - - // Clear dirty state - stateBufferObject->ClearDirty(); - m_prevBufferSize = bufferSize; - } - - void BackendBufferObject::SyncToBackend_glBufferData( - const SharedPtr& stateBufferObject) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - - MGLOG_D("Syncing buffer data (glBufferData) for object with ID : %u", m_backendBufferId); - - const void* data = stateBufferObject->GetDataReadOnly()->data(); - SizeT size = stateBufferObject->GetSize(); - GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(stateBufferObject->GetUsage()); - - Bind(); - g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, data, usage); - } - - void BackendBufferObject::SyncToBackend_glBufferSubData( - const SharedPtr& stateBufferObject) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - - MGLOG_D("Syncing buffer sub-data (glBufferSubData) for object with ID : %u", m_backendBufferId); - - const void* data = stateBufferObject->GetDataReadOnly()->data(); - // dirty range: [range.start, range.end) - auto& ranges = stateBufferObject->GetDirtyRanges(); - if (ranges.empty()) { - MGLOG_D("No dirty range to sync for buffer with ID: %u", m_backendBufferId); - return; - } - - for (const auto& range : ranges) { - Bind(); - g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)range.start, - (GLintptr)(range.end - range.start), - reinterpret_cast(data) + range.start); - } - } - - void BackendBufferObject::SyncToBackend_glMapBufferRange( - const SharedPtr& stateBufferObject, Bool invalidate, Bool unsynchronized) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - - MGLOG_D("Syncing buffer map (glMapBuffer) for object with ID : %u", m_backendBufferId); - MGLOG_D("Mapping buffer with ID: %u", m_backendBufferId); - auto& ranges = stateBufferObject->GetDirtyRanges(); - if (ranges.empty()) { - MGLOG_D("No dirty range to sync for buffer with ID: %u", m_backendBufferId); - return; - } - SizeT minStart = ranges.GetOverallMinStart(); - SizeT maxEnd = ranges.GetOverallMaxEnd(); - Bind(); - void* mappedData = g_GLESFuncs.glMapBufferRange( - TempBufferTarget, (GLintptr)minStart, (GLintptr)(maxEnd - minStart), - (invalidate ? GL_MAP_INVALIDATE_RANGE_BIT : 0) | (unsynchronized ? GL_MAP_UNSYNCHRONIZED_BIT : 0) | - GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT); - const void* data = stateBufferObject->GetDataReadOnly()->data(); - if (mappedData) { - MGLOG_D("Mapped buffer data successfully for object with ID: %u", m_backendBufferId); - Memcpy(mappedData, reinterpret_cast(data) + minStart, maxEnd - minStart); - // Explicitly flush the dirty ranges - for (const auto& range : ranges) { - g_GLESFuncs.glFlushMappedBufferRange(TempBufferTarget, (GLintptr)(range.start - minStart), - (GLintptr)(range.end - range.start)); - } - g_GLESFuncs.glUnmapBuffer(TempBufferTarget); - } else { - MGLOG_E("Failed to map buffer with ID: %u", m_backendBufferId); - } - } - - void BackendBufferObject::Bind(GLenum target) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (target == GL_ARRAY_BUFFER) { - if (g_boundVertexBufferObject == this) { + void Ops_Respecify(BufferObject& bufferObject) { + auto* resource = ResourceOf(bufferObject); + if (!resource) return; // lazy: EnsureBufferResource full-uploads on creation + if (!CanTouchGLNow() || resource->id == 0) { + resource->pendingRespecify = true; + resource->pendingRanges.clear(); return; } - g_boundVertexBufferObject = this; + if (bufferObject.GetSize() == 0) { + resource->storageInitialized = false; + resource->storageSize = 0; + resource->pendingRespecify = false; + resource->pendingRanges.clear(); + return; + } + RespecifyStorageNow(*resource, bufferObject); } - g_GLESFuncs.glBindBuffer(target, m_backendBufferId); + + void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) { + auto* resource = ResourceOf(bufferObject); + if (!resource) return; + if (resource->pendingRespecify) return; // full re-upload pending anyway + if (!CanTouchGLNow() || resource->id == 0 || !StorageMatches(*resource, bufferObject)) { + resource->pendingRanges.Add({offset, offset + size}); + return; + } + UploadRangeNow(*resource, bufferObject, offset, offset + size); + } + + void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range, + Flags appAccess) { + auto* resource = ResourceOf(bufferObject); + if (!resource) return; + if (resource->pendingRespecify) return; + if (!CanTouchGLNow() || resource->id == 0 || !StorageMatches(*resource, bufferObject)) { + resource->pendingRanges.Add(range); + return; + } + + // Honour the app's real mapping flags per call: only reach for a + // mapped upload when the app allowed invalidation/unsynchronized + // access, otherwise a plain glBufferSubData carries the exact + // synchronization semantics. + const Bool invalidate = (appAccess & BufferMappingAccessBit::InvalidateRange) || + (appAccess & BufferMappingAccessBit::InvalidateBuffer); + const Bool unsynchronized = static_cast(appAccess & BufferMappingAccessBit::Unsynchronized); + if (PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC && (invalidate || unsynchronized)) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + BindBufferId(TempBufferTarget, resource->id); + void* mappedData = g_GLESFuncs.glMapBufferRange( + TempBufferTarget, (GLintptr)range.start, (GLsizeiptr)(range.end - range.start), + 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, + range.end - range.start); + g_GLESFuncs.glUnmapBuffer(TempBufferTarget); + return; + } + MGLOG_E("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData", + resource->id); + } + UploadRangeNow(*resource, bufferObject, range.start, range.end); + } + + void Ops_OnDestroy(SharedPtr&& resource) { + if (!resource) return; + auto* glesResource = static_cast(resource.get()); + if (CanTouchGLNow()) { + if (glesResource->id != 0) { + if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { + InvalidateArrayBufferBindingCache(); + } + g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); + glesResource->id = 0; + } + return; + } + const std::lock_guard lock(g_deferredBufferReleasesMutex); + g_deferredBufferReleases.push_back(std::move(resource)); + } + + const BufferBackendOps g_glesBufferBackendOps = { + .Respecify = Ops_Respecify, + .SubData = Ops_SubData, + .FlushMappedRange = Ops_FlushMappedRange, + .OnDestroy = Ops_OnDestroy, + }; + } // namespace + + void RegisterBufferBackendOps() { + MG_State::GLState::SetBufferBackendOps(&g_glesBufferBackendOps); } - StateBackendObjectRegistry g_backendBufferObjects; - BackendBufferObject* g_boundVertexBufferObject = nullptr; + void UnregisterBufferBackendOps() { + if (MG_State::GLState::GetBufferBackendOps() == &g_glesBufferBackendOps) { + MG_State::GLState::SetBufferBackendOps(nullptr); + } + InvalidateArrayBufferBindingCache(); + const std::lock_guard lock(g_deferredBufferReleasesMutex); + // The ES context owning these ids is going away; just drop the handles. + g_deferredBufferReleases.clear(); + } + + void ProcessDeferredBufferReleases() { + if (!CanTouchGLNow()) return; + Vector> releases; + { + const std::lock_guard lock(g_deferredBufferReleasesMutex); + releases.swap(g_deferredBufferReleases); + } + for (auto& resource : releases) { + auto* glesResource = static_cast(resource.get()); + if (glesResource->id != 0) { + if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { + InvalidateArrayBufferBindingCache(); + } + g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); + glesResource->id = 0; + } + } + } + + GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject) { + if (!bufferObject) return nullptr; + return static_cast(bufferObject->GetBackendResource().get()); + } + + GLESBufferResource* EnsureBufferResource(const SharedPtr& bufferObject) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (!bufferObject) return nullptr; + + auto* resource = static_cast(bufferObject->GetBackendResource().get()); + if (!resource) { + auto newResource = MakeShared(); + newResource->pendingRespecify = true; + resource = newResource.get(); + bufferObject->SetBackendResource(std::move(newResource)); + } + + if (resource->id == 0) { + g_GLESFuncs.glGenBuffers(1, &resource->id); + if (resource->id == 0) { + MGLOG_E("Failed to generate buffer object."); + MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + return resource; + } + resource->storageInitialized = false; + resource->pendingRespecify = true; + } + + // Push persistently-mapped writes first; lands either as an immediate + // SubData (fresh storage) or as part of the full re-upload below. + bufferObject->SyncPersistentMappedRange(); + + if (bufferObject->GetSize() == 0) { + return resource; + } + + if (resource->pendingRespecify || !resource->storageInitialized || + resource->storageSize != bufferObject->GetSize()) { + RespecifyStorageNow(*resource, *bufferObject); + } else if (!resource->pendingRanges.empty()) { + for (const auto& range : resource->pendingRanges) { + const SizeT end = std::min(range.end, bufferObject->GetSize()); + UploadRangeNow(*resource, *bufferObject, std::min(range.start, end), end); + } + resource->pendingRanges.clear(); + } + return resource; + } + + void BindBufferId(GLenum target, Uint id) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (false && target == GL_ARRAY_BUFFER) { // BISECT2: cache disabled + if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) { + return; + } + g_boundArrayBufferId = id; + g_boundArrayBufferKnown = true; + } + g_GLESFuncs.glBindBuffer(target, id); + } + + void InvalidateArrayBufferBindingCache() { + g_boundArrayBufferId = 0; + g_boundArrayBufferKnown = false; + } } // namespace BufferImpl namespace VertexArrayImpl { @@ -346,14 +429,13 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } - const auto& backendBufferIt = BufferImpl::g_backendBufferObjects.find(bufferObject.get()); - if (backendBufferIt == BufferImpl::g_backendBufferObjects.end()) { + auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject); + if (!backendResource || backendResource->id == 0) { MGLOG_E("No backend buffer found for attribute's buffer, cannot bind attribute."); return false; } - const auto& backendBufferObject = backendBufferIt->second; - backendBufferObject->Bind(GL_ARRAY_BUFFER); + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, backendResource->id); return true; } @@ -416,10 +498,9 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& indexBufferBinding = stateVAOObject->GetIndexBufferBindingSlot().GetBoundObject(); Bool indexBufferSynced = false; if (indexBufferBinding) { - const auto& backendBufferIt = BufferImpl::g_backendBufferObjects.find(indexBufferBinding.get()); - if (backendBufferIt != BufferImpl::g_backendBufferObjects.end()) { - const auto& backendBufferObject = backendBufferIt->second; - backendBufferObject->Bind(GL_ELEMENT_ARRAY_BUFFER); + auto* backendResource = BufferImpl::EnsureBufferResource(indexBufferBinding); + if (backendResource && backendResource->id != 0) { + BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, backendResource->id); indexBufferSynced = true; } else { MGLOG_W("No backend buffer found for index buffer binding, cannot bind index buffer."); @@ -471,7 +552,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - g_GLESFuncs.glBindBuffer(GL_ARRAY_BUFFER, bufferId); + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId); g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast(uploadSize), clientData, GL_STREAM_DRAW); @@ -486,7 +567,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - BufferImpl::g_boundVertexBufferObject = nullptr; } StateBackendObjectRegistry @@ -1112,22 +1192,15 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool needsRegeneration = !m_isInitialized || (currentTextureInfo != m_prevTextureInfo); // Need to sync texture buffer if not synced yet - auto& backendBuffers = BufferImpl::g_backendBufferObjects; - SharedPtr backendBufferObject; - const auto& backendBufferIt = backendBuffers.find(buffer.get()); - if (backendBufferIt == backendBuffers.end()) { - auto& backendBufferSlot = backendBuffers.GetOrCreate(buffer); - if (!backendBufferSlot) { - backendBufferSlot = MakeShared(); - } - backendBufferObject = backendBufferSlot; - } else { - backendBufferObject = backendBufferIt->second; + auto* backendBufferResource = BufferImpl::EnsureBufferResource(buffer); + if (!backendBufferResource || backendBufferResource->id == 0) { + MGLOG_E("Failed to sync backing buffer for texture buffer with ID: %u", + stateTextureObject->GetExternalIndex()); + return; } - backendBufferObject->SyncToBackend(buffer); // Bind buffer to texture - auto backendId = backendBufferObject->GetBackendBufferId(); + auto backendId = backendBufferResource->id; GLenum glInternalFormat, glType, glFormat; TextureImpl::GenerateTextureFormatInfo(textureBufferObject->GetFormat(), &glInternalFormat, &glFormat, diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 19c04241..736a2969 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -115,26 +115,42 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace BufferImpl { const GLenum TempBufferTarget = GL_ARRAY_BUFFER; - class BackendBufferObject { + + // The DirectGLES storage behind one frontend buffer. Owned (refcounted) by + // the frontend BufferObject; immediate BufferBackendOps keep it current, so + // draw-time "sync" reduces to ensuring the storage exists. + class GLESBufferResource : public MG_State::GLState::BackendBufferResource { public: - BackendBufferObject(); - void SyncToBackend(const SharedPtr& stateBufferObject); - Uint GetBackendBufferId() const { return m_backendBufferId; } - void Bind(GLenum target = TempBufferTarget); + ~GLESBufferResource() override = default; - private: - void SyncToBackend_glBufferData(const SharedPtr& stateBufferObject); - void SyncToBackend_glBufferSubData(const SharedPtr& stateBufferObject); - void SyncToBackend_glMapBufferRange(const SharedPtr& stateBufferObject, - Bool invalidate = true, Bool unsynchronized = true); - - Uint m_backendBufferId = 0; - SizeT m_prevBufferSize = 0; - Bool m_isInitialized = false; + Uint id = 0; + SizeT storageSize = 0; + Bool storageInitialized = false; + // Ops that arrived while no ES context was current on the calling thread + // (or before storage existed); replayed by EnsureBufferResource. + Bool pendingRespecify = false; + VecRange1D pendingRanges; }; - extern BackendBufferObject* g_boundVertexBufferObject; - extern StateBackendObjectRegistry g_backendBufferObjects; + // Registered as the frontend's BufferBackendOps at backend init. + void RegisterBufferBackendOps(); + void UnregisterBufferBackendOps(); + + // Get-or-create the backend resource and bring its storage up to date + // (creates the GL buffer, replays pending ops, pushes persistent-mapped + // ranges). Requires the ES context to be current. Returns nullptr only + // for null input. + GLESBufferResource* EnsureBufferResource(const SharedPtr& bufferObject); + // Existing resource or nullptr; performs no GL calls. + GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject); + + // Deletes GL buffers whose owning frontend objects died (possibly on a + // thread without a current ES context). Called from draw-time sync. + void ProcessDeferredBufferReleases(); + + // glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER. + void BindBufferId(GLenum target, Uint id); + void InvalidateArrayBufferBindingCache(); } // namespace BufferImpl namespace VertexArrayImpl { diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index b01c94e4..1ef665fa 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -229,7 +229,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) { auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); if (drawBuffer) { - drawBuffer->MarkPersistentMappedRangeDirty(); + drawBuffer->SyncPersistentMappedRange(); const auto drawData = drawBuffer->GetDataReadOnly(); const SizeT commandOffset = reinterpret_cast(indirect); if (!drawData || commandOffset + requiredBytes > drawData->size()) { @@ -480,7 +480,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return; } - parameterBuffer->MarkPersistentMappedRangeDirty(); + parameterBuffer->SyncPersistentMappedRange(); const auto parameterData = parameterBuffer->GetDataReadOnly(); if (!parameterData) { MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer"); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 09496835..af92fc65 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -382,7 +382,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } BufferSlice slice{}; - if (!m_bufferManager->SyncResidentBuffer(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) { + if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) { MGLOG_E("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u", bufferObject->GetExternalIndex(), texture->GetExternalIndex()); return false; @@ -457,7 +457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } BufferSlice slice{}; - if (!m_bufferManager->SyncResidentBuffer(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) { + if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) { MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'", bufferObject->GetExternalIndex(), programObj.storageBlockNameByBinding[binding].c_str()); return false; @@ -622,7 +622,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(bufferObject != nullptr, "ResolveUniformBufferPayload: no UBO bound at frontend binding %u for block '%s'", frontendBinding, program.GetUniformBlockName(static_cast(blockIndex)).c_str()); - bufferObject->MarkPersistentMappedRangeDirty(); + bufferObject->SyncPersistentMappedRange(); const auto bufferData = bufferObject->GetDataReadOnly(); MOBILEGL_ASSERT(bufferData != nullptr && !bufferData->empty(), diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 99e8a5df..a8312026 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -10,9 +10,52 @@ namespace MobileGL::MG_Backend::DirectVulkan { namespace { - constexpr Uint32 kResidentBufferGCInterval = 60; constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + constexpr SizeT kLiveResourcePruneThreshold = 256; + + using MG_State::GLState::BackendBufferResource; + using MG_State::GLState::BufferBackendOps; + using MG_State::GLState::BufferObject; + + // The manager owned by the active VulkanRenderer; immediate ops route here. + VkBufferManager* g_activeBufferManager = nullptr; + + void Ops_Respecify(BufferObject& bufferObject) { + if (g_activeBufferManager) { + g_activeBufferManager->OnRespecify(bufferObject); + } + } + + void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) { + if (g_activeBufferManager) { + g_activeBufferManager->OnSubData(bufferObject, offset, size); + } + } + + void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range, + Flags appAccess) { + if (g_activeBufferManager) { + g_activeBufferManager->OnFlushMappedRange(bufferObject, range, appAccess); + } + } + + void Ops_OnDestroy(SharedPtr&& resource) { + if (g_activeBufferManager) { + g_activeBufferManager->OnResourceDestroyed(std::move(resource)); + } + // No active manager: the device/allocator is gone or going away and + // Shutdown() already destroyed the storage; dropping the handle here + // must not touch Vulkan. VkBufferResource's dtor destroys via VMA only + // when the allocation is still valid, which Shutdown() cleared. + } + + const BufferBackendOps g_vulkanBufferBackendOps = { + .Respecify = Ops_Respecify, + .SubData = Ops_SubData, + .FlushMappedRange = Ops_FlushMappedRange, + .OnDestroy = Ops_OnDestroy, + }; } // namespace Bool VkBufferManager::Initialize(const VkBufferManagerInitInfo& initInfo) { @@ -22,40 +65,81 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkBufferManager::Initialize requires non-zero frame count"); m_initInfo = initInfo; - m_deferredResidentReleases.resize(initInfo.frameCount); + m_deferredBufferReleases.resize(initInfo.frameCount); + m_deferredResourceReleases.resize(initInfo.frameCount); m_currentFrameIndex = 0; - return InitializeTransientArenas(); + m_frameSerial = 1; + m_completedSerialFloor = 0; + if (!InitializeTransientArenas()) { + return false; + } + g_activeBufferManager = this; + MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps); + return true; } void VkBufferManager::Shutdown() { + if (g_activeBufferManager == this) { + g_activeBufferManager = nullptr; + if (MG_State::GLState::GetBufferBackendOps() == &g_vulkanBufferBackendOps) { + MG_State::GLState::SetBufferBackendOps(nullptr); + } + } m_transientUploadArena.Shutdown(); - DestroyResidentBuffers(); - DestroyDeferredResidentReleases(); + DestroyAllDeferredReleases(); + ReleaseAllLiveResources(); + m_copyProvider = nullptr; m_initInfo = {}; m_currentFrameIndex = 0; - m_residentGcTick = 0; + m_frameSerial = 1; + m_completedSerialFloor = 0; } Bool VkBufferManager::RecreateTransientArenas(Uint32 frameCount) { - MOBILEGL_ASSERT(m_initInfo.allocator != nullptr, "VkBufferManager::RecreateTransientArenas requires initialized manager"); + MOBILEGL_ASSERT(m_initInfo.allocator != nullptr, + "VkBufferManager::RecreateTransientArenas requires initialized manager"); MOBILEGL_ASSERT(frameCount > 0, "VkBufferManager::RecreateTransientArenas requires non-zero frame count"); + // Callers guarantee the device is idle around arena recreation. + NotifyDeviceIdle(); m_transientUploadArena.Shutdown(); m_initInfo.frameCount = frameCount; - DestroyDeferredResidentReleases(); - m_deferredResidentReleases.resize(frameCount); + DestroyAllDeferredReleases(); + m_deferredBufferReleases.resize(frameCount); + m_deferredResourceReleases.resize(frameCount); m_currentFrameIndex = 0; return InitializeTransientArenas(); } void VkBufferManager::BeginFrame(Uint32 frameIndex) { - MOBILEGL_ASSERT(frameIndex < m_deferredResidentReleases.size(), + MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(), "VkBufferManager::BeginFrame frame index out of range"); m_currentFrameIndex = frameIndex; - CollectDeferredResidentReleases(frameIndex); + ++m_frameSerial; + CollectDeferredReleases(frameIndex); m_transientUploadArena.BeginFrame(frameIndex); } + void VkBufferManager::NotifyDeviceIdle() { + // Everything submitted so far has completed. Work recorded for the + // current frame has not been submitted yet, so the current serial + // remains busy. + if (m_frameSerial > 0) { + m_completedSerialFloor = m_frameSerial - 1; + } + } + + void VkBufferManager::SetCopyCommandProvider(IBufferCopyCommandProvider* provider) { + m_copyProvider = provider; + } + + Bool VkBufferManager::IsResourceBusy(const VkBufferResource& resource) const { + const Uint64 frameCount = m_initInfo.frameCount > 0 ? m_initInfo.frameCount : 1; + Uint64 completed = m_frameSerial > frameCount ? m_frameSerial - frameCount : 0; + completed = std::max(completed, m_completedSerialFloor); + return resource.lastUseSerial > completed; + } + Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) { (void)kind; @@ -67,7 +151,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { .allocator = m_initInfo.allocator, .frameCount = m_initInfo.frameCount, .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | - VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .memoryUsage = m_initInfo.transientMemoryUsage, .allocationFlags = m_initInfo.transientAllocationFlags, .minBufferSize = m_initInfo.minUploadBytes, @@ -75,116 +160,341 @@ namespace MobileGL::MG_Backend::DirectVulkan { }); } - Bool VkBufferManager::SyncResidentBuffer(BufferKind kind, - const SharedPtr& bufferObject, - BufferSlice& outSlice) { - const VkBufferUsageFlags requiredUsage = GetVkBufferUsage(kind); - MOBILEGL_ASSERT(requiredUsage != 0, - "VkBufferManager::SyncResidentBuffer unsupported resident buffer kind"); - MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::SyncResidentBuffer requires valid buffer object"); - CollectResidentGarbageIfNeeded(); + VkBufferResource* VkBufferManager::ResourceOf(MG_State::GLState::BufferObject& bufferObject) { + return static_cast(bufferObject.GetBackendResource().get()); + } - const auto* bufferData = bufferObject->GetDataReadOnly().get(); - MOBILEGL_ASSERT(bufferData != nullptr, "VkBufferManager::SyncResidentBuffer requires frontend buffer data"); - bufferObject->MarkPersistentMappedRangeDirty(); + SharedPtr VkBufferManager::GetOrCreateResource( + const SharedPtr& bufferObject) { + auto existing = std::static_pointer_cast(bufferObject->GetBackendResource()); + if (existing) { + return existing; + } + auto resource = MakeShared(); + bufferObject->SetBackendResource(resource); + TrackLiveResource(resource); + return resource; + } - const VkDeviceSize bufferSize = static_cast(bufferObject->GetSize()); - if (bufferSize == 0) { - MGLOG_E("VkBufferManager::SyncResidentBuffer failed: buffer size is zero"); + void VkBufferManager::TrackLiveResource(const SharedPtr& resource) { + if (m_liveResources.size() >= kLiveResourcePruneThreshold) { + std::erase_if(m_liveResources, [](const WeakPtr& weak) { return weak.expired(); }); + } + m_liveResources.push_back(resource); + } + + void VkBufferManager::ReleaseAllLiveResources() { + for (auto& weak : m_liveResources) { + if (auto resource = weak.lock()) { + resource->buffer.Destroy(); + resource->storageSize = 0; + resource->usageFlags = 0; + resource->lastUseSerial = 0; + resource->pendingFullUpload = true; + resource->transientSlice = {}; + resource->transientFrameSerial = 0; + } + } + m_liveResources.clear(); + } + + Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, + VkBufferUsageFlags usage) { + // Staged range copies write resident storage with vkCmdCopyBuffer. + usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT; + const Bool created = resource.buffer.Create({ + .allocator = m_initInfo.allocator, + .size = size, + .usage = usage, + .memoryUsage = VMA_MEMORY_USAGE_AUTO, + .allocationFlags = kResidentBufferAllocationFlags, + }); + if (!created || resource.buffer.Map() == nullptr) { + MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)", + static_cast(size)); + resource.buffer.Destroy(); + resource.storageSize = 0; + resource.usageFlags = 0; return false; } - - auto& entry = m_residentBuffers[bufferObject.get()]; - entry.aliveRef = bufferObject; - - const auto changeBits = bufferObject->GetChangeBits(); - const Bool needsRecreate = !entry.buffer.IsValid() || entry.size != bufferSize || - ((entry.usage & requiredUsage) != requiredUsage) || - (changeBits & BufferChangeBits::PreferReallocationBit); - if (needsRecreate) { - const VkBufferUsageFlags recreatedUsage = entry.usage | requiredUsage; - DeferResidentRelease(std::move(entry.buffer)); - const Bool created = entry.buffer.Create({ - .allocator = m_initInfo.allocator, - .size = bufferSize, - .usage = recreatedUsage, - .memoryUsage = VMA_MEMORY_USAGE_AUTO, - .allocationFlags = kResidentBufferAllocationFlags, - }); - if (!created || entry.buffer.Map() == nullptr) { - MGLOG_E("VkBufferManager::SyncResidentBuffer failed: unable to create resident buffer"); - entry.buffer.Destroy(); - entry.size = 0; - entry.usage = 0; - return false; - } - if (!entry.buffer.Upload(bufferData->data(), bufferSize, 0)) { - MGLOG_E("VkBufferManager::SyncResidentBuffer failed: initial upload failed"); - entry.buffer.Destroy(); - entry.size = 0; - entry.usage = 0; - return false; - } - entry.size = bufferSize; - entry.usage = recreatedUsage; - bufferObject->ClearDirty(); - outSlice = entry.buffer.GetSlice(0, bufferSize); - return true; - } - - if (changeBits & BufferChangeBits::DirtyBit) { - const auto& dirtyRanges = bufferObject->GetDirtyRanges(); - for (const auto& range : dirtyRanges) { - const VkDeviceSize rangeOffset = static_cast(range.start); - const VkDeviceSize rangeSize = static_cast(range.end - range.start); - if (rangeSize == 0) { - continue; - } - if (!entry.buffer.Upload(bufferData->data() + range.start, rangeSize, rangeOffset)) { - MGLOG_E("VkBufferManager::SyncResidentBuffer failed: dirty range upload failed"); - return false; - } - } - bufferObject->ClearDirty(); - } - - outSlice = entry.buffer.GetSlice(0, bufferSize); + resource.storageSize = size; + resource.usageFlags = usage; return true; } - void VkBufferManager::DowngradeResidentBufferToTransient(const SharedPtr& bufferObject) { - if (bufferObject == nullptr) { - return; + Bool VkBufferManager::SwapStorageAndUploadAll(VkBufferResource& resource, + MG_State::GLState::BufferObject& bufferObject) { + const VkDeviceSize size = static_cast(bufferObject.GetSize()); + const VkBufferUsageFlags usage = resource.usageFlags; + DeferRelease(std::move(resource.buffer)); + if (!CreateResidentStorage(resource, size, usage)) { + resource.pendingFullUpload = true; + return false; } - - auto it = m_residentBuffers.find(bufferObject.get()); - if (it == m_residentBuffers.end()) { - return; + if (!resource.buffer.Upload(bufferObject.GetDataReadOnly()->data(), size, 0)) { + MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed"); + resource.pendingFullUpload = true; + return false; } - - DeferResidentRelease(std::move(it->second.buffer)); - m_residentBuffers.erase(it); + resource.pendingFullUpload = false; + return true; } - void VkBufferManager::DeferResidentRelease(VkBufferObject&& buffer) { + Bool VkBufferManager::StagedRangeCopy(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject, + SizeT offset, SizeT size) { + if (!m_copyProvider) { + return false; + } + BufferSlice staging{}; + if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject.GetDataReadOnly()->data() + offset, + static_cast(size), 16, staging)) { + return false; + } + VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer(); + if (commandBuffer == VK_NULL_HANDLE) { + return false; + } + + // Order the copy after every prior read/write of this buffer, both from + // in-flight frames (submission order) and from commands already recorded + // in this frame's command buffer. + VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, + &beforeBarrier, 0, nullptr, 0, nullptr); + + VkBufferCopy region{}; + region.srcOffset = staging.offset; + region.dstOffset = static_cast(offset); + region.size = static_cast(size); + vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, ®ion); + + VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, + &afterBarrier, 0, nullptr, 0, nullptr); + + resource.lastUseSerial = m_frameSerial; + return true; + } + + void VkBufferManager::OnRespecify(MG_State::GLState::BufferObject& bufferObject) { + auto* resource = ResourceOf(bufferObject); + if (!resource) { + return; // lazy: AcquireResidentSlice performs a full upload on creation + } + // Any cached streaming slice refers to the previous contents. + resource->transientFrameSerial = 0; + if (!resource->buffer.IsValid()) { + return; // streaming-only resource: shadow + serial are enough + } + + const VkDeviceSize size = static_cast(bufferObject.GetSize()); + if (size == 0) { + DeferRelease(std::move(resource->buffer)); + resource->storageSize = 0; + resource->pendingFullUpload = false; + return; + } + + if (size != resource->storageSize || IsResourceBusy(*resource)) { + // Conditional orphan: only swap the storage when the old one is + // still referenced by the GPU (or no longer fits). + SwapStorageAndUploadAll(*resource, bufferObject); + return; + } + + if (!resource->buffer.Upload(bufferObject.GetDataReadOnly()->data(), size, 0)) { + MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed"); + resource->pendingFullUpload = true; + } + } + + void VkBufferManager::OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size) { + auto* resource = ResourceOf(bufferObject); + if (!resource) { + return; + } + resource->transientFrameSerial = 0; + if (!resource->buffer.IsValid() || resource->pendingFullUpload) { + return; + } + if (static_cast(bufferObject.GetSize()) != resource->storageSize) { + resource->pendingFullUpload = true; + return; + } + + if (!IsResourceBusy(*resource)) { + if (!resource->buffer.Upload(bufferObject.GetDataReadOnly()->data() + offset, + static_cast(size), static_cast(offset))) { + MGLOG_E("VkBufferManager::OnSubData: host upload failed"); + resource->pendingFullUpload = true; + } + return; + } + + // Busy partial write: stage + GPU copy preserves GL ordering within the + // frame and leaves bytes outside the range (possibly GPU-written, e.g. + // SSBO) intact. Fall back to a storage swap if staging is unavailable. + if (!StagedRangeCopy(*resource, bufferObject, offset, size)) { + SwapStorageAndUploadAll(*resource, bufferObject); + } + } + + void VkBufferManager::OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range, + Flags appAccess) { + auto* resource = ResourceOf(bufferObject); + if (!resource) { + return; + } + resource->transientFrameSerial = 0; + if (!resource->buffer.IsValid() || resource->pendingFullUpload) { + return; + } + if (static_cast(bufferObject.GetSize()) != resource->storageSize) { + resource->pendingFullUpload = true; + return; + } + + const SizeT offset = range.start; + const SizeT size = range.end - range.start; + // 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, + static_cast(size), static_cast(offset))) { + MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed"); + resource->pendingFullUpload = true; + } + return; + } + + if (!StagedRangeCopy(*resource, bufferObject, offset, size)) { + SwapStorageAndUploadAll(*resource, bufferObject); + } + } + + void VkBufferManager::OnResourceDestroyed(SharedPtr&& resource) { + if (!resource) { + return; + } + auto vkResource = std::static_pointer_cast(std::move(resource)); + if (!vkResource->buffer.IsValid()) { + return; + } + if (m_deferredResourceReleases.empty()) { + vkResource->buffer.Destroy(); + return; + } + MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredResourceReleases.size(), + "VkBufferManager::OnResourceDestroyed current frame index out of range"); + // Keep the whole resource alive until this frame slot's fence has been + // waited, then the storage is destroyed with it. + m_deferredResourceReleases[m_currentFrameIndex].push_back(std::move(vkResource)); + } + + Bool VkBufferManager::AcquireResidentSlice(BufferKind kind, + const SharedPtr& bufferObject, + BufferSlice& outSlice) { + const VkBufferUsageFlags requiredUsage = GetVkBufferUsage(kind); + MOBILEGL_ASSERT(requiredUsage != 0, "VkBufferManager::AcquireResidentSlice unsupported buffer kind"); + MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireResidentSlice requires valid buffer object"); + + auto resource = GetOrCreateResource(bufferObject); + bufferObject->SyncPersistentMappedRange(); + + const VkDeviceSize size = static_cast(bufferObject->GetSize()); + if (size == 0) { + MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero"); + return false; + } + + const Bool needsRecreate = !resource->buffer.IsValid() || resource->storageSize != size || + ((resource->usageFlags & requiredUsage) != requiredUsage) || + resource->pendingFullUpload; + if (needsRecreate) { + const VkBufferUsageFlags usage = resource->usageFlags | requiredUsage; + DeferRelease(std::move(resource->buffer)); + if (!CreateResidentStorage(*resource, size, usage)) { + return false; + } + if (!resource->buffer.Upload(bufferObject->GetDataReadOnly()->data(), size, 0)) { + MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed"); + resource->buffer.Destroy(); + resource->storageSize = 0; + resource->usageFlags = 0; + return false; + } + resource->pendingFullUpload = false; + } + + resource->lastUseSerial = m_frameSerial; + outSlice = resource->buffer.GetSlice(0, size); + return true; + } + + Bool VkBufferManager::AcquireStreamedSlice(BufferKind kind, + const SharedPtr& bufferObject, + BufferSlice& outSlice) { + (void)kind; + MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireStreamedSlice requires valid buffer object"); + + auto resource = GetOrCreateResource(bufferObject); + bufferObject->SyncPersistentMappedRange(); + + const VkDeviceSize size = static_cast(bufferObject->GetSize()); + if (size == 0) { + MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero"); + return false; + } + + const Uint64 changeSerial = bufferObject->GetChangeSerial(); + if (resource->transientFrameSerial == m_frameSerial && resource->transientChangeSerial == changeSerial && + resource->transientSize == size && resource->transientSlice.IsValid()) { + outSlice = resource->transientSlice; + return true; + } + + if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->GetDataReadOnly()->data(), size, 16, + outSlice)) { + return false; + } + resource->transientSlice = outSlice; + resource->transientFrameSerial = m_frameSerial; + resource->transientChangeSerial = changeSerial; + resource->transientSize = size; + + // Streaming path is authoritative now; release resident storage so we do + // not keep a second, stale copy alive (downgrade). + if (resource->buffer.IsValid()) { + DeferRelease(std::move(resource->buffer)); + resource->storageSize = 0; + } + return true; + } + + void VkBufferManager::DeferRelease(VkBufferObject&& buffer) { if (!buffer.IsValid()) { return; } - if (m_deferredResidentReleases.empty()) { + if (m_deferredBufferReleases.empty()) { buffer.Destroy(); return; } - MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredResidentReleases.size(), - "VkBufferManager::DeferResidentRelease current frame index out of range"); - m_deferredResidentReleases[m_currentFrameIndex].push_back(std::move(buffer)); + MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredBufferReleases.size(), + "VkBufferManager::DeferRelease current frame index out of range"); + m_deferredBufferReleases[m_currentFrameIndex].push_back(std::move(buffer)); } - void VkBufferManager::CollectDeferredResidentReleases(Uint32 frameIndex) { - MOBILEGL_ASSERT(frameIndex < m_deferredResidentReleases.size(), - "VkBufferManager::CollectDeferredResidentReleases frame index out of range"); - m_deferredResidentReleases[frameIndex].clear(); + void VkBufferManager::CollectDeferredReleases(Uint32 frameIndex) { + MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(), + "VkBufferManager::CollectDeferredReleases frame index out of range"); + m_deferredBufferReleases[frameIndex].clear(); + m_deferredResourceReleases[frameIndex].clear(); } VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) { @@ -209,46 +519,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - void VkBufferManager::CollectResidentGarbageIfNeeded() { - ++m_residentGcTick; - if (m_residentGcTick < kResidentBufferGCInterval) { - return; - } - CollectResidentGarbageNow(); - m_residentGcTick = 0; - } - - void VkBufferManager::CollectResidentGarbageNow() { - Vector staleBuffers; - staleBuffers.reserve(m_residentBuffers.size()); - - for (const auto& [rawBuffer, entry] : m_residentBuffers) { - if (entry.aliveRef.expired()) { - staleBuffers.push_back(rawBuffer); + void VkBufferManager::DestroyAllDeferredReleases() { + for (auto& releases : m_deferredBufferReleases) { + for (auto& buffer : releases) { + buffer.Destroy(); } + releases.clear(); } - - for (const auto* rawBuffer : staleBuffers) { - auto it = m_residentBuffers.find(const_cast(rawBuffer)); - if (it == m_residentBuffers.end()) { - continue; + m_deferredBufferReleases.clear(); + for (auto& releases : m_deferredResourceReleases) { + for (auto& resource : releases) { + resource->buffer.Destroy(); } - DeferResidentRelease(std::move(it->second.buffer)); - m_residentBuffers.erase(it); + releases.clear(); } - } - - void VkBufferManager::DestroyDeferredResidentReleases() { - for (auto& deferredReleases : m_deferredResidentReleases) { - deferredReleases.clear(); - } - m_deferredResidentReleases.clear(); - } - - void VkBufferManager::DestroyResidentBuffers() { - for (auto& [_, entry] : m_residentBuffers) { - entry.buffer.Destroy(); - } - m_residentBuffers.clear(); + m_deferredResourceReleases.clear(); } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 127d3296..9fac8e2a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -33,6 +33,38 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool transientPersistentMapping = false; }; + // The DirectVulkan storage behind one frontend buffer (pipe_resource analogue). + // Owned (refcounted) by the frontend BufferObject; the manager holds only weak + // references (for shutdown) plus strong references on deferred-release lists. + class VkBufferResource : public MG_State::GLState::BackendBufferResource { + public: + ~VkBufferResource() override = default; + + // Resident storage (may be invalid for streaming-only buffers). + VkBufferObject buffer; + VkDeviceSize storageSize = 0; + VkBufferUsageFlags usageFlags = 0; + // Frame serial of the last GPU reference; drives busy tracking. + Uint64 lastUseSerial = 0; + // Set when an immediate op could not be applied; forces a full re-upload + // on the next AcquireResidentSlice. + Bool pendingFullUpload = false; + + // Cached transient (streaming) slice for the current frame. + BufferSlice transientSlice{}; + Uint64 transientFrameSerial = 0; + Uint64 transientChangeSerial = 0; + VkDeviceSize transientSize = 0; + }; + + // Supplies a command buffer that is recording and outside any render pass, + // for staged buffer-range copies. Implemented by VulkanRenderer. + class IBufferCopyCommandProvider { + public: + virtual ~IBufferCopyCommandProvider() = default; + virtual VkCommandBuffer AcquireBufferCopyCommandBuffer() = 0; + }; + class VkBufferManager { public: Bool Initialize(const VkBufferManagerInitInfo& initInfo); @@ -41,35 +73,61 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Recreate all per-frame transient arenas Bool RecreateTransientArenas(Uint32 frameCount); void BeginFrame(Uint32 frameIndex); + // All previously submitted GPU work has completed (vkDeviceWaitIdle). + void NotifyDeviceIdle(); + void SetCopyCommandProvider(IBufferCopyCommandProvider* provider); Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice); - Bool SyncResidentBuffer(BufferKind kind, const SharedPtr& bufferObject, - BufferSlice& outSlice); - void DowngradeResidentBufferToTransient(const SharedPtr& bufferObject); + + // Draw-time acquire for resident (device-storage) buffers: ensures the + // resource exists and is fully uploaded, marks it used this frame. + Bool AcquireResidentSlice(BufferKind kind, const SharedPtr& bufferObject, + BufferSlice& outSlice); + // Draw-time acquire for streamed buffers: uploads the whole shadow into + // the per-frame arena (cached by change serial), releasing any resident + // storage the buffer may still own. + Bool AcquireStreamedSlice(BufferKind kind, const SharedPtr& bufferObject, + BufferSlice& outSlice); + + // 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); + void OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range, + Flags appAccess); + void OnResourceDestroyed(SharedPtr&& resource); + + Uint64 GetFrameSerial() const { return m_frameSerial; } + // Busy = potentially referenced by GPU work that has not been fenced yet + // (including commands recorded for the current, unsubmitted frame). + Bool IsResourceBusy(const VkBufferResource& resource) const; private: - struct ResidentBufferEntry { - WeakPtr aliveRef; - VkBufferObject buffer; - VkDeviceSize size = 0; - VkBufferUsageFlags usage = 0; - }; - Bool InitializeTransientArenas(); static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind); - void DeferResidentRelease(VkBufferObject&& buffer); - void CollectDeferredResidentReleases(Uint32 frameIndex); - void CollectResidentGarbageIfNeeded(); - void CollectResidentGarbageNow(); - void DestroyDeferredResidentReleases(); - void DestroyResidentBuffers(); + SharedPtr GetOrCreateResource(const SharedPtr& bufferObject); + static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject); + Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage); + // 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 + // in-flight and already-recorded GPU work. + Bool StagedRangeCopy(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject, + SizeT offset, SizeT size); + void DeferRelease(VkBufferObject&& buffer); + void CollectDeferredReleases(Uint32 frameIndex); + void DestroyAllDeferredReleases(); + void TrackLiveResource(const SharedPtr& resource); + void ReleaseAllLiveResources(); VkBufferManagerInitInfo m_initInfo{}; BufferArena m_transientUploadArena; - UnorderedMap m_residentBuffers; - Vector> m_deferredResidentReleases; + IBufferCopyCommandProvider* m_copyProvider = nullptr; + Vector> m_deferredBufferReleases; + Vector>> m_deferredResourceReleases; + Vector> m_liveResources; Uint32 m_currentFrameIndex = 0; - Uint32 m_residentGcTick = 0; + Uint64 m_frameSerial = 1; + Uint64 m_completedSerialFloor = 0; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index f4ce460f..2262dd09 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -1792,7 +1792,7 @@ void main() { MGLOG_E("DirectVulkan readback skipped: pixel pack buffer is too small"); return false; } - pixelPackBufferObject->UploadSubData({packed.data(), packed.size()}, pboOffset); + pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); return true; } @@ -1893,6 +1893,7 @@ void main() { .transientPersistentMapping = true, }); MOBILEGL_ASSERT(succeeded, "VkBufferManager initialization failed."); + m_bufferManager.SetCopyCommandProvider(this); m_textureManager = MakeUnique(); MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed."); succeeded = m_textureManager->Initialize( @@ -1956,7 +1957,6 @@ void main() { VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage"); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); - m_transientVertexIndexBufferSlicesThisFrame.clear(); MGLOG_D("VulkanRenderer initialized"); } @@ -1986,7 +1986,6 @@ void main() { } m_vertexInputStateFactory.reset(); m_bufferManager.Shutdown(); - m_transientVertexIndexBufferSlicesThisFrame.clear(); if (m_device != VK_NULL_HANDLE) { m_frameContext.Destroy(m_device, m_commandPool); @@ -2147,32 +2146,15 @@ void main() { auto sourceBufferShared = MG_State::pGLContext->GetBufferObject(sourceBuffer->GetExternalIndex()); MOBILEGL_ASSERT(sourceBufferShared != nullptr, "UploadAndBindVertexStreams failed to resolve shared source buffer"); - sourceBufferShared->MarkPersistentMappedRangeDirty(); BufferSlice slice{}; - const Bool isDirty = (sourceBufferShared->GetChangeBits() & BufferChangeBits::DirtyBit); - const Uint64 changeSerial = sourceBufferShared->GetChangeSerial(); const SizeT sourceSize = sourceBufferShared->GetSize(); - auto cachedTransient = m_transientVertexIndexBufferSlicesThisFrame.find(sourceBufferShared.get()); - if (cachedTransient != m_transientVertexIndexBufferSlicesThisFrame.end() && !isDirty && - cachedTransient->second.changeSerial == changeSerial && cachedTransient->second.size == sourceSize) { - slice = cachedTransient->second.slice; - } else if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared) || isDirty) { - const auto sourceData = sourceBufferShared->GetDataReadOnly(); - if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), - sourceData->data(), static_cast(sourceSize), 16, - slice)) { + if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared)) { + if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Vertex, sourceBufferShared, slice)) { MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding); return false; } - m_transientVertexIndexBufferSlicesThisFrame[sourceBufferShared.get()] = { - .slice = slice, - .changeSerial = changeSerial, - .size = sourceSize, - }; - m_bufferManager.DowngradeResidentBufferToTransient(sourceBufferShared); - sourceBufferShared->ClearDirty(); } else { - if (!m_bufferManager.SyncResidentBuffer(BufferKind::Vertex, sourceBufferShared, slice)) { + if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, sourceBufferShared, slice)) { MGLOG_E("UploadAndBindVertexStreams skipped: failed to sync resident binding %zu", binding); return false; } @@ -2247,7 +2229,6 @@ void main() { const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get(); MOBILEGL_ASSERT(indexBuffer != nullptr, "UploadAndBindIndexBuffer requires bound EBO"); - const SizeT indexSize = MG_Util::GetGLTypeSize(pIndexBufferView->indexType); const SizeT indexDataSizeBytes = pIndexBufferView->indexByteSize; MOBILEGL_ASSERT(pIndexBufferView->indexByteOffset + indexDataSizeBytes <= indexBuffer->GetSize(), "DrawElements index range out of bounds"); @@ -2255,44 +2236,14 @@ void main() { BufferSlice slice{}; auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex()); MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO"); - indexBufferShared->MarkPersistentMappedRangeDirty(); - const Bool isDirty = (indexBufferShared->GetChangeBits() & BufferChangeBits::DirtyBit); - const Uint64 changeSerial = indexBufferShared->GetChangeSerial(); - const SizeT indexBufferSize = indexBufferShared->GetSize(); - auto cachedTransient = m_transientVertexIndexBufferSlicesThisFrame.find(indexBufferShared.get()); - if (cachedTransient != m_transientVertexIndexBufferSlicesThisFrame.end() && !isDirty && - cachedTransient->second.changeSerial == changeSerial && cachedTransient->second.size == indexBufferSize) { - slice = cachedTransient->second.slice; - vkCmdBindIndexBuffer(frame.commandBuffer, - slice.buffer, - slice.offset + static_cast(pIndexBufferView->indexByteOffset), - vkIndexType); - return true; - } - if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared) || isDirty) { + if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) { const auto indexData = indexBufferShared->GetDataReadOnly(); MOBILEGL_ASSERT(indexData != nullptr && !indexData->empty(), "DrawElements requires non-empty EBO data"); - if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), - indexData->data(), - static_cast(indexBufferSize), indexSize, - slice)) { + if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Index, indexBufferShared, slice)) { MOBILEGL_ASSERT(false, "DrawElements skipped: failed to prepare transient index buffer"); return false; } - m_transientVertexIndexBufferSlicesThisFrame[indexBufferShared.get()] = { - .slice = slice, - .changeSerial = changeSerial, - .size = indexBufferSize, - }; - m_bufferManager.DowngradeResidentBufferToTransient(indexBufferShared); - indexBufferShared->ClearDirty(); - vkCmdBindIndexBuffer(frame.commandBuffer, - slice.buffer, - slice.offset + static_cast(pIndexBufferView->indexByteOffset), - vkIndexType); - return true; - } - if (!m_bufferManager.SyncResidentBuffer(BufferKind::Index, indexBufferShared, slice)) { + } else if (!m_bufferManager.AcquireResidentSlice(BufferKind::Index, indexBufferShared, slice)) { MGLOG_E("DrawElements skipped: failed to sync resident index buffer"); return false; } @@ -3449,10 +3400,10 @@ void main() { MGLOG_E("DispatchComputeIndirect skipped: GL_DISPATCH_INDIRECT_BUFFER is not bound"); return; } - indirectBuffer->MarkPersistentMappedRangeDirty(); + indirectBuffer->SyncPersistentMappedRange(); BufferSlice slice{}; - if (!m_bufferManager.SyncResidentBuffer(BufferKind::Indirect, indirectBuffer, slice)) { + if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, indirectBuffer, slice)) { MGLOG_E("DispatchComputeIndirect skipped: failed to sync indirect dispatch buffer"); return; } @@ -5259,16 +5210,16 @@ void main() { return; } - drawBuffer->MarkPersistentMappedRangeDirty(); - parameterBuffer->MarkPersistentMappedRangeDirty(); + drawBuffer->SyncPersistentMappedRange(); + parameterBuffer->SyncPersistentMappedRange(); BufferSlice drawSlice{}; - if (!m_bufferManager.SyncResidentBuffer(BufferKind::Indirect, drawBuffer, drawSlice)) { + if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, drawBuffer, drawSlice)) { MGLOG_E("MultiDrawElementsIndirectCount skipped: failed to sync draw indirect buffer"); return; } BufferSlice parameterSlice{}; - if (!m_bufferManager.SyncResidentBuffer(BufferKind::Indirect, parameterBuffer, parameterSlice)) { + if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, parameterBuffer, parameterSlice)) { MGLOG_E("MultiDrawElementsIndirectCount skipped: failed to sync parameter buffer"); return; } @@ -5303,6 +5254,22 @@ void main() { } } + VkCommandBuffer VulkanRenderer::AcquireBufferCopyCommandBuffer() { + if (m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) { + return VK_NULL_HANDLE; + } + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) { + m_frameContext.BeginCommandRecording(); + } + // vkCmdCopyBuffer must be recorded outside a render pass; draws re-begin + // their render pass lazily, matching the existing blit/clear pattern. + if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + } + return frame.commandBuffer; + } + void VulkanRenderer::Present() { MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(), "Present, acquired image index out of range"); @@ -5469,7 +5436,6 @@ void main() { CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); - m_transientVertexIndexBufferSlicesThisFrame.clear(); } void VulkanRenderer::CreateInstance() { @@ -6171,7 +6137,6 @@ void main() { m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); } m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); - m_transientVertexIndexBufferSlicesThisFrame.clear(); } } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index dc316088..f275d724 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -101,7 +101,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } }; - class VulkanRenderer { + class VulkanRenderer : public IBufferCopyCommandProvider { public: VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {}); ~VulkanRenderer(); @@ -109,6 +109,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { void Initialize(); void Shutdown(); + // IBufferCopyCommandProvider: recording command buffer, outside any + // render pass, for immediate staged buffer copies. + VkCommandBuffer AcquireBufferCopyCommandBuffer() override; + Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView = nullptr); @@ -233,15 +237,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkCommandPool m_commandPool = VK_NULL_HANDLE; - struct TransientBufferSliceCacheEntry { - BufferSlice slice; - Uint64 changeSerial = 0; - SizeT size = 0; - }; - VkBufferManager m_bufferManager; - UnorderedMap - m_transientVertexIndexBufferSlicesThisFrame; Uint m_imageIndexAcquired = 0; FrameContext m_frameContext; diff --git a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp index b4e2a454..7ae87d30 100644 --- a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp @@ -834,10 +834,7 @@ namespace MobileGL::MG_Impl::GLImpl { } bufferObject->SetUsage(bufferUsage); - bufferObject->Resize(size); - if (data) { - bufferObject->UploadData({(void*)data, (SizeT)size}, 0); - } + bufferObject->Respecify(size, data); } void BufferStorage_State(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) { @@ -928,10 +925,7 @@ namespace MobileGL::MG_Impl::GLImpl { } bufferObject->SetUsage(bufferUsage); - bufferObject->Resize(size); - if (data) { - bufferObject->UploadData({(void*)data, (SizeT)size}, 0); - } + bufferObject->Respecify(size, data); } void NamedBufferSubData_State(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data) { diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index 43dc031e..f937be91 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -9,24 +9,67 @@ #include "BufferObject.h" 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_change(BufferChangeBits::DirtyBit | BufferChangeBits::PreferReallocationBit), m_mappedRange({0, 0}), - m_dataPtr(MakeShared()), m_ownsStagingData{} { - m_change.DirtyRanges.reserve(BufferChange::DEFAULT_RESERVED_DIRTY_RANGES_COUNT); + namespace { + const BufferBackendOps* g_bufferBackendOps = nullptr; } - void BufferObject::Resize(SizeT size) { + void SetBufferBackendOps(const BufferBackendOps* ops) { + g_bufferBackendOps = ops; + } + + const BufferBackendOps* GetBufferBackendOps() { + return g_bufferBackendOps; + } + + 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{} {} + + BufferObject::~BufferObject() { + if (m_backendResource && g_bufferBackendOps && g_bufferBackendOps->OnDestroy) { + g_bufferBackendOps->OnDestroy(std::move(m_backendResource)); + } + } + + void BufferObject::NotifyRespecify() { + ++m_changeSerial; + if (g_bufferBackendOps && g_bufferBackendOps->Respecify) { + g_bufferBackendOps->Respecify(*this); + } + } + + void BufferObject::NotifySubData(SizeT offset, SizeT size) { + ++m_changeSerial; + if (size == 0) return; + if (g_bufferBackendOps && g_bufferBackendOps->SubData) { + g_bufferBackendOps->SubData(*this, offset, size); + } + } + + void BufferObject::NotifyFlushMappedRange(Range1D range, Flags appAccess) { + ++m_changeSerial; + if (range.start >= range.end) return; + if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) { + g_bufferBackendOps->FlushMappedRange(*this, range, appAccess); + } + } + + 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); + if (data && size > 0) { + Memcpy(m_dataPtr->data(), data, size); + } m_isImmutableStorage = false; m_storageFlags = 0; - m_change.Bits |= BufferChangeBits::DirtyBit; - m_change.Bits |= BufferChangeBits::PreferReallocationBit; - ++m_changeSerial; + NotifyRespecify(); + } + + void BufferObject::Resize(SizeT size) { + Respecify(size, nullptr); } void BufferObject::AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags) { @@ -41,10 +84,7 @@ namespace MobileGL::MG_State::GLState { } m_isImmutableStorage = true; m_storageFlags = storageFlags; - m_change.DirtyRanges.clear(); - m_change.DirtyRanges.Add({0, size}); - m_change.Bits = BufferChangeBits::DirtyBit | BufferChangeBits::PreferReallocationBit; - ++m_changeSerial; + NotifyRespecify(); } void BufferObject::UploadData(DataPtr data, SizeT atOffset) { @@ -54,15 +94,7 @@ namespace MobileGL::MG_State::GLState { 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); - m_change.DirtyRanges.Add({atOffset, atOffset + data.size}); - m_change.Bits |= BufferChangeBits::DirtyBit; - m_change.Bits |= BufferChangeBits::ForbidInvalidationBit; - m_change.Bits |= BufferChangeBits::ForbidUnsynchronizationBit; - ++m_changeSerial; - // This function may be called by `glBufferData`, but we still set the forbid bits above, - // because when `PreferReallocationBit` is set, those bits are ignored anyway. - // The bits can fit the `glBufferSubData` semantics - // (though `glBufferSubData` calls `UploadSubData` instead). + NotifySubData(atOffset, data.size); } void BufferObject::SetUsage(BufferUsage usage) { @@ -78,9 +110,7 @@ namespace MobileGL::MG_State::GLState { Memcpy(m_dataPtr->data() + m_mappedRange.start, m_stagingData.data(), m_mappedRange.end - m_mappedRange.start); } - m_change.DirtyRanges.Add({m_mappedRange.start, m_mappedRange.end}); - m_change.Bits |= BufferChangeBits::DirtyBit; - ++m_changeSerial; + NotifyFlushMappedRange(m_mappedRange, m_mappingAccess); } m_stagingData.clear(); @@ -107,20 +137,24 @@ namespace MobileGL::MG_State::GLState { if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) { Memcpy(m_dataPtr->data() + start, m_stagingData.data() + offset, length); } - m_change.DirtyRanges.Add({start, end}); - m_change.Bits |= BufferChangeBits::DirtyBit; - ++m_changeSerial; + NotifyFlushMappedRange({start, end}, m_mappingAccess); } - void BufferObject::MarkPersistentMappedRangeDirty() { + void BufferObject::SyncPersistentMappedRange() { if (!m_isMapped) return; if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) return; if (!(m_mappingAccess & BufferMappingAccessBit::Write)) return; if (m_mappingAccess & BufferMappingAccessBit::FlushExplicit) return; if (m_mappedRange.start >= m_mappedRange.end) return; - m_change.DirtyRanges.Add(m_mappedRange); - m_change.Bits |= BufferChangeBits::DirtyBit; + NotifySubData(m_mappedRange.start, m_mappedRange.end - m_mappedRange.start); + } + + void BufferObject::WritebackFromBackend(DataPtr data, SizeT atOffset) { + 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); ++m_changeSerial; } @@ -132,11 +166,7 @@ namespace MobileGL::MG_State::GLState { data.size, m_size); Memcpy(m_dataPtr->data() + atOffset, data.data, data.size); - m_change.DirtyRanges.Add({atOffset, atOffset + data.size}); - m_change.Bits |= BufferChangeBits::DirtyBit; - m_change.Bits |= BufferChangeBits::ForbidInvalidationBit; - m_change.Bits |= BufferChangeBits::ForbidUnsynchronizationBit; - ++m_changeSerial; + NotifySubData(atOffset, data.size); } void BufferObject::CopyDataFrom(const SharedPtr& src, SizeT srcOffset, SizeT dstOffset, SizeT size) { @@ -153,9 +183,7 @@ namespace MobileGL::MG_State::GLState { const Uint8* srcData = src->m_dataPtr->data() + srcOffset; Memcpy(m_dataPtr->data() + dstOffset, srcData, size); - m_change.DirtyRanges.Add({dstOffset, dstOffset + size}); - m_change.Bits |= BufferChangeBits::DirtyBit; - ++m_changeSerial; + NotifySubData(dstOffset, size); } void* BufferObject::AcquireMemory(Bool markMapped, Bool read, Bool write) { @@ -164,10 +192,6 @@ namespace MobileGL::MG_State::GLState { m_mappingAccess = (read ? BufferMappingAccessBit::Read : BufferMappingAccessBit::Null) | (write ? BufferMappingAccessBit::Write : BufferMappingAccessBit::Null); m_mappedRange = {0, m_size}; - if (write) { - m_change.Bits |= BufferChangeBits::ForbidInvalidationBit; - m_change.Bits |= BufferChangeBits::ForbidUnsynchronizationBit; - } if (m_mappingAccess & BufferMappingAccessBit::Write) { m_stagingData.resize(m_size); @@ -192,21 +216,6 @@ namespace MobileGL::MG_State::GLState { m_isMapped = true; m_mappingAccess = access; m_mappedRange = range; - m_change.Bits |= - !(access & BufferMappingAccessBit::InvalidateBuffer || access & BufferMappingAccessBit::InvalidateRange) - ? BufferChangeBits::ForbidInvalidationBit - : BufferChangeBits::None; - m_change.Bits |= !(access & BufferMappingAccessBit::Unsynchronized) - ? BufferChangeBits::ForbidUnsynchronizationBit - : BufferChangeBits::None; - - m_change.Bits |= - !(access & BufferMappingAccessBit::InvalidateBuffer || access & BufferMappingAccessBit::InvalidateRange) - ? BufferChangeBits::ForbidInvalidationBit - : BufferChangeBits::None; - m_change.Bits |= !(access & BufferMappingAccessBit::Unsynchronized) - ? BufferChangeBits::ForbidUnsynchronizationBit - : BufferChangeBits::None; if (access & BufferMappingAccessBit::Persistent) { m_ownsStagingData = false; @@ -232,11 +241,6 @@ namespace MobileGL::MG_State::GLState { return m_dataPtr; } - void BufferObject::ClearDirty() { - m_change.DirtyRanges.clear(); - m_change.Bits = BufferChangeBits::None; - } - SizeT BufferObject::GetSize() const { return m_size; } @@ -249,18 +253,18 @@ namespace MobileGL::MG_State::GLState { return m_usage; } - const VecRange1D& BufferObject::GetDirtyRanges() const { - return m_change.DirtyRanges; - } - - Flags BufferObject::GetChangeBits() const { - return m_change.Bits; - } - Uint64 BufferObject::GetChangeSerial() const { return m_changeSerial; } + const SharedPtr& BufferObject::GetBackendResource() const { + return m_backendResource; + } + + void BufferObject::SetBackendResource(SharedPtr resource) { + m_backendResource = std::move(resource); + } + Bool BufferObject::IsMapped() const { return m_isMapped; } diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.h b/MobileGL/MG_State/GLState/BufferState/BufferObject.h index e1d31f2f..9327585c 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.h @@ -56,42 +56,81 @@ namespace MobileGL { Coherent = 0x80 }; - enum class BufferChangeBits : Uint8 { - None = 0, - DirtyBit = 1 << 0, // When not set, bits below are ignored and nothing should be synced to backend - PreferReallocationBit = - 1 << 1, // <=> `glBufferData`; When set, ForbidInvalidationBit and ForbidUnsynchronizationBit are ignored - ForbidInvalidationBit = 1 << 2, // Indidate that invalidation flags were not used during mapping, else we're - // allowed to act as `GL_MAP_INVALIDATE_*` in backend - ForbidUnsynchronizationBit = 1 << 3, // (the same description as above, but for unsynchronization) - }; - - struct BufferChange { - static constexpr int DEFAULT_RESERVED_DIRTY_RANGES_COUNT = 50; - - Flags Bits = BufferChangeBits::None; - VecRange1D DirtyRanges; - }; - 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; + }; + + // Immediate buffer transfer interface implemented by the active backend + // (the pipe_context buffer-op analogue). Ops are invoked at GL call time, + // right after the shadow copy has been updated; contents are always read + // from the shadow so ops carry only ranges and flags. + // + // Every op must tolerate bufferObject.GetBackendResource() == nullptr: + // resources are created lazily by the backend's draw/bind-time ensure + // path, which performs a full upload from the shadow and thereby covers + // all ops that happened before the resource existed. + struct BufferBackendOps { + // Storage (re)definition: glBufferData / glBufferStorage. The orphaning + // point - the backend decides (busy-tracking) whether to swap storage + // or write in place. Shadow already holds the new contents. + void (*Respecify)(BufferObject& bufferObject) = nullptr; + // Contents update of [offset, offset + size) from the shadow. + void (*SubData)(BufferObject& bufferObject, SizeT offset, SizeT size) = nullptr; + // Write-map flush (glUnmapBuffer / glFlushMappedBufferRange). Carries the + // app's real mapping flags so the backend can honour INVALIDATE_* / + // UNSYNCHRONIZED semantics per call instead of merging them. + void (*FlushMappedRange)(BufferObject& bufferObject, Range1D range, + Flags appAccess) = nullptr; + // 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; + }; + + // Registered by the active backend at init, cleared at shutdown. + // Null table (unit tests, benchmarks) => shadow-only state tracking. + void SetBufferBackendOps(const BufferBackendOps* ops); + const BufferBackendOps* GetBufferBackendOps(); + class BufferObject { public: using TargetEnum = BufferTarget; BufferObject(Uint externalIndex); + ~BufferObject(); + BufferObject(const BufferObject&) = delete; + BufferObject& operator=(const BufferObject&) = delete; + + // Storage definition (single backend Respecify): glBufferData. + void Respecify(SizeT size, const void* data); + // Storage definition without contents; equivalent to Respecify(size, nullptr). void Resize(SizeT size); void AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags); - void UploadData(DataPtr data, SizeT atOffset); void SetUsage(BufferUsage usage); + + void UploadData(DataPtr data, SizeT atOffset); + void UploadSubData(DataPtr data, SizeT atOffset); + void CopyDataFrom(const SharedPtr& src, SizeT srcOffset, SizeT dstOffset, SizeT size); + void* AcquireMemory(Bool markMapped, Bool read, Bool write); void* AcquireMemoryRange(Range1D range, Flags access); void ReleaseMemory(); void FlushMemoryRange(SizeT offset, SizeT length); - void MarkPersistentMappedRangeDirty(); - void UploadSubData(DataPtr data, SizeT atOffset); - void CopyDataFrom(const SharedPtr& src, SizeT srcOffset, SizeT dstOffset, SizeT size); - void ClearDirty(); + + // Pushes the persistently-mapped write range to the backend; called by + // backends at draw time (persistent maps mutate the shadow without API calls). + void SyncPersistentMappedRange(); + // Shadow-only write used when the backend copies GPU results (e.g. ReadPixels + // into a pixel-pack buffer) back into the frontend mirror. Does not issue a + // backend op: the backend storage already holds these bytes. + void WritebackFromBackend(DataPtr data, SizeT atOffset); Bool IsMapped() const; Bool IsImmutableStorage() const; @@ -103,11 +142,18 @@ namespace MobileGL { Flags GetMappingAccess() const; GLbitfield GetStorageFlags() const; Uint GetExternalIndex() const; - const VecRange1D& GetDirtyRanges() const; - Flags GetChangeBits() const; + // Monotonic counter bumped on every shadow mutation; backends use it to + // validate cached transient slices. Uint64 GetChangeSerial() const; + const SharedPtr& GetBackendResource() const; + void SetBackendResource(SharedPtr resource); + private: + void NotifyRespecify(); + void NotifySubData(SizeT offset, SizeT size); + void NotifyFlushMappedRange(Range1D range, Flags appAccess); + const Uint m_externalIndex = 0; SizeT m_size = 0; BufferUsage m_usage = BufferUsage::StaticDraw; @@ -116,11 +162,11 @@ namespace MobileGL { Flags m_mappingAccess; Bool m_isImmutableStorage = false; GLbitfield m_storageFlags = 0; - BufferChange m_change; Uint64 m_changeSerial = 0; Range1D m_mappedRange; Vector m_stagingData; Bool m_ownsStagingData; + SharedPtr m_backendResource; }; } // namespace MG_State::GLState } // namespace MobileGL diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index 61edc8ba..0e4dd14d 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -74,10 +74,8 @@ TEST_F(BufferTest, PingPong) { Vector bufdata(data.size()); memcpy(bufdata.data(), p, byteSize); ASSERT_EQ(data, bufdata); - ASSERT_EQ(bufRead->GetDirtyRanges().size() >= 1, true); - auto range = bufRead->GetDirtyRanges()[0]; - ASSERT_EQ(range.start, 0); - ASSERT_EQ(range.end, byteSize); + // Writes bump the change serial so backends can invalidate cached slices. + ASSERT_GT(bufRead->GetChangeSerial(), 0u); } TEST_F(BufferTest, GenerateManyNames_NoPrematureCreation) { @@ -117,7 +115,7 @@ TEST_F(BufferTest, AcquireMemory) { bufObj->Resize(byteSize); DataPtr ptr{.data = initData.data(), .size = byteSize}; bufObj->UploadData(ptr, 0); - bufObj->ClearDirty(); + const Uint64 baseSerial = bufObj->GetChangeSerial(); Int* mappedPtr = static_cast(bufObj->AcquireMemory(true, true, true)); mappedPtr[0] = 100; mappedPtr[1] = 200; @@ -128,11 +126,8 @@ TEST_F(BufferTest, AcquireMemory) { void* p = bufObj->AcquireMemory(false, true, false); memcpy(actual.data(), p, byteSize); ASSERT_EQ(actual, expected); - ASSERT_EQ(bufObj->GetDirtyRanges().size() >= 1, true); - auto dirty = bufObj->GetDirtyRanges()[0]; - - ASSERT_EQ(dirty.start, 0); - ASSERT_EQ(dirty.end, sizeof(Int) * 5); + // Unmapping a write map flushes the mapped range and bumps the serial. + ASSERT_GT(bufObj->GetChangeSerial(), baseSerial); } TEST_F(BufferTest, AcquireMemoryRangeWithoutExplicit) { @@ -146,7 +141,7 @@ TEST_F(BufferTest, AcquireMemoryRangeWithoutExplicit) { bufObj->Resize(byteSize); DataPtr ptr{.data = initData.data(), .size = byteSize}; bufObj->UploadData(ptr, 0); - bufObj->ClearDirty(); + const Uint64 baseSerial = bufObj->GetChangeSerial(); Range1D mapRange{.start = sizeof(Int), .end = sizeof(Int) * 4}; Int* mappedPtr = static_cast(bufObj->AcquireMemoryRange(mapRange, BufferMappingAccessBit::Write)); @@ -158,10 +153,7 @@ TEST_F(BufferTest, AcquireMemoryRangeWithoutExplicit) { void* p = bufObj->AcquireMemory(false, true, false); memcpy(actual.data(), p, byteSize); ASSERT_EQ(actual, expected); - ASSERT_EQ(bufObj->GetDirtyRanges().size() >= 1, true); - auto dirty = bufObj->GetDirtyRanges()[0]; - ASSERT_EQ(dirty.start, sizeof(Int)); - ASSERT_EQ(dirty.end, sizeof(Int) * 4); + ASSERT_GT(bufObj->GetChangeSerial(), baseSerial); } TEST_F(BufferTest, AcquireMemoryRangeWithExplicit) { @@ -177,7 +169,7 @@ TEST_F(BufferTest, AcquireMemoryRangeWithExplicit) { DataPtr ptr{.data = initData.data(), .size = byteSize}; bufObj->UploadData(ptr, 0); - bufObj->ClearDirty(); + const Uint64 baseSerial = bufObj->GetChangeSerial(); Range1D mapRange{.start = sizeof(Int), .end = sizeof(Int) * 4}; Int* mappedPtr = static_cast( @@ -186,29 +178,20 @@ TEST_F(BufferTest, AcquireMemoryRangeWithExplicit) { mappedPtr[0] = 200; mappedPtr[1] = 300; + // Only the explicitly flushed range reaches the shadow (and the backend). bufObj->FlushMemoryRange(0, sizeof(Int)); - ASSERT_EQ(bufObj->GetDirtyRanges().size() >= 1, true); - auto dirty = bufObj->GetDirtyRanges()[0]; - ASSERT_EQ(dirty.start, sizeof(Int)); - ASSERT_EQ(dirty.end, sizeof(Int) * 2); + const Uint64 flushedSerial = bufObj->GetChangeSerial(); + ASSERT_GT(flushedSerial, baseSerial); + // FlushExplicit unmap must not flush the rest of the mapped range. bufObj->ReleaseMemory(); - - ASSERT_EQ(bufObj->GetDirtyRanges().size() >= 1, true); - dirty = bufObj->GetDirtyRanges()[0]; - ASSERT_EQ(dirty.start, sizeof(Int)); - ASSERT_EQ(dirty.end, sizeof(Int) * 2); + ASSERT_EQ(bufObj->GetChangeSerial(), flushedSerial); Vector expected{10, 200, 30, 40, 50}; Vector actual(5); void* p = bufObj->AcquireMemory(false, true, false); memcpy(actual.data(), p, byteSize); ASSERT_EQ(actual, expected); - - ASSERT_EQ(bufObj->GetDirtyRanges().size() >= 1, true); - dirty = bufObj->GetDirtyRanges()[0]; - ASSERT_EQ(dirty.start, sizeof(Int)); - ASSERT_EQ(dirty.end, sizeof(Int) * 2); } TEST_F(BufferTest, CopyBufferSubData) { @@ -237,8 +220,8 @@ TEST_F(BufferTest, CopyBufferSubData) { DataPtr dstPtr{.data = dstData.data(), .size = dstSize}; dstObj->UploadData(dstPtr, 0); - srcObj->ClearDirty(); - dstObj->ClearDirty(); + const Uint64 srcSerial = srcObj->GetChangeSerial(); + const Uint64 dstSerial = dstObj->GetChangeSerial(); dstObj->CopyDataFrom(srcObj, 2 * sizeof(Int), 5 * sizeof(Int), 4 * sizeof(Int)); @@ -250,10 +233,9 @@ TEST_F(BufferTest, CopyBufferSubData) { ASSERT_EQ(actual, expected); - ASSERT_EQ(dstObj->GetDirtyRanges().size() >= 1, true); - auto dirty = dstObj->GetDirtyRanges()[0]; - ASSERT_EQ(dirty.start, 5 * sizeof(Int)); - ASSERT_EQ(dirty.end, 9 * sizeof(Int)); + // The copy mutates only the destination. + ASSERT_GT(dstObj->GetChangeSerial(), dstSerial); + ASSERT_EQ(srcObj->GetChangeSerial(), srcSerial); } TEST_F(BufferTest, WriteWhileMapped) { @@ -282,10 +264,7 @@ TEST_F(BufferTest, WriteWhileMapped) { ASSERT_EQ(actual, expected); - ASSERT_EQ(bufObj->GetDirtyRanges().size() >= 1, true); - auto dirty = bufObj->GetDirtyRanges()[0]; - ASSERT_EQ(dirty.start, 0); - ASSERT_EQ(dirty.end, byteSize); + ASSERT_GT(bufObj->GetChangeSerial(), 0u); } TEST_F(BufferTest, PartialUpdate) { @@ -300,7 +279,7 @@ TEST_F(BufferTest, PartialUpdate) { bufObj->Resize(byteSize); DataPtr ptr{.data = initData.data(), .size = byteSize}; bufObj->UploadData(ptr, 0); - bufObj->ClearDirty(); + const Uint64 baseSerial = bufObj->GetChangeSerial(); Vector update{999, 888}; bufObj->UploadSubData({(void*)(update.data()), (SizeT)(update.size() * sizeof(Int))}, sizeof(Int)); @@ -312,10 +291,7 @@ TEST_F(BufferTest, PartialUpdate) { ASSERT_EQ(actual, expected); - ASSERT_EQ(bufObj->GetDirtyRanges().size() >= 1, true); - auto dirty = bufObj->GetDirtyRanges()[0]; - ASSERT_EQ(dirty.start, sizeof(Int)); - ASSERT_EQ(dirty.end, 3 * sizeof(Int)); + ASSERT_GT(bufObj->GetChangeSerial(), baseSerial); } TEST_F(BufferTest, DeleteBufferObject) { @@ -662,7 +638,7 @@ TEST_F(GeneralBufferTest, General_PersistentCoherentWriteDirtyWithoutUnmap) { auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer); ASSERT_NE(bufferObject, nullptr); - bufferObject->ClearDirty(); + const Uint64 baseSerial = bufferObject->GetChangeSerial(); auto* mapped = static_cast( MapBufferRange(GL_ARRAY_BUFFER, 0, sizeof(initial), @@ -670,10 +646,9 @@ TEST_F(GeneralBufferTest, General_PersistentCoherentWriteDirtyWithoutUnmap) { ASSERT_NE(mapped, nullptr); mapped[2] = 1234; - bufferObject->MarkPersistentMappedRangeDirty(); - ASSERT_FALSE(bufferObject->GetDirtyRanges().empty()); - EXPECT_EQ(bufferObject->GetDirtyRanges()[0].start, 0); - EXPECT_EQ(bufferObject->GetDirtyRanges()[0].end, sizeof(initial)); + // Draw-time hook: pushes the persistently mapped write range to the backend. + bufferObject->SyncPersistentMappedRange(); + EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial); const auto data = bufferObject->GetDataReadOnly(); EXPECT_EQ(reinterpret_cast(data->data())[2], 1234); @@ -692,7 +667,7 @@ TEST_F(GeneralBufferTest, General_PersistentExplicitFlushOnlyDirtiesFlushedRange auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer); ASSERT_NE(bufferObject, nullptr); - bufferObject->ClearDirty(); + const Uint64 baseSerial = bufferObject->GetChangeSerial(); auto* mapped = static_cast( MapBufferRange(GL_ARRAY_BUFFER, 0, sizeof(initial), @@ -701,13 +676,12 @@ TEST_F(GeneralBufferTest, General_PersistentExplicitFlushOnlyDirtiesFlushedRange mapped[1] = 200; mapped[3] = 400; - bufferObject->MarkPersistentMappedRangeDirty(); - EXPECT_TRUE(bufferObject->GetDirtyRanges().empty()); + // FlushExplicit persistent maps only reach the backend via explicit flushes. + bufferObject->SyncPersistentMappedRange(); + EXPECT_EQ(bufferObject->GetChangeSerial(), baseSerial); FlushMappedBufferRange(GL_ARRAY_BUFFER, sizeof(GLint), sizeof(GLint)); - ASSERT_FALSE(bufferObject->GetDirtyRanges().empty()); - EXPECT_EQ(bufferObject->GetDirtyRanges()[0].start, sizeof(GLint)); - EXPECT_EQ(bufferObject->GetDirtyRanges()[0].end, sizeof(GLint) * 2); + EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial); EXPECT_TRUE(UnmapBuffer(GL_ARRAY_BUFFER)); EXPECT_EQ(GetError(), GL_NO_ERROR); @@ -728,7 +702,7 @@ TEST_F(GeneralBufferTest, General_NamedBufferStorageMappingWrappers) { auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer); ASSERT_NE(bufferObject, nullptr); - bufferObject->ClearDirty(); + const Uint64 baseSerial = bufferObject->GetChangeSerial(); auto* mapped = static_cast( MapNamedBufferRange(buffer, 0, sizeof(initial), @@ -741,9 +715,7 @@ TEST_F(GeneralBufferTest, General_NamedBufferStorageMappingWrappers) { EXPECT_EQ(mapPointer, mapped); FlushMappedNamedBufferRange(buffer, 0, sizeof(GLint)); - ASSERT_FALSE(bufferObject->GetDirtyRanges().empty()); - EXPECT_EQ(bufferObject->GetDirtyRanges()[0].start, 0); - EXPECT_EQ(bufferObject->GetDirtyRanges()[0].end, sizeof(GLint)); + EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial); EXPECT_TRUE(UnmapNamedBuffer(buffer)); EXPECT_EQ(GetError(), GL_NO_ERROR);