From 12a9b138941d0f27860cc437dedec34d59a2debc Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 19 Sep 2026 23:44:03 -0400 Subject: [PATCH] [MG_Backend, MG_Remote, MG_Test] (Disaggregated): P5f fs - isolate context epochs and server liveness --- MobileGL/MG_Backend/DirectGLES/ContextEpoch.h | 23 +++ MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 125 ++++++++++++++- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 17 ++ MobileGL/MG_Backend/DirectGLES/Managers.h | 4 + MobileGL/MG_Backend/MGPipe/PipeInputs.cpp | 11 +- MobileGL/MG_Backend/MGPipe/PipeInputs.h | 4 + MobileGL/MG_Impl/Pipe/PipeFill.cpp | 8 +- MobileGL/MG_Remote/Server/ServerLoop.cpp | 7 + .../MG_Test/Backend/DirectGLES/CMakeLists.txt | 4 + .../Backend/DirectGLES/ContextEpochTest.cpp | 149 ++++++++++++++++++ MobileGL/MG_Test/Wire/ServerLoopTest.cpp | 57 +++++++ 11 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectGLES/ContextEpoch.h create mode 100644 MobileGL/MG_Test/Backend/DirectGLES/ContextEpochTest.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/ContextEpoch.h b/MobileGL/MG_Backend/DirectGLES/ContextEpoch.h new file mode 100644 index 00000000..4b5fd3f4 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/ContextEpoch.h @@ -0,0 +1,23 @@ +// MobileGL - DirectGLES context-owned static state identities (P5f fs). +#pragma once + +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include + +namespace MobileGL::MG_Backend::DirectGLES { + extern Uint g_backendContextGeneration; + + struct ContextEpoch { + Uint Native = 0; + Uint64 Served = 0; + Bool Server = false; + Bool operator==(const ContextEpoch&) const = default; + }; + + inline ContextEpoch CurrentContextEpoch() { + const Bool server = MG_Config::Transport != MG_Config::TransportMode::Monolith; + return {g_backendContextGeneration, server ? MG_Pipe::MGPipeApplierContextSerial() : 0, server}; + } +} +#endif diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 266d4d28..a6d675ef 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "DirectGLES.h" +#include "ContextEpoch.h" #include "EGL/egl.h" #include "MG_Util/Types.h" #include "Utils.h" @@ -308,6 +309,25 @@ namespace MobileGL::MG_Backend::DirectGLES { // grep and a comment naming it is a hit) and for an MG_State type inside // MGPipeResourceOps, and this is neither. SamplerImpl::BackendSamplerObject* GetRawDepthFetchSampler() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + static UniquePtr native; + static Uint generation = 0; + if (generation != g_backendContextGeneration) { + native.reset(); // old generation's destructor never deletes a successor id + generation = g_backendContextGeneration; + } + if (!native) { + native = MakeUnique(); + const GLuint id = native->GetBackendSamplerId(); + g_GLESFuncs.glSamplerParameteri(id, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + g_GLESFuncs.glSamplerParameteri(id, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + g_GLESFuncs.glSamplerParameteri(id, GL_TEXTURE_COMPARE_MODE, GL_NONE); + g_GLESFuncs.glSamplerParameteri(id, GL_TEXTURE_COMPARE_FUNC, GL_ALWAYS); + } + return native.get(); + } +#endif if (!g_rawDepthFetchSamplerState) { g_rawDepthFetchSamplerState = MakeShared(0); g_rawDepthFetchSamplerState->SetMinFilter(SamplerFilterMode::Nearest); @@ -1600,7 +1620,50 @@ namespace MobileGL::MG_Backend::DirectGLES { // null instead of reasoning about stability, and CurrentXfb re-resolves lazily. XfbObjectState* g_currentXfbState = nullptr; +#if MOBILEGL_BUILD_DISAGGREGATED + struct RoleXfbState { + Uint NativeGeneration = 0; + // Server keys are record-supplied, never-reused XFB lifetime ids. + // The monolith role keeps its existing GL-name semantics separately. + UnorderedMap Objects; + Uint64 Current = 0; + XfbObjectState* Cached = nullptr; + GLuint ScatterBuffer = 0; + SizeT ScatterSize = 0; + Bool NeedsBind = true; + }; + Array g_roleXfbState; + Int g_activeXfbRole = -1; + + RoleXfbState& ActiveXfbState() { + const Int role = MG_Config::Transport != MG_Config::TransportMode::Monolith; + auto& state = g_roleXfbState[role]; + if (state.NativeGeneration != g_backendContextGeneration) { + state = {}; + state.NativeGeneration = g_backendContextGeneration; + } + if (g_activeXfbRole != role) { + state.NeedsBind = true; + g_activeXfbRole = role; + } + return state; + } +#endif + XfbObjectState& CurrentXfb() { +#if MOBILEGL_BUILD_DISAGGREGATED + auto& state = ActiveXfbState(); + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const Uint64 lifetime = MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId; + if (state.Current != lifetime || state.Cached == nullptr || state.NeedsBind) + BindTransformFeedback(0); // identity is the applier's lifetime, not this argument + } else if (state.NeedsBind) { + BindTransformFeedback(static_cast(state.Current)); + } + auto& g_currentXfbState = state.Cached; + auto& g_xfbObjects = state.Objects; + const auto g_currentXfbName = state.Current; +#endif if (g_currentXfbState == nullptr) { g_currentXfbState = &g_xfbObjects[g_currentXfbName]; } @@ -1734,6 +1797,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // scratch storage cannot be provided, in which case the caller falls back to the // direct binding (which produces a wrong layout, but is what happened before). Bool BindScatterCaptureBuffer(SizeT packedStride, SizeT capacityVertices) { +#if MOBILEGL_BUILD_DISAGGREGATED + auto& g_scatterBufferId = ActiveXfbState().ScatterBuffer; + auto& g_scatterBufferSize = ActiveXfbState().ScatterSize; +#endif if (packedStride == 0 || capacityVertices == 0) return false; if (g_GLESFuncs.glGenBuffers == nullptr || g_GLESFuncs.glBufferData == nullptr) return false; const SizeT required = packedStride * capacityVertices; @@ -1770,6 +1837,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // occupies are written, so the holes gl_SkipComponents asks for keep whatever the // application had put there - which is the whole point of the feature. void ScatterCapturedRecords(XfbObjectState& xfb) { +#if MOBILEGL_BUILD_DISAGGREGATED + auto& g_scatterBufferId = ActiveXfbState().ScatterBuffer; +#endif #if MOBILEGL_BUILD_DISAGGREGATED const XfbProgramSource program{xfb.scatterProgram, xfb.scatterArchive}; #else @@ -2126,6 +2196,15 @@ namespace MobileGL::MG_Backend::DirectGLES { } void BindTransformFeedback(GLuint name) { +#if MOBILEGL_BUILD_DISAGGREGATED + auto& roleState = ActiveXfbState(); + auto& g_currentXfbState = roleState.Cached; + auto& g_xfbObjects = roleState.Objects; + auto& g_currentXfbName = roleState.Current; + const Bool server = MG_Config::Transport != MG_Config::TransportMode::Monolith; + const Uint64 key = server ? MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId : name; + roleState.NeedsBind = false; +#endif g_currentXfbState = nullptr; // name changes; operator[] below may also rehash // The capture buffer bindings are the OBJECT's, not the context's: the bind below // swaps all of them for whatever the target object holds, which the redundant-bind @@ -2134,18 +2213,44 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!AreTransformFeedbackObjectsSupported()) { // Without driver objects there is only the default span; keep the frontend // name so the bookkeeping below stays consistent. - g_currentXfbName = name; + g_currentXfbName = +#if MOBILEGL_BUILD_DISAGGREGATED + key; +#else + name; +#endif return; } +#if MOBILEGL_BUILD_DISAGGREGATED + auto& xfb = g_xfbObjects[key]; + // Even each virtual context's name-0 object gets its own native object. + if ((server || name != 0) && xfb.esId == 0) { +#else auto& xfb = g_xfbObjects[name]; if (name != 0 && xfb.esId == 0) { +#endif g_GLESFuncs.glGenTransformFeedbacks(1, &xfb.esId); } g_GLESFuncs.glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, xfb.esId); - g_currentXfbName = name; + g_currentXfbName = +#if MOBILEGL_BUILD_DISAGGREGATED + key; +#else + name; +#endif } void DeleteTransformFeedback(GLuint name) { +#if MOBILEGL_BUILD_DISAGGREGATED + // No delete_stream_output opcode exists yet; the client refuses this class-C + // verb before emission. Never reinterpret its GL name as a lifetime id. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) + MG_Pipe::MGPipeUnmigratedEmulation("delete-transform-feedback"); + auto& roleState = ActiveXfbState(); + auto& g_currentXfbState = roleState.Cached; + auto& g_xfbObjects = roleState.Objects; + auto& g_currentXfbName = roleState.Current; +#endif const auto it = g_xfbObjects.find(name); if (it == g_xfbObjects.end()) return; if (it->second.esId != 0 && g_GLESFuncs.glDeleteTransformFeedbacks != nullptr) { @@ -2162,6 +2267,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // The ES context went away (or is being torn down): the spans, their buffer ids, the // driver objects and the frontend objects they pinned all belonged to it. void OnBackendContextDestroyed() { +#if MOBILEGL_BUILD_DISAGGREGATED + for (auto& state : g_roleXfbState) state = {}; + g_activeXfbRole = -1; +#endif g_currentXfbState = nullptr; g_xfbObjects.clear(); g_currentXfbName = 0; @@ -4719,6 +4828,14 @@ namespace MobileGL::MG_Backend::DirectGLES { g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1); } void SyncRenderState(Bool forColorClear) { +#if MOBILEGL_BUILD_DISAGGREGATED + static ContextEpoch syncedEpoch{}; + const auto epoch = CurrentContextEpoch(); + if (syncedEpoch != epoch) { + InvalidateSyncedRenderState(); + syncedEpoch = epoch; + } +#endif #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif @@ -16041,6 +16158,10 @@ namespace MobileGL::MG_Backend::DirectGLES { } void DestroyEGLContext() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) + MG_Pipe::MGPipeServerSetContextLive(false); +#endif BufferImpl::OnBackendContextDestroyed(); XfbImpl::OnBackendContextDestroyed(); MultiDrawImpl::OnBackendContextDestroyed(); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 0c79d997..a80e8d86 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -63,6 +63,7 @@ #include #include #include +#include "ContextEpoch.h" namespace MobileGL::MG_Backend::DirectGLES { Uint g_backendContextGeneration = 1; @@ -6279,6 +6280,13 @@ namespace MobileGL::MG_Backend::DirectGLES { private: static void EnsureShadowSynced() { +#if MOBILEGL_BUILD_DISAGGREGATED + const auto epoch = CurrentContextEpoch(); + if (s_epoch != epoch) { + s_synced = false; + s_epoch = epoch; + } +#endif if (s_synced) { return; } @@ -6316,6 +6324,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // Shadow of the backend GL unpack state (GL defaults). See class comment. static inline Bool s_synced = false; +#if MOBILEGL_BUILD_DISAGGREGATED + static inline ContextEpoch s_epoch{}; +#endif static inline GLint s_alignment = 4; static inline GLint s_rowLength = 0; static inline GLint s_skipRows = 0; @@ -6324,6 +6335,12 @@ namespace MobileGL::MG_Backend::DirectGLES { static inline GLint s_skipImages = 0; }; +#if MOBILEGL_BUILD_DISAGGREGATED + void ExerciseDefaultUnpackScopeForTesting() { + const ScopedDefaultUnpackState scope; + } +#endif + // --- Unpack-ring staging (see BufferImpl::UnpackRingAvailable) --------------- // One rectangular (or whole-level) region of a level shadow, repacked TIGHTLY // into the persistently-mapped unpack PBO. The upload that follows passes the diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 60ce40db..e28e5cf2 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -1659,6 +1659,10 @@ namespace MobileGL::MG_Backend::DirectGLES { } // namespace VertexArrayImpl namespace TextureImpl { +#if MOBILEGL_BUILD_DISAGGREGATED + // Drives the actual unpack save/restore helper without needing a texture upload. + void ExerciseDefaultUnpackScopeForTesting(); +#endif inline Bool IsSupportedTextureTarget(TextureTarget target) { // Every desktop-only target is stored on an ES one; see MapToBackendTextureTarget. (void)target; diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp index d8bcba59..4cdcd814 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp @@ -302,6 +302,15 @@ namespace MobileGL::MG_Pipe { MGPipeStampAccess::SetServerStamped(MGPipeClientInputs(), false); } + namespace { Bool g_serverContextLive = false; } + + void MGPipeServerSetContextLive(Bool live) { + g_serverContextLive = live; + MGPipeServerBlockNoteIdentity(); + } + + Bool MGPipeServerContextIsLive() { return g_serverContextLive; } + void MGPipeServerBlockNoteIdentity() { if (!MGPipeRoleSplitActive()) return; // The server cannot name the client's GLContext - under a real transport it is in @@ -313,7 +322,7 @@ namespace MobileGL::MG_Pipe { // hands out a null slot - an unnamed crash where the rehearsal exists to produce a // named one. const Uint64 serial = MGPipeApplierContextSerial(); - MGPipeStampAccess::SetIdentity(gPipeInputs, true, + MGPipeStampAccess::SetIdentity(gPipeInputs, g_serverContextLive, reinterpret_cast(static_cast( (serial << 1) | Uint64{1}))); } diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.h b/MobileGL/MG_Backend/MGPipe/PipeInputs.h index 6f7cf80f..f99bb8b1 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.h +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.h @@ -881,6 +881,10 @@ namespace MobileGL::MG_Pipe { // unnamed crash where the rehearsal exists to produce a named one. A no-op with the // rehearsal off, so the client's per-verb SetIdentity keeps owning the shared block there. void MGPipeServerBlockNoteIdentity(); + // P5f fs: owned by the server's control lifecycle, never inferred from a client + // GLContext or resurrected by stamping a verb after teardown. + void MGPipeServerSetContextLive(Bool live); + Bool MGPipeServerContextIsLive(); #else // The push-without-transport build has one role and one block, so the fill side's spelling // folds onto gPipeInputs and PipeFill.cpp reads identically in both build flavours. An diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 07f1cea7..0ab342e1 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -2099,7 +2099,13 @@ namespace MobileGL::MG_Pipe { } // ---- liveness ---- - Bool PipeInputs::IsLive() const { return LiveContext() != nullptr; } + Bool PipeInputs::IsLive() const { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) + return MGPipeServerContextIsLive(); +#endif + return LiveContext() != nullptr; + } // ---- the seven F-class forwarders ---- // diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index a2017266..c318b504 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -9,6 +9,7 @@ // P5 package v1: the apply thread, its affinity, its parking, and the EGL ownership move. #include "ServerLoop.h" +#include #include #include @@ -286,10 +287,12 @@ namespace MobileGL::MG_Remote::Server { // removes. m_haveCurrentTuple is left intact so a later identical bind is still a // RepeatNoOp (the driver never lost the context). m_clientReleases.fetch_add(1, std::memory_order_acq_rel); + MG_Pipe::MGPipeServerSetContextLive(false); outcome.ok = true; outcome.boundNatively = false; return outcome; case EglBindAction::RepeatNoOp: + MG_Pipe::MGPipeServerSetContextLive(true); // ID-54's "a no-op apart from the R-12 republish decision": the decision for an // identical repeat is NO republish, because nothing ran that could have moved the // caps - InitCapabilities runs inside the backend's MakeEGLCurrent, which this arm @@ -308,6 +311,7 @@ namespace MobileGL::MG_Remote::Server { // control counts THAT at the EGL function table. This counter counts forwards. outcome.ok = backend->MakeEGLCurrent(dpy, draw, read, ctx); if (!outcome.ok) return outcome; + MG_Pipe::MGPipeServerSetContextLive(true); m_haveCurrentTuple = true; m_curDpy = dpy; m_curDraw = draw; @@ -468,6 +472,7 @@ namespace MobileGL::MG_Remote::Server { "which is the context owner"); m_backend.reset(); } + MG_Pipe::MGPipeServerSetContextLive(false); // N-3: the context died with the backend; a tuple that outlives it would make the next // session's first make-current onto the same (recycled) handle values a RepeatNoOp. ForgetCurrentTuple(); @@ -753,6 +758,7 @@ namespace MobileGL::MG_Remote::Server { // thread called Stop. No context was ever made current from another thread in that // case, which is exactly the condition that makes this safe. if (m_backend != nullptr) m_backend.reset(); + MG_Pipe::MGPipeServerSetContextLive(false); // N-3, same reason as ApplyThreadMain's exit: no thread runs, so the apply-thread-only // rule on the tuple has no other writer to race. ForgetCurrentTuple(); @@ -997,6 +1003,7 @@ namespace MobileGL::MG_Remote::Server { // DestroyEGLContext - eglMakeCurrent(NO_SURFACE), eglDestroyContext, eglTerminate - // on the thread that made the context current. backend->ReleaseEGLResources(); + MG_Pipe::MGPipeServerSetContextLive(false); // N-3: DestroyEGLContext just ran; the tuple names nothing. Without this a // destroy-recreate with the same handle values (every EGL handle on this host is // 0x1) classified as a RepeatNoOp, bound nothing, and republished no caps. Red once diff --git a/MobileGL/MG_Test/Backend/DirectGLES/CMakeLists.txt b/MobileGL/MG_Test/Backend/DirectGLES/CMakeLists.txt index 8393a411..d8dedeaf 100644 --- a/MobileGL/MG_Test/Backend/DirectGLES/CMakeLists.txt +++ b/MobileGL/MG_Test/Backend/DirectGLES/CMakeLists.txt @@ -49,6 +49,10 @@ target_link_libraries( ) include(GoogleTest) +add_executable(ContextEpochTest ContextEpochTest.cpp) +target_include_directories(ContextEpochTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL) +target_link_libraries(ContextEpochTest PRIVATE GTest::gtest_main ${LINK_LIBRARIES}) +gtest_discover_tests(ContextEpochTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(BaseInstanceInjectionTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(ViewportIndexRoutingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) diff --git a/MobileGL/MG_Test/Backend/DirectGLES/ContextEpochTest.cpp b/MobileGL/MG_Test/Backend/DirectGLES/ContextEpochTest.cpp new file mode 100644 index 00000000..001e7de1 --- /dev/null +++ b/MobileGL/MG_Test/Backend/DirectGLES/ContextEpochTest.cpp @@ -0,0 +1,149 @@ +// MobileGL - P5f fs production-state epoch regression tests. +#include +#include +#include +#include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include +#include + +namespace MobileGL::MG_Backend::DirectGLES { + SamplerImpl::BackendSamplerObject* GetRawDepthFetchSampler(); +} +#endif + +using namespace MobileGL; +using namespace MobileGL::MG_Backend::DirectGLES; + +namespace { +#if MOBILEGL_BUILD_DISAGGREGATED + Uint unpackCalls = 0, samplerParameters = 0, nextNativeId = 100, boundXfb = 0; + Vector unpackNames; + void GL_APIENTRY PixelStore(GLenum name, GLint) { ++unpackCalls; unpackNames.push_back(name); } + void GL_APIENTRY GenNames(GLsizei count, GLuint* names) { while (count-- > 0) *names++ = ++nextNativeId; } + void GL_APIENTRY DeleteNames(GLsizei, const GLuint*) {} + void GL_APIENTRY SamplerParam(GLuint, GLenum, GLint) { ++samplerParameters; } + void GL_APIENTRY BindXfb(GLenum, GLuint id) { boundXfb = id; } + void GL_APIENTRY NoArgs() {} + void GL_APIENTRY BeginXfb(GLenum) {} + struct DriverScope { + MG_External::GLESFunctionsTable Functions = g_GLESFuncs; + MG_Config::TransportMode Transport = MG_Config::Transport; + DriverScope() { + ++g_backendContextGeneration; + MG_Config::Transport = MG_Config::TransportMode::InProcess; + g_GLESFuncs.glPixelStorei = &PixelStore; + g_GLESFuncs.glGenSamplers = &GenNames; + g_GLESFuncs.glDeleteSamplers = &DeleteNames; + g_GLESFuncs.glSamplerParameteri = &SamplerParam; + g_GLESFuncs.glGenTransformFeedbacks = &GenNames; + g_GLESFuncs.glDeleteTransformFeedbacks = &DeleteNames; + g_GLESFuncs.glBindTransformFeedback = &BindXfb; + g_GLESFuncs.glPauseTransformFeedback = &NoArgs; + g_GLESFuncs.glResumeTransformFeedback = &NoArgs; + g_GLESFuncs.glBeginTransformFeedback = &BeginXfb; + g_GLESFuncs.glEndTransformFeedback = &NoArgs; + } + ~DriverScope() { + XfbImpl::OnBackendContextDestroyed(); + ++g_backendContextGeneration; + g_GLESFuncs = Functions; + MG_Config::Transport = Transport; + } + }; +#endif +} + +TEST(ContextEpochTest, UnpackDefaultIsRepublishedForANewNativeContextOrRole) { +#if MOBILEGL_BUILD_DISAGGREGATED + DriverScope scope; + unpackCalls = 0; + TextureImpl::ExerciseDefaultUnpackScopeForTesting(); + EXPECT_EQ(unpackCalls, 8u); // six defaults, tight alignment, restore + unpackCalls = 0; + TextureImpl::ExerciseDefaultUnpackScopeForTesting(); + EXPECT_EQ(unpackCalls, 2u); + ++g_backendContextGeneration; + unpackCalls = 0; + TextureImpl::ExerciseDefaultUnpackScopeForTesting(); + EXPECT_EQ(unpackCalls, 8u) << "a previous context's unpack shadow suppressed the new context's default writes"; + MG_Config::Transport = MG_Config::TransportMode::Monolith; + unpackCalls = 0; + TextureImpl::ExerciseDefaultUnpackScopeForTesting(); + EXPECT_EQ(unpackCalls, 8u) << "the other role inherited the server's unpack shadow"; +#else + GTEST_SKIP() << "context-role isolation is a disaggregated-build mechanism"; +#endif +} + +TEST(ContextEpochTest, RawDepthSamplerOwnsANativeIdForEachContextGeneration) { +#if MOBILEGL_BUILD_DISAGGREGATED + DriverScope scope; + samplerParameters = 0; + const Uint first = GetRawDepthFetchSampler()->GetBackendSamplerId(); + EXPECT_EQ(samplerParameters, 4u); + EXPECT_EQ(GetRawDepthFetchSampler()->GetBackendSamplerId(), first); + EXPECT_EQ(samplerParameters, 4u); + ++g_backendContextGeneration; + const Uint second = GetRawDepthFetchSampler()->GetBackendSamplerId(); + EXPECT_NE(first, second) << "a sampler id outlived its native context"; + EXPECT_EQ(samplerParameters, 8u); +#else + GTEST_SKIP() << "native server sampler exists only in the disaggregated build"; +#endif +} + +TEST(ContextEpochTest, SameXfbNameInTwoServedContextsDoesNotAliasThePausedSpan) { +#if MOBILEGL_BUILD_DISAGGREGATED + DriverScope scope; + auto& state = MG_Pipe::MGPipeApplier(); + const auto saved = state.BoundStreamOutputLifetimeId; + state.BoundStreamOutputLifetimeId = 101; + XfbImpl::BindTransformFeedback(1); + const Uint first = boundXfb; + XfbImpl::BeginTransformFeedback(GL_POINTS); + EXPECT_TRUE(XfbImpl::IsCaptureSpanOpen()); + XfbImpl::PauseTransformFeedback(); + EXPECT_FALSE(XfbImpl::IsCaptureSpanOpen()); + state.BoundStreamOutputLifetimeId = 202; + XfbImpl::BindTransformFeedback(1); + EXPECT_NE(first, boundXfb) << "the GL name was mistaken for cross-context identity"; + EXPECT_FALSE(XfbImpl::IsCaptureSpanOpen()); + XfbImpl::BeginTransformFeedback(GL_POINTS); + EXPECT_TRUE(XfbImpl::IsCaptureSpanOpen()); + state.BoundStreamOutputLifetimeId = 101; + EXPECT_FALSE(XfbImpl::IsCaptureSpanOpen()) << "returning to the paused object inherited another context's span"; + EXPECT_EQ(boundXfb, first); + XfbImpl::ResumeTransformFeedback(); + EXPECT_TRUE(XfbImpl::IsCaptureSpanOpen()); + ++g_backendContextGeneration; + EXPECT_FALSE(XfbImpl::IsCaptureSpanOpen()) << "native context replacement retained a dead capture"; + EXPECT_NE(boundXfb, first); + state.BoundStreamOutputLifetimeId = saved; +#else + GTEST_SKIP() << "server XFB identity exists only in the disaggregated build"; +#endif +} + +TEST(ContextEpochTest, ServerLivenessDoesNotFollowTheClientContextOrAVerbStamp) { +#if MOBILEGL_BUILD_DISAGGREGATED + DriverScope scope; + auto context = Move(MG_State::pGLContext); + const Bool previous = MG_Pipe::MGPipeServerContextIsLive(); + MG_Pipe::MGPipeServerSetContextLive(false); + MG_State::pGLContext = MakeUnique(); + EXPECT_FALSE(MG_Pipe::gPipeInputs.IsLive()); + MG_Pipe::MGPipeServerSetContextLive(true); + MG_State::pGLContext.reset(); + EXPECT_TRUE(MG_Pipe::gPipeInputs.IsLive()); + MG_Pipe::MGPipeServerSetContextLive(false); + MG_Pipe::MGPipeServerStampVerbBoundary(MG_Pipe::MGPipeVerb::DrawArrays); + EXPECT_FALSE(MG_Pipe::gPipeInputs.IsLive()) << "a stale command resurrected a destroyed server context"; + MG_Pipe::MGPipeServerClearVerbBoundary(); + MG_State::pGLContext = Move(context); + MG_Pipe::MGPipeServerSetContextLive(previous); +#else + GTEST_SKIP() << "server liveness exists only in the disaggregated build"; +#endif +} diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index 4d062139..e5a172ee 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -1694,6 +1694,63 @@ TEST(ServerLoopEglTest, ADestroyedContextForgetsTheTupleSoTheSameHandleValuesBin fixture.TearDown(); } +TEST(ServerLoopEglTest, RenderShadowCannotSkipAnUnchangedVersionAfterContextEpochChanges) { + EglServerFixture fixture; + MGL_EGL_BRING_UP_OR_BAIL(fixture); + ASSERT_TRUE(fixture.MakeCurrent()); + Bool nativeRestored = false, servedRestored = false; + ASSERT_EQ(OnApply([&] { + using namespace MG_Backend::DirectGLES; + MG_Pipe::MGPipeServerStampVerbBoundary(MG_Pipe::MGPipeVerb::Clear); + RenderStateImpl::SyncRenderState(true); + // Simulate the successor driver's default/foreign state while retaining exactly + // the same parameter bytes and version. Epoch, not a value diff, must force it. + g_GLESFuncs.glEnable(GL_BLEND); + ++g_backendContextGeneration; + RenderStateImpl::SyncRenderState(true); + nativeRestored = g_GLESFuncs.glIsEnabled(GL_BLEND) == GL_FALSE; + g_GLESFuncs.glEnable(GL_BLEND); + MG_Pipe::MGPipeApplierReset(); + MG_Pipe::MGPipeServerStampVerbBoundary(MG_Pipe::MGPipeVerb::Clear); + RenderStateImpl::SyncRenderState(true); + servedRestored = g_GLESFuncs.glIsEnabled(GL_BLEND) == GL_FALSE; + MG_Pipe::MGPipeServerClearVerbBoundary(); + }), MOBILEGL_OK); + EXPECT_TRUE(nativeRestored) << "unchanged render version hid a new native context"; + EXPECT_TRUE(servedRestored) << "unchanged render version hid a new served context"; + fixture.TearDown(); +} + +TEST(ServerLoopEglTest, ServerLivenessFollowsControlFramesAcrossReleaseAndRecreation) { + EglServerFixture fixture; + MGL_EGL_BRING_UP_OR_BAIL(fixture); + const auto live = [] { + Bool answer = false; + EXPECT_EQ(OnApply([&] { answer = MG_Pipe::gPipeInputs.IsLive(); }), MOBILEGL_OK); + return answer; + }; + EXPECT_FALSE(live()) << "surface creation alone is not a served current context"; + ASSERT_TRUE(fixture.MakeCurrent()); + EXPECT_TRUE(live()); + auto client = Move(MG_State::pGLContext); + EXPECT_TRUE(live()) << "server liveness must not consult the client GLContext"; + MG_State::pGLContext = Move(client); + ASSERT_TRUE(fixture.ReleaseCurrent()); + EXPECT_FALSE(live()); + ASSERT_TRUE(fixture.MakeCurrent()); // identical held native tuple, new logical binding + EXPECT_TRUE(live()); + Server::ServerReleaseEGLResources(); + EXPECT_FALSE(live()); + EGLint major = 0, minor = 0; + ASSERT_TRUE(Server::ServerInitializeEGLDisplay(EglServerFixture::Dpy(), &major, &minor)); + ASSERT_TRUE(Server::ServerCreateEGLPbufferSurface(EglServerFixture::Surf(), 64, 64)); + EXPECT_FALSE(live()); + ASSERT_TRUE(fixture.MakeCurrent()); + EXPECT_TRUE(live()); + fixture.TearDown(); + EXPECT_FALSE(MG_Pipe::MGPipeServerContextIsLive()); +} + // P5f (fc): InitializeEGLDisplay's out-pointers are the frame's reply fields now (the one place // the old mailbox carried pointers INTO the poster's stack). The answer is the driver's real // version through the frame - the BringUp already consumed one init; a second call is an