diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 18f69d40..85eec084 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -2028,7 +2028,9 @@ namespace MobileGL::MG_Backend::DirectGLES { g_currentDrawFrontendProgram = nullptr; g_currentDrawBackendProgram = nullptr; - if (!currentProgram || !currentProgram->GetLinkStatus()) { + // ... || !GetSpirvStatus(): see BackendProgramObjectImpl::SyncToBackend - a + // program whose SPIR-V never arrived is linked but not drawable. + if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) { g_GLESFuncs.glUseProgram(0); g_lastUsedBackendProgramId = 0; return; @@ -2589,7 +2591,7 @@ namespace MobileGL::MG_Backend::DirectGLES { static void BindCurrentProgramWithResources( const SharedPtr& currentProgram, const TextureImpl::DrawTextureSyncKeys& keys) { - if (currentProgram && currentProgram->GetLinkStatus()) { + if (currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus()) { #ifdef TRACY_ENABLE ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND); #endif @@ -2859,7 +2861,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // is pinned for the duration. Prefers the per-draw stash those preparations wrote. static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() { const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); - if (!currentProgram || !currentProgram->GetLinkStatus()) { + if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) { return nullptr; } if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) { @@ -3017,7 +3019,7 @@ namespace MobileGL::MG_Backend::DirectGLES { TextureImpl::SyncImageTextureBindings(); PrgramImpl::SyncCurrentProgram(currentProgram); - if (!currentProgram || !currentProgram->GetLinkStatus()) { + if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) { g_GLESFuncs.glUseProgram(0); PrgramImpl::g_lastUsedBackendProgramId = 0; return; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 4a915fcb..ac4335e5 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -4156,8 +4156,14 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - if (!stateProgramObject->GetLinkStatus()) { - MGLOG_E("Program object is not linked, skipping backend sync. State program ID: %u", + // GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was + // cancelled (teardown) or whose optimizer run failed is fully linked and fully + // queryable, but has no SPIR-V to build a driver program out of. GL cannot retract + // a LINK_STATUS it already reported true, so "linked but not drawable" is the + // answer, and this is where the ES backend expresses it. + if (!stateProgramObject->GetLinkStatus() || !stateProgramObject->GetSpirvStatus()) { + MGLOG_E("Program object is not linked or has no generated SPIR-V, skipping backend sync. State " + "program ID: %u", stateProgramObject->GetExternalIndex()); return; } diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index e22cc9c7..a6483b96 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -380,8 +380,14 @@ namespace MobileGL::MG_State { // inside the same draw when it finally touched an artifact, and cache under a // version the publish had already superseded. Settling here means every // version a backend reads during a draw describes the program it is drawing. - // One null check in steady state. - currentProgram->JoinLink(); + // Two null checks in steady state. + // + // BOTH phases, and that is not optional: the phase-B publish bumps those same + // versions, so joining only phase A here would leave exactly the hazard this + // site exists to close - a backend samples a version, then trips the phase-B + // gate through GetGeneratedSpirv() deeper inside the same draw, and memoizes + // under a version the publish has already superseded. + currentProgram->JoinLinkAndSpirv(); return currentProgram; } if (m_boundProgramPipeline == 0) return nullProgram; @@ -398,7 +404,7 @@ namespace MobileGL::MG_State { // programs. In steady state this is a null check per stage. for (SizeT stage = 0; stage < static_cast(ShaderStage::ShaderStageCount); ++stage) { const auto& stageProgram = pipeline->GetStageProgram(static_cast(stage)); - if (stageProgram) stageProgram->JoinLink(); + if (stageProgram) stageProgram->JoinLinkAndSpirv(); } const auto signature = pipeline->ComputeDrawProgramSignature(); @@ -430,8 +436,9 @@ namespace MobileGL::MG_State { composite->Link(true); // P1 join site J2. The draw that asked for this program is the very next thing to // happen, so enqueueing the composite's link buys nothing and only moves the wait - // to whichever backend accessor happens to touch its artifacts first. - composite->JoinLink(); + // to whichever backend accessor happens to touch its artifacts first. Both phases, + // for the same reason: the backend is about to read its SPIR-V. + composite->JoinLinkAndSpirv(); pipeline->SetCachedDrawProgram(signature, Move(composite)); return pipeline->GetCachedDrawProgram(signature); } diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp index 5b28acec..ab5a7a11 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp @@ -111,9 +111,13 @@ namespace MobileGL::MG_State::GLState { // that can grow, and a reallocation underneath this loop would be a use-after-free // that only shows up on the one GL call that walks the whole table. The copy costs a // refcount bump on a path a mode switch takes at most once. + // BOTH phases per program. This is the glMaxShaderCompilerThreadsKHR(0) path, whose + // contract is that nothing is outstanding when it returns - a program left with its + // SPIR-V job in flight would make the very next GL_COMPLETION_STATUS_KHR read GL_FALSE + // in a mode the extension says cannot have anything pending. for (SizeT i = 0; i < m_programObjects.size(); ++i) { const SharedPtr program = m_programObjects[i]; - if (program) program->JoinLink(); + if (program) program->JoinLinkAndSpirv(); } for (SizeT i = 0; i < m_shaderObjects.size(); ++i) { const SharedPtr shader = m_shaderObjects[i]; @@ -122,7 +126,7 @@ namespace MobileGL::MG_State::GLState { // The currently-used program is reachable through m_programObjects unless // glDeleteProgram already freed its slot while it stayed current. Nothing else holds // a GL-visible name for it, but a draw would still join it, so settle it here too. - if (m_currentProgram) m_currentProgram->JoinLink(); + if (m_currentProgram) m_currentProgram->JoinLinkAndSpirv(); } void ProgramState::MarkShaderObjectForDeletion(Uint shader) {