[Fix] (MG_State, MG_Backend/DirectGLES): join both link phases on the draw and drain paths, and refuse to bind a program whose SPIR-V never arrived

This commit is contained in:
2026-08-10 06:08:30 -04:00
parent 1958934594
commit e02e5caa17
4 changed files with 32 additions and 13 deletions
@@ -2028,7 +2028,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_currentDrawFrontendProgram = nullptr; g_currentDrawFrontendProgram = nullptr;
g_currentDrawBackendProgram = 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_GLESFuncs.glUseProgram(0);
g_lastUsedBackendProgramId = 0; g_lastUsedBackendProgramId = 0;
return; return;
@@ -2589,7 +2591,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static void BindCurrentProgramWithResources( static void BindCurrentProgramWithResources(
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram, const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
const TextureImpl::DrawTextureSyncKeys& keys) { const TextureImpl::DrawTextureSyncKeys& keys) {
if (currentProgram && currentProgram->GetLinkStatus()) { if (currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus()) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND); ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
#endif #endif
@@ -2859,7 +2861,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// is pinned for the duration. Prefers the per-draw stash those preparations wrote. // is pinned for the duration. Prefers the per-draw stash those preparations wrote.
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() { static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (!currentProgram || !currentProgram->GetLinkStatus()) { if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
return nullptr; return nullptr;
} }
if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) { if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) {
@@ -3017,7 +3019,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureImpl::SyncImageTextureBindings(); TextureImpl::SyncImageTextureBindings();
PrgramImpl::SyncCurrentProgram(currentProgram); PrgramImpl::SyncCurrentProgram(currentProgram);
if (!currentProgram || !currentProgram->GetLinkStatus()) { if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
g_GLESFuncs.glUseProgram(0); g_GLESFuncs.glUseProgram(0);
PrgramImpl::g_lastUsedBackendProgramId = 0; PrgramImpl::g_lastUsedBackendProgramId = 0;
return; return;
+8 -2
View File
@@ -4156,8 +4156,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
return; return;
} }
if (!stateProgramObject->GetLinkStatus()) { // GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was
MGLOG_E("Program object is not linked, skipping backend sync. State program ID: %u", // 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()); stateProgramObject->GetExternalIndex());
return; return;
} }
+12 -5
View File
@@ -380,8 +380,14 @@ namespace MobileGL::MG_State {
// inside the same draw when it finally touched an artifact, and cache under a // 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 the publish had already superseded. Settling here means every
// version a backend reads during a draw describes the program it is drawing. // version a backend reads during a draw describes the program it is drawing.
// One null check in steady state. // Two null checks in steady state.
currentProgram->JoinLink(); //
// 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; return currentProgram;
} }
if (m_boundProgramPipeline == 0) return nullProgram; 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. // programs. In steady state this is a null check per stage.
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) { for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage)); const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (stageProgram) stageProgram->JoinLink(); if (stageProgram) stageProgram->JoinLinkAndSpirv();
} }
const auto signature = pipeline->ComputeDrawProgramSignature(); const auto signature = pipeline->ComputeDrawProgramSignature();
@@ -430,8 +436,9 @@ namespace MobileGL::MG_State {
composite->Link(true); composite->Link(true);
// P1 join site J2. The draw that asked for this program is the very next thing to // 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 // happen, so enqueueing the composite's link buys nothing and only moves the wait
// to whichever backend accessor happens to touch its artifacts first. // to whichever backend accessor happens to touch its artifacts first. Both phases,
composite->JoinLink(); // for the same reason: the backend is about to read its SPIR-V.
composite->JoinLinkAndSpirv();
pipeline->SetCachedDrawProgram(signature, Move(composite)); pipeline->SetCachedDrawProgram(signature, Move(composite));
return pipeline->GetCachedDrawProgram(signature); return pipeline->GetCachedDrawProgram(signature);
} }
@@ -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 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 // 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. // 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) { for (SizeT i = 0; i < m_programObjects.size(); ++i) {
const SharedPtr<ProgramObject> program = m_programObjects[i]; const SharedPtr<ProgramObject> program = m_programObjects[i];
if (program) program->JoinLink(); if (program) program->JoinLinkAndSpirv();
} }
for (SizeT i = 0; i < m_shaderObjects.size(); ++i) { for (SizeT i = 0; i < m_shaderObjects.size(); ++i) {
const SharedPtr<ShaderObject> shader = m_shaderObjects[i]; const SharedPtr<ShaderObject> shader = m_shaderObjects[i];
@@ -122,7 +126,7 @@ namespace MobileGL::MG_State::GLState {
// The currently-used program is reachable through m_programObjects unless // The currently-used program is reachable through m_programObjects unless
// glDeleteProgram already freed its slot while it stayed current. Nothing else holds // 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. // 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) { void ProgramState::MarkShaderObjectForDeletion(Uint shader) {