Espryt: the surface size is memoized per session until a swap or a draw-surface change (FCL monolith CPU 3.18 -> 3.05 ms/frame; red-once: an uninvalidated memo fails DefaultScissorFollowsSurfaceScenario).

This commit is contained in:
2026-10-09 04:10:20 -04:00
parent 98b2c37384
commit 65d4b808c1
4 changed files with 130 additions and 1 deletions
+43 -1
View File
@@ -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<Uint64> 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<Uint64> 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<Int>(width);
outHeight = static_cast<Int>(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<NativeWindowType>(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<std::mutex> lock(g_nativeRegistryMutex);
if (auto* entry = FindSessionSurfaceLocked(session, drawSurface)) ++entry->Presents;
@@ -195,6 +195,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/DefaultFramebufferAcrossSwapScenario.cpp
Scenarios/ScenarioIsolationScenario.cpp
Scenarios/UploadOrderScenario.cpp
Scenarios/DefaultScissorFollowsSurfaceScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -36,6 +36,7 @@ set(MGL_P8A_AMBIENT_CASES
CopyImagePacked16Scenario.*
CrossFrameBufferScenario.*
DefaultFramebufferAcrossSwapScenario.*
DefaultScissorFollowsSurfaceScenario.*
DepthStencilReadbackAttachmentShapeScenario.DepthOfALayeredCubeAttachmentReadsBack
DepthStencilReadbackAttachmentShapeScenario.DepthOfAPlainTexture2DAttachmentReadsBack
DepthStencilReadbackAttachmentShapeScenario.DepthOfAnArrayLayerAttachmentReadsBack
@@ -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 <array>
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
#include <EGL/egl.h>
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<GLubyte, 4> 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