[Fix] (Espryt): turn the scissor off around backend copies by what the driver has, not another session's render-state shadow

This commit is contained in:
2026-10-02 20:57:18 -04:00
parent 7a5b0755b3
commit 815217cc07
3 changed files with 148 additions and 12 deletions
+52 -12
View File
@@ -5239,19 +5239,49 @@ namespace MobileGL::MG_Backend::DirectGLES {
// SyncRenderState runs on the GL thread only.
static Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS>
g_dualSourceDeclinedBlendStates;
#if MOBILEGL_BUILD_DISAGGREGATED
// The context epoch the shadow was last synced in (SyncRenderState).
static ContextEpoch g_syncedRenderStateEpoch{};
#endif
void InvalidateSyncedRenderState() {
g_forceFullRenderStateResync = true;
g_hasSyncedRenderState = false;
g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
}
// Whether the shadow's bits are the current native context's. An invalidation (a context
// switch, the backend's turn moving to another session's thread) only raises flags - the
// bits stay, and they are then another context's.
static Bool SyncedRenderStateDescribesCurrentContext() {
if (!g_hasSyncedRenderState || g_forceFullRenderStateResync) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
if (g_syncedRenderStateEpoch != CurrentContextEpoch()) return false;
#endif
return true;
}
Bool DriverScissorTestEnabled() {
if (SyncedRenderStateDescribesCurrentContext())
return (g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
return g_GLESFuncs.glIsEnabled != nullptr && g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE;
}
void SetSyncedScissorTestForTesting(Bool enabled) {
g_syncedRenderStateParameters.ScissorTestEnabledMask = enabled ? 1u : 0u;
g_hasSyncedRenderState = true;
g_forceFullRenderStateResync = false;
#if MOBILEGL_BUILD_DISAGGREGATED
g_syncedRenderStateEpoch = CurrentContextEpoch();
#endif
}
void SyncRenderState(Bool forColorClear) {
#if MOBILEGL_BUILD_DISAGGREGATED
static ContextEpoch syncedEpoch{};
const auto epoch = CurrentContextEpoch();
if (syncedEpoch != epoch) {
if (g_syncedRenderStateEpoch != epoch) {
InvalidateSyncedRenderState();
syncedEpoch = epoch;
g_syncedRenderStateEpoch = epoch;
}
#endif
#ifdef TRACY_ENABLE
@@ -10252,8 +10282,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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;
const Bool scissorWasEnabled = RenderStateImpl::DriverScissorTestEnabled();
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);
@@ -10987,8 +11016,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool oneToOne = width > 0 && height > 0 && (dstX1 - dstX0) == width && (dstY1 - dstY0) == height;
// The scissor clips a blit and does not clip a copy, so an enabled scissor makes the two
// different operations no matter how the rectangles line up.
const Bool scissorEnabled =
(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
const Bool scissorEnabled = RenderStateImpl::DriverScissorTestEnabled();
using MobileGL::FramebufferAttachmentType;
struct AspectPlan {
@@ -11169,8 +11197,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool oneToOne = width > 0 && height > 0 && (dstX1 - dstX0) == width && (dstY1 - dstY0) == height;
// The scissor clips a blit and does not clip a copy, so an enabled scissor makes the two
// different operations no matter how the rectangles line up.
const Bool scissorEnabled =
(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
const Bool scissorEnabled = RenderStateImpl::DriverScissorTestEnabled();
using MobileGL::FramebufferAttachmentType;
struct AspectPlan {
@@ -11605,11 +11632,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Force-disables GL_SCISSOR_TEST for the scope (emulation blits and clears are
// scissored; readback copies must not be clipped by app scissor state) and
// restores the app state on exit, tracked via the render-state shadow.
// restores the app state on exit.
//
// WHAT THE DRIVER HAS, NOT WHAT THE SHADOW LAST SAID. The render-state shadow is one per
// process and is synced at draws; a session that presents or reads back without drawing
// since another session's context synced (a frame that redraws nothing, a client whose
// turn came after the compositor's) found the other context's bit there. Its own scissor,
// still on from its last draw, then clipped the shared-image copy to a box - flipped into
// the wrong rows, too - and the image kept the frame it held before.
class ScopedScissorDisable {
public:
ScopedScissorDisable()
: m_wasEnabled((RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0) {
ScopedScissorDisable() : m_wasEnabled(RenderStateImpl::DriverScissorTestEnabled()) {
if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
}
~ScopedScissorDisable() {
@@ -11622,6 +11655,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool m_wasEnabled;
};
namespace RenderStateImpl {
void RunScissorDisabledForTesting(void (*inside)(void*), void* data) {
ScopedScissorDisable scissorOff;
inside(data);
}
} // namespace RenderStateImpl
// Binds the shared scratch FBO at READ (isRead) or DRAW for one temp operation,
// restoring the previous binding on exit. Attachments are managed through the
// ScratchFBOImpl attachment shadow by the caller (see Framebuffer()).
@@ -495,6 +495,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// answers from the frontend state, which this function only reads.
void SyncRenderState(Bool forColorClear = false);
void InvalidateSyncedRenderState();
// GL_SCISSOR_TEST as the driver has it in the native context current on this thread -
// what a guard that turns the test off around its own blit, and puts it back, needs to
// know. The shadow answers only while it describes this context (synced since the last
// invalidation, in this context epoch); otherwise the driver is asked. The shadow is one
// per process: once another session's context synced into it, or a context switch
// invalidated it, its bits are some other context's.
Bool DriverScissorTestEnabled();
// TESTING: the shadow as SyncRenderState leaves it in the current context epoch, with
// GL_SCISSOR_TEST `enabled`.
void SetSyncedScissorTestForTesting(Bool enabled);
// TESTING: `inside` runs within the guard the shared-image present and the readbacks
// copy under (ScopedScissorDisable).
void RunScissorDisabledForTesting(void (*inside)(void*), void* data);
} // namespace RenderStateImpl
#if MOBILEGL_BUILD_DISAGGREGATED
@@ -250,3 +250,84 @@ TEST(ContextEpochTest, DualBlockApplierDiagnosticIsRoleLocalWhileSharedControlRe
GTEST_SKIP() << "role-local observer requires the disaggregated build";
#endif
}
// THE SCISSOR GUARD AROUND A COPY THE BACKEND MAKES ON ITS OWN (the shared-image present, the
// readbacks). The render-state shadow is one per process and is only synced at draws, so after a
// context switch or another session's turn its bits are another context's. A client that presents
// without drawing since then (a frame that redraws nothing) kept its own scissor on through the
// copy - the image was clipped to the client's last box and kept the frame it held before.
#if MOBILEGL_BUILD_DISAGGREGATED
namespace {
Bool fakeScissor = false;
Uint isEnabledCalls = 0;
void GL_APIENTRY FakeEnable(GLenum cap) { if (cap == GL_SCISSOR_TEST) fakeScissor = true; }
void GL_APIENTRY FakeDisable(GLenum cap) { if (cap == GL_SCISSOR_TEST) fakeScissor = false; }
GLboolean GL_APIENTRY FakeIsEnabled(GLenum cap) {
++isEnabledCalls;
return cap == GL_SCISSOR_TEST && fakeScissor ? GL_TRUE : GL_FALSE;
}
struct ScissorDriver : DriverScope {
ScissorDriver() {
g_GLESFuncs.glEnable = &FakeEnable;
g_GLESFuncs.glDisable = &FakeDisable;
g_GLESFuncs.glIsEnabled = &FakeIsEnabled;
isEnabledCalls = 0;
}
~ScissorDriver() { RenderStateImpl::InvalidateSyncedRenderState(); }
};
// The driver's scissor as the copy inside the guard sees it.
Bool ScissorDuringGuard() {
Bool seen = true;
RenderStateImpl::RunScissorDisabledForTesting([](void* out) { *static_cast<Bool*>(out) = fakeScissor; },
&seen);
return seen;
}
} // namespace
#endif
TEST(ContextEpochTest, ScissorGuardTurnsOffTheScissorTheShadowNoLongerDescribes) {
#if MOBILEGL_BUILD_DISAGGREGATED
ScissorDriver scope;
// Another context last synced the shadow with the test off; then the switch to this one.
RenderStateImpl::SetSyncedScissorTestForTesting(false);
RenderStateImpl::InvalidateSyncedRenderState();
fakeScissor = true; // this context's application left its scissor on
EXPECT_FALSE(ScissorDuringGuard()) << "the copy ran clipped by the application's scissor box";
EXPECT_TRUE(fakeScissor) << "the guard did not put the application's scissor back";
// The same through a context epoch moving without an explicit invalidation.
RenderStateImpl::SetSyncedScissorTestForTesting(false);
++g_backendContextGeneration;
fakeScissor = true;
EXPECT_FALSE(ScissorDuringGuard()) << "a shadow synced in another context epoch was trusted";
EXPECT_TRUE(fakeScissor);
#else
GTEST_SKIP() << "the per-session render-state shadow hazard is a multi-session server's";
#endif
}
TEST(ContextEpochTest, ScissorGuardLeavesAnOffScissorOffWhateverAStaleShadowSays) {
#if MOBILEGL_BUILD_DISAGGREGATED
ScissorDriver scope;
RenderStateImpl::SetSyncedScissorTestForTesting(true);
RenderStateImpl::InvalidateSyncedRenderState();
fakeScissor = false;
EXPECT_FALSE(ScissorDuringGuard());
EXPECT_FALSE(fakeScissor) << "the guard turned on a scissor this context never had";
#else
GTEST_SKIP() << "the per-session render-state shadow hazard is a multi-session server's";
#endif
}
TEST(ContextEpochTest, ScissorGuardTakesACurrentShadowsWordWithoutAskingTheDriver) {
#if MOBILEGL_BUILD_DISAGGREGATED
ScissorDriver scope;
RenderStateImpl::SetSyncedScissorTestForTesting(true);
fakeScissor = true;
EXPECT_FALSE(ScissorDuringGuard());
EXPECT_TRUE(fakeScissor);
EXPECT_EQ(isEnabledCalls, 0u) << "a shadow synced in this context should answer without a driver query";
#else
GTEST_SKIP() << "the per-session render-state shadow hazard is a multi-session server's";
#endif
}