diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 1e4f8968..37eb0d3d 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -11683,6 +11683,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // Bumped on every registry mutation. The per-thread active-tuple cache below is keyed on it, so // a destroyed tuple can never be handed out from a stale cache entry. static std::atomic g_nativeRegistryEpoch{1}; + // P15: moves whenever the size QueryCurrentSurfaceSize would answer may have moved - after every + // swap (a window surface takes a new size at its next swap) and at every change of a session's + // draw surface (NoteDrawSurfaceMoved at each `session.Draw =`). Surface creation and destruction + // also move g_nativeRegistryEpoch, which the size memo keys on as well. + static std::atomic g_surfaceSizeEpoch{1}; + static void NoteDrawSurfaceMoved() { g_surfaceSizeEpoch.fetch_add(1, std::memory_order_acq_rel); } // Never erased while the process lives: a context teardown marks its tuple dead (Context = // EGL_NO_CONTEXT) instead of freeing it, so a cached pointer is never dangling between the // epoch bump and the next resolution. @@ -11716,6 +11722,11 @@ namespace MobileGL::MG_Backend::DirectGLES { EGLContext boundContext = EGL_NO_CONTEXT; EGLSurface boundDraw = EGL_NO_SURFACE; Uint64 boundSerial = 0; + // QueryCurrentSurfaceSize's memo (P15): the last answer, good while the session, the + // registry epoch and the surface-size epoch all hold. + Bool sizeMemoValid = false; + Uint64 sizeMemoSession = 0, sizeMemoToken = 0, sizeMemoRegistryEpoch = 0, sizeMemoSizeEpoch = 0; + Int sizeMemoWidth = 0, sizeMemoHeight = 0; }; thread_local NativeThreadState t_native; @@ -11940,6 +11951,20 @@ namespace MobileGL::MG_Backend::DirectGLES { static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) { outWidth = 0; outHeight = 0; + // P15: ANSWERED FROM A MEMO while nothing that moves the size moved. Two eglQuerySurface + // calls behind the registry lock on every dirty render-state sync (draws and clears whose + // scissor box the app never wrote) cost 0.065 ms/frame on FCL Espryt. + Uint64 memoSession = 0, memoToken = 0; + ResolveNativeKey(memoSession, memoToken); + const Uint64 registryEpoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); + const Uint64 sizeEpoch = g_surfaceSizeEpoch.load(std::memory_order_acquire); + NativeThreadState& memo = t_native; + if (memo.sizeMemoValid && memo.sizeMemoSession == memoSession && memo.sizeMemoToken == memoToken && + memo.sizeMemoRegistryEpoch == registryEpoch && memo.sizeMemoSizeEpoch == sizeEpoch) { + outWidth = memo.sizeMemoWidth; + outHeight = memo.sizeMemoHeight; + return true; + } // P14 S4: the surface is the SESSION's, not the process's - the tuple that draws to it is // one of the session's contexts. const EGLSurface surface = ActiveNativeSession().Draw; @@ -11957,6 +11982,13 @@ namespace MobileGL::MG_Backend::DirectGLES { outWidth = static_cast(width); outHeight = static_cast(height); + memo.sizeMemoValid = true; + memo.sizeMemoSession = memoSession; + memo.sizeMemoToken = memoToken; + memo.sizeMemoRegistryEpoch = registryEpoch; + memo.sizeMemoSizeEpoch = sizeEpoch; + memo.sizeMemoWidth = outWidth; + memo.sizeMemoHeight = outHeight; return true; } @@ -12575,6 +12607,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (tuple.Draw == EGL_NO_SURFACE) return true; if (tuple.Draw != session.Draw && SessionOwnsSurface(session, tuple.Draw)) { session.Draw = tuple.Draw; + NoteDrawSurfaceMoved(); session.Read = SessionOwnsSurface(session, tuple.Read) ? tuple.Read : tuple.Draw; } if (t_native.boundContext == tuple.Context && t_native.boundDraw == session.Draw) return true; @@ -12717,6 +12750,7 @@ namespace MobileGL::MG_Backend::DirectGLES { NativeSessionState& session = ActiveNativeSession(); if (!SessionOwnsSurface(session, surface)) return false; session.Draw = surface; + NoteDrawSurfaceMoved(); session.Read = surface; return true; } @@ -12725,6 +12759,7 @@ namespace MobileGL::MG_Backend::DirectGLES { NativeSessionState& session = ActiveNativeSession(); if (!SessionOwnsSurface(session, surface)) return false; session.Draw = surface; + NoteDrawSurfaceMoved(); session.Read = surface; if (ActiveNativeContext().Context == EGL_NO_CONTEXT) return true; if (t_native.boundContext == ActiveNativeContext().Context && t_native.boundDraw == surface) return true; @@ -12762,7 +12797,10 @@ namespace MobileGL::MG_Backend::DirectGLES { entries.end()); g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); } - if (session.Draw == previous) session.Draw = replacement; + if (session.Draw == previous) { + session.Draw = replacement; + NoteDrawSurfaceMoved(); + } if (session.Read == previous) session.Read = replacement; // The calling thread drew to the old one: draw to the new one from here (the old is then // no longer current, and its destruction below is immediate). @@ -12791,6 +12829,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // Fall back onto a surface the session still has, so the next bind has somewhere to go. const EGLSurface fallback = session.Surfaces.empty() ? EGL_NO_SURFACE : session.Surfaces.front().Surface; session.Draw = fallback; + NoteDrawSurfaceMoved(); session.Read = fallback; if (t_native.boundDraw == surface && fallback != EGL_NO_SURFACE && ActiveNativeContext().Context != EGL_NO_CONTEXT) { @@ -12952,6 +12991,7 @@ namespace MobileGL::MG_Backend::DirectGLES { NativeSessionState& session = ActiveNativeSession(); session.Surfaces.push_back({window, surface}); session.Draw = surface; + NoteDrawSurfaceMoved(); session.Read = surface; tuple.Draw = surface; tuple.Read = surface; @@ -12980,6 +13020,7 @@ namespace MobileGL::MG_Backend::DirectGLES { NativeSessionState& session = ActiveNativeSession(); session.Surfaces.push_back({static_cast(0), surface}); session.Draw = surface; + NoteDrawSurfaceMoved(); session.Read = surface; tuple.Draw = surface; tuple.Read = surface; @@ -13767,6 +13808,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } else { g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); } + NoteDrawSurfaceMoved(); // a window surface takes a new size at its swap if (drawSurface != EGL_NO_SURFACE) { const std::lock_guard lock(g_nativeRegistryMutex); if (auto* entry = FindSessionSurfaceLocked(session, drawSurface)) ++entry->Presents; diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 4cdc5336..c0440692 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -195,6 +195,7 @@ add_executable(MobileGLIntegrationTest Scenarios/DefaultFramebufferAcrossSwapScenario.cpp Scenarios/ScenarioIsolationScenario.cpp Scenarios/UploadOrderScenario.cpp + Scenarios/DefaultScissorFollowsSurfaceScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE diff --git a/MobileGL/MG_IntegrationTest/P8Coverage.cmake b/MobileGL/MG_IntegrationTest/P8Coverage.cmake index 9b8ee1ed..e6ff372d 100644 --- a/MobileGL/MG_IntegrationTest/P8Coverage.cmake +++ b/MobileGL/MG_IntegrationTest/P8Coverage.cmake @@ -36,6 +36,7 @@ set(MGL_P8A_AMBIENT_CASES CopyImagePacked16Scenario.* CrossFrameBufferScenario.* DefaultFramebufferAcrossSwapScenario.* + DefaultScissorFollowsSurfaceScenario.* DepthStencilReadbackAttachmentShapeScenario.DepthOfALayeredCubeAttachmentReadsBack DepthStencilReadbackAttachmentShapeScenario.DepthOfAPlainTexture2DAttachmentReadsBack DepthStencilReadbackAttachmentShapeScenario.DepthOfAnArrayLayerAttachmentReadsBack diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DefaultScissorFollowsSurfaceScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DefaultScissorFollowsSurfaceScenario.cpp new file mode 100644 index 00000000..bd7473d9 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/DefaultScissorFollowsSurfaceScenario.cpp @@ -0,0 +1,85 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DefaultScissorFollowsSurfaceScenario.cpp +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - A SCISSOR BOX THE APPLICATION NEVER WROTE FOLLOWS THE CURRENT DRAW SURFACE. +// +// Until the first glScissor, DirectGLES substitutes the current draw surface's size for the +// scissor box (the Minecraft protection described at RenderStateImpl::SyncRenderState), and it +// keeps the surface size in a memo that a swap, a change of draw surface, or a surface's creation +// or destruction invalidates. This case makes a second, larger surface current on the same +// context and clears it with the scissor test on: the clear must reach the larger surface's far +// corner, which a size remembered from the first surface would cut off. + +#include + +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +#include + +namespace MGITest { +namespace { + +class DefaultScissorFollowsSurfaceScenario : public ScenarioTest {}; + +TEST_F(DefaultScissorFollowsSurfaceScenario, AClearOnALargerSurfaceMadeCurrentReachesItsFarCorner) { + if (!Ready() || IsSkipped()) return; + if (Gl().BackendName() != "DirectGLES") GTEST_SKIP() << "the substituted scissor box is DirectGLES's"; + const EGLDisplay display = eglGetCurrentDisplay(); + const EGLSurface first = eglGetCurrentSurface(EGL_DRAW); + const EGLContext context = eglGetCurrentContext(); + ASSERT_NE(display, EGL_NO_DISPLAY); + ASSERT_NE(first, EGL_NO_SURFACE); + EGLint width = 0, height = 0, configId = 0, configs = 0; + ASSERT_TRUE(eglQuerySurface(display, first, EGL_WIDTH, &width)); + ASSERT_TRUE(eglQuerySurface(display, first, EGL_HEIGHT, &height)); + ASSERT_TRUE(eglQueryContext(display, context, EGL_CONFIG_ID, &configId)); + const EGLint pick[] = {EGL_CONFIG_ID, configId, EGL_NONE}; + EGLConfig config = nullptr; + ASSERT_TRUE(eglChooseConfig(display, pick, &config, 1, &configs)); + ASSERT_EQ(configs, 1); + + // The first surface's size goes into the memo: a clear with the scissor test on and no box. + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDisable(GL_CULL_FACE); + glEnable(GL_SCISSOR_TEST); + glClearColor(1.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glFinish(); + + const EGLint attribs[] = {EGL_WIDTH, width * 2, EGL_HEIGHT, height * 2, EGL_NONE}; + const EGLSurface second = eglCreatePbufferSurface(display, config, attribs); + if (second == EGL_NO_SURFACE) GTEST_SKIP() << "no second pbuffer surface on this platform"; + ASSERT_TRUE(eglMakeCurrent(display, second, second, context)); + glViewport(0, 0, width * 2, height * 2); + glEnable(GL_CULL_FACE); // a capability change, so the render state is synced again for the clear + glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + std::array corner{}; + glReadPixels(width * 2 - 1, height * 2 - 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, corner.data()); + EXPECT_EQ(int(corner[0]), 0) << "the clear reaches the larger surface's far corner"; + EXPECT_EQ(int(corner[1]), 255) << "the clear reaches the larger surface's far corner"; + + glDisable(GL_CULL_FACE); + glDisable(GL_SCISSOR_TEST); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + ASSERT_TRUE(eglMakeCurrent(display, first, first, context)); + glViewport(0, 0, width, height); + eglDestroySurface(display, second); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); +} + +} // namespace +} // namespace MGITest