From a6a5edf573d57b3fc7e95c66b9691de9680fd5b5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 9 Jul 2026 10:04:58 +0000 Subject: [PATCH] [Perf] (MG_Backend/DirectGLES, MG_Impl): cache link-time lookups, bound unit scans, honor eglSwapInterval - BackendProgramObjectImpl::CacheResourceLocations resolves every glGetUniformBlockIndex / glGetUniformLocation string query once per link and establishes the block binding points there. Per draw, BindCurrentProgramWithResources now uses the cached indices, re-issues glUniform1i only when a sampler's unit actually changed (program state persists), uploads the global UBO only when its content version moved, and skips redundant glUseProgram binds (guard reset on program-name reuse, MakeCurrent, and every explicit glUseProgram(0)). The caches are invalidated through ProgramObject's link version, which also makes a relinked program finally re-sync its backend program. - Track a texture-unit high-water mark (fed by glBindTexture / glBindTextureUnit / glBindSampler / glBindImageTexture) so the two per-draw unit scans (MAX_TEXTURE_IMAGE_UNITS is 192) and the texture-deletion unbind loop only walk units that were ever touched. - Forward the app's eglSwapInterval to the native EGL surface through a new BackendObject::SetEGLSwapInterval hook (applied immediately when the surface exists, otherwise deferred to surface creation / MakeCurrent). "VSync off" finally reaches the hardware - DirectGLES was hard-locked to the display refresh before. The driver-side cost of the per-draw string lookups was about half of a 30% Adreno driver hotspot; libMobileGL's share of the vanilla render thread fell from 22% to 9% (simpleperf, Adreno 830). Co-Authored-By: Claude Fable 5 --- MobileGL/MG_Backend/BackendObject.cpp | 7 + MobileGL/MG_Backend/BackendObject.h | 6 + .../DirectGLES/BackendObject_DirectGLES.cpp | 1 + MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 153 +++++++++--------- MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 3 + MobileGL/MG_Backend/DirectGLES/Managers.cpp | 72 +++++++++ MobileGL/MG_Backend/DirectGLES/Managers.h | 58 +++++++ MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp | 12 +- .../MG_Impl/GLImpl/Program/GL_Program.cpp | 1 + .../MG_Impl/GLImpl/Sampler/GL_Sampler.cpp | 1 + .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 4 + MobileGL/MG_State/GLState/Core.h | 2 + .../GLState/TextureState/TextureState.cpp | 10 +- .../GLState/TextureState/TextureState.h | 9 ++ 14 files changed, 255 insertions(+), 84 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.cpp b/MobileGL/MG_Backend/BackendObject.cpp index e5374140..2ceb4611 100644 --- a/MobileGL/MG_Backend/BackendObject.cpp +++ b/MobileGL/MG_Backend/BackendObject.cpp @@ -409,6 +409,13 @@ namespace MobileGL::MG_Backend { return true; } + void BackendObject::SetEGLSwapInterval(Int interval) { + const auto& backendFunctions = GetBackendFunctions(); + if (backendFunctions.SetSwapInterval) { + backendFunctions.SetSwapInterval(interval); + } + } + Bool BackendObject::IsEGLSurfaceCurrent(EGLSurface surface) const { if (surface == EGL_NO_SURFACE) { return false; diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 1bf479d8..63ae1f90 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -175,6 +175,9 @@ namespace MobileGL { struct GlobalBackendFunctionsTable { GLFunctionsTable GL; void (*Present)(); + // Optional: applies the app-requested eglSwapInterval to the native + // presentation path (null = backend keeps its own pacing policy). + void (*SetSwapInterval)(Int interval); }; struct DynamicBackendParameters { @@ -264,6 +267,9 @@ namespace MobileGL { virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height); virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx); virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw); + // Forwards the app-requested eglSwapInterval to the backend's native + // presentation path (no-op for backends without a SetSwapInterval hook). + virtual void SetEGLSwapInterval(Int interval); virtual void ReleaseEGLSurface(EGLSurface surface); virtual void ReleaseEGLResources(); diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index a6e086fe..0901b5d7 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -818,6 +818,7 @@ namespace MobileGL::MG_Backend::DirectGLES { static Bool funcsTableInitialized = false; if (!funcsTableInitialized) { funcsTable.Present = DirectGLES::Present; + funcsTable.SetSwapInterval = DirectGLES::SetSwapInterval; funcsTable.GL.DrawArrays = DrawArrays; funcsTable.GL.DrawElements = DrawElements; funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 4aa167d6..648953fc 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -355,7 +355,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // 2. textures used in current FBO // 3. textures bound to image units (TODO) - for (int index = 0; index < MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS; ++index) { + // Units past the frontend's high-water mark have provably-empty slots. + const Int maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit(); + for (Int index = 0; index <= maxTouchedUnit; ++index) { auto& unit = MG_State::pGLContext->GetTextureUnitObject(index); for (const auto& bindingSlot : unit.GetAllBindingSlots()) { auto& textureObject = bindingSlot.GetBoundObject(); @@ -748,6 +750,7 @@ namespace MobileGL::MG_Backend::DirectGLES { auto& currentProgram = MG_State::pGLContext->GetCurrentProgram(); if (!currentProgram || !currentProgram->GetLinkStatus()) { g_GLESFuncs.glUseProgram(0); + g_lastUsedBackendProgramId = 0; return; } const auto& backendProgramIt = g_backendProgramObjects.find(currentProgram.get()); @@ -757,7 +760,11 @@ namespace MobileGL::MG_Backend::DirectGLES { backendObj = MakeShared(); backendObj->SyncToBackend(currentProgram); } else { + // A link-version mismatch means the program was relinked: the backend + // shaders and every cache built by CacheResourceLocations (block + // indices, sampler locations, UBO upload gate) are stale. if (!backendObj->GetBackendProgramId() || + backendObj->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() || backendObj->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask || backendObj->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask) { backendObj->SyncToBackend(currentProgram); @@ -827,32 +834,6 @@ namespace MobileGL::MG_Backend::DirectGLES { static void BindCurrentProgramWithResources(); static void BindCurrentTextures(); - // Image uniforms take their unit from the layout(binding=N) qualifier baked into - // the transpiled ESSL; unlike samplers they must not (and in ES cannot) be - // assigned through glUniform1i. - static Bool IsImageUniformType(GLenum type) { - switch (type) { - case 0x904D: /*GL_IMAGE_2D*/ - case 0x904E: /*GL_IMAGE_3D*/ - case 0x9050: /*GL_IMAGE_CUBE*/ - case 0x9051: /*GL_IMAGE_BUFFER*/ - case 0x9053: /*GL_IMAGE_2D_ARRAY*/ - case 0x9058: /*GL_INT_IMAGE_2D*/ - case 0x9059: /*GL_INT_IMAGE_3D*/ - case 0x905B: /*GL_INT_IMAGE_CUBE*/ - case 0x905C: /*GL_INT_IMAGE_BUFFER*/ - case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/ - case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/ - case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/ - case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/ - case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/ - case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/ - return true; - default: - return false; - } - } - void PrepareForDraw(DrawSyncBit syncBit) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -896,8 +877,9 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND); #endif - Int maxTextureUnits = MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS; - for (Int unit = 0; unit < maxTextureUnits; ++unit) { + // Units past the frontend's high-water mark have provably-empty slots. + const Int maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit(); + for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) { @@ -945,56 +927,45 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif const auto& backendProgramIt = PrgramImpl::g_backendProgramObjects.find(currentProgram.get()); if (backendProgramIt != PrgramImpl::g_backendProgramObjects.end()) { - backendProgramIt->second->Use(); - auto backendProgramId = backendProgramIt->second->GetBackendProgramId(); + auto& backendProgram = *backendProgramIt->second; + backendProgram.Use(); - // Global UBO - if (currentProgram->GetUBOSize() > 0) { + // Global UBO: block index and binding-point assignment are cached at + // link time (CacheResourceLocations); re-upload only when the CPU shadow + // actually changed since the last upload for this program. + if (currentProgram->GetUBOSize() > 0 && backendProgram.HasGlobalUboBlock()) { #ifdef TRACY_ENABLE ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND); #endif - g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgramIt->second->GetBackendGlobalUBOId()); - g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(), - currentProgram->MapUBO()); - g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); - - Uint blockIndex = g_GLESFuncs.glGetUniformBlockIndex(backendProgramId, - MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); - if (blockIndex == GL_INVALID_INDEX) { - MGLOG_W("Program %u has frontend global UBO storage, but backend has no %s block.", - currentProgram->GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); - } else { - g_GLESFuncs.glUniformBlockBinding(backendProgramId, blockIndex, 0); - - g_GLESFuncs.glBindBufferBase(GL_UNIFORM_BUFFER, 0, - backendProgramIt->second->GetBackendGlobalUBOId()); + const Uint32 uboContentVersion = currentProgram->GetUBOContentVersion(); + if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) { + g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId()); + g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(), + currentProgram->MapUBO()); + g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); + backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion); } + g_GLESFuncs.glBindBufferBase(GL_UNIFORM_BUFFER, 0, backendProgram.GetBackendGlobalUBOId()); } { #ifdef TRACY_ENABLE ZoneScopedNC("UpdateUBO", TRACY_ZONECOLOR_BACKEND); #endif - // Normal UBO - auto uboCount = currentProgram->GetActiveUniformBlocksCount(); - Uint lastUBOBinding = 0; // to prevent overlapping bindings between global UBO and normal UBOs + // Normal UBOs: backend block indices and glUniformBlockBinding + // assignments are cached at link time; per draw only the buffer + // bindings are re-established (they follow frontend binding points). + const auto& blockBackendIndices = backendProgram.GetUniformBlockBackendIndices(); + const auto uboCount = static_cast(blockBackendIndices.size()); + Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO for (Int i = 0; i < uboCount; ++i) { ++lastUBOBinding; - // program state binding index == backend binding index - - // Connect program ubo index to backend binding point - auto binding = currentProgram->GetUniformBlockBinding(i); - auto& name = currentProgram->GetUniformBlockName(i); - GLuint backendBlkIdx = g_GLESFuncs.glGetUniformBlockIndex(backendProgramId, name.c_str()); - if (backendBlkIdx == GL_INVALID_INDEX) { - // Not a uniform block in the backend program: either eliminated as - // unused, or an SSBO block (the frontend's reflection lists those - // among uniform blocks); SSBO bindings are baked into the ESSL. + if (blockBackendIndices[i] < 0) { continue; } - g_GLESFuncs.glUniformBlockBinding(backendProgramId, backendBlkIdx, lastUBOBinding); // Connect buffer to backend binding point + auto binding = currentProgram->GetUniformBlockBinding(i); auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, binding); auto& bufferObj = point.GetBoundObject(); auto range = point.GetRange(); @@ -1022,23 +993,17 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND); #endif - // Sampler unit binding - auto maxUniformLoc = currentProgram->GetMaxUniformLocation(); - for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { - auto& name = currentProgram->GetUniformName(loc); - if (name.empty()) continue; - auto unit = currentProgram->GetUniformSamplerOrImageUnitIndex(loc); + // Sampler unit binding: backend locations are cached at link time; + // glUniform1i is program state, so it is only re-issued when the + // frontend-assigned unit differs from what this program last saw. + for (auto& samplerBinding : backendProgram.GetSamplerUniformBindings()) { + const auto unit = + currentProgram->GetUniformSamplerOrImageUnitIndex(samplerBinding.frontendLocation); if (unit == -1) continue; - const auto uniformType = currentProgram->GetUniformType(loc); - if (IsImageUniformType(uniformType)) { - // ES image units come exclusively from the layout(binding=N) - // qualifier (preserved in the transpiled ESSL); glUniform1i on an - // image uniform is an INVALID_OPERATION. - continue; + if (samplerBinding.lastAssignedUnit != unit) { + g_GLESFuncs.glUniform1i(samplerBinding.backendLocation, unit); + samplerBinding.lastAssignedUnit = unit; } - auto locAtBackend = g_GLESFuncs.glGetUniformLocation( - backendProgramIt->second->GetBackendProgramId(), name.c_str()); - g_GLESFuncs.glUniform1i(locAtBackend, unit); auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); auto& samplerObject = textureUnit.GetSamplerObject(); @@ -1048,10 +1013,10 @@ namespace MobileGL::MG_Backend::DirectGLES { rawDepthSamplerObject = &texture2D->GetSamplerObject(); } - if (uniformType == GL_SAMPLER_2D && texture2D && + if (samplerBinding.uniformType == GL_SAMPLER_2D && texture2D && NeedsRawDepthFetchSampler(*rawDepthSamplerObject, texture2D->GetFormat())) { GetRawDepthFetchSampler()->Bind(unit); - MGLOG_D("Using raw depth fetch sampler for uniform %s on unit %d.", name.c_str(), unit); + MGLOG_D("Using raw depth fetch sampler on unit %d.", unit); } else if (samplerObject) { const auto& backendSamplerIt = SamplerImpl::g_backendSamplerObjects.find(samplerObject.get()); @@ -1069,6 +1034,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } else { g_GLESFuncs.glUseProgram(0); + PrgramImpl::g_lastUsedBackendProgramId = 0; MGLOG_E("No backend program found (maybe not synced) for current program, cannot use program."); } } @@ -1222,6 +1188,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram(); if (!currentProgram || !currentProgram->GetLinkStatus()) { g_GLESFuncs.glUseProgram(0); + PrgramImpl::g_lastUsedBackendProgramId = 0; return; } @@ -3594,6 +3561,26 @@ namespace MobileGL::MG_Backend::DirectGLES { return g_Context != EGL_NO_CONTEXT; } + namespace { + // Last swap interval the app requested through eglSwapInterval; -1 = never + // requested (keep the EGL default of 1). Re-applied when the window surface + // is (re)created since interval is per-surface state. + Int g_requestedSwapInterval = -1; + + void ApplyRequestedSwapInterval() { + if (g_requestedSwapInterval < 0) return; + if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE) return; + const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval); + MGLOG_I("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval, + ok ? "ok" : "failed"); + } + } // namespace + + void SetSwapInterval(Int interval) { + g_requestedSwapInterval = interval; + ApplyRequestedSwapInterval(); + } + Bool InitWindowSurface(NativeWindowType window) { if (!window) return false; @@ -3604,6 +3591,8 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!MakeCurrent()) return false; + ApplyRequestedSwapInterval(); + MGLOG_D("EGL context created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display, g_Surface, g_Context, window); return true; @@ -3663,6 +3652,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // The ops table may have been unregistered when a previous ES context was // destroyed (e.g. a probe context); re-register now that GL is usable. BufferImpl::RegisterBufferBackendOps(); + // Conservatively drop the redundant-glUseProgram guard: re-issuing one bind + // after a MakeCurrent is cheaper than trusting a possibly-reset context. + PrgramImpl::g_lastUsedBackendProgramId = 0; + // eglSwapInterval requires a current context; a request made while none was + // current (and dropped by the driver) is retried here. + ApplyRequestedSwapInterval(); return true; } diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index ed645f33..eeeec512 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -110,6 +110,9 @@ namespace MobileGL::MG_Backend::DirectGLES { void DeleteSync(BackendSyncHandle sync); Bool GetSyncStatus(BackendSyncHandle sync); void Present(); + // Applies (or defers until the window surface exists) the app-requested + // eglSwapInterval on the native EGL surface. + void SetSwapInterval(Int interval); void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs); void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs); void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 3de867d4..17bc2fee 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1957,6 +1957,7 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace PrgramImpl { Uint32 g_snormFallbackClampOutputMask = 0; Uint32 g_unormFallbackClampOutputMask = 0; + Uint g_lastUsedBackendProgramId = 0; StateBackendObjectRegistry g_backendProgramObjects; BackendProgramObjectImpl::BackendProgramObjectImpl() { @@ -1980,6 +1981,11 @@ namespace MobileGL::MG_Backend::DirectGLES { if (m_backendProgramId != 0) { MGLOG_D("Deleting backend program object with ID: %u", m_backendProgramId); g_GLESFuncs.glDeleteProgram(m_backendProgramId); + // The driver may recycle this GL name for a future program; a stale + // guard entry would then wrongly skip the glUseProgram for it. + if (g_lastUsedBackendProgramId == m_backendProgramId) { + g_lastUsedBackendProgramId = 0; + } } } @@ -2168,16 +2174,82 @@ namespace MobileGL::MG_Backend::DirectGLES { m_backendGlobalUBOId = 0; } + CacheResourceLocations(stateProgramObject); + m_syncedLinkVersion = stateProgramObject->GetLinkVersion(); + m_isInitialized = true; MGLOG_D("Program sync completed. backend ID %u", m_backendProgramId); } + // Resolves every name-based resource lookup once per link so the per-draw path + // (BindCurrentProgramWithResources) never issues glGetUniformBlockIndex / + // glGetUniformLocation string queries; block-to-binding-point assignments are + // program state and only need to be established here. + void BackendProgramObjectImpl::CacheResourceLocations( + const SharedPtr& stateProgramObject) { + m_globalUboBackendBlockIndex = -1; + m_lastUploadedGlobalUboVersion = ~0u; + if (stateProgramObject->GetUBOSize() > 0) { + const Uint blockIndex = + g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); + if (blockIndex != GL_INVALID_INDEX) { + m_globalUboBackendBlockIndex = static_cast(blockIndex); + g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, blockIndex, 0); + } else { + MGLOG_W("Program %u has frontend global UBO storage, but backend has no %s block.", + stateProgramObject->GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); + } + } + + const Int uboCount = stateProgramObject->GetActiveUniformBlocksCount(); + m_uniformBlockBackendIndices.assign(static_cast(std::max(uboCount, 0)), -1); + Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO + for (Int i = 0; i < uboCount; ++i) { + ++lastUBOBinding; + const auto& name = stateProgramObject->GetUniformBlockName(static_cast(i)); + const GLuint backendBlkIdx = g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, name.c_str()); + if (backendBlkIdx == GL_INVALID_INDEX) { + // Either eliminated as unused, or an SSBO block (frontend reflection + // lists those among uniform blocks); SSBO bindings are baked into the ESSL. + continue; + } + m_uniformBlockBackendIndices[static_cast(i)] = static_cast(backendBlkIdx); + g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, backendBlkIdx, lastUBOBinding); + } + + m_samplerUniformBindings.clear(); + const Uint maxUniformLoc = stateProgramObject->GetMaxUniformLocation(); + for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { + const auto& name = stateProgramObject->GetUniformName(loc); + if (name.empty()) continue; + const GLenum uniformType = stateProgramObject->GetUniformType(loc); + if (IsImageUniformType(uniformType)) { + // ES image units come exclusively from the layout(binding=N) qualifier + // (preserved in the transpiled ESSL); glUniform1i on an image uniform + // is an INVALID_OPERATION. + continue; + } + const Int backendLoc = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, name.c_str()); + if (backendLoc < 0) continue; + SamplerUniformBinding binding; + binding.frontendLocation = loc; + binding.backendLocation = backendLoc; + binding.uniformType = uniformType; + binding.lastAssignedUnit = -1; + m_samplerUniformBindings.push_back(binding); + } + } + void BackendProgramObjectImpl::Use() const { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (g_lastUsedBackendProgramId == m_backendProgramId) { + return; + } MGLOG_D("Using program %u", m_backendProgramId); g_GLESFuncs.glUseProgram(m_backendProgramId); + g_lastUsedBackendProgramId = m_backendProgramId; } void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 204ca9f5..b5bba17b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -315,9 +315,46 @@ namespace MobileGL::MG_Backend::DirectGLES { extern Array g_fboBindVersions; } // namespace FramebufferImpl + // Image uniforms take their unit from the layout(binding=N) qualifier baked into + // the transpiled ESSL; unlike samplers they must not (and in ES cannot) be + // assigned through glUniform1i. + inline Bool IsImageUniformType(GLenum type) { + switch (type) { + case 0x904D: /*GL_IMAGE_2D*/ + case 0x904E: /*GL_IMAGE_3D*/ + case 0x9050: /*GL_IMAGE_CUBE*/ + case 0x9051: /*GL_IMAGE_BUFFER*/ + case 0x9053: /*GL_IMAGE_2D_ARRAY*/ + case 0x9058: /*GL_INT_IMAGE_2D*/ + case 0x9059: /*GL_INT_IMAGE_3D*/ + case 0x905B: /*GL_INT_IMAGE_CUBE*/ + case 0x905C: /*GL_INT_IMAGE_BUFFER*/ + case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/ + case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/ + case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/ + case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/ + case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/ + case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/ + return true; + default: + return false; + } + } + namespace PrgramImpl { class BackendProgramObjectImpl { public: + // Per-link cache of a sampler-style uniform's backend location: built once in + // SyncToBackend so draws stop issuing glGetUniformLocation string queries. + // lastAssignedUnit mirrors the program-state value set through glUniform1i + // (program state persists across binds, so caching per program is exact). + struct SamplerUniformBinding { + Uint frontendLocation = 0; + Int backendLocation = -1; + GLenum uniformType = 0; + Int lastAssignedUnit = -1; + }; + BackendProgramObjectImpl(); ~BackendProgramObjectImpl(); void SyncToBackend(const SharedPtr& stateProgramObject); @@ -331,7 +368,18 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; } Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; } + Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; } + const Vector& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; } + Vector& GetSamplerUniformBindings() { return m_samplerUniformBindings; } + Uint32 GetLastUploadedGlobalUboVersion() const { return m_lastUploadedGlobalUboVersion; } + void SetLastUploadedGlobalUboVersion(Uint32 version) { m_lastUploadedGlobalUboVersion = version; } + // Frontend link version this backend program (and its resource caches) was + // built from; a mismatch means every link-derived cache here is stale. + Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; } + private: + void CacheResourceLocations(const SharedPtr& stateProgramObject); + Uint m_backendProgramId = 0; Uint m_backendGlobalUBOId = 0; Int m_baseInstanceUniformLocation = -1; @@ -341,10 +389,20 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint32 m_snormFallbackClampOutputMask = 0; Uint32 m_unormFallbackClampOutputMask = 0; Bool m_isInitialized = false; + + Int m_globalUboBackendBlockIndex = -1; + Vector m_uniformBlockBackendIndices; // frontend block index -> backend index (-1 = absent) + Vector m_samplerUniformBindings; + Uint32 m_lastUploadedGlobalUboVersion = ~0u; + Uint32 m_syncedLinkVersion = ~0u; }; extern Uint32 g_snormFallbackClampOutputMask; extern Uint32 g_unormFallbackClampOutputMask; + // Backend id of the last glUseProgram issued through this backend; lets Use() + // skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the + // ES context is recreated. + extern Uint g_lastUsedBackendProgramId; extern StateBackendObjectRegistry g_backendProgramObjects; } // namespace PrgramImpl diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index e30b54d1..1c49fdf3 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -415,7 +415,17 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state) { return EGL_FALSE; } - return state->SwapInterval(dpy, interval) ? EGL_TRUE : EGL_FALSE; + if (!state->SwapInterval(dpy, interval)) { + return EGL_FALSE; + } + // Forward the request to the backend's native presentation path; without this + // the app's vsync setting only ever reaches MobileGL's shadow state and the + // native surface stays at the driver default (interval 1 = always vsynced). + auto* backendObject = GetBackendObject(state); + if (backendObject) { + backendObject->SetEGLSwapInterval(static_cast(interval)); + } + return EGL_TRUE; } EGLSurface CreatePbufferSurface(EGLDisplay dpy, EGLConfig config, const EGLint* attrib_list) { diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index b0d80c1e..dc693582 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -812,6 +812,7 @@ namespace MobileGL::MG_Impl::GLImpl { MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(), location, offset + byteOffsetInsideUniform); Memcpy((char*)programObject.MapUBO() + offset + byteOffsetInsideUniform, value, ItemCount * sizeof(T)); + programObject.MarkUBOContentDirty(); } else { auto* ttype = programObject.GetUniformTType(location); if (!ttype->isTexture() && !ttype->isImage()) return; diff --git a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp index aab9d528..ceec7248 100644 --- a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp @@ -202,6 +202,7 @@ namespace MobileGL::MG_Impl::GLImpl { } auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject((Int)unit); + MG_State::pGLContext->NoteTextureUnitTouched((Int)unit); if (sampler == 0) { textureUnit.SetSamplerObject(nullptr); } else { diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 2c7fa7ad..2a146de3 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -2495,6 +2495,7 @@ namespace MobileGL::MG_Impl::GLImpl { auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit); auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget); bindingSlot.Bind(nullptr); + MG_State::pGLContext->NoteTextureUnitTouched(activeUnit); return; } @@ -2528,6 +2529,7 @@ namespace MobileGL::MG_Impl::GLImpl { auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget); bindingSlot.Bind(textureObject); + MG_State::pGLContext->NoteTextureUnitTouched(MG_State::pGLContext->GetActiveTextureUnit()); } void ActiveTexture_State(GLenum texture) { @@ -3112,6 +3114,7 @@ namespace MobileGL::MG_Impl::GLImpl { } auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast(unit)); + MG_State::pGLContext->NoteTextureUnitTouched(static_cast(unit)); if (texture == 0) { for (auto& slot : textureUnit.GetAllBindingSlots()) { slot.Bind(nullptr); @@ -3274,6 +3277,7 @@ namespace MobileGL::MG_Impl::GLImpl { MG_State::pGLContext->GetImageTextureBinding(static_cast(unit)) .Bind(textureObject, level, layered, layer, access, format); + MG_State::pGLContext->NoteTextureUnitTouched(static_cast(unit)); bindImageTexture(unit, texture, level, layered, layer, access, format); } diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index e216726f..e6df3ea7 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -80,6 +80,8 @@ namespace MobileGL { TextureUnit& GetTextureUnitObject(Int unit); ImageTextureBinding& GetImageTextureBinding(Int unit); const ImageTextureBinding& GetImageTextureBinding(Int unit) const; + void NoteTextureUnitTouched(Int unit) { m_textureState.NoteUnitTouched(unit); } + Int GetMaxTouchedTextureUnit() const { return m_textureState.GetMaxTouchedUnit(); } Bool ValidateTextureName(Uint index) const; Bool ValidateTextureObject(Uint index) const; Int GetActiveTextureUnit() const; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index c40a217f..64586bc6 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -89,20 +89,22 @@ namespace MobileGL::MG_State::GLState { if (m_indexGenerator.IsValid(index)) { auto it = m_textureObjects.find(index); if (it != m_textureObjects.end()) { - for (auto& unit : m_textureUnits) { - auto& bindingSlots = unit.GetAllBindingSlots(); + // Units past the touched high-water mark can never reference a texture. + for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) { + auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots(); for (auto& bindingSlot : bindingSlots) { if (bindingSlot.GetBoundObject() == it->second) { bindingSlot.Bind(nullptr); } } } - for (auto& imageBinding : m_imageTextureBindings) { + for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) { + auto& imageBinding = m_imageTextureBindings[unit]; if (imageBinding.Texture == it->second) { imageBinding.Bind(nullptr, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8); } } - m_textureObjects.erase(it); + m_textureObjects.erase(index); } m_indexGenerator.Delete(index); } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index 4072e294..29285eff 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -52,7 +52,16 @@ namespace MobileGL::MG_State::GLState { Bool ValidateName(Uint index) const; Bool ValidateTextureObject(Uint index) const; + // High-water mark of texture units ever touched by a texture or sampler bind. + // Units above it have provably-empty binding slots, so per-draw backend scans + // can stop there instead of walking all MAX_TEXTURE_IMAGE_UNITS units. + void NoteUnitTouched(Int unit) { + if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) m_maxTouchedUnit = unit; + } + Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; } + private: + Int m_maxTouchedUnit = -1; Int m_activeTextureUnit = 0; Array m_textureUnits; Array m_imageTextureBindings;