[WIP] Mesa-style buffer overhaul: resource abstraction + immediate transfer ops

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 14:47:51 +08:00
co-authored by Claude Fable 5
parent 2ed96e9678
commit b8ffd25148
15 changed files with 1048 additions and 642 deletions
@@ -9,6 +9,7 @@
#include "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h"
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
@@ -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;
}
+43 -50
View File
@@ -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<SizeT>(indirect);
if (!drawData || commandOffset + requiredBytes > drawData->size()) {
@@ -177,16 +177,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace BufferImpl {
void CreateAndSyncBufferObject(const SharedPtr<MG_State::GLState::BufferObject>& 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<BackendBufferObject>();
}
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<GLuint>(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) {
@@ -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);
+248 -175
View File
@@ -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<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
std::mutex g_deferredBufferReleasesMutex;
GLESBufferResource* ResourceOf(BufferObject& bufferObject) {
return static_cast<GLESBufferResource*>(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<MG_State::GLState::BufferObject>& 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<MG_State::GLState::BufferObject>& 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<MG_State::GLState::BufferObject>& 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<const char*>(data) + range.start);
}
}
void BackendBufferObject::SyncToBackend_glMapBufferRange(
const SharedPtr<MG_State::GLState::BufferObject>& 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<const char*>(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<BufferMappingAccessBit> 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<Bool>(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<BackendBufferResource>&& resource) {
if (!resource) return;
auto* glesResource = static_cast<GLESBufferResource*>(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<std::mutex> 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<MG_State::GLState::BufferObject, BackendBufferObject> 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<std::mutex> 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<SharedPtr<BackendBufferResource>> releases;
{
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
releases.swap(g_deferredBufferReleases);
}
for (auto& resource : releases) {
auto* glesResource = static_cast<GLESBufferResource*>(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<GLESBufferResource*>(bufferObject->GetBackendResource().get());
}
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!bufferObject) return nullptr;
auto* resource = static_cast<GLESBufferResource*>(bufferObject->GetBackendResource().get());
if (!resource) {
auto newResource = MakeShared<GLESBufferResource>();
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<GLsizeiptr>(uploadSize), clientData,
GL_STREAM_DRAW);
@@ -486,7 +567,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
BufferImpl::g_boundVertexBufferObject = nullptr;
}
StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
@@ -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<BufferImpl::BackendBufferObject> backendBufferObject;
const auto& backendBufferIt = backendBuffers.find(buffer.get());
if (backendBufferIt == backendBuffers.end()) {
auto& backendBufferSlot = backendBuffers.GetOrCreate(buffer);
if (!backendBufferSlot) {
backendBufferSlot = MakeShared<BufferImpl::BackendBufferObject>();
}
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,
+32 -16
View File
@@ -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<MG_State::GLState::BufferObject>& stateBufferObject);
Uint GetBackendBufferId() const { return m_backendBufferId; }
void Bind(GLenum target = TempBufferTarget);
~GLESBufferResource() override = default;
private:
void SyncToBackend_glBufferData(const SharedPtr<MG_State::GLState::BufferObject>& stateBufferObject);
void SyncToBackend_glBufferSubData(const SharedPtr<MG_State::GLState::BufferObject>& stateBufferObject);
void SyncToBackend_glMapBufferRange(const SharedPtr<MG_State::GLState::BufferObject>& 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<MG_State::GLState::BufferObject, BackendBufferObject> 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<MG_State::GLState::BufferObject>& 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 {
@@ -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<SizeT>(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");
@@ -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<Uint32>(blockIndex)).c_str());
bufferObject->MarkPersistentMappedRangeDirty();
bufferObject->SyncPersistentMappedRange();
const auto bufferData = bufferObject->GetDataReadOnly();
MOBILEGL_ASSERT(bufferData != nullptr && !bufferData->empty(),
@@ -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<BufferMappingAccessBit> appAccess) {
if (g_activeBufferManager) {
g_activeBufferManager->OnFlushMappedRange(bufferObject, range, appAccess);
}
}
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& 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<MG_State::GLState::BufferObject>& 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<VkBufferResource*>(bufferObject.GetBackendResource().get());
}
const auto* bufferData = bufferObject->GetDataReadOnly().get();
MOBILEGL_ASSERT(bufferData != nullptr, "VkBufferManager::SyncResidentBuffer requires frontend buffer data");
bufferObject->MarkPersistentMappedRangeDirty();
SharedPtr<VkBufferResource> VkBufferManager::GetOrCreateResource(
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
auto existing = std::static_pointer_cast<VkBufferResource>(bufferObject->GetBackendResource());
if (existing) {
return existing;
}
auto resource = MakeShared<VkBufferResource>();
bufferObject->SetBackendResource(resource);
TrackLiveResource(resource);
return resource;
}
const VkDeviceSize bufferSize = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (bufferSize == 0) {
MGLOG_E("VkBufferManager::SyncResidentBuffer failed: buffer size is zero");
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
if (m_liveResources.size() >= kLiveResourcePruneThreshold) {
std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& 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<unsigned long long>(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<VkDeviceSize>(range.start);
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(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<MG_State::GLState::BufferObject>& bufferObject) {
if (bufferObject == nullptr) {
return;
Bool VkBufferManager::SwapStorageAndUploadAll(VkBufferResource& resource,
MG_State::GLState::BufferObject& bufferObject) {
const VkDeviceSize size = static_cast<VkDeviceSize>(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<VkDeviceSize>(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<VkDeviceSize>(offset);
region.size = static_cast<VkDeviceSize>(size);
vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, &region);
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<VkDeviceSize>(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<VkDeviceSize>(bufferObject.GetSize()) != resource->storageSize) {
resource->pendingFullUpload = true;
return;
}
if (!IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.GetDataReadOnly()->data() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(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<BufferMappingAccessBit> appAccess) {
auto* resource = ResourceOf(bufferObject);
if (!resource) {
return;
}
resource->transientFrameSerial = 0;
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
return;
}
if (static_cast<VkDeviceSize>(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<VkDeviceSize>(size), static_cast<VkDeviceSize>(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<MG_State::GLState::BackendBufferResource>&& resource) {
if (!resource) {
return;
}
auto vkResource = std::static_pointer_cast<VkBufferResource>(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<MG_State::GLState::BufferObject>& 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<VkDeviceSize>(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<MG_State::GLState::BufferObject>& 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<VkDeviceSize>(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<MG_State::GLState::BufferObject*> 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<MG_State::GLState::BufferObject*>(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
@@ -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<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice);
void DowngradeResidentBufferToTransient(const SharedPtr<MG_State::GLState::BufferObject>& 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<MG_State::GLState::BufferObject>& 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<MG_State::GLState::BufferObject>& 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<BufferMappingAccessBit> appAccess);
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& 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<MG_State::GLState::BufferObject> 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<VkBufferResource> GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& 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<VkBufferResource>& resource);
void ReleaseAllLiveResources();
VkBufferManagerInitInfo m_initInfo{};
BufferArena m_transientUploadArena;
UnorderedMap<MG_State::GLState::BufferObject*, ResidentBufferEntry> m_residentBuffers;
Vector<Vector<VkBufferObject>> m_deferredResidentReleases;
IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
Vector<WeakPtr<VkBufferResource>> m_liveResources;
Uint32 m_currentFrameIndex = 0;
Uint32 m_residentGcTick = 0;
Uint64 m_frameSerial = 1;
Uint64 m_completedSerialFloor = 0;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -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<VkTextureManager>();
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<VkDeviceSize>(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<VkDeviceSize>(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<VkDeviceSize>(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<VkDeviceSize>(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();
}
}
@@ -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<DrawSetupAspect> 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<const MG_State::GLState::BufferObject*, TransientBufferSliceCacheEntry>
m_transientVertexIndexBufferSlicesThisFrame;
Uint m_imageIndexAcquired = 0;
FrameContext m_frameContext;
+2 -8
View File
@@ -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) {
@@ -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<Data>()), 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<Data>()),
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<BufferMappingAccessBit> 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<BufferObject>& 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<BufferChangeBits> BufferObject::GetChangeBits() const {
return m_change.Bits;
}
Uint64 BufferObject::GetChangeSerial() const {
return m_changeSerial;
}
const SharedPtr<BackendBufferResource>& BufferObject::GetBackendResource() const {
return m_backendResource;
}
void BufferObject::SetBackendResource(SharedPtr<BackendBufferResource> resource) {
m_backendResource = std::move(resource);
}
Bool BufferObject::IsMapped() const {
return m_isMapped;
}
@@ -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<BufferChangeBits> 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<BufferMappingAccessBit> 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<BackendBufferResource>&& 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<BufferObject>& src, SizeT srcOffset, SizeT dstOffset, SizeT size);
void* AcquireMemory(Bool markMapped, Bool read, Bool write);
void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access);
void ReleaseMemory();
void FlushMemoryRange(SizeT offset, SizeT length);
void MarkPersistentMappedRangeDirty();
void UploadSubData(DataPtr data, SizeT atOffset);
void CopyDataFrom(const SharedPtr<BufferObject>& 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<BufferMappingAccessBit> GetMappingAccess() const;
GLbitfield GetStorageFlags() const;
Uint GetExternalIndex() const;
const VecRange1D& GetDirtyRanges() const;
Flags<BufferChangeBits> GetChangeBits() const;
// Monotonic counter bumped on every shadow mutation; backends use it to
// validate cached transient slices.
Uint64 GetChangeSerial() const;
const SharedPtr<BackendBufferResource>& GetBackendResource() const;
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
private:
void NotifyRespecify();
void NotifySubData(SizeT offset, SizeT size);
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
const Uint m_externalIndex = 0;
SizeT m_size = 0;
BufferUsage m_usage = BufferUsage::StaticDraw;
@@ -116,11 +162,11 @@ namespace MobileGL {
Flags<BufferMappingAccessBit> m_mappingAccess;
Bool m_isImmutableStorage = false;
GLbitfield m_storageFlags = 0;
BufferChange m_change;
Uint64 m_changeSerial = 0;
Range1D m_mappedRange;
Vector<Uint8> m_stagingData;
Bool m_ownsStagingData;
SharedPtr<BackendBufferResource> m_backendResource;
};
} // namespace MG_State::GLState
} // namespace MobileGL
+32 -60
View File
@@ -74,10 +74,8 @@ TEST_F(BufferTest, PingPong) {
Vector<Int> 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<Int*>(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<Int*>(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<Int*>(
@@ -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<Int> expected{10, 200, 30, 40, 50};
Vector<Int> 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<Int> 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<GLint*>(
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<const GLint*>(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<GLint*>(
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<GLint*>(
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);