From 37b20f2fca668c864dcfa217c7d17823438b0925 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 17:05:05 +0000 Subject: [PATCH] [Fix] (DirectGLES): repair the emulation-guard mask restore and scissor the resolve fallback's staging blit correctly Final audit round over the DirectGLES scratch/shadow mechanisms; three verified defects fixed: - ~ScopedEmulationDrawState restored the APPLICATION's per-buffer colour masks, not what SyncRenderState actually pushed: a widened attachment's alpha-off doctoring (g_syncedColorMaskAlphaWidenMask) was dropped while the memo still claimed it applied, so the next sync early-outed and draws wrote fragment alpha into the widened buffer - breaking the stored-alpha==1.0 invariant the widen discipline exists to protect. The restore now re-applies the doctoring. - The same restore loop gated on the core glColorMaski name only, while the sync push falls back to glColorMaskiEXT/OES: EXT/OES-only devices were left holding buffer 0's mask broadcast across every draw buffer with the shadow recording the divergent set (never repaired). The restore now uses the same three-way pointer fallback. - ResolveThenBlit ran its resolve-into-scratch staging blit under the application's scissor: a box not covering the scratch-origin rect clipped the resolve silently (no GL error), and the second blit then copied stale scratch renderbuffer texels into the destination. The staging blit now runs scissor-off (shadow-tracked, like ScopedScissorDisable); the caller-visible blit keeps its native scissor semantics. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MsqQQF7ugn7MqZXcmnmz1z --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 0d7909f2..a22ac79d 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3966,8 +3966,19 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool resolved = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; if (resolved) { DrainBlitErrors(); + // A blit is scissored like a draw (the replicate path's guard documents the + // same rule): the application's box would clip this resolve into the + // scratch, and the second blit would then copy never-written scratch texels + // into the destination - silently, since scissor clipping raises no GL + // error. Disable for the staging blit only; the caller-visible blit below + // keeps the blit's native scissor semantics. Tracked via the render-state + // shadow, exactly like ScopedScissorDisable. + const Bool scissorWasEnabled = + (RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0; + if (scissorWasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST); g_GLESFuncs.glBlitFramebuffer(left, bottom, right, top, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); + if (scissorWasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST); resolved = g_GLESFuncs.glGetError() == GL_NO_ERROR; } if (resolved) { @@ -4113,13 +4124,25 @@ namespace MobileGL::MG_Backend::DirectGLES { } } // The per-draw-buffer colour masks are not covered by the non-indexed - // glColorMask above. - for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) { - const BoolVec4& colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index]; - if (g_GLESFuncs.glColorMaski) { - g_GLESFuncs.glColorMaski(index, colorMask.x() ? GL_TRUE : GL_FALSE, - colorMask.y() ? GL_TRUE : GL_FALSE, colorMask.z() ? GL_TRUE : GL_FALSE, - colorMask.w() ? GL_TRUE : GL_FALSE); + // glColorMask above. Restore what the SYNC actually pushed, not the raw + // application masks: a widened attachment's alpha write is forced off by + // SyncRenderState and memoized in g_syncedColorMaskAlphaWidenMask, and the + // next sync early-outs on an unchanged version - restoring the undoctored + // mask here would leave alpha writes enabled on the widened buffer with + // nothing left to repair it. Same three-way pointer fallback as + // SyncRenderState's push: gating on the core name alone left EXT/OES-only + // devices holding buffer 0's mask broadcast across every buffer. + const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski + : g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT + : g_GLESFuncs.glColorMaskiOES; + if (colorMaskiFn) { + for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) { + BoolVec4 colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index]; + if (index < 32 && (RenderStateImpl::g_syncedColorMaskAlphaWidenMask & (1u << index)) != 0) { + colorMask.w() = false; + } + colorMaskiFn(index, colorMask.x() ? GL_TRUE : GL_FALSE, colorMask.y() ? GL_TRUE : GL_FALSE, + colorMask.z() ? GL_TRUE : GL_FALSE, colorMask.w() ? GL_TRUE : GL_FALSE); } } if (m_pausedTransformFeedback && g_GLESFuncs.glResumeTransformFeedback) {