[MG_Backend, MG_Remote, MG_Test] (Disaggregated): P5f fs - isolate context epochs and server liveness

This commit is contained in:
2026-09-19 23:44:03 -04:00
parent 4667e13be6
commit 12a9b13894
11 changed files with 405 additions and 4 deletions
@@ -0,0 +1,23 @@
// MobileGL - DirectGLES context-owned static state identities (P5f fs).
#pragma once
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Pipe/PipeApply.h>
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
+123 -2
View File
@@ -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<SamplerImpl::BackendSamplerObject> 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<SamplerImpl::BackendSamplerObject>();
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<MG_State::GLState::SamplerObject>(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<Uint64, XfbObjectState> Objects;
Uint64 Current = 0;
XfbObjectState* Cached = nullptr;
GLuint ScatterBuffer = 0;
SizeT ScatterSize = 0;
Bool NeedsBind = true;
};
Array<RoleXfbState, 2> 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<GLuint>(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();
@@ -63,6 +63,7 @@
#include <mutex>
#include <cstring>
#include <regex>
#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
@@ -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;
+10 -1
View File
@@ -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<const void*>(static_cast<std::uintptr_t>(
(serial << 1) | Uint64{1})));
}
+4
View File
@@ -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
+7 -1
View File
@@ -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 ----
//
+7
View File
@@ -9,6 +9,7 @@
// P5 package v1: the apply thread, its affinity, its parking, and the EGL ownership move.
#include "ServerLoop.h"
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
@@ -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
@@ -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)
@@ -0,0 +1,149 @@
// MobileGL - P5f fs production-state epoch regression tests.
#include <gtest/gtest.h>
#include <Config.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_State/GLState/Core.h>
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<GLenum> 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<MG_State::GLState::GLContext>();
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
}
+57
View File
@@ -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