diff --git a/MobileGL/Init.cpp b/MobileGL/Init.cpp index 905be751..7cb615e2 100644 --- a/MobileGL/Init.cpp +++ b/MobileGL/Init.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -45,12 +46,13 @@ namespace MobileGL { // both of which this function is about to destroy. This is the one // cancellation path in the whole design that waits. MG_Util::Async::ShaderCompilePool::Get().StopAndDrain(); - // GL syncs die with their contexts, and every context is gone by the - // time full teardown runs: drain the live-sync registry while the - // backend function table can still release the backend handles (and - // before a re-initialized library could pair them with the wrong - // backend's DeleteSync). + // GL syncs and queries die with their contexts, and every context is gone + // by the time full teardown runs: drain both live registries while the + // backend function table can still release the backend handles (and before + // a re-initialized library could pair them with the wrong backend's + // DeleteSync / DeleteBackendQuery). MG_Impl::GLImpl::DestroyAllSyncObjects(); + MG_Impl::GLImpl::DestroyAllQueryObjects(); MG_Backend::pActiveBackendObject.reset(); MG_State::pGLContext.reset(); MG_State::pEGLContext.reset(); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index a7ebf22a..d12eafdf 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -2205,6 +2205,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // accident - and it never covered the monolithic glUseProgram path at all - so the // dependency is stated here instead. if (!twin->GetBackendProgramId() || + twin->GetContextGeneration() != g_backendContextGeneration || twin->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() || twin->GetSyncedImageUnitVersion() != currentProgram->GetImageUnitVersion() || twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask || @@ -3054,6 +3055,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); const auto program = GetCurrentBackendProgram(); if (!currentProgram || program == nullptr || + program->GetContextGeneration() != g_backendContextGeneration || program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) { return true; } @@ -5899,6 +5901,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // fallen behind is about to be rebuilt anyway, and its current driver interface is // the PREVIOUS link's - applying to it could land the binding on an unrelated block. if (!backendObj->GetBackendProgramId() || + backendObj->GetContextGeneration() != g_backendContextGeneration || backendObj->GetSyncedLinkVersion() != programObject->GetLinkVersion()) { return; // SyncToBackend's reseed will carry it } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 31eb5d68..da6fedec 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1468,6 +1468,7 @@ namespace MobileGL::MG_Backend::DirectGLES { ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif m_clientAttributeBufferIds.fill(0); + m_contextGeneration = g_backendContextGeneration; g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId); if (m_backendVAOId == 0) { MGLOG_E_ONCE("Failed to generate vertex array object."); @@ -1481,17 +1482,28 @@ namespace MobileGL::MG_Backend::DirectGLES { if (InProcessTeardown()) { return; // see InProcessTeardown(): the driver may be unloaded already } + const Bool contextCurrent = m_contextGeneration == g_backendContextGeneration; if (m_backendVAOId != 0) { + // Scrub the binding shadow whether or not the id can still be + // deleted: a recycled name must never satisfy the shadow's dedup. NoteVAOIdDeleted(m_backendVAOId); - g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId); + if (contextCurrent && g_GLESFuncs.glDeleteVertexArrays) { + g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId); + } m_backendVAOId = 0; } for (auto& bufferId : m_clientAttributeBufferIds) { - if (bufferId != 0) { - BufferImpl::NoteBufferIdDeleted(bufferId); - g_GLESFuncs.glDeleteBuffers(1, &bufferId); - bufferId = 0; + if (bufferId == 0) { + continue; } + // Same discipline as the VAO id itself: a buffer id from a dead + // context belongs to that context and must never be deleted as a + // recycled name in a successor context. + BufferImpl::NoteBufferIdDeleted(bufferId); + if (contextCurrent && g_GLESFuncs.glDeleteBuffers) { + g_GLESFuncs.glDeleteBuffers(1, &bufferId); + } + bufferId = 0; } } @@ -1635,6 +1647,30 @@ namespace MobileGL::MG_Backend::DirectGLES { // PrepareForDraw's BindCurrentVAO establishes the draw binding regardless. const Uint32 currentConfigVersion = stateVAOObject->GetConfigVersion(); const Uint16 currentIndexBufferVersion = stateVAOObject->GetIndexBufferBindingSlot().GetVersion(); + + // The ES context was recreated since this twin last ran. Its GL names belong to + // the dead context and are gone; mint a fresh VAO and force every attribute / + // index-binding cache to re-emit. No glDelete* here: the old names are not ours + // to delete in the successor context. + if (m_contextGeneration != g_backendContextGeneration) { + InvalidateVAOBindingCache(); + m_backendVAOId = 0; + m_contextGeneration = g_backendContextGeneration; + m_clientAttributeBufferIds.fill(0); + m_isInitialized = false; + m_resolvedDrawBuffers = {}; + m_pendingAttribValueMask = {}; + m_hasSyncedConfigVersion = false; + m_syncedConfigVersion = 0; + m_syncedIndexBufferVersion = static_cast(currentIndexBufferVersion + 1); + m_syncedAttributeVersions.fill({}); + m_syncedFetchBaseInstance = 0; + g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId); + if (m_backendVAOId == 0) { + MGLOG_E_ONCE("Failed to recreate vertex array object for a new ES context."); + } + } + const Bool attributesDirty = !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion; const Bool indexBufferDirty = currentIndexBufferVersion != m_syncedIndexBufferVersion; @@ -1978,6 +2014,7 @@ namespace MobileGL::MG_Backend::DirectGLES { TextureSwizzleParam::Alpha}; m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT; m_forceTextureParamsResync = true; + m_forceSamplerResync = true; } // Sets the backend GL unpack state to MobileGL's upload default for the scope, @@ -2395,6 +2432,13 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + // The ES context was recreated since this twin last ran. Recreate the + // texture id before any version-based early-out below: those versions are + // frontend versions and do not move when only the backend context changed. + if (m_contextGeneration != g_backendContextGeneration) { + RecreateBackendTexture(); + } + #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif @@ -3119,14 +3163,19 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + if (m_contextGeneration != g_backendContextGeneration) { + RecreateBackendTexture(); + } + auto* samplerObject = stateTextureObject->GetSamplerObject().get(); Uint currentSamplerVersion = samplerObject->GetVersion(); - if (m_syncedSamplerVersion == currentSamplerVersion) { + if (m_syncedSamplerVersion == currentSamplerVersion && !m_forceSamplerResync) { MGLOG_D("Sampler parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId); return; } m_syncedSamplerVersion = currentSamplerVersion; + m_forceSamplerResync = false; MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); @@ -3229,6 +3278,10 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + if (m_contextGeneration != g_backendContextGeneration) { + RecreateBackendTexture(); + } + Uint16 currentTextureParamsVersion = stateTextureObject->GetTextureParamsVersion(); if (m_syncedTextureParamsVersion == currentTextureParamsVersion && !m_forceTextureParamsResync) { MGLOG_D("Texture parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId); @@ -3902,6 +3955,19 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E_ONCE("State FBO object is null, cannot sync to backend."); return; } + // Recreate the driver FBO when the ES context has moved on. The old id is + // gone with the old context; calling glDeleteFramebuffers on its recycled + // numeric value could delete a new live FBO, so simply abandon it. + if (m_contextGeneration != g_backendContextGeneration) { + m_backendFBOId = 0; + m_contextGeneration = g_backendContextGeneration; + g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId); + if (m_backendFBOId == 0) { + MGLOG_E_ONCE("Failed to recreate framebuffer object for a new ES context."); + } + InvalidateFramebufferBindingCache(); + InvalidateSyncedState(); + } MGLOG_D("Syncing FBO with backend ID %u to backend for state ID %u, as %s FBO", m_backendFBOId, stateFBOObject->GetExternalIndex(), (asTarget == FramebufferTarget::Draw ? "DRAW" : "READ")); GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget); @@ -4412,10 +4478,27 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint g_lastUsedBackendProgramId = 0; StateBackendObjectRegistry g_backendProgramObjects; + void DeleteBackendProgramGlobalUbo(Uint& bufferId, Uint contextGeneration) { + if (bufferId == 0) { + return; + } + // Only a buffer that belongs to the LIVE context may be deleted. A stale + // generation means the old ES context already reclaimed it; handing its + // recycled numeric id to glDeleteBuffers could delete a new live buffer. + if (contextGeneration == g_backendContextGeneration) { + BufferImpl::NoteBufferIdDeleted(bufferId); + if (g_GLESFuncs.glDeleteBuffers) { + g_GLESFuncs.glDeleteBuffers(1, &bufferId); + } + } + bufferId = 0; + } + BackendProgramObjectImpl::BackendProgramObjectImpl() { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + m_contextGeneration = g_backendContextGeneration; m_backendProgramId = g_GLESFuncs.glCreateProgram(); if (m_backendProgramId == 0) { MGLOG_E_ONCE("Failed to create program object in backend."); @@ -4433,14 +4516,23 @@ namespace MobileGL::MG_Backend::DirectGLES { if (InProcessTeardown()) { return; // see InProcessTeardown(): the driver may be unloaded already } + DeleteBackendProgramGlobalUbo(m_backendGlobalUBOId, m_contextGeneration); if (m_backendProgramId != 0) { - MGLOG_D("Deleting backend program object with ID: %u", m_backendProgramId); - g_GLESFuncs.glDeleteProgram(m_backendProgramId); + // Same generation rule as the global UBO: a program id from a dead + // context is gone already and must not be deleted as a recycled name + // in a successor context. + if (m_contextGeneration == g_backendContextGeneration) { + MGLOG_D("Deleting backend program object with ID: %u", m_backendProgramId); + if (g_GLESFuncs.glDeleteProgram) { + 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; } + m_backendProgramId = 0; } } @@ -4677,6 +4769,31 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + // The ES context was recreated since this twin last ran. The old program id + // and global UBO id belong to the dead context; drop them without GL calls + // and mint a fresh program before reusing any cached reflection/version data. + if (m_contextGeneration != g_backendContextGeneration) { + DeleteBackendProgramGlobalUbo(m_backendGlobalUBOId, m_contextGeneration); + m_backendProgramId = 0; + m_contextGeneration = g_backendContextGeneration; + m_backendProgramId = g_GLESFuncs.glCreateProgram(); + if (m_backendProgramId == 0) { + MGLOG_E_ONCE("Failed to recreate backend program object for a new ES context."); + } + m_isInitialized = false; + m_backendProgramUsable = false; + m_syncedLinkVersion = ~0u; + m_syncedImageUnitVersion = ~0u; + m_lastUploadedGlobalUboVersion = ~0u; + m_globalUboBackendBlockIndex = -1; + m_globalUboBackendBlockSize = 0; + m_uniformBlockBackendIndices.clear(); + m_samplerUniformBindings.clear(); + m_formatlessImageUnits.clear(); + m_imageUnitFormatSignature = 0; + m_globalUboRingAllocation = {}; + } + MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u", stateProgramObject->GetExternalIndex(), m_backendProgramId); // Every link-derived cache below (incl. m_samplerUniformBindings and its @@ -5154,7 +5271,9 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - // Create global UBO + // Create global UBO. Delete any previous one first: relink reuses this + // backend program, and without this every relink leaked the old buffer. + DeleteBackendProgramGlobalUbo(m_backendGlobalUBOId, m_contextGeneration); if (stateProgramObject->GetUBOSize() > 0) { g_GLESFuncs.glGenBuffers(1, &m_backendGlobalUBOId); g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, m_backendGlobalUBOId); @@ -5389,6 +5508,20 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + if (m_contextGeneration != g_backendContextGeneration) { + // Old sampler id died with the old context; abandon it and mint a new + // one before the version-based early-out below can reuse a dead name. + m_backendSamplerId = 0; + m_contextGeneration = g_backendContextGeneration; + g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId); + if (m_backendSamplerId == 0) { + MGLOG_E_ONCE("Failed to recreate sampler object for a new ES context."); + } + m_isInitialized = false; + m_cacheSamplerParameters = {}; + g_boundSamplersCache.fill(nullptr); + } + Uint currentSamplerVersion = stateSamplerObject->GetVersion(); if (m_isInitialized && m_syncedSamplerVersion == currentSamplerVersion) { MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.", @@ -5533,6 +5666,23 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + if (m_contextGeneration != g_backendContextGeneration) { + // The old renderbuffer id died with the old context. Abandon it and + // force a fresh allocation instead of letting the parameter early-out + // below keep using a dead name. + m_backendRBOId = 0; + m_contextGeneration = g_backendContextGeneration; + g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId); + if (m_backendRBOId == 0) { + MGLOG_E_ONCE("Failed to recreate renderbuffer object for a new ES context."); + } + m_isInitialized = false; + m_cacheInternalFormat = TextureInternalFormat::Unknown; + m_cacheWidth = -1; + m_cacheHeight = -1; + m_cacheSamples = -1; + } + MGLOG_D("Syncing RBO with backend ID %u to backend for state ID %u", m_backendRBOId, stateRBOObject->GetExternalIndex()); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 7cb98587..566f79cd 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -406,6 +406,7 @@ namespace MobileGL::MG_Backend::DirectGLES { void SyncClientSideAttributesForDrawArrays( const SharedPtr& stateVAOObject, GLint first, GLsizei count); Uint GetBackendVertexArrayId() const { return m_backendVAOId; } + Uint GetContextGeneration() const { return m_contextGeneration; } void Bind() const; // Draw-path memo of SyncNeccessaryBuffers' attribute walk for this VAO: the @@ -462,6 +463,10 @@ namespace MobileGL::MG_Backend::DirectGLES { ResolvedDrawBuffers m_resolvedDrawBuffers; PendingAttribValueMask m_pendingAttribValueMask; Uint m_backendVAOId = 0; + // ES context generation the VAO id and client-attribute buffer ids were + // created under; ids from a dead context must never be deleted against a + // successor context (both contexts restart GL names at 1). + Uint m_contextGeneration = 0; Array m_clientAttributeBufferIds; Bool m_isInitialized = false; Uint16 m_syncedIndexBufferVersion = 0; @@ -711,6 +716,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // parameter already pushed onto it: the params-version early-out has to be overridden // once, or an unchanged version would skip the re-push forever. Bool m_forceTextureParamsResync = false; + // Same latch for the built-in sampler parameters. + Bool m_forceSamplerResync = false; }; void ActivateTextureUnit(Uint unit); @@ -1087,6 +1094,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; } Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; } Uint GetBackendProgramId() const { return m_backendProgramId; } + Uint GetContextGeneration() const { return m_contextGeneration; } // False when the last SyncToBackend could not produce a usable program (a // shader failed to transpile or compile, or the link itself failed). Use() // must not leave the previously bound program current in that case. @@ -1149,6 +1157,10 @@ namespace MobileGL::MG_Backend::DirectGLES { void CacheResourceLocations(const SharedPtr& stateProgramObject); Uint m_backendProgramId = 0; + // ES context generation the backend program and its global UBO were created + // under. A stale twin must be recreated, never deleted against a successor + // context (both contexts restart GL names at 1). + Uint m_contextGeneration = 0; // GL name of the frontend program this was last synced from; diagnostics only, so // an unusable backend program can be traced back to the glCreateProgram id the app // knows it by. @@ -1196,6 +1208,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the // ES context is recreated. extern Uint g_lastUsedBackendProgramId; + // Deletes `bufferId` only while it still belongs to the live ES context. Stale + // generations are abandoned without a GL call: the old context already reclaimed + // the buffer, and its numeric id may now name a live buffer in a successor context. + void DeleteBackendProgramGlobalUbo(Uint& bufferId, Uint contextGeneration); extern StateBackendObjectRegistry g_backendProgramObjects; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index 44a46ec6..60587905 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -194,54 +194,54 @@ namespace MobileGL::MG_Backend::DirectVulkan { } PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const { - XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); + XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion)); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.programHash, sizeof(payload.programHash))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.vertexInputHash, sizeof(payload.vertexInputHash))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.pipelineLayout, sizeof(payload.pipelineLayout))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.renderPass, sizeof(payload.renderPass))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.subpass, sizeof(payload.subpass))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.topology, sizeof(payload.topology))); XXHASH_VERIFY( - XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace))); + XXH64_update(m_hashState.Get(), &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.patchControlPoints, sizeof(payload.patchControlPoints))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.viewportCount, sizeof(payload.viewportCount))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.polygonMode, sizeof(payload.polygonMode))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.cullMode, sizeof(payload.cullMode))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontFace, sizeof(payload.frontFace))); XXHASH_VERIFY( - XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable))); + XXH64_update(m_hashState.Get(), &payload.provokingVertexMode, sizeof(payload.provokingVertexMode))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthTestEnable, sizeof(payload.depthTestEnable))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthWriteEnable, sizeof(payload.depthWriteEnable))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthBiasEnable, sizeof(payload.depthBiasEnable))); XXHASH_VERIFY( - XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp))); + XXH64_update(m_hashState.Get(), &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.logicOpEnable, sizeof(payload.logicOpEnable))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.stencilTestEnable, sizeof(payload.stencilTestEnable))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthCompareOp, sizeof(payload.depthCompareOp))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.logicOp, sizeof(payload.logicOp))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp))); XXHASH_VERIFY( - XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp))); + XXH64_update(m_hashState.Get(), &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp))); XXHASH_VERIFY( - XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp))); + XXH64_update(m_hashState.Get(), &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.backStencilFailOp, sizeof(payload.backStencilFailOp))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.backStencilPassOp, sizeof(payload.backStencilPassOp))); XXHASH_VERIFY( - XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp))); + XXH64_update(m_hashState.Get(), &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp))); XXHASH_VERIFY( - XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp))); + XXH64_update(m_hashState.Get(), &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp))); XXHASH_VERIFY( - XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth))); + XXH64_update(m_hashState.Get(), &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth))); if (payload.colorAttachmentCount > 0) { XXHASH_VERIFY(XXH64_update( - m_hashState, + m_hashState.Get(), payload.colorBlendAttachments.data(), sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount)); } - return XXH64_digest(m_hashState); + return XXH64_digest(m_hashState.Get()); } VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index cf846a3d..7ad2707b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -12,6 +12,7 @@ #include "../VkIncludes.h" #include "MG_State/GLState/FramebufferState/FramebufferObject.h" #include +#include namespace MobileGL::MG_Backend::DirectVulkan { // Enough of a fingerprint to identify the exact module the driver rejected without keeping the @@ -165,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { UnorderedMap m_cache; // Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging. Uint64 m_frameCounter = 0; - static inline XXH64_state_t* m_hashState = XXH64_createState(); + static inline MobileGL::XXH64State m_hashState; static inline Bool s_suppressBlendedDepthWrite = false; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 531ef6da..5a077872 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -2156,26 +2156,26 @@ namespace MobileGL::MG_Backend::DirectVulkan { ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const { - XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); + XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion)); // We expect shader stages in program object are sorted const auto& spirvs = program.GetGeneratedSpirv(); for (const auto& spv : spirvs) { - XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), spv.data(), spv.size() * sizeof(Uint))); } - XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &flags, sizeof(CompileOptionFlags))); // Only FragCoordYFlip variants bake the height in, so mixing it unconditionally would // re-key every program in the cache on a resize for no reason. if (flags & CompileOptionBit::FragCoordYFlip) { - XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferHeight, + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &m_defaultFramebufferHeight, sizeof(m_defaultFramebufferHeight))); } // Include UBO block bindings in hash so different binding configurations produce different entries const Uint32 blockCount = static_cast(program.GetActiveUniformBlocksCount()); - XXHASH_VERIFY(XXH64_update(m_hashState, &blockCount, sizeof(blockCount))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &blockCount, sizeof(blockCount))); for (Uint32 i = 0; i < blockCount; ++i) { const Uint32 binding = program.GetUniformBlockBinding(i); - XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding, sizeof(binding))); } // The transform feedback capture layout is baked into the modules by @@ -2186,18 +2186,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { // hashed for a capturing compile, so nothing else changes key. if (flags & CompileOptionBit::XfbCapture) { for (const auto& varying : program.GetTransformFeedbackVaryings()) { - XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size())); - XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex))); - XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), varying.name.data(), varying.name.size())); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &varying.bufferIndex, sizeof(varying.bufferIndex))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &varying.offsetBytes, sizeof(varying.offsetBytes))); } const SizeT bufferCount = program.GetTransformFeedbackBufferCount(); for (SizeT i = 0; i < bufferCount; ++i) { const Uint32 stride = program.GetTransformFeedbackStride(static_cast(i)); - XXHASH_VERIFY(XXH64_update(m_hashState, &stride, sizeof(stride))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &stride, sizeof(stride))); } } - HashType hash = XXH64_digest(m_hashState); + HashType hash = XXH64_digest(m_hashState.Get()); return hash; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index aa138b09..1969d9da 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -15,6 +15,7 @@ #include "MG_State/GLState/TextureState/TextureEnum.h" #include +#include #include namespace MobileGL::MG_Backend::DirectVulkan { @@ -489,6 +490,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // ever built from one keeps referencing its module. A failed build is cached as // VK_NULL_HANDLE so a broken generator costs one compile, not one per draw. UnorderedMap m_passthroughTessControlStages; - static inline XXH64_state_t* m_hashState = XXH64_createState(); + static inline MobileGL::XXH64State m_hashState; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index 47ddbde9..e9e1359a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -13,25 +13,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash( const MG_State::GLState::VertexArrayObject& vao) const { - XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); + XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion)); for (Int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) { const auto& attr = vao.GetAttribute(i); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Enabled, sizeof(attr.Enabled))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Enabled, sizeof(attr.Enabled))); if (!attr.Enabled) { continue; } - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Size, sizeof(attr.Size))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Type, sizeof(attr.Type))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Normalized, sizeof(attr.Normalized))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Size, sizeof(attr.Size))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Type, sizeof(attr.Type))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Normalized, sizeof(attr.Normalized))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Stride, sizeof(attr.Stride))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Offset, sizeof(attr.Offset))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsInteger, sizeof(attr.IsInteger))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsLong, sizeof(attr.IsLong))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsBgra, sizeof(attr.IsBgra))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Divisor, sizeof(attr.Divisor))); // The bound buffer's IDENTITY is a component of the key, and it has to be the // buffer's never-reused lifetime id - NOT its heap address, which this used to @@ -45,10 +45,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // test's positions) instead of its own. // Zero for client memory (no buffer), which is a distinct identity of its own. const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0; - XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &bufferKey, sizeof(bufferKey))); } - return XXH64_digest(m_hashState); + return XXH64_digest(m_hashState.Get()); } VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash( @@ -225,24 +225,24 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.attributes = builder.GetAttributes(); // See the layoutHash declaration: hash only the resolved layout, never // buffer identities, so identical layouts across VAOs/buffers agree. - XXHASH_VERIFY(XXH64_reset(m_hashState, 0)); + XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), 0)); for (const auto& binding : entry.bindings) { - XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding))); - XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride))); - XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.binding, sizeof(binding.binding))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.stride, sizeof(binding.stride))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.inputRate, sizeof(binding.inputRate))); } for (const auto& attribute : entry.attributes) { - XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.location, sizeof(attribute.location))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.binding, sizeof(attribute.binding))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.format, sizeof(attribute.format))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.offset, sizeof(attribute.offset))); } for (const auto& divisor : entry.bindingDivisors) { - XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding))); - XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &divisor.binding, sizeof(divisor.binding))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &divisor.divisor, sizeof(divisor.divisor))); } - XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask))); - entry.layoutHash = XXH64_digest(m_hashState); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &unsupportedAttribMask, sizeof(unsupportedAttribMask))); + entry.layoutHash = XXH64_digest(m_hashState.Get()); entry.attributeLocationMask = 0; for (const auto& attribute : entry.attributes) { if (attribute.location < 32u) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h index e5642fbd..6ef28ffd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h @@ -12,6 +12,7 @@ #include "VertexInputStateBuilder.h" #include "MG_State/GLState/VertexArrayState/VertexArrayObject.h" #include +#include #include "../VkIncludes.h" namespace MobileGL::MG_Backend::DirectVulkan { @@ -126,6 +127,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // matches, so an evicted entry can never be dereferenced through a // stale memo. Uint64 m_evictionEpoch = 1; - static inline XXH64_state_t* m_hashState = XXH64_createState(); + static inline MobileGL::XXH64State m_hashState; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 6203b443..62b603f5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -594,27 +594,27 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkRenderPassManager::HashType VkRenderPassManager::ComputeHash( const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear, Bool includeDefaultFboDepthStencil) { - XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); + XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion)); const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); if (isDefaultFbo) { - XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &swapchainImageIndex, sizeof(swapchainImageIndex))); } // sRGB attachments switch between their sRGB and UNORM-twin views with this // capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats. const Bool framebufferSrgbEnabled = MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb); - XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled))); auto& drawBuffers = fbo.GetDrawBuffers(); - XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0]))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0]))); auto readBuffer = fbo.GetReadBuffer(); - XXHASH_VERIFY(XXH64_update(m_hashState, &readBuffer, sizeof(FramebufferAttachmentType))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &readBuffer, sizeof(FramebufferAttachmentType))); Int validDrawBufCount = 0; for (Int i = 0; i < drawBuffers.size(); ++i) { auto drawbuf = drawBuffers[i]; if (drawbuf != FramebufferAttachmentType::None) validDrawBufCount = std::max(validDrawBufCount, i + 1); } - XXHASH_VERIFY(XXH64_update(m_hashState, &validDrawBufCount, sizeof(validDrawBufCount))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &validDrawBufCount, sizeof(validDrawBufCount))); auto combineFramebufferAttachmentObjHash = [&](FramebufferAttachmentType attachment) { auto& att = fbo.GetAttachment(attachment); @@ -623,49 +623,49 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (att.IsEmpty()) type = 0; else if (att.IsTexture()) type = 1; else if (att.IsRenderbuffer()) type = 2; - XXHASH_VERIFY(XXH64_update(m_hashState, &type, sizeof(type))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &type, sizeof(type))); void* contentPtr = nullptr; if (att.IsTexture()) contentPtr = att.GetTexture().get(); else if (att.IsRenderbuffer()) contentPtr = att.GetRenderbuffer().get(); - XXHASH_VERIFY(XXH64_update(m_hashState, &contentPtr, sizeof(contentPtr))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &contentPtr, sizeof(contentPtr))); if (att.IsTexture()) { const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId(); - XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLifetimeId, sizeof(textureLifetimeId))); const Int textureLevel = att.GetTextureLevel(); - XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLevel, sizeof(textureLevel))); const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget(); - XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureUploadTarget, sizeof(textureUploadTarget))); const Int textureLayer = att.GetTextureLayer(); - XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLayer, sizeof(textureLayer))); const Bool textureLayered = att.IsLayered(); - XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLayered, sizeof(textureLayered))); Uint64 imageIdentity = 0; auto* texture = att.GetTexture().get(); auto* resource = m_textureManager.SyncTextureAndGetDescriptor(*texture); if (resource != nullptr) { imageIdentity = reinterpret_cast(resource->image); - XXHASH_VERIFY(XXH64_update(m_hashState, &resource->sampleCount, sizeof(resource->sampleCount))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &resource->sampleCount, sizeof(resource->sampleCount))); } else { const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT; - XXHASH_VERIFY(XXH64_update(m_hashState, &fallbackSampleCount, sizeof(fallbackSampleCount))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &fallbackSampleCount, sizeof(fallbackSampleCount))); } - XXHASH_VERIFY(XXH64_update(m_hashState, &imageIdentity, sizeof(imageIdentity))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &imageIdentity, sizeof(imageIdentity))); } if (includePendingClear && att.IsTexture()) { auto* texture = att.GetTexture().get(); const auto pendingClearKey = VkClearManager::MakePendingClearKey(att); auto hasClear = m_clearManager.HasPendingClear(pendingClearKey); - XXHASH_VERIFY(XXH64_update(m_hashState, &hasClear, sizeof(hasClear))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasClear, sizeof(hasClear))); if (hasClear) { ClearAttachmentPayload clearPayload{}; Bool hasPayload = m_clearManager.GetPendingClear(pendingClearKey, clearPayload); - XXHASH_VERIFY(XXH64_update(m_hashState, &hasPayload, sizeof(hasPayload))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasPayload, sizeof(hasPayload))); if (hasPayload) { - XXHASH_VERIFY(XXH64_update(m_hashState, &clearPayload.mask, sizeof(clearPayload.mask))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &clearPayload.mask, sizeof(clearPayload.mask))); } } @@ -695,7 +695,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { currentLayout = textureResource->layout; } } - XXHASH_VERIFY(XXH64_update(m_hashState, ¤tLayout, sizeof(currentLayout))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), ¤tLayout, sizeof(currentLayout))); } if (att.IsRenderbuffer() && att.GetRenderbuffer()) { const auto& renderbuffer = att.GetRenderbuffer(); @@ -703,10 +703,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Int width = renderbuffer->GetWidth(); const Int height = renderbuffer->GetHeight(); const Int samples = renderbuffer->GetSamples(); - XXHASH_VERIFY(XXH64_update(m_hashState, &internalFormat, sizeof(internalFormat))); - XXHASH_VERIFY(XXH64_update(m_hashState, &width, sizeof(width))); - XXHASH_VERIFY(XXH64_update(m_hashState, &height, sizeof(height))); - XXHASH_VERIFY(XXH64_update(m_hashState, &samples, sizeof(samples))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &internalFormat, sizeof(internalFormat))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &width, sizeof(width))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &height, sizeof(height))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &samples, sizeof(samples))); Uint64 imageIdentity = 0; VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED; @@ -714,25 +714,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (resource != nullptr) { imageIdentity = reinterpret_cast(resource->image); currentLayout = resource->layout; - XXHASH_VERIFY(XXH64_update(m_hashState, &resource->sampleCount, sizeof(resource->sampleCount))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &resource->sampleCount, sizeof(resource->sampleCount))); } else { const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT; - XXHASH_VERIFY(XXH64_update(m_hashState, &fallbackSampleCount, sizeof(fallbackSampleCount))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &fallbackSampleCount, sizeof(fallbackSampleCount))); } - XXHASH_VERIFY(XXH64_update(m_hashState, &imageIdentity, sizeof(imageIdentity))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &imageIdentity, sizeof(imageIdentity))); if (includePendingClear) { const Bool hasClear = HasPendingRenderbufferClear(att); - XXHASH_VERIFY(XXH64_update(m_hashState, &hasClear, sizeof(hasClear))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasClear, sizeof(hasClear))); if (hasClear) { ClearAttachmentPayload clearPayload{}; const Bool hasPayload = GetPendingRenderbufferClear(renderbuffer.get(), clearPayload); - XXHASH_VERIFY(XXH64_update(m_hashState, &hasPayload, sizeof(hasPayload))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasPayload, sizeof(hasPayload))); if (hasPayload) { - XXHASH_VERIFY(XXH64_update(m_hashState, &clearPayload.mask, sizeof(clearPayload.mask))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &clearPayload.mask, sizeof(clearPayload.mask))); } } - XXHASH_VERIFY(XXH64_update(m_hashState, ¤tLayout, sizeof(currentLayout))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), ¤tLayout, sizeof(currentLayout))); } } }; @@ -745,13 +745,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { // The depth-less default-FBO flavor omits the depth/stencil attachment // entirely, so it must hash differently from the depth-full flavor. const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil; - XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &depthStencilIncluded, sizeof(depthStencilIncluded))); if (depthStencilIncluded) { combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth); combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil); } - return XXH64_digest(m_hashState); + return XXH64_digest(m_hashState.Get()); } RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index 531a46be..53b7c877 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -16,6 +16,7 @@ #include "MG_State/GLState/FramebufferState/FramebufferObject.h" #include +#include #include #include @@ -391,7 +392,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { void DeferRenderbufferBackingRelease(RenderbufferResource& resource); void CollectDeferredRenderbufferReleases(Bool destroyAll); - static inline XXH64_state_t* m_hashState = XXH64_createState(); + static inline MobileGL::XXH64State m_hashState; static inline ActiveRenderPassInfo s_activeRenderPass{}; static inline Bool s_hasActiveRenderPass = false; static inline VkClearManager* s_clearManager = nullptr; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp index f94f0918..69a5b697 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp @@ -134,41 +134,41 @@ namespace MobileGL::MG_Backend::DirectVulkan { const MG_State::GLState::ITextureObject& texture, Bool forceNearestFiltering, Bool singleLevelView) const { MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null"); - XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion)); + XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config->CacheVersion)); - XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering))); - XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &forceNearestFiltering, sizeof(forceNearestFiltering))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &singleLevelView, sizeof(singleLevelView))); const auto minFilter = sampler.GetMinFilter(); - XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &minFilter, sizeof(minFilter))); const auto magFilter = sampler.GetMagFilter(); - XXHASH_VERIFY(XXH64_update(m_hashState, &magFilter, sizeof(magFilter))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &magFilter, sizeof(magFilter))); const auto mipmapMode = sampler.GetMipmapMode(); - XXHASH_VERIFY(XXH64_update(m_hashState, &mipmapMode, sizeof(mipmapMode))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &mipmapMode, sizeof(mipmapMode))); const auto wrapS = sampler.GetWrapS(); - XXHASH_VERIFY(XXH64_update(m_hashState, &wrapS, sizeof(wrapS))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &wrapS, sizeof(wrapS))); const auto wrapT = sampler.GetWrapT(); - XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &wrapT, sizeof(wrapT))); const auto wrapR = sampler.GetWrapR(); - XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &wrapR, sizeof(wrapR))); const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView); const auto minLod = ResolveEffectiveMinLod(sampler, maxLod); - XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod))); - XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &minLod, sizeof(minLod))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &maxLod, sizeof(maxLod))); const auto lodBias = sampler.GetLodBias(); - XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &lodBias, sizeof(lodBias))); // The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan // will not apply (NEAREST filtering, or requests past the device limit) must still share one // VkSampler, while two samplers that really do differ must not collide onto the first one's. const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering); - XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &maxAnisotropy, sizeof(maxAnisotropy))); const auto compareMode = sampler.GetCompareMode(); - XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &compareMode, sizeof(compareMode))); const auto compareFunc = sampler.GetSamplerCompareFunc(); - XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc))); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &compareFunc, sizeof(compareFunc))); const auto borderColor = ResolveVkBorderColor(sampler, texture); - XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor))); - return XXH64_digest(m_hashState); + XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &borderColor, sizeof(borderColor))); + return XXH64_digest(m_hashState.Get()); } VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h index c00484d0..3947f6cb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h @@ -11,6 +11,7 @@ #include "../VkIncludes.h" #include "../VulkanRendererConfig.h" #include +#include #include namespace MobileGL::MG_State::GLState { @@ -85,6 +86,6 @@ private: UnorderedMap m_samplers; // Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging. Uint64 m_frameBoundaryCounter = 0; - static inline XXH64_state_t* m_hashState = XXH64_createState(); + static inline MobileGL::XXH64State m_hashState; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index c1a008fe..9376ffcd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1994,7 +1994,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Bound the idle pool: a one-off giant upload (initial atlas define) // must not pin its staging memory forever. constexpr VkDeviceSize kMaxFreeUploadStagingBytes = 32u * 1024u * 1024u; - if (m_allocator == nullptr || m_freeUploadStagingBytes + block.capacity > kMaxFreeUploadStagingBytes) { + if (m_allocator == nullptr) { + // The normal shutdown path destroys the free list through + // DestroyUploadPools while the allocator is still valid, so this is a + // defensive backstop only. Never pass a null allocator to VMA. + MGLOG_W_ONCE("VkTextureManager::RecycleUploadStagingBlock called with a null allocator"); + return; + } + if (m_freeUploadStagingBytes + block.capacity > kMaxFreeUploadStagingBytes) { vmaDestroyBuffer(m_allocator, block.buffer, block.allocation); return; } diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp index 15fc7f92..93910373 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -344,6 +344,41 @@ namespace MobileGL::MG_Impl::GLImpl { } } + void DestroyAllQueryObjects() { + // Detach the registry under the lock and release it outside. Entries the app + // already deleted were erased by DeleteQueries, so nothing here double-frees; + // a DeleteQueries racing this sweep finds an empty registry and ignores the + // names. The active-query slots and the name allocator are reset under the + // same lock: query names are context-owned state, so a fresh context must + // start clean instead of inheriting the dead context's allocator cursor or + // a stale "a query is already active on this target" latch. + UnorderedMap orphans; + { + const std::lock_guard lock(g_queryObjectsMutex); + orphans.swap(g_liveQueryObjects); + g_nextQueryId = 1; + g_activeTimeElapsedQueryId = 0; + g_activePrimitivesWrittenQueryId = 0; + g_activePrimitivesGeneratedQueryId = 0; + g_activeSamplesPassedQueryId = 0; + } + if (orphans.empty()) { + return; + } + // Both backends' DeleteBackendQuery only free the heap wrapper once their GL + // context/renderer is gone (generation/current-thread guards), so this is + // safe after the backend has released its EGL resources - but not after the + // function table itself is cleared. + const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery; + for (const auto& [_, queryObject] : orphans) { + if (deleteBackendQuery && queryObject->backendHandle) { + deleteBackendQuery(queryObject->backendHandle); + } + delete queryObject; + } + MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size()); + } + GLboolean IsQuery(GLuint id) { if (id == 0) { return GL_FALSE; diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h index b85dfb5e..c6661c29 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h @@ -13,6 +13,15 @@ namespace MobileGL::MG_Impl::GLImpl { void GenQueries(GLsizei n, GLuint* ids); void CreateQueries(GLenum target, GLsizei n, GLuint* ids); void DeleteQueries(GLsizei n, const GLuint* ids); + // Destroys every still-registered query object exactly as DeleteQueries would. + // Query objects are context-owned, and MobileGL::Destroy() tears every context + // down, so the process-global registry has to be drained there: without this the + // QueryObject and any backend timer-query wrapper leaked across every + // eglTerminate/eglInitialize cycle, and the active-query/name-allocator state + // from the dead context survived into the next one. Must run while the backend + // function table is still populated, and before a re-initialized library could + // pair the handles with the wrong backend's DeleteBackendQuery. + void DestroyAllQueryObjects(); GLboolean IsQuery(GLuint id); void BeginQuery(GLenum target, GLuint id); void EndQuery(GLenum target); diff --git a/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp b/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp index 951f3fe7..d6c558ac 100644 --- a/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp @@ -14,10 +14,29 @@ namespace MobileGL::MG_Impl::GLImpl { // Frontend sync object: wraps an optional backend fence handle. A null // backend handle (backend has no fence support, or could not create a // fence at call time) keeps the legacy always-signaled behavior. + // + // SharedPtr-owned, not raw: DeleteSync can remove the registry entry while + // another thread is inside ClientWaitSync/GetSynciv. Those callers hold a + // SharedPtr copy, so the object stays alive until the last reader leaves. + // `mutex` then serializes backend-handle reads against the one-time + // backend-handle release performed by DeleteSync / DestroyAllSyncObjects. struct SyncObject { + std::mutex mutex; MG_Backend::BackendSyncHandle backendHandle = nullptr; GLenum condition = GL_SYNC_GPU_COMMANDS_COMPLETE; GLbitfield flags = 0; + + void ReleaseBackendHandle() { + const std::lock_guard lock(mutex); + if (backendHandle == nullptr) { + return; + } + const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync; + if (backendDeleteSync) { + backendDeleteSync(backendHandle); + } + backendHandle = nullptr; + } }; // Sync calls may arrive from any thread (launchers migrate the context @@ -25,9 +44,9 @@ namespace MobileGL::MG_Impl::GLImpl { // Entries left at process shutdown are simply dropped; their backend // handles die with the backend. std::mutex g_syncObjectsMutex; - UnorderedMap g_liveSyncObjects; + UnorderedMap> g_liveSyncObjects; - SyncObject* FindSyncObject(GLsync sync) { + SharedPtr FindSyncObject(GLsync sync) { const std::lock_guard lock(g_syncObjectsMutex); const auto it = g_liveSyncObjects.find(sync); return it != g_liveSyncObjects.end() ? it->second : nullptr; @@ -35,13 +54,13 @@ namespace MobileGL::MG_Impl::GLImpl { } // namespace GLsync FenceSync(GLenum condition, GLbitfield flags) { - auto* syncObject = new SyncObject; + auto syncObject = MakeShared(); syncObject->condition = condition; syncObject->flags = flags; if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) { syncObject->backendHandle = backendFenceSync(); } - const GLsync handle = reinterpret_cast(syncObject); + const GLsync handle = reinterpret_cast(syncObject.get()); const std::lock_guard lock(g_syncObjectsMutex); g_liveSyncObjects[handle] = syncObject; return handle; @@ -52,24 +71,31 @@ namespace MobileGL::MG_Impl::GLImpl { } GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) { - const auto* syncObject = FindSyncObject(sync); + const SharedPtr syncObject = FindSyncObject(sync); if (!syncObject) { return GL_WAIT_FAILED; } const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync; - if (!backendClientWaitSync || !syncObject->backendHandle) { + // Hold the per-object lock across the backend call: a concurrent + // DeleteSync may already have removed this object from the registry, but + // it cannot free the backend handle (or the wrapper) until this reader + // finishes. ClientWaitSync can block for `timeout`; that blocks only this + // sync object, never the registry or unrelated syncs. + const std::lock_guard lock(syncObject->mutex); + if (!backendClientWaitSync || syncObject->backendHandle == nullptr) { return GL_ALREADY_SIGNALED; // legacy always-signaled fallback } return backendClientWaitSync(syncObject->backendHandle, flags, timeout); } void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) { - const auto* syncObject = FindSyncObject(sync); + const SharedPtr syncObject = FindSyncObject(sync); if (!syncObject) { return; } const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync; - if (backendWaitSync && syncObject->backendHandle) { + const std::lock_guard lock(syncObject->mutex); + if (backendWaitSync && syncObject->backendHandle != nullptr) { backendWaitSync(syncObject->backendHandle, flags, timeout); } } @@ -78,7 +104,7 @@ namespace MobileGL::MG_Impl::GLImpl { if (sync == nullptr) { return; // glDeleteSync(0) is silently ignored } - SyncObject* syncObject = nullptr; + SharedPtr syncObject; { const std::lock_guard lock(g_syncObjectsMutex); const auto it = g_liveSyncObjects.find(sync); @@ -88,15 +114,14 @@ namespace MobileGL::MG_Impl::GLImpl { syncObject = it->second; g_liveSyncObjects.erase(it); } - const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync; - if (backendDeleteSync && syncObject->backendHandle) { - backendDeleteSync(syncObject->backendHandle); - } - delete syncObject; + // Release the backend handle under the object lock. The local SharedPtr + // (and any reader's SharedPtr) keeps the wrapper itself alive until every + // in-flight backend call has returned. + syncObject->ReleaseBackendHandle(); } void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) { - const auto* syncObject = FindSyncObject(sync); + const SharedPtr syncObject = FindSyncObject(sync); if (!syncObject) { if (length) { *length = 0; @@ -111,7 +136,8 @@ namespace MobileGL::MG_Impl::GLImpl { break; case GL_SYNC_STATUS: { const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus; - const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle || + const std::lock_guard lock(syncObject->mutex); + const Bool signaled = !backendGetSyncStatus || syncObject->backendHandle == nullptr || backendGetSyncStatus(syncObject->backendHandle); value = signaled ? GL_SIGNALED : GL_UNSIGNALED; break; @@ -137,11 +163,10 @@ namespace MobileGL::MG_Impl::GLImpl { void DestroyAllSyncObjects() { // Detach the registry under the lock, release outside it. Entries the app // already deleted were erased by DeleteSync, so nothing here double-frees; - // a DeleteSync racing this sweep finds an empty registry and returns. A - // thread still blocked inside ClientWaitSync/GetSynciv during teardown - // holds a raw SyncObject* these deletes invalidate - the same undefined - // race an app-driven DeleteSync already has. - UnorderedMap orphans; + // a DeleteSync racing this sweep finds an empty registry and returns. + // Readers racing this sweep keep their SharedPtr copy alive, and each + // object's own lock makes the backend-handle release wait for them. + UnorderedMap> orphans; { const std::lock_guard lock(g_syncObjectsMutex); orphans.swap(g_liveSyncObjects); @@ -153,12 +178,10 @@ namespace MobileGL::MG_Impl::GLImpl { // context/renderer is gone (generation/current-thread guards), so this is // safe after the backend has released its EGL resources - but not after // the function table itself is cleared. - const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync; for (const auto& [_, syncObject] : orphans) { - if (backendDeleteSync && syncObject->backendHandle) { - backendDeleteSync(syncObject->backendHandle); + if (syncObject) { + syncObject->ReleaseBackendHandle(); } - delete syncObject; } MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size()); } diff --git a/MobileGL/MG_Test/Query/QueryTest.cpp b/MobileGL/MG_Test/Query/QueryTest.cpp index 78c3c7d9..bb9b02b0 100644 --- a/MobileGL/MG_Test/Query/QueryTest.cpp +++ b/MobileGL/MG_Test/Query/QueryTest.cpp @@ -448,6 +448,39 @@ TEST_F(QueryTest, BackendResultsPropagateThroughFrontend) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +TEST_F(QueryTest, DestroyAllQueryObjectsReclaimsRegistryAndResetsContextState) { + const ScopedFeaturesOverride featuresGuard; + const ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendTimerQueries(); + MG_Config::Features.DisableTimerQuery = false; + + GLuint id = 0; + MG_Impl::GLImpl::GenQueries(1, &id); + ASSERT_NE(id, 0u); + MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id); + + GLint currentQuery = -1; + MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, ¤tQuery); + EXPECT_EQ(currentQuery, static_cast(id)); + + // Full teardown drains the registry through this function while the backend + // table is still valid. The unread backend handle must be released, the query + // must disappear, and a fresh context must restart with no active query and a + // fresh name allocator. + MG_Impl::GLImpl::DestroyAllQueryObjects(); + EXPECT_EQ(g_stubDeleteCount, 1); + EXPECT_EQ(MG_Impl::GLImpl::IsQuery(id), GL_FALSE); + + MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, ¤tQuery); + EXPECT_EQ(currentQuery, 0); + + GLuint freshId = 0; + MG_Impl::GLImpl::GenQueries(1, &freshId); + EXPECT_EQ(freshId, 1u); + MG_Impl::GLImpl::DeleteQueries(1, &freshId); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + // Environment-agnostic property test for the env -> ConfigLoader -> Features // chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of // this test process, MG_ConfigLoader::Init must have parsed it with the diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index f0fccae9..8b52147b 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -1971,6 +1972,9 @@ namespace { MobileGL::Vector framebuffers; MobileGL::Vector renderbuffers; MobileGL::Vector samplers; + MobileGL::Vector vertexArrays; + MobileGL::Vector programs; + MobileGL::Vector buffers; }; TwinDeletionSinks* g_twinDeletionSinks = nullptr; @@ -1997,6 +2001,24 @@ namespace { if (!g_twinDeletionSinks) return; for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->samplers.push_back(ids[i]); } + void TW_GenVertexArrays(GLsizei count, GLuint* ids) { + for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++; + } + void TW_DeleteVertexArrays(GLsizei count, const GLuint* ids) { + if (!g_twinDeletionSinks) return; + for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->vertexArrays.push_back(ids[i]); + } + GLuint TW_CreateProgram() { return g_nextTwinDriverId++; } + void TW_DeleteProgram(GLuint program) { + if (g_twinDeletionSinks) g_twinDeletionSinks->programs.push_back(program); + } + void TW_GenBuffers(GLsizei count, GLuint* ids) { + for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++; + } + void TW_DeleteBuffers(GLsizei count, const GLuint* ids) { + if (!g_twinDeletionSinks) return; + for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->buffers.push_back(ids[i]); + } void TW_BindFramebuffer(GLenum target, GLuint framebuffer) { SG_Log("BindFramebuffer:" + std::to_string(target) + ":" + std::to_string(framebuffer)); } @@ -2018,6 +2040,12 @@ namespace { functions.glGenSamplers = TW_GenSamplers; functions.glDeleteSamplers = TW_DeleteSamplers; functions.glBindSampler = TW_BindSampler; + functions.glGenVertexArrays = TW_GenVertexArrays; + functions.glDeleteVertexArrays = TW_DeleteVertexArrays; + functions.glCreateProgram = TW_CreateProgram; + functions.glDeleteProgram = TW_DeleteProgram; + functions.glGenBuffers = TW_GenBuffers; + functions.glDeleteBuffers = TW_DeleteBuffers; functions.glGetError = SG_NoError; MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions); g_twinDeletionSinks = &sinks; @@ -2117,6 +2145,145 @@ TEST(DirectGLESBackendSampler, DestructorDeletesIdAndScrubsUnitCache) { } } +TEST(DirectGLESBackendVertexArray, DestructorDeletesIdAndHonorsContextGeneration) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedBackendTwinMocks mocks; + + GLuint id = 0; + { + auto backendVao = MobileGL::MakeShared(); + id = backendVao->GetBackendVertexArrayId(); + ASSERT_NE(id, 0u); + } + ASSERT_EQ(mocks.sinks.vertexArrays.size(), 1u); + EXPECT_EQ(mocks.sinks.vertexArrays[0], id); + + // A twin whose context died must NOT delete a VAO name a successor context + // may already have recycled (both contexts restart GL names at 1). + { + auto backendVao = MobileGL::MakeShared(); + ++g_backendContextGeneration; + backendVao.reset(); + --g_backendContextGeneration; // restore for later tests + EXPECT_EQ(mocks.sinks.vertexArrays.size(), 1u); + } +} + +TEST(DirectGLESBackendProgram, DestructorDeletesIdAndHonorsContextGeneration) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedBackendTwinMocks mocks; + + GLuint id = 0; + { + auto backendProgram = MobileGL::MakeShared(); + id = backendProgram->GetBackendProgramId(); + ASSERT_NE(id, 0u); + } + ASSERT_EQ(mocks.sinks.programs.size(), 1u); + EXPECT_EQ(mocks.sinks.programs[0], id); + + { + auto backendProgram = MobileGL::MakeShared(); + ++g_backendContextGeneration; + backendProgram.reset(); + --g_backendContextGeneration; + EXPECT_EQ(mocks.sinks.programs.size(), 1u); + } +} + +TEST(DirectGLESBackendProgram, GlobalUboDeletionHonorsContextGeneration) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedBackendTwinMocks mocks; + + MobileGL::Uint id = 123; + PrgramImpl::DeleteBackendProgramGlobalUbo(id, g_backendContextGeneration); + EXPECT_EQ(id, 0u); + ASSERT_EQ(mocks.sinks.buffers.size(), 1u); + EXPECT_EQ(mocks.sinks.buffers[0], 123u); + + // A buffer belonging to a dead context must be abandoned, never deleted as a + // recycled name in the successor context. + id = 124; + PrgramImpl::DeleteBackendProgramGlobalUbo(id, g_backendContextGeneration - 1); + EXPECT_EQ(id, 0u); + EXPECT_EQ(mocks.sinks.buffers.size(), 1u); +} + +namespace { + struct SyncDeleteRacePayload { + std::atomic alive{true}; + }; + std::atomic g_syncRaceDeleteCount{0}; + + MobileGL::MG_Backend::BackendSyncHandle SyncRaceFenceSync() { + return new SyncDeleteRacePayload(); + } + + GLenum SyncRaceClientWaitSync(MobileGL::MG_Backend::BackendSyncHandle handle, GLbitfield, GLuint64) { + auto* payload = static_cast(handle); + // Keep the backend call in flight while the GL thread runs DeleteSync. The + // frontend must not release the backend handle (or the SyncObject wrapper) + // until this call has returned. + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + return payload->alive.load(std::memory_order_acquire) ? GL_ALREADY_SIGNALED : GL_WAIT_FAILED; + } + + void SyncRaceWaitSync(MobileGL::MG_Backend::BackendSyncHandle, GLbitfield, GLuint64) {} + + void SyncRaceDeleteSync(MobileGL::MG_Backend::BackendSyncHandle handle) { + auto* payload = static_cast(handle); + payload->alive.store(false, std::memory_order_release); + delete payload; + g_syncRaceDeleteCount.fetch_add(1, std::memory_order_relaxed); + } + + MobileGL::Bool SyncRaceGetSyncStatus(MobileGL::MG_Backend::BackendSyncHandle) { return true; } + + struct ScopedSyncRaceBackend { + ScopedSyncRaceBackend(): previous(MobileGL::MG_Backend::gBackendFunctionsTable) { + MobileGL::MG_Backend::GlobalBackendFunctionsTable functions{}; + functions.GL.FenceSync = SyncRaceFenceSync; + functions.GL.ClientWaitSync = SyncRaceClientWaitSync; + functions.GL.WaitSync = SyncRaceWaitSync; + functions.GL.DeleteSync = SyncRaceDeleteSync; + functions.GL.GetSyncStatus = SyncRaceGetSyncStatus; + MobileGL::MG_Backend::gBackendFunctionsTable = functions; + g_syncRaceDeleteCount.store(0, std::memory_order_relaxed); + } + + ~ScopedSyncRaceBackend() { + MobileGL::MG_Impl::GLImpl::DestroyAllSyncObjects(); + MobileGL::MG_Backend::gBackendFunctionsTable = previous; + } + + ScopedSyncRaceBackend(const ScopedSyncRaceBackend&) = delete; + ScopedSyncRaceBackend& operator=(const ScopedSyncRaceBackend&) = delete; + + MobileGL::MG_Backend::GlobalBackendFunctionsTable previous; + }; +} // namespace + +TEST(SyncLifetime, DeleteWaitsForInFlightClientWait) { + ScopedSyncRaceBackend backend; + + const GLsync sync = MobileGL::MG_Impl::GLImpl::FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + ASSERT_NE(sync, nullptr); + + GLenum clientResult = GL_WAIT_FAILED; + std::thread waiter([sync, &clientResult] { + clientResult = MobileGL::MG_Impl::GLImpl::ClientWaitSync(sync, 0, 0); + }); + + // Give the worker a head start so ClientWaitSync is already inside the stub + // (and therefore holds the per-object lock) when DeleteSync runs. + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + MobileGL::MG_Impl::GLImpl::DeleteSync(sync); + waiter.join(); + + EXPECT_EQ(clientResult, GL_ALREADY_SIGNALED); + EXPECT_EQ(g_syncRaceDeleteCount.load(std::memory_order_relaxed), 1); +} + TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) { using namespace MobileGL::MG_Backend::DirectGLES; ScopedStateGuardMocks mocks; diff --git a/MobileGL/MG_Util/Types.h b/MobileGL/MG_Util/Types.h index 40a9568f..c4c74a78 100644 --- a/MobileGL/MG_Util/Types.h +++ b/MobileGL/MG_Util/Types.h @@ -52,6 +52,22 @@ namespace MobileGL { inline UniquePtr MakeUnique(Args&&... args) { return std::make_unique(std::forward(args)...); } + // RAII owner for the one-shot XXH64 state used by the Vulkan cache hashers. + // The previous `static inline XXH64_state_t*` form allocated five states per + // process and never called XXH64_freeState; a destructor here is independent of + // Vulkan/glslang teardown, so it is safe at static destruction time. + class XXH64State { + public: + XXH64State() : m_state(XXH64_createState()) {} + ~XXH64State() { XXH64_freeState(m_state); } + XXH64State(const XXH64State&) = delete; + XXH64State& operator=(const XXH64State&) = delete; + + XXH64_state_t* Get() const { return m_state; } + + private: + XXH64_state_t* m_state = nullptr; + }; using SizeT = std::size_t; template using Array = std::array;