Merge client-recover: open a fresh server session after a device loss.

This commit is contained in:
2026-10-02 23:38:56 -04:00
32 changed files with 1570 additions and 99 deletions
+7
View File
@@ -634,6 +634,13 @@ namespace MobileGL::MG_Config {
// poll loop cannot outlive a late report. 0 = every poll crosses, as before P10: the A/B
// arm and the red-once of the no-reply gate (FencePollScenario).
Uint32 PollEscalate = 64;
// MOBILEGL_IPC_RECOVER (client): 1 (default) = after a device loss ends this client's
// session, the application's next eglCreateContext / eglCreate*Surface / eglInitialize
// dials the server for a FRESH session, and contexts created from then on work. Contexts
// (and their objects) from before the loss stay lost, as EGL's power-management event
// has them; surfaces survive and are re-created on the new session when next used.
// 0 = the loss is final for the process (every verb DECLINED for the rest of its life).
Uint32 RecoverAfterLoss = 1;
// MOBILEGL_IPC_ADOPT_TIER, THE ZERO-COPY SWITCH (ruling ID-P11-14, CONTRACT-P11 B2):
// UNSET (kAdoptTierUnset, the default since 2026-09-29, the user's decision) = T0 where the
// session can run it, T2 QUIETLY where it cannot (MGLOG_D only, still counted);
+2
View File
@@ -559,6 +559,8 @@ namespace MobileGL::MG_ConfigLoader {
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
// A lost session is replaced by a fresh one on the application's next create (Config.h).
ipc.RecoverAfterLoss = QueryEnvUint32("MOBILEGL_IPC_RECOVER", 1, 0, 1);
// The verify harness compares the pushed block against the applier per verb; a
// batched queue lets the comparer read a supplied field mid-apply, which is a
// torn read rather than a divergence. The batch is therefore off whenever the
+4
View File
@@ -46,5 +46,9 @@ namespace MobileGL {
namespace MG_Impl {
void Init();
// GL framebuffer 0, built into the calling thread's GLContext (the process default at
// Init) and installed as the one every later context starts with. Init's step, and the
// rebuild after a recovered device loss, whose fresh session holds none of the old one's.
void BuildDefaultFramebuffer();
} // namespace MG_Impl
} // namespace MobileGL
+160 -3
View File
@@ -24,6 +24,13 @@
// (PipeFill.cpp's emitters) and because the frame channel belongs to the server role - this
// file must not name ServerLoop's forwarders directly.
#include <MG_Remote/Client/WireTables.h>
// The recovery after a device loss (RecoverLostSession below): the session, the remote backend's
// surface book, and the GL framebuffer 0 that is rebuilt on the fresh session.
#include <MG_Remote/Client/BackendObject_Remote.h>
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_State/GLState/Core.h>
#include <optional>
#endif
#include <cstring>
#include <mutex>
@@ -347,6 +354,80 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
using MG_Remote::Client::ClientSession;
// IS `context` LOST. It was built against a server session that has since ended (a device
// loss, recovered), or it lives on the session that is lost right now: either way its
// objects are on no server this process can reach, and EGL's answer for it is
// EGL_CONTEXT_LOST until the application destroys it.
Bool ContextIsLost(EGLStateContext* state, EGLContext context) {
if (!StreamGateActive() || context == EGL_NO_CONTEXT) return false;
const SharedPtr<MG_State::GLState::GLContext> glState = state->GetContextGLState(context);
if (!glState) return false;
return glState->IsFromEndedWireSession() || ClientSession::SessionLatchedLost();
}
// THE RECOVERY'S EGL HALF. The caller holds EGLOperationMutex - which every GL entry point
// takes too (GLStreamScope), so no other thread is in the middle of a GL call - and is an
// entry point an application reaches when it rebuilds after a loss: eglCreateContext,
// eglCreate*Surface, eglInitialize. A no-op unless the session is latched lost.
//
// 1. ClientSession ends the lost session and dials a fresh one; its FinishStartup forgets
// every client cache of the old one and moves the wire epoch, which is what makes every
// context built before lost (their objects die later without a word on the new wire).
// 2. The process default context and GL framebuffer 0 are rebuilt on the fresh session:
// every context created from now on starts with framebuffer 0 bound, and the old one's
// attachments are textures the fresh server never heard of.
// 3. The remote backend initializes the display again on the new server and forgets which
// surfaces and shared images the old one had; each surface is re-created there when it
// is next made current (EGL keeps surfaces across a power-management loss).
// 4. Wayland presentations drop the lost session's shared images and allocate new ones.
Bool RecoverLostSession() {
if (!StreamGateActive() || !ClientSession::SessionLatchedLost()) return true;
ClientSession* session = ClientSession::Active();
{
const ClientSession::ScopedWireMute mute;
if (session->RecoverAfterDeviceLoss() != MOBILEGL_OK) return false;
}
// Nothing is bound on the fresh session: the next thread that writes binds its own.
g_boundContextToken = 0;
g_boundShareGroupToken = 0;
const ClientSession::ScopedCurrentSessionWork work;
SharedPtr<MG_State::GLState::GLContext> lostDefault = MG_State::ReplaceProcessDefaultGLContext();
UniquePtr<MG_Impl::GLImpl::FramebufferImpl::DefaultFramebufferInfo> lostFramebuffer =
Move(MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo);
{
// Built into the new default context, whatever this thread has current (a lost
// context stays this thread's GL state until the application releases it).
SharedPtr<MG_State::GLState::GLContext> caller = MG_State::pGLContext;
MG_State::pGLContext = MG_State::ProcessDefaultGLContext();
MG_Impl::BuildDefaultFramebuffer();
MG_State::pGLContext = Move(caller);
}
{
const ClientSession::ScopedWireMute mute;
lostFramebuffer.reset();
lostDefault.reset();
}
if (auto* remote = dynamic_cast<MG_Remote::Client::BackendObject_Remote*>(MG_Backend::pActiveBackendObject.get()))
remote->OnSessionReplaced();
#if MOBILEGL_WAYLAND_WINDOWS
for (auto& [surface, presentation] : WaylandSurfaces()) presentation->OnSessionReplaced();
#endif
return true;
}
// The surface entry points' guard: a surface created after a loss is created on a fresh
// session. EGL_BAD_ALLOC when no session could be brought up.
Bool RecoverBeforeCreatingSurface(EGLStateContext* state) {
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
if (RecoverLostSession()) return true;
state->SetError(EGL_BAD_ALLOC);
return false;
}
#endif
} // namespace
EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
@@ -355,6 +436,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_NO_SURFACE;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (!RecoverBeforeCreatingSurface(state)) return EGL_NO_SURFACE;
#endif
#if MOBILEGL_WAYLAND_WINDOWS
if (RefuseGbmWindow(state, dpy)) return EGL_NO_SURFACE;
{
@@ -467,6 +551,13 @@ namespace MobileGL::MG_Impl::EGLImpl {
state->SetError(EGL_BAD_SURFACE);
return EGL_FALSE;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// A swap of a lost context's frame: EGL_CONTEXT_LOST (EGL 1.5 3.2), and nothing presented.
if (ContextIsLost(state, state->GetCurrentContext())) {
state->SetError(EGL_CONTEXT_LOST);
return EGL_FALSE;
}
#endif
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
@@ -524,7 +615,27 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_NO_CONTEXT;
}
const EGLContext context = state->CreateContext(dpy, config, shareCtx, attrib_list);
#if MOBILEGL_BUILD_DISAGGREGATED
// AFTER A DEVICE LOSS THIS IS WHERE AN APPLICATION REBUILDS, so this is where a fresh
// session comes up (RecoverLostSession) - before the context's GL state is built, because
// building it already emits the creates of its default objects. Sharing with a lost
// context would share objects no reachable server holds.
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
if (StreamGateActive()) {
if (!RecoverLostSession()) {
state->SetError(EGL_BAD_ALLOC);
return EGL_NO_CONTEXT;
}
if (shareCtx != EGL_NO_CONTEXT && ContextIsLost(state, shareCtx)) {
state->SetError(EGL_BAD_CONTEXT);
return EGL_NO_CONTEXT;
}
}
// The calling thread may still have a lost context current: what is built here is the
// fresh session's all the same.
const ClientSession::ScopedCurrentSessionWork currentSession;
#endif
EGLContext context = state->CreateContext(dpy, config, shareCtx, attrib_list);
if (context == EGL_NO_CONTEXT) {
return EGL_NO_CONTEXT;
}
@@ -543,8 +654,26 @@ namespace MobileGL::MG_Impl::EGLImpl {
// pre-Start bring-up, the server role) keeps it: there is no server to refuse.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Client::RunsAsTheServerRole()) {
const auto emitted = MG_Remote::Client::SendCreateContextFrame(
auto emitted = MG_Remote::Client::SendCreateContextFrame(
state->GetContextClientToken(context), state->GetContextShareGroupToken(context), 0);
// THE LOSS CAN BE FOUND OUT HERE: a session whose server is gone, which nothing had
// noticed yet, refuses the create by hanging up on it. That is the loss, not a refusal:
// the half-built context goes (its objects silently - they were never on any server),
// a fresh session comes up, and the context is built again on it, once.
if (emitted == MG_Remote::Client::ContextFrameEmit::Refused && ClientSession::SessionLatchedLost()) {
{
const ClientSession::ScopedWireMute mute;
state->DestroyContext(dpy, context);
}
context = EGL_NO_CONTEXT;
if (RecoverLostSession()) context = state->CreateContext(dpy, config, shareCtx, attrib_list);
if (context == EGL_NO_CONTEXT) {
state->SetError(EGL_BAD_ALLOC);
return EGL_NO_CONTEXT;
}
emitted = MG_Remote::Client::SendCreateContextFrame(state->GetContextClientToken(context),
state->GetContextShareGroupToken(context), 0);
}
if (emitted == MG_Remote::Client::ContextFrameEmit::Refused) {
state->DestroyContext(dpy, context);
state->SetError(EGL_BAD_ACCESS);
@@ -560,6 +689,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_FALSE;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// An application that re-initializes a display after a loss (without the eglTerminate that
// would have torn the library down) gets a fresh session here, as eglCreateContext would.
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
(void)RecoverLostSession();
#endif
if (!state->InitializeDisplay(dpy, major, minor)) {
return EGL_FALSE;
}
@@ -618,6 +753,15 @@ namespace MobileGL::MG_Impl::EGLImpl {
MGLOG_D("eglMakeCurrent begin thread=%s dpy=%p draw=%p read=%p ctx=%p oldDpy=%p oldDraw=%p oldRead=%p oldCtx=%p",
threadId.c_str(), dpy, draw, read, ctx, oldDisplay, oldDraw, oldRead, oldContext);
#if MOBILEGL_BUILD_DISAGGREGATED
// EGL 1.5 3.7.3 / 3.2: after a power-management event - a lost device, here - making a
// context created before it current fails with EGL_CONTEXT_LOST. The application destroys
// it and creates another; a release, and every context created since, go on as usual.
if (ctx != EGL_NO_CONTEXT && ContextIsLost(state, ctx)) {
state->SetError(EGL_CONTEXT_LOST);
return EGL_FALSE;
}
#endif
#if MOBILEGL_WAYLAND_WINDOWS
if (draw != EGL_NO_SURFACE) ApplyWaylandResize(state, dpy, draw);
@@ -695,12 +839,19 @@ namespace MobileGL::MG_Impl::EGLImpl {
// context still current on some thread, EGL_BAD_ACCESS) never reaches the server - the
// two ends stay in step without the server having to guess why a token vanished.
const Uint64 contextToken = state->GetContextClientToken(ctx);
// A LOST CONTEXT IS DESTROYED IN SILENCE. Its objects die here, and a death that reached
// the wire would land on whichever session is current now - a fresh one after a recovery,
// which never created them. Under the operation lock, like every GL call.
const std::lock_guard<std::recursive_mutex> destroyLock(EGLOperationMutex());
const Bool lost = ContextIsLost(state, ctx);
std::optional<ClientSession::ScopedWireMute> silence;
if (lost) silence.emplace();
#endif
if (!state->DestroyContext(dpy, ctx)) {
return EGL_FALSE;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (contextToken != 0 && MG_Config::Transport != MG_Config::TransportMode::Monolith &&
if (contextToken != 0 && !lost && MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Client::RunsAsTheServerRole()) {
const std::lock_guard<std::recursive_mutex> streamLock(EGLOperationMutex());
// The server drops its binding to 0 when the current context is destroyed
@@ -986,6 +1137,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_NO_SURFACE;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (!RecoverBeforeCreatingSurface(state)) return EGL_NO_SURFACE;
#endif
const EGLint width = GetAttribValue(attrib_list, EGL_WIDTH, 1);
const EGLint height = GetAttribValue(attrib_list, EGL_HEIGHT, 1);
EGLSurface surface = state->CreatePbufferSurface(dpy, config, attrib_list);
@@ -1321,6 +1475,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_NO_SURFACE;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (!RecoverBeforeCreatingSurface(state)) return EGL_NO_SURFACE;
#endif
#if MOBILEGL_WAYLAND_WINDOWS
if (RefuseGbmWindow(state, dpy)) return EGL_NO_SURFACE;
{
@@ -806,6 +806,22 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
return true;
}
void WindowSurface::OnSessionReplaced() {
Impl& impl = *m_impl;
const Api& api = WaylandApi();
for (auto& buffer : impl.dmabufBuffers) {
if (buffer.buffer) {
api.marshalFlags(buffer.buffer, kBufferDestroy, nullptr, api.getVersion(buffer.buffer),
kMarshalFlagDestroy);
}
buffer = DmabufBuffer{};
}
impl.dmabufDamage.InvalidateAll();
// The linux-dmabuf global is still bound (FallBackToShm keeps it); a window that never had
// it, or gave it up at creation because nothing could be allocated, stays on wl_shm.
impl.useDmabuf = impl.dmabufWanted && impl.dmabuf != nullptr;
}
Bool WindowSurface::Present(const MG_Util::Damage::Region& damage, Int swapInterval) {
Impl& impl = *m_impl;
if (impl.window == nullptr) return false;
+7
View File
@@ -92,6 +92,13 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
// its next buffer is that size - and the caller resizes the drawable behind it.
Bool TakeResize(EGLint* width, EGLint* height);
// The server session that allocated this window's shared images was lost and a fresh one
// replaced it: the images went with their server. Their wl_buffers are destroyed here,
// nothing is released (the fresh server never issued those ids), and the window goes
// back to shared images - allocated anew, on the fresh server, at the next present - if
// it was using them, or had fallen back to wl_shm only for a present the loss broke.
void OnSessionReplaced();
struct Impl;
private:
@@ -2878,6 +2878,16 @@ namespace MobileGL::MG_Impl::GLImpl {
}
GLenum GetError() {
#if MOBILEGL_BUILD_DISAGGREGATED
// KHR_robustness: a lost context's commands generate GL_CONTEXT_LOST. Reported ONCE per
// context and ahead of whatever else is queued, so an application that learns of the loss
// from glGetError (Chromium's decoder does) learns of it, and one that drains errors in a
// loop until GL_NO_ERROR still terminates.
if (MG_Remote::Client::ClientSession::DeviceLost() && MG_State::pGLContext &&
MG_State::pGLContext->TakeContextLostReport()) {
return GL_CONTEXT_LOST;
}
#endif
auto error = MG_State::pGLContext->PopGLError();
if (!error || !error->get()) {
return GL_NO_ERROR;
+7 -3
View File
@@ -7,6 +7,7 @@
// End of Source File Header
#include <Includes.h>
#include <Init.h>
#include "GLImpl/Texture/ProxyTexture.h"
#include "GLImpl/Framebuffer/GL_Framebuffer.h"
@@ -22,7 +23,13 @@ namespace MobileGL::MG_Impl {
void Init() {
MGLOG_D("Initializing MobileGL Implementation...");
GLImpl::TextureImpl::pProxyTextureManager = MakeUnique<GLImpl::TextureImpl::ProxyTextureManager>();
BuildDefaultFramebuffer();
#if defined(__APPLE__) && !defined(MOBILEGL_IOS)
NSOpenGLImpl::InstallHooks();
#endif
}
void BuildDefaultFramebuffer() {
// TODO: get real info in EGL
auto& fbo0 = MG_State::pGLContext->CreateFramebufferObject(0);
auto colorTex = MakeShared<MG_State::GLState::TextureObject2D>(0);
@@ -52,8 +59,5 @@ namespace MobileGL::MG_Impl {
// framebuffer, so a context the application creates after this one is not left with an
// unbound framebuffer 0.
MG_State::SetDefaultFramebuffer(fbo0);
#if defined(__APPLE__) && !defined(MOBILEGL_IOS)
NSOpenGLImpl::InstallHooks();
#endif
}
} // namespace MobileGL::MG_Impl
+42
View File
@@ -1828,6 +1828,12 @@ namespace MobileGL::MG_Pipe {
*entry = Entry{};
}
// A fresh server holds no record at all (MGPipeForgetEndedSession).
void ForgetAll() {
for (Vector<Entry>& table : m_kinds) table.clear();
m_band.clear();
}
private:
struct Entry {
Uint32 Gen = 0;
@@ -1942,6 +1948,13 @@ namespace MobileGL::MG_Pipe {
void MGPipeNoteTextureLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level) {
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// AN UPLOAD ON A LOST CONTEXT IS OWED TO NOBODY. The drain list is process-wide and the
// next verb of ANY context drains it; queued from a thread still on a lost context (or
// while the session is lost), it would be carried to a fresh session that never created
// the texture. The level stays dirty on the object, which no live context can reach.
if (MG_Remote::Client::ClientSession::DeviceLost()) return;
#endif
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
MGPipeTextureEmitterInstance(),
[&](auto& emitter) { emitter.NoteLevelDirty(storageOwner, uploadTarget, level); });
@@ -3973,4 +3986,33 @@ namespace MobileGL::MG_Pipe {
EntryCompare(inputs, mask);
#endif
}
#if MOBILEGL_BUILD_DISAGGREGATED
// Declared in PipeMutation.h, where the list is argued. THE ORDER: the latch goes first, so
// everything after it - a pin's last owner dying, a deferred destroy replayed, an evicted
// cache entry - finds its handle unpublished and puts nothing on the new wire; then the
// references the emitters hold into the caches go back, then the caches themselves.
void MGPipeForgetEndedSession() {
PublicationLatch().ForgetAll();
MGPipeResourceTrackerInstance().ForgetPublication();
MGPipeReleaseResidualFillPins();
MGPipeDrainDeferredDestroys();
MGPipeTrackerInstance().Reset();
// The texture emitter's records and its owed uploads belong to the ended server; its
// sampler references go back to the cache before the cache is emptied.
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeSamplerEmitterInstance().Reset();
MGPipeSamplerCsoCacheInstance().ResetForTest();
MGPipeCsoCacheInstance().Reset();
MGPipeSetHashSuppressorInstance().InvalidateAll();
MGPipeVertexInputEmitterInstance().Reset();
MGPipeFramebufferEmitterInstance().ResetForTest();
MGPipeImageEmitterInstance().Reset();
MGPipeProgramEmitterInstance().Reset();
MGPipeShaderBufferEmitterInstance().Reset();
g_residualDue = true;
g_attribDefaultLastHeader = MGPVertexAttribDefaults{};
g_lastFillWasBarriered = true;
}
#endif
} // namespace MobileGL::MG_Pipe
+6
View File
@@ -340,6 +340,12 @@ namespace MobileGL::MG_Pipe {
const SizeT slot = handle.Slot;
return slot < m_bySlot.size() && m_bySlot[slot].Published;
}
// A session that replaces an ended one has no record for any buffer the ended one was
// told about: a live buffer of the ended session's contexts dies with no destroy on the
// new wire, and the inverse entries stay so the handles still resolve locally.
void ForgetPublication() {
for (Entry& entry : m_bySlot) entry.Published = false;
}
// The sticky everBoundAs mask. Sticky exactly as MGPResourceDesc::ImageBindableHint's
// everImageBound is: ORed, never cleared, so a buffer that was an element array once
+2 -1
View File
@@ -373,7 +373,8 @@ namespace MobileGL::MG_Pipe {
return false;
}
// A unit test's fixture, and nothing else. NOT called from the validate point's
// A unit test's fixture, and a session that replaced an ended one (MGPipeForgetEndedSession:
// the fresh server holds none of these records). NOT called from the validate point's
// FreshlyPrimed arm: MGPipeApplierReset is a make-current and does NOT drop object
// records, so a sampler CSO the applier holds outlives a context switch. Dropping the
// cache there would leak the applier's record and re-mint a value it already has.
+9 -1
View File
@@ -1244,6 +1244,13 @@ namespace MobileGL::MG_Pipe {
return entry.Gen == handle.Gen ? entry.BuiltinSampler : kMGPipeNullHandle;
}
SizeT DrainListSize() const { return m_drain.size(); }
// Whether a level of `handle` is still owed to the server (on the drain list).
Bool OwesUpload(MGPipeHandle handle) const {
for (const DrainEntry& pending : m_drain) {
if (pending.Handle == handle) return true;
}
return false;
}
// A fresh context: what the server has is no longer what this emitter last sent. Only
// LATCHES reset here - the applier's object records survive a make-current and
@@ -1263,7 +1270,8 @@ namespace MobileGL::MG_Pipe {
m_deadResolves = 0;
}
// A unit fixture's per-case reset; the library never calls it. See
// A unit fixture's per-case reset, and the reset of a session that replaced an ended one
// (MGPipeForgetEndedSession: the fresh server holds none of these records). See
// MGPipeResourceTracker::ResetForTest for the rule this restates: a texture handle and
// the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is
// per-context and no re-publication path exists or may exist.
+11
View File
@@ -266,6 +266,17 @@ namespace MobileGL::MG_Pipe {
void MGPipeClientFinish();
// Submit a stream link's published prefix; never wait for appliedSeq.
void MGPipeClientFlush();
// A FRESH SESSION REPLACES AN ENDED ONE (a device loss recovered, or an eglTerminate /
// eglInitialize): every client-side answer to "what does the server hold" describes the
// ended session's server, and the new one holds nothing. The publication latch, the
// content-addressed caches (render-state and sampler CSOs, their slots freed), the
// resource tracker's published bits, the texture emitter's records and owed uploads, every
// emitter's per-context latches, the dirty tracker, the deferred destroys and the residual
// fill's pins all start over. Emits nothing; ClientSession::FinishStartup calls it before
// the new session serves its first record. Objects of the ended session that are still
// alive find themselves unpublished, so their deaths put nothing on the new wire.
void MGPipeForgetEndedSession();
#endif
void MGPipeEmitResourceCreate(MG_State::GLState::BufferObject& buffer);
@@ -227,11 +227,20 @@ namespace MobileGL::MG_Remote::Client {
// depth24+stencil8 surface, and every buffer allocated from that answer is
// blit-incompatible with the real thing).
if (ClientSession* session = ClientSession::Active()) session->DrainPublishedEvents();
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, handle);
if (!MG_Backend::BackendObject::CreateEGLWindowSurface(surface, handle)) return false;
NoteHomed(surface);
return true;
}
Bool BackendObject_Remote::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
WaitForApplyBeforeEglForwarder("ResizeEGLWindowSurface");
// A surface with no server side on this session (its session was lost) takes the size
// into its registration only; it is re-created at that size when it is next used.
{
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (GetRegisteredEGLSurface(surface) != nullptr && m_homedSurfaces.count(surface) == 0)
return MG_Backend::BackendObject::ResizeEGLWindowSurface(surface, width, height);
}
// P12 review fix: a surface on the SERVER's window is resized by resizing that window. The
// server asks its display for the size and answers with the extent the window really took,
// and that - not the size asked for, which EGLImpl already wrote into the EGL state - is what
@@ -296,7 +305,9 @@ namespace MobileGL::MG_Remote::Client {
// Same drain as the window surface: InitPbufferSurface publishes the default
// framebuffer's depth/stencil format on SEG_EVENT from inside this very RPC.
if (ClientSession* session = ClientSession::Active()) session->DrainPublishedEvents();
return MG_Backend::BackendObject::CreateEGLPbufferSurface(surface, width, height);
if (!MG_Backend::BackendObject::CreateEGLPbufferSurface(surface, width, height)) return false;
NoteHomed(surface);
return true;
}
namespace {
@@ -368,7 +379,9 @@ namespace MobileGL::MG_Remote::Client {
if (local.Handle == nullptr) local.Handle = &g_serverOwnedWindowPlaceholder;
local.Width = reply.width;
local.Height = reply.height;
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, local);
if (!MG_Backend::BackendObject::CreateEGLWindowSurface(surface, local)) return false;
NoteHomed(surface);
return true;
}
Bool BackendObject_Remote::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read,
@@ -378,6 +391,15 @@ namespace MobileGL::MG_Remote::Client {
// InitCapabilities' answer comes from a snapshot the server can only publish once its
// own InitCapabilities has run - and ServerMakeEGLCurrent is what publishes it.
WaitForApplyBeforeEglForwarder("MakeEGLCurrent");
const Bool release = draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
// A RELEASE ALWAYS SUCCEEDS ON A LOST SESSION. Its server has nothing left to unbind, and
// an application releasing a lost context before it destroys it - the order EGL asks for -
// must not be told EGL_BAD_ACCESS and left with the lost context still current.
if (release && ClientSession::SessionLatchedLost()) {
return MG_Backend::BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
}
// A surface a lost session had gets its server side back before anything is bound to it.
if (!release && (!EnsureSurfaceHomed(draw) || (read != draw && !EnsureSurfaceHomed(read)))) return false;
// The frame carries the context's CLIENT TOKEN rather than its handle (the server's
// MakeCurrent arm binds the session to it before the native bind). Identity is all the
// server reads from the field otherwise, and a token is as unique as a handle.
@@ -419,6 +441,7 @@ namespace MobileGL::MG_Remote::Client {
// on the server directly and put no record on the wire, which is the shape every gate
// in this phase exists to catch. The forwarder exists for a spawned P6 client whose
// Present record cannot carry the swap; in P5 it has no caller and that is deliberate.
if (!EnsureSurfaceHomed(draw)) return false;
return MG_Backend::BackendObject::SwapEGLBuffers(dpy, draw);
}
@@ -440,18 +463,92 @@ namespace MobileGL::MG_Remote::Client {
void BackendObject_Remote::ReleaseEGLSurface(EGLSurface surface) {
// P12: a released server-owned surface stops taking the server window's geometry.
if (ClientSession* session = ClientSession::Active()) session->ForgetServerOwnedWindowSurface(surface);
Server::ServerReleaseEGLSurface(surface);
// Only a surface the current session's server has: one a lost session had is gone with it.
Bool homed = false;
{
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
homed = m_homedSurfaces.erase(surface) != 0;
}
if (homed && !ClientSession::SessionLatchedLost()) Server::ServerReleaseEGLSurface(surface);
MG_Backend::BackendObject::ReleaseEGLSurface(surface);
}
void BackendObject_Remote::ReleaseEGLResources() {
// BLOCKING BY CONTRACT (ServerLoop.h's header note): MobileGL::Destroy()
// (MobileGL/Init.cpp:68) walks on the moment this returns, and the server still holds
// the context until the apply thread has run it.
Server::ServerReleaseEGLResources();
// the context until the apply thread has run it. A lost session's server holds nothing.
if (!ClientSession::SessionLatchedLost()) Server::ServerReleaseEGLResources();
{
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_homedSurfaces.clear();
}
MG_Backend::BackendObject::ReleaseEGLResources();
}
void BackendObject_Remote::OnSessionReplaced() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
ResetEGLRuntimeState();
m_homedSurfaces.clear();
for (const auto& [id, references] : m_sessionSharedImages) m_lostSessionSharedImages.insert(id);
m_sessionSharedImages.clear();
// The display the application initialized is initialized on the new server too: its
// surfaces and contexts are created against it from here on.
if (m_eglDisplayInitialized) {
EGLint major = 0;
EGLint minor = 0;
if (!Server::ServerInitializeEGLDisplay(m_eglDisplay, &major, &minor)) {
MGLOG_E("MG_Remote client: the fresh session's server did not initialize the display; "
"surfaces and contexts on it will fail");
}
}
if (ClientSession* session = ClientSession::Active()) session->PumpControlPlane();
RefreshFormatCapabilities();
}
Bool BackendObject_Remote::EnsureSurfaceHomed(EGLSurface surface) {
if (surface == EGL_NO_SURFACE) return true;
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (m_homedSurfaces.count(surface) != 0) return true;
const EGLSurfaceState* registered = GetRegisteredEGLSurface(surface);
// Unknown to this object: the base class's own checks answer for it.
if (registered == nullptr) return true;
const EGLSurfaceState state = *registered;
Bool recreated = false;
const char* kind = "pbuffer";
if (state.Kind == SurfaceKind::Pbuffer) {
recreated = Server::ServerCreateEGLPbufferSurface(surface, state.Width, state.Height);
} else if (ClientSession::IsServerOwnedWindowSurface(surface)) {
kind = "server-owned window";
const Server::ServerOwnedWindowReply reply =
Server::ServerCreateServerOwnedWindowSurface(surface, state.Window.Width, state.Window.Height);
recreated = reply.ok;
if (recreated && reply.width != 0 && reply.height != 0) {
if (MG_State::pEGLContext) {
(void)MG_State::pEGLContext->SetSurfaceExtent(surface, static_cast<EGLint>(reply.width),
static_cast<EGLint>(reply.height));
}
if (ClientSession* session = ClientSession::Active())
session->NoteServerOwnedWindowSurface(surface, reply.width, reply.height);
}
} else {
kind = "window";
Server::ServerSetWindowHandle(state.Window);
recreated = Server::ServerCreateEGLWindowSurface(surface, state.Window);
}
if (!recreated) {
MGLOG_E("MG_Remote client: the %s surface %p (%dx%d) of a lost session could not be re-created on "
"the fresh one",
kind, static_cast<void*>(surface), state.Width, state.Height);
return false;
}
if (ClientSession* session = ClientSession::Active()) session->DrainPublishedEvents();
m_homedSurfaces.insert(surface);
MGLOG_I("MG_Remote client: the %s surface %p (%dx%d) of a lost session is re-created on the fresh one "
"(EGL keeps surfaces across a context loss; their contents are undefined)",
kind, static_cast<void*>(surface), state.Width, state.Height);
return true;
}
// ---- shared images ----------------------------------------------------------------------
//
// One shared_image record each (ClientSession::EmitSharedImage). Allocate/Import/Release are
@@ -461,6 +558,9 @@ namespace MobileGL::MG_Remote::Client {
MG_Backend::SharedImageExport* out) {
ClientSession* session = ClientSession::Active();
if (session == nullptr || out == nullptr) return false;
// A table operation, not a draw: the calling thread's GL state (a lost context, or none) has
// no say in whether the server allocates.
const ClientSession::ScopedCurrentSessionWork tableOp;
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImageAllocate;
op.Format = fourcc;
@@ -477,12 +577,17 @@ namespace MobileGL::MG_Remote::Client {
out->Stride = reply.Stride;
out->Offset = reply.Offset;
out->Modifier = reply.Modifier;
{
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
++m_sessionSharedImages[reply.ImageId];
}
return true;
}
Bool BackendObject_Remote::ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) {
ClientSession* session = ClientSession::Active();
if (session == nullptr || fd < 0) return false;
const ClientSession::ScopedCurrentSessionWork tableOp;
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImageImport;
op.Format = fourcc;
@@ -491,12 +596,26 @@ namespace MobileGL::MG_Remote::Client {
MG_Pipe::MGPSharedImageReply reply{};
if (!session->EmitSharedImage(op, fd, &reply, nullptr)) return false;
if (outId != nullptr) *outId = reply.ImageId;
{
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
++m_sessionSharedImages[reply.ImageId];
}
return true;
}
Bool BackendObject_Remote::ReleaseSharedImage(Uint64 id) {
ClientSession* session = ClientSession::Active();
if (session == nullptr || !session->Started()) return false;
// An image of a lost session is gone with its server; the fresh one never issued its id.
{
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
const auto held = m_sessionSharedImages.find(id);
// Nothing to release on the new server, and nothing that failed: the reference died with
// its session.
if (held == m_sessionSharedImages.end()) return m_lostSessionSharedImages.erase(id) != 0;
if (--held->second == 0) m_sessionSharedImages.erase(held);
}
const ClientSession::ScopedCurrentSessionWork tableOp;
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImageRelease;
op.ImageId = id;
@@ -506,6 +625,7 @@ namespace MobileGL::MG_Remote::Client {
Bool BackendObject_Remote::PresentToSharedImage(Uint64 id, const MG_Util::Damage::Region& region) {
ClientSession* session = ClientSession::Active();
if (session == nullptr) return false;
if (!HoldsSharedImage(id)) return false; // a lost session's image: gone with its server
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImagePresent;
op.ImageId = id;
@@ -539,6 +659,7 @@ namespace MobileGL::MG_Remote::Client {
Bool BackendObject_Remote::AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) {
ClientSession* session = ClientSession::Active();
if (session == nullptr) return false;
if (!HoldsSharedImage(id)) return false; // an EGLImage of a lost session: its image is gone
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImageAttach;
op.ImageId = id;
@@ -51,6 +51,9 @@
#include <MG_Backend/BackendObject.h>
#include <unordered_map>
#include <unordered_set>
namespace MobileGL::MG_Remote::Client {
class BackendObject_Remote final : public MG_Backend::BackendObject {
@@ -97,6 +100,14 @@ namespace MobileGL::MG_Remote::Client {
// and this is what it calls; it is the whole of trap 2's answer.
void RefreshFormatCapabilities();
// A FRESH SESSION REPLACED A LOST ONE (ClientSession::RecoverAfterDeviceLoss). The
// initialized display is initialized on the new server; every surface keeps its client
// registration (EGL keeps surfaces across a context loss, only their contents go) but
// has no server side yet, and is re-created there the next time it is made current,
// swapped or resized; no thread is current any more; the shared images of the lost
// session are forgotten without a release (their server is gone).
void OnSessionReplaced();
protected:
Bool InitPbufferSurface(EGLint width, EGLint height) override;
@@ -110,6 +121,28 @@ namespace MobileGL::MG_Remote::Client {
// through the log line on a refresh: a cache that silently stopped tracking the mirror
// is exactly the shape trap 2 exists to prevent.
Uint64 m_formatsGeneration = 0;
// The server side of `surface` on the CURRENT session: re-created from its client
// registration when the session that had it was replaced. True when it exists now.
Bool EnsureSurfaceHomed(EGLSurface surface);
void NoteHomed(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_homedSurfaces.insert(surface);
}
Bool HoldsSharedImage(Uint64 id) const {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return m_sessionSharedImages.count(id) != 0;
}
// The surfaces and shared images the CURRENT session's server holds. A surface missing
// from the first is one a lost session had (EnsureSurfaceHomed); an image id missing from
// the second (counted per reference taken) is a lost session's, and is neither released
// nor presented to nor attached on the new server, which never issued it. Guarded by the
// base's m_eglStateMutex.
std::unordered_set<EGLSurface> m_homedSurfaces;
std::unordered_map<Uint64, Uint32> m_sessionSharedImages; // id -> references taken
// The ids a replaced session held: releasing one is a success with nothing sent.
std::unordered_set<Uint64> m_lostSessionSharedImages;
};
} // namespace MobileGL::MG_Remote::Client
+144 -7
View File
@@ -24,12 +24,15 @@
#include "../Transport/InProcessTransport.h"
#include "WireTables.h"
#include "EmitTables.h"
#include "PersistentMapTracker.h"
#include "ServerProbe.h"
#include <MGGitHash.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Pipe/MGPipeCallbacks.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_State/EGLState/Core.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
@@ -94,6 +97,13 @@ namespace MobileGL::MG_Remote::Client {
ClientSession* g_active = nullptr;
// THE RECOVERY'S THREE PIECES OF PROCESS STATE (ClientSession.h). The two scopes are
// per thread and nest; the count is how many sessions with a remote server this process
// has started, so the second and later know there is an ended one to forget.
thread_local Uint32 tl_wireMute = 0;
thread_local Uint32 tl_currentSessionWork = 0;
std::atomic<Uint64> g_remoteSessionsStarted{0};
// The same bounded handshake deadline the server uses. Bounded, not kWaitForever: a
// bring-up that never answers has to be a red lane rather than a wedged CI job.
constexpr Uint32 kHandshakeTimeoutMs = 5000;
@@ -593,10 +603,29 @@ namespace MobileGL::MG_Remote::Client {
ClientSession* ClientSession::Active() { return g_active; }
Bool ClientSession::DeviceLost() {
if (tl_wireMute != 0) return true;
const ClientSession* session = Active();
if (session == nullptr) return false;
if (session->m_deviceLost.load(std::memory_order_acquire)) return true;
if (tl_currentSessionWork != 0) return false;
// A thread still on a context from before a recovered loss: lost for good, whatever the
// session it would now be writing into.
const SharedPtr<MG_State::GLState::GLContext>& context = MG_State::pGLContext;
return context != nullptr && context->IsFromEndedWireSession();
}
Bool ClientSession::SessionLatchedLost() {
const ClientSession* session = Active();
return session != nullptr && session->m_deviceLost.load(std::memory_order_acquire);
}
ClientSession::ScopedWireMute::ScopedWireMute() { ++tl_wireMute; }
ClientSession::ScopedWireMute::~ScopedWireMute() { --tl_wireMute; }
ClientSession::ScopedCurrentSessionWork::ScopedCurrentSessionWork() { ++tl_currentSessionWork; }
ClientSession::ScopedCurrentSessionWork::~ScopedCurrentSessionWork() { --tl_currentSessionWork; }
Uint64 ClientSession::SessionsStarted() { return g_remoteSessionsStarted.load(std::memory_order_acquire); }
void ClientSession::LatchDeviceLost(const char* why) {
// exchange, so the log line is written EXACTLY once however many waits notice the same
// hangup at the same moment - and there will be several: the barrier, the control wait
@@ -604,13 +633,91 @@ namespace MobileGL::MG_Remote::Client {
if (m_deviceLost.exchange(true, std::memory_order_acq_rel)) {
return;
}
MGLOG_E("MGPipe: DEVICE LOST - %s. The server process is gone; every verb from here is "
"DECLINED and glGetGraphicsResetStatus reports GL_UNKNOWN_CONTEXT_RESET. This is "
"latched from the peer's HANGUP and not from any deadline (CONTRACT-P6 5.4): a "
"server one frame behind is P5e's intended steady state and must never read as a "
"dead one. There is no recovery - MOBILEGL_IPC_RESPAWN is the stage that would "
"add one and it is refused by name until then.",
why == nullptr ? "the peer hung up" : why);
MGLOG_E("MGPipe: DEVICE LOST - %s. This session's server side is gone; every verb from here "
"is DECLINED and glGetGraphicsResetStatus reports GL_UNKNOWN_CONTEXT_RESET. This is "
"latched from the peer's HANGUP (or its named SessionFault) and not from any "
"deadline (CONTRACT-P6 5.4): a server one frame behind is P5e's intended steady "
"state and must never read as a dead one. The contexts of this session stay lost; "
"the application's next eglCreateContext / eglCreate*Surface / eglInitialize brings "
"a fresh session up (MOBILEGL_IPC_RECOVER=%u).",
why == nullptr ? "the peer hung up" : why, MG_Config::Ipc.RecoverAfterLoss);
}
Bool ClientSession::ConfirmLossAfterDecline(Uint32 waitMs) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMs);
for (;;) {
if (m_deviceLost.load(std::memory_order_acquire)) return true;
PumpControlPlane();
if (const Transport::Doorbell* bell = m_producer.SelfDoorbell(); bell != nullptr && bell->PeerHungUp()) {
LatchDeviceLost("a readback was declined and the peer had hung up");
}
if (m_link && m_link->PeerHungUp()) LatchDeviceLost("a readback was declined and the link had hung up");
if (m_deviceLost.load(std::memory_order_acquire)) return true;
if (std::chrono::steady_clock::now() >= deadline) return false;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
}
namespace {
// A LOST SESSION'S T0 STORES STAY MAPPED. A buffer of a lost context may still hold the
// pointer of the store it was adopted into (the application has not destroyed it yet, and
// may write through its persistent mapping), so the recovery's Stop must not unmap it.
// Bounded by what the lost session had adopted; never touched again.
Vector<Transport::AdoptT0::HeldStore>& LostSessionT0Stores() {
static auto* stores = new Vector<Transport::AdoptT0::HeldStore>();
return *stores;
}
} // namespace
MobileGLResult ClientSession::RecoverAfterDeviceLoss() {
if (!m_started || !m_deviceLost.load(std::memory_order_acquire)) return MOBILEGL_OK;
if (MG_Config::Ipc.RecoverAfterLoss == 0) {
MGLOG_E_ONCE("MG_Remote client: Refuse{Recover} the session is lost and MOBILEGL_IPC_RECOVER=0 - "
"no fresh session is dialed; every context of this process stays lost");
return MOBILEGL_ERR_UNSUPPORTED;
}
const std::string control(MG_Config::Ipc.Control.c_str());
if (MG_Config::Transport != MG_Config::TransportMode::Spawn || control.compare(0, 3, "fd:") == 0) {
MGLOG_E_ONCE("MG_Remote client: Refuse{Recover} the session is lost and this transport cannot "
"reach its server again (%s): an in-process server is this process, and a "
"pre-connected fd: pair was handed over once. Every context stays lost",
control.empty() ? "inproc" : control.c_str());
return MOBILEGL_ERR_UNSUPPORTED;
}
// THE SERVER FIRST, THEN THE TEARDOWN. A session that is stopped and cannot be replaced
// leaves no session at all, and a GL call then has nowhere honest to go; a latched session
// that is kept declines every verb, which is the answer the application already has.
if (ProbeConfiguredServer() == ServerProbeResult::Unreachable) {
MGLOG_E("MG_Remote client: the session is lost and no server answers at %s; it stays lost "
"(the next create will ask again)",
control.c_str());
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
const Uint32 lostServerPid = m_peerServerPid;
for (auto& [key, store] : m_t0Stores) LostSessionT0Stores().push_back(store);
m_t0Stores.clear();
Stop();
const MobileGLResult started = StartSpawned();
if (started != MOBILEGL_OK) {
MGLOG_E("MG_Remote client: the fresh session after a device loss did not come up (rc=%d); "
"there is no session now, and the next create will try again",
static_cast<int>(started));
return started;
}
MGLOG_I("MG_Remote client: RECOVERED - a fresh session (#%llu of this process) replaces the one "
"lost on server pid %u; it is served by pid %u. Contexts created before stay lost",
static_cast<unsigned long long>(SessionsStarted()), lostServerPid, m_peerServerPid);
return MOBILEGL_OK;
}
void ClientSession::ForgetEndedSession() {
MGLOG_I("MG_Remote client: session #%llu follows an ended one - the client's caches of that "
"session's objects (publication, CSOs, samplers, emitter latches, the create window, "
"the reset serial, persistent maps) start over",
static_cast<unsigned long long>(SessionsStarted()));
ForgetWireTablesOfEndedSession();
PersistentMapTracker::Instance().ForgetEndedSession();
MG_Pipe::MGPipeForgetEndedSession();
}
ClientSession& ClientSessionInstance() {
@@ -1690,6 +1797,17 @@ namespace MobileGL::MG_Remote::Client {
PumpControlPlane();
}
// A SESSION WITH A REMOTE SERVER STARTS FROM A CLEAN CLIENT. Every client-side cache that
// describes what a server holds (published handles, CSO and sampler caches, emitter
// latches, the create window, the applier-reset serial, tracked persistent maps) describes
// the PREVIOUS session's server once this one replaces it - after a device loss, or an
// eglTerminate/eglInitialize - and a fresh server holds none of it. And the wire epoch
// moves, which is what makes every GLContext built against the previous session lost.
// Inproc is left as it was: its server role is this process, and it never recovers.
if (!startApplyThreadHere) {
if (g_remoteSessionsStarted.fetch_add(1, std::memory_order_acq_rel) != 0) ForgetEndedSession();
MG_State::AdvanceWireEpoch();
}
// BOTH, AND m_started FIRST. `Active()` is what the integration lane's skip reads and
// `m_started` is what `EmitAndWait` reads, and c1's round-1 rewrite of this function
// set only the second of the two - so a fully-handshaken session with an apply thread
@@ -1879,6 +1997,12 @@ namespace MobileGL::MG_Remote::Client {
// The cold-start window is the next server's, not this one's (RunRemoteSurfaceControlFrame).
m_serverBackendWarm = false;
m_presentsSent = 0;
// And so are the latch and the control seq space: the next session's server counts its
// ops from 1 and has lost nothing. (The latch is read through Active(), which is null
// from here until a next session is started.)
m_deviceLost.store(false, std::memory_order_release);
m_remoteControlSeq = 0;
m_logFlushSeq = 0;
if (g_active == this) {
g_active = nullptr;
// Mirror order, and the probe goes with it: a dangling function pointer into a
@@ -2608,6 +2732,19 @@ namespace MobileGL::MG_Remote::Client {
static_cast<unsigned long long>(frame.size()));
continue;
}
if (envelope->msg_type() == ::MobileGL::Wire::CtrlMsg::Fatal) {
// The server latched this session on a named fault (a lost GPU device among them)
// and said so before declining the rest: the session is lost, from the server's
// own mouth rather than from the hangup that follows.
if (const auto* fatal = envelope->msg_as_Fatal()) {
MGLOG_E("MG_Remote client: the server ended this session on a named fault - Fatal "
"family %s: %s",
fatal->family() ? fatal->family()->c_str() : "<none>",
fatal->message() ? fatal->message()->c_str() : "");
}
LatchDeviceLost("the server published a SessionFault for this session");
continue;
}
if (envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::CapsSnapshot) {
// SurfaceOp / SurfaceReply / ResyncRequest / AuxRequest / LogLine are P6's and
// P7's. Named rather than ignored, so a phase that starts sending one does not
+56 -7
View File
@@ -74,23 +74,69 @@ namespace MobileGL::MG_Remote::Client {
static ClientSession* Active();
// P6 `dl` (CONTRACT-P6 5.3). THE DEVICE-LOST LATCH, and the two words that matter are
// SESSION-SCOPED and LATCH: it is set at most once per session and never cleared, because
// a session whose server died has nothing left to recover into. MOBILEGL_IPC_RESPAWN is
// where the recovery would go and it is a named refusal until some stage writes it.
// SESSION-SCOPED and LATCH: it is set at most once per session and never cleared while
// that session lives. What recovers is the PROCESS, not the session: the application's
// next eglCreateContext / eglCreate*Surface / eglInitialize ends the lost session and
// dials a fresh one (RecoverAfterDeviceLoss), and every context built before stays lost.
//
// SET FROM Doorbell::PeerHungUp(), NEVER FROM A TIMEOUT. 5.4's whole point is that slow
// and dead are different states and must be told apart without a threshold: under P5e
// run-ahead a server one frame behind is the INTENDED steady state, so a latch armed by a
// deadline would fire on a healthy session under load. The descriptor answers instead.
// SET FROM Doorbell::PeerHungUp() OR THE SERVER'S OWN SessionFault, NEVER FROM A
// TIMEOUT. 5.4's whole point is that slow and dead are different states and must be told
// apart without a threshold: under P5e run-ahead a server one frame behind is the
// INTENDED steady state, so a latch armed by a deadline would fire on a healthy session
// under load. The descriptor (or the server, naming its fault) answers instead.
//
// WHAT THE CALLING THREAD SEES, which is more than the latch: the thread's GL state may
// belong to a session that has already been replaced (a context from before a recovered
// loss, still current on some thread), and for that thread the device is lost for good -
// nothing it does may reach the new session. ScopedWireMute below answers true as well;
// ScopedCurrentSessionWork lifts the thread's own staleness (not the latch).
//
// The static form answers false whenever no session is active, so monolith and the pull
// build get the honest answer with no branch of their own.
static Bool DeviceLost();
// The latch alone: is the ACTIVE session lost. What the EGL layer asks before recovering.
static Bool SessionLatchedLost();
// Arms the latch and says why, once. Safe to call repeatedly and from any thread; only
// the first call logs.
void LatchDeviceLost(const char* why);
// A reply-owning record came back DECLINED where only a latched server declines (a
// readback). The server publishes its SessionFault on the control plane BEFORE it declines,
// so the frame - or the hangup that follows it - is what tells "lost" from a refusal. Pumps
// the control plane and asks the bell for up to `waitMs`; true when the session is lost.
Bool ConfirmLossAfterDecline(Uint32 waitMs);
// THE RECOVERY. Ends this lost session (Stop) and dials the configured server for a fresh
// one (StartSpawned); the new session forgets every client-side cache of the old one and
// advances MG_State's wire epoch, which is what makes every older GLContext lost. The
// caller (EGLImpl) holds the EGL operation lock, which every GL entry point also takes, so
// nothing else is emitting. Refused by name - and the session stays latched - under
// MOBILEGL_IPC_RECOVER=0, for a transport whose server cannot be dialed again (inproc,
// fd:), and when the server does not answer a probe. OK when a fresh session is up.
MobileGLResult RecoverAfterDeviceLoss();
// Sessions brought up in this process so far (the recovery's arm proof for a test).
static Uint64 SessionsStarted();
// While one is open on a thread, that thread's emissions are DECLINED as on a lost
// device: the lost contexts' objects die under it, and their deaths must not reach the
// fresh session.
struct ScopedWireMute {
ScopedWireMute();
~ScopedWireMute();
ScopedWireMute(const ScopedWireMute&) = delete;
ScopedWireMute& operator=(const ScopedWireMute&) = delete;
};
// While one is open on a thread, the thread's own GL state does not make it "lost": EGL
// work that targets the current session on behalf of a thread whose current context is
// a lost one (it creates the replacement context, re-creates a surface).
struct ScopedCurrentSessionWork {
ScopedCurrentSessionWork();
~ScopedCurrentSessionWork();
ScopedCurrentSessionWork(const ScopedCurrentSessionWork&) = delete;
ScopedCurrentSessionWork& operator=(const ScopedCurrentSessionWork&) = delete;
};
// The bell this session parks on, for the `dl` red-once ONLY. A test cannot otherwise
// ask the question the latch turns on - "did the peer hang up" is a fact about a
// descriptor, and the pair of tests that pin D5c has to be able to kill a server and
@@ -632,6 +678,9 @@ namespace MobileGL::MG_Remote::Client {
Uint64 m_t0Fallbacks = 0;
std::function<void(Uint64)> m_prePublish;
void ResetT0(Bool releaseStores);
// Every client-side cache keyed to the session that just ended, dropped before the next
// one serves a record (FinishStartup, on every start after the first).
void ForgetEndedSession();
};
// One per process in P5, because P5 serves one context, and LEAKED AT EXIT like every other
+27 -1
View File
@@ -897,6 +897,11 @@ namespace MobileGL::MG_Remote::Client {
// control, so R-16's "drive the production predicate" holds rather than a second copy of
// the rule in the test.
// How long a declined readback waits for the control plane or the bell to say "lost"
// before it is judged a refusal. The SessionFault is sent before the decline is posted,
// so this is only ever spent on a server that refused for real.
constexpr Uint32 kDeclineVerdictMs = 1000;
// The same predicate at the call site, with the named Fatal each failure mode owns.
// status > expected cannot reach here: EmitAndWait already aborts Fatal{ReplyTooLarge} on
// an oversize reply, so the only failures left are a wrong status or a SHORT one.
@@ -909,6 +914,17 @@ namespace MobileGL::MG_Remote::Client {
// the caller leaves the destination untouched and returns.
Bool RequireReadbackReplyComplete(Int32 status, Uint64 replySize, Uint64 expected) {
if (status != Wire::ReplySink::kStatusOk && ClientSession::DeviceLost()) return false;
// THE DECLINE CAN OUTRUN THE NEWS. A server whose device was lost inside THIS readback
// latches the session - publishing its SessionFault on the control plane - and then
// declines the record; the client can read the declined answer before it has looked at
// the control plane or seen the hangup that follows. So a decline asks once more,
// briefly, before it is taken for a server without GL.ReadPixels.
if (status == Wire::ReplySink::kStatusDeclined) {
if (ClientSession* session = ClientSession::Active();
session != nullptr && session->ConfirmLossAfterDecline(kDeclineVerdictMs)) {
return false;
}
}
if (status == Wire::ReplySink::kStatusError) {
SessionFail(MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"ReadPixels\"} - the readback answered ERROR; "
"the destination is left untouched rather than filled with stale bytes");
@@ -1608,6 +1624,11 @@ namespace MobileGL::MG_Remote::Client {
MG_Pipe::MGPipeHandle Handle;
// P10: zero-timeout polls answered "not yet" in a row since the last escalation.
Uint32 Unanswered = 0;
// The session the fence was created on. A fence of an ENDED session (a recovered
// device loss) names nothing on the current server: it is answered as the no-op a
// lost device gives (signaled), and deleting it puts nothing on the wire.
Uint64 Epoch = MG_State::CurrentWireEpoch();
Bool FromEndedSession() const { return Epoch != MG_State::CurrentWireEpoch(); }
};
std::mutex g_fenceMutex;
UnorderedMap<MG_Backend::BackendSyncHandle, UniquePtr<RemoteFenceProxy>> g_fenceProxies;
@@ -1754,6 +1775,7 @@ namespace MobileGL::MG_Remote::Client {
ClientSession& session = RequireSession("ClientWaitSync");
const std::lock_guard<std::mutex> lock(g_fenceMutex);
RemoteFenceProxy& fence = FenceProxy(proxy);
if (fence.FromEndedSession()) return GL_ALREADY_SIGNALED;
switch (AnswerFencePollLocally(session, fence, /*mayWait=*/timeout != 0)) {
case FencePollAnswer::Signaled: return GL_ALREADY_SIGNALED;
case FencePollAnswer::NotYet: return GL_TIMEOUT_EXPIRED;
@@ -1770,6 +1792,7 @@ namespace MobileGL::MG_Remote::Client {
ClientSession& session = RequireSession("GetSyncStatus");
const std::lock_guard<std::mutex> lock(g_fenceMutex);
RemoteFenceProxy& fence = FenceProxy(proxy);
if (fence.FromEndedSession()) return true;
switch (AnswerFencePollLocally(session, fence, /*mayWait=*/false)) {
case FencePollAnswer::Signaled: return true;
case FencePollAnswer::NotYet: return false;
@@ -1786,6 +1809,7 @@ namespace MobileGL::MG_Remote::Client {
void EmitWaitSync(MG_Backend::BackendSyncHandle proxy, GLbitfield flags, GLuint64 timeout) {
ClientSession& session = RequireSession("WaitSync");
const std::lock_guard<std::mutex> lock(g_fenceMutex);
if (FenceProxy(proxy).FromEndedSession()) return;
const MG_Pipe::MGPFenceWait request{FenceHandle(proxy), timeout, flags, 0};
session.EmitAndWait(MG_Pipe::MGPWireOp::FenceWaitServer, &request, sizeof(request),
nullptr, 0, nullptr, 0, nullptr);
@@ -1796,7 +1820,9 @@ namespace MobileGL::MG_Remote::Client {
const auto handle = FenceHandle(proxy);
// MobileGL::Destroy stops the server BEFORE DestroyAllSyncObjects. Detach already
// released those native objects on the apply thread; only the local proxy remains.
if (auto* session = ClientSession::Active(); session != nullptr && session->Started()) {
// A fence of an ended session likewise: its server is gone.
if (auto* session = ClientSession::Active();
session != nullptr && session->Started() && !FenceProxy(proxy).FromEndedSession()) {
const MG_Pipe::MGPHandleOnly desc{handle, static_cast<Uint32>(MG_Pipe::MGPipeKind::Fence), 0};
session->EmitAndWait(MG_Pipe::MGPWireOp::FenceDestroy, &desc, sizeof(desc),
nullptr, 0, nullptr, 0, nullptr);
@@ -7,6 +7,7 @@
// End of Source File Header
#include "PersistentMapTracker.h"
#include "ClientSession.h"
#include <MG_Remote/FatalFunnel.h>
#include <MG_Remote/Transport/AdoptTier.h>
@@ -751,7 +752,10 @@ namespace MobileGL::MG_Remote::Client {
// member that was never large-untracked.
m_untrackedMembers.erase(key);
m_edgedMembers.erase(key);
if (IsLivePersistentMap(buffer)) {
// A buffer mapped on a LOST context (a thread still on one, or a session that is lost)
// never becomes a member: the push before every verb would carry its bytes to whichever
// session is current, and a fresh one never created the buffer.
if (IsLivePersistentMap(buffer) && !ClientSession::DeviceLost()) {
const auto range = buffer.GetMappedRange();
// The mprotect arm registers beside the hash arm: tracked buffers are pushed
// from the fault bitmap, everything else keeps the content scan. A failed
@@ -784,13 +788,21 @@ namespace MobileGL::MG_Remote::Client {
UntrackWriteMap(key);
}
void PersistentMapTracker::ClearForTest() {
void PersistentMapTracker::ForgetEndedSession() {
// Every member is a buffer of the ended session: its bytes have nowhere to go, and a push
// before the next verb would name a handle the fresh server never created. The write
// tracking goes with them (pages unprotected); a member that is unmapped or destroyed
// later finds nothing here, which Forget already tolerates.
m_livePersistentMaps.clear();
m_blockHashes.clear();
m_untrackedMembers.clear();
m_edgedMembers.clear();
UntrackAllWriteMaps();
m_lastFaultEpoch = 0;
}
void PersistentMapTracker::ClearForTest() {
ForgetEndedSession();
ResetCountersForTest();
}
@@ -158,6 +158,8 @@ namespace MobileGL::MG_Remote::Client {
// Unit tests only: clears the membership, the hash cache and the mprotect tracked
// slots (restoring every tracked page writable first), and resets the counters.
void ClearForTest();
// A fresh session replaces an ended one: drop every member (ClientSession).
void ForgetEndedSession();
// Unit tests only: the mprotect arm's white-box surface, for the alignment cases
// (SplitBufferTest) that the public GL surface cannot see - which pages a map
// protected is not observable through any push count. MprotectArmAvailableForTest
+16 -2
View File
@@ -1,8 +1,19 @@
// Opaque local proxies carry only {slot, generation, target} across the wire.
struct RemoteQueryProxy { MG_Pipe::MGPQueryDesc Desc{}; };
// Epoch: the session the query was created on. A query of an ENDED session (a recovered
// device loss) names nothing on the current server: it answers "no result yet", and its
// end and delete put nothing on the wire.
struct RemoteQueryProxy {
MG_Pipe::MGPQueryDesc Desc{};
Uint64 Epoch = MG_State::CurrentWireEpoch();
};
std::mutex g_queryProxyMutex;
UnorderedMap<MG_Backend::BackendQueryHandle, UniquePtr<RemoteQueryProxy>> g_queryProxies;
Bool QueryFromEndedSession(MG_Backend::BackendQueryHandle handle) {
const auto found = g_queryProxies.find(handle);
return found != g_queryProxies.end() && found->second->Epoch != MG_State::CurrentWireEpoch();
}
const MG_Pipe::MGPQueryDesc& QueryDesc(MG_Backend::BackendQueryHandle handle) {
const auto found = g_queryProxies.find(handle);
if (found == g_queryProxies.end())
@@ -69,6 +80,7 @@
void EmitEndQuery(MG_Backend::BackendQueryHandle handle) {
auto& session = RequireSession("QueryEnd");
const std::lock_guard<std::mutex> lock(g_queryProxyMutex);
if (QueryFromEndedSession(handle)) return;
const auto& desc = QueryDesc(handle);
BeforeReadOnlyVerb();
session.EmitAndWait(MG_Pipe::MGPWireOp::QueryEnd, &desc, sizeof(desc), nullptr, 0, nullptr, 0, nullptr);
@@ -76,6 +88,7 @@
Bool EmitIsQueryResultAvailable(MG_Backend::BackendQueryHandle handle) {
auto& session = RequireSession("IsQueryResultAvailable");
const std::lock_guard<std::mutex> lock(g_queryProxyMutex);
if (QueryFromEndedSession(handle)) return false;
const MG_Pipe::MGPHandleOnly request{QueryDesc(handle).Query, static_cast<Uint32>(MG_Pipe::MGPipeKind::Query), 0};
const auto result = ReadQueryReply<Uint32>(session, MG_Pipe::MGPWireOp::QueryAvailable, &request, sizeof(request));
if (result > 1) Wire::WireProtocolFatal("QueryAvailable.reply", "availability is not boolean");
@@ -84,6 +97,7 @@
Bool EmitGetQueryResult64(MG_Backend::BackendQueryHandle handle, Bool wait, Uint64* value) {
auto& session = RequireSession("GetQueryResult64");
const std::lock_guard<std::mutex> lock(g_queryProxyMutex);
if (QueryFromEndedSession(handle)) return false;
MG_Pipe::MGPQueryResultRequest request{};
request.Query = QueryDesc(handle).Query;
request.Wait = wait ? 1 : 0;
@@ -98,7 +112,7 @@
void EmitDeleteBackendQuery(MG_Backend::BackendQueryHandle handle) {
const std::lock_guard<std::mutex> lock(g_queryProxyMutex);
const auto query = QueryDesc(handle).Query;
if (auto* session = ClientSession::Active(); session && session->Started()) {
if (auto* session = ClientSession::Active(); session && session->Started() && !QueryFromEndedSession(handle)) {
const MG_Pipe::MGPHandleOnly request{query, static_cast<Uint32>(MG_Pipe::MGPipeKind::Query), 0};
session->EmitAndWait(MG_Pipe::MGPWireOp::QueryDestroy, &request, sizeof(request),
nullptr, 0, nullptr, 0, nullptr);
+24 -6
View File
@@ -1160,10 +1160,13 @@ namespace MobileGL::MG_Remote::Client {
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) return false;
MG_Pipe::MGPBindContext record{};
record.ClientContextToken = clientContextToken;
session->EmitAndWait(MGPWireOp::BindContext, &record, sizeof(record), nullptr, 0, nullptr, 0,
nullptr);
const Uint64 seq = session->EmitAndWait(MGPWireOp::BindContext, &record, sizeof(record), nullptr, 0,
nullptr, 0, nullptr);
++g_emitted;
return true;
// A DECLINED binding moved nothing on the server (a lost session, or a thread whose
// context is from an ended one), so the caller must not believe the session is bound to
// it: the next thread that writes re-binds its own context.
return seq != Wire::kInvalidSeq;
}
Bool EmitApplierResetRecord() {
@@ -1177,13 +1180,28 @@ namespace MobileGL::MG_Remote::Client {
if (session == nullptr || !session->Started()) return false;
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) return false;
MG_Pipe::MGPApplierReset record{};
record.ContextSerial = g_applierResetContextSerial++;
session->EmitAndWait(MGPWireOp::ApplierReset, &record, sizeof(record), nullptr, 0, nullptr,
0, nullptr);
record.ContextSerial = g_applierResetContextSerial;
const Uint64 seq = session->EmitAndWait(MGPWireOp::ApplierReset, &record, sizeof(record), nullptr, 0,
nullptr, 0, nullptr);
// THE SERIAL COUNTS RESETS THE SERVER WAS SENT, not resets this client primed: a declined
// record (a lost session, a thread on an ended session's context) never reached the
// server's count, and moving ours past it would latch the next session that hears one.
if (seq != Wire::kInvalidSeq) ++g_applierResetContextSerial;
++g_emitted;
return true;
}
void ForgetWireTablesOfEndedSession() {
// The ended session's deferred create answers are not coming: their seqs name a ring
// that is gone, and a drain on the new session would wait for them (or read another
// record's answer). The objects behind them are the ended session's too.
g_createWindowHead = 0;
g_createWindowCount = 0;
g_createSuspects.clear();
// A fresh session's server counts applier resets from 0.
g_applierResetContextSerial = 0;
}
// P5c (rv), CONTRACT-P5C.md §5.3. Whether set_context_values can cross RIGHT NOW: a live,
// started session whose tables are not being torn down. PipeFill.cpp gates BOTH halves of
// the row on this - the emission and the residual-fill skip - so a configured-but-wireless
+5
View File
@@ -125,6 +125,11 @@ namespace MobileGL::MG_Remote::Client {
// caller then makes the direct call the record replaced.
Bool EmitApplierResetRecord();
// A fresh session replaces an ended one: the create window's deferred answers, its suspect
// list and the applier-reset serial all belonged to the ended session (ClientSession's
// ForgetEndedSession).
void ForgetWireTablesOfEndedSession();
// ---- P14 S1: the EGL context lifecycle's control-plane half -----------------------------
//
// NOT ring records and NOT routed rows: eglCreateContext/eglDestroyContext are the EGL
+5
View File
@@ -249,6 +249,11 @@ namespace MobileGL {
// context was made current on it.
if (currentIt->second.RestoreThreadGLState) {
pGLContext = Move(currentIt->second.RestoreThreadGLState);
#if MOBILEGL_BUILD_DISAGGREGATED
// A fallback built against a server session that has since ended (a device
// loss, recovered) is lost with it: the thread falls back to today's default.
if (pGLContext->IsFromEndedWireSession()) pGLContext = ProcessDefaultGLContext();
#endif
}
m_threadCurrents.erase(currentIt);
DestroyPendingSurfaceIfUnused(drawSurface);
+25
View File
@@ -29,8 +29,27 @@ namespace MobileGL::MG_State {
static SharedPtr<GLState::FramebufferObject>* slot = new SharedPtr<GLState::FramebufferObject>();
return *slot;
}
#if MOBILEGL_BUILD_DISAGGREGATED
std::atomic<Uint64> g_wireEpoch{0};
#endif
}
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 CurrentWireEpoch() { return g_wireEpoch.load(std::memory_order_acquire); }
void AdvanceWireEpoch() { g_wireEpoch.fetch_add(1, std::memory_order_acq_rel); }
SharedPtr<GLState::GLContext> ReplaceProcessDefaultGLContext() {
auto& slot = DefaultGLContextSlot();
SharedPtr<GLState::GLContext> old = slot;
const Bool callerOnOld = old && pGLContext == old;
DefaultFramebufferSlot().reset();
slot = MakeShared<GLState::GLContext>();
if (callerOnOld || !pGLContext) pGLContext = slot;
return old;
}
#endif
const SharedPtr<GLState::GLContext>& ProcessDefaultGLContext() {
auto& slot = DefaultGLContextSlot();
if (!slot) {
@@ -1354,6 +1373,12 @@ namespace MobileGL::MG_State {
return nextId.fetch_add(1, std::memory_order_relaxed);
}
#if MOBILEGL_BUILD_DISAGGREGATED
Bool GLContext::IsFromEndedWireSession() const {
return m_wireEpoch != 0 && m_wireEpoch != CurrentWireEpoch();
}
#endif
GLContext::GLContext() : GLContext(MakeShared<ShareGroupState>()) {}
GLContext::GLContext(SharedPtr<ShareGroupState> shareGroup)
+40
View File
@@ -30,6 +30,16 @@ namespace MobileGL {
namespace MG_State {
void Init();
#if MOBILEGL_BUILD_DISAGGREGATED
// THE SERVER SESSION GENERATION. A split client advances it each time a session with a
// server comes up; every GLContext records the value it was built under. A device loss
// ends a session, and once a fresh one replaces it every context built before is lost:
// its objects exist only on the dead session's server. 0 is "no session yet" (a monolith
// run, or a context built before the first bring-up), which no later session ends.
Uint64 CurrentWireEpoch();
void AdvanceWireEpoch();
#endif
namespace GLState {
#if MOBILEGL_PIPE_PUSH
// MGPAttribValue::ValueClass' encoding (MG_Pipe/MGPipeTypes.h documents the order
@@ -91,6 +101,22 @@ namespace MobileGL {
// its table entry and the Draw/Read bindings the context starts with.
void InstallDefaultFramebuffer(const SharedPtr<FramebufferObject>& framebuffer);
#if MOBILEGL_BUILD_DISAGGREGATED
// The server session this context was built against (CurrentWireEpoch() at its
// construction). Its objects live on that session's server and nowhere else, so
// once a later session has replaced it - after a device loss - the context is
// lost for good: nothing it holds may reach the new session.
Uint64 WireEpoch() const { return m_wireEpoch; }
Bool IsFromEndedWireSession() const;
// GL_CONTEXT_LOST is reported by glGetError once per lost context, then the
// error queue is clear again (a caller draining errors in a loop terminates).
Bool TakeContextLostReport() {
if (m_contextLostReported) return false;
m_contextLostReported = true;
return true;
}
#endif
// Error
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
Bool HasGLError() const;
@@ -713,6 +739,11 @@ namespace MobileGL {
// Identity of the backend object m_compileEnv was captured against; a plain
// pointer compare, never dereferenced.
const void* m_compileEnvBackend = nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// See WireEpoch(); taken when the context is constructed.
Uint64 m_wireEpoch = CurrentWireEpoch();
Bool m_contextLostReported = false;
#endif
};
} // namespace GLState
@@ -735,6 +766,15 @@ namespace MobileGL {
// glslang::FinalizeProcess() through a thread that kept its fallback binding.
void ReleaseProcessDefaultGLContext();
#if MOBILEGL_BUILD_DISAGGREGATED
// A fresh process default context, after a device loss has ended the session the old one
// was built against; the default framebuffer slot is emptied for MG_Impl to rebuild. The
// calling thread moves to the new context if it was on the old one. Other threads keep
// the old (lost) one until their next EGL release edge, which restores to this one.
// Returns the old one, so the caller decides where its last reference goes.
SharedPtr<GLState::GLContext> ReplaceProcessDefaultGLContext();
#endif
// P14 S3. GL framebuffer 0, built by MG_Impl once the backend is up (it needs texture
// objects, which need the backend's resource plumbing) and handed back here so every
// GLContext a later eglCreateContext builds starts with the same default framebuffer
+6 -1
View File
@@ -76,6 +76,10 @@ if (NOT WIN32)
# live at once, mint the same names without colliding, and one closing leaves the other serving.
# It forks its own server and its own clients, so it needs no server image.
list(APPEND MOBILEGL_WIRE_TESTS MultiSessionTest)
# A client outlives its lost session: after an injected device loss a real EGL/GL client sees
# the loss reported, creates new contexts on a FRESH session and renders. Forks the in-process
# display server and its client, like MultiSessionTest, so it needs no server image either.
list(APPEND MOBILEGL_WIRE_TESTS ClientRecoveryTest)
# P11 A1: MOBILEGL_IPC_ADOPT_TIER=0/1 on a stream data plane, against a real `--serve`
# supervisor over loopback TCP (P12ServerRig.h): refused by name once per session on the side
# that names it, map_persistent DECLINED, never a Fatal. Needs the server image too.
@@ -103,7 +107,8 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS)
)
if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR
test STREQUAL "EglInfoSequenceTest" OR
test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest" OR test STREQUAL "ServerProbeTest")
test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest" OR test STREQUAL "ServerProbeTest" OR
test STREQUAL "ClientRecoveryTest")
# It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State
# the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy
# session, P12ServerRig.h, and so its include paths.)
@@ -0,0 +1,694 @@
// MobileGL - MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp
// Copyright (c) 2025-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
// A CLIENT OUTLIVES ITS LOST SESSION. A GPU device loss ends exactly one session on the server
// (MultiSessionTest.ADeviceLossEndsOnlyItsOwnSession); this suite is the client's half. A real
// MobileGL client - the EGL and GL entry points, MobileGL's own initialisation, a unix spawn
// session - against the in-process display server, whose debug knob makes the second device check
// in the server process (this client's second read-back) report the device lost.
//
// What an application that handles context loss (Chrome through ANGLE) needs, each case asserting
// it from the application's side:
// * the loss is REPORTED: glGetGraphicsResetStatus answers a reset, glGetError GL_CONTEXT_LOST
// (once), eglSwapBuffers and eglMakeCurrent on the old context fail with EGL_CONTEXT_LOST;
// * the old context is SAFE TO USE AND TO DESTROY: a draw and a texture upload on it after the
// loss do nothing, and destroying it puts nothing on the new session's wire;
// * a context created after the loss gets a FRESH session (the server starts session #2, nothing
// latches again) and renders: a textured draw through a new program, a clear, and a texture
// framebuffer, each read back;
// * nothing of the lost session reaches the fresh one: no record names a handle it never created
// (the server's per-session refusal line stays silent), and the old texture's name is gone;
// * the surface the application kept works with the new context (EGL keeps surfaces across a
// power-management loss; only their contents go);
// * the other route back - eglTerminate, then eglInitialize - brings a fresh session up too.
#include "P12ServerRig.h"
#include <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Impl/Pipe/TextureEmit.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_State/EGLState/Core.h>
#include <condition_variable>
#include <mutex>
extern "C" {
void glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
void glClear(GLbitfield mask);
void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
GLenum glGetError(void);
GLenum glGetGraphicsResetStatus(void);
void glGenTextures(GLsizei n, GLuint* textures);
void glBindTexture(GLenum target, GLuint texture);
void glActiveTexture(GLenum texture);
void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border,
GLenum format, GLenum type, const void* pixels);
void glTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height,
GLenum format, GLenum type, const void* pixels);
void glTexParameteri(GLenum target, GLenum pname, GLint param);
GLboolean glIsTexture(GLuint texture);
void glGenFramebuffers(GLsizei n, GLuint* framebuffers);
void glBindFramebuffer(GLenum target, GLuint framebuffer);
void glFramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
GLenum glCheckFramebufferStatus(GLenum target);
GLuint glCreateShader(GLenum type);
void glShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length);
void glCompileShader(GLuint shader);
void glGetShaderiv(GLuint shader, GLenum pname, GLint* params);
GLuint glCreateProgram(void);
void glAttachShader(GLuint program, GLuint shader);
void glLinkProgram(GLuint program);
void glGetProgramiv(GLuint program, GLenum pname, GLint* params);
void glUseProgram(GLuint program);
GLint glGetUniformLocation(GLuint program, const GLchar* name);
void glUniform1i(GLint location, GLint v0);
void glGenVertexArrays(GLsizei n, GLuint* arrays);
void glBindVertexArray(GLuint array);
void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
}
using namespace MobileGL;
using namespace P12;
namespace {
namespace EGL = MobileGL::MG_Impl::EGLImpl;
// A<<24|B<<16|G<<8|R, the little-endian bytes of a GL_RGBA/GL_UNSIGNED_BYTE texel.
constexpr Uint32 kGreen = 0xFF00FF00u;
constexpr Uint32 kBlue = 0xFFFF0000u;
constexpr Uint32 kRed = 0xFF0000FFu;
constexpr Uint32 kYellow = 0xFF00FFFFu;
constexpr Uint32 kWhite = 0xFFFFFFFFu;
constexpr Uint32 kUntouched = 0x5A5A5A5Au;
enum class Route {
NewContext, // destroy the lost context, create another, keep the surface
TerminateInitialize, // destroy everything, eglTerminate, eglInitialize, start over
};
struct RecoveryReport {
Int32 initialized = 0;
Uint32 firstPixel = 0;
Uint32 lostPixel = 0;
Uint32 resetStatusLost = 0;
Uint32 errorLost = 0;
Uint32 errorAfterLost = 0;
Int32 swapLost = -1;
Int32 swapLostError = 0;
Int32 releaseOk = 0;
Int32 oldCurrent = -1;
Int32 oldCurrentError = 0;
Int32 destroyOldOk = 0;
Int64 recordsByOldDestroy = -1;
Int32 newContextOk = 0;
Int32 newContextError = 0;
Int32 newCurrentOk = 0;
Int32 newCurrentError = 0;
Uint32 resetStatusNew = 0xFFFFFFFFu;
Int32 oldNameIsTexture = -1;
// What the client believes about the lost context's texture: whether the server has its
// record (the publication latch) and whether an upload is still owed to it (the drain).
Int32 oldTexturePublishedBefore = -1;
Int32 oldTextureOwedBefore = -1;
Int32 oldTexturePublishedAfter = -1;
Int32 oldTextureOwedAfter = -1;
Uint32 drawnPixel = 0;
Uint32 secondPixel = 0;
Uint32 fboStatus = 0;
Uint32 fboPixel = 0;
Int32 swapNew = -1;
Uint32 resetStatusEnd = 0xFFFFFFFFu;
Uint32 serverPidBefore = 0;
Uint32 serverPidAfter = 0;
char note[256] = {};
};
void Note(RecoveryReport& r, const char* text) {
if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text);
}
Uint32 ReadPixel() {
Uint32 pixel = kUntouched;
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
return pixel;
}
Uint32 ClearAndRead(GLfloat red, GLfloat green, GLfloat blue) {
glClearColor(red, green, blue, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
return ReadPixel();
}
// A full-surface triangle that samples one texel: a program, a vertex array, a texture with its
// sampler parameters and a draw - the shader, sampler, texture-upload and render-state families.
struct TexturedDraw {
GLuint program = 0;
GLuint vertexArray = 0;
GLuint texture = 0;
Bool Build(RecoveryReport& r, Uint32 rgba) {
static const char* kVertex = "#version 330 core\n"
"const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));\n"
"void main() { gl_Position = vec4(corners[gl_VertexID], 0.0, 1.0); }\n";
static const char* kFragment = "#version 330 core\n"
"uniform sampler2D image;\n"
"out vec4 color;\n"
"void main() { color = texture(image, vec2(0.5)); }\n";
const GLuint vertex = glCreateShader(GL_VERTEX_SHADER);
const GLuint fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(vertex, 1, &kVertex, nullptr);
glShaderSource(fragment, 1, &kFragment, nullptr);
glCompileShader(vertex);
glCompileShader(fragment);
GLint compiled = 0;
glGetShaderiv(vertex, GL_COMPILE_STATUS, &compiled);
if (!compiled) return Note(r, "the vertex shader did not compile"), false;
glGetShaderiv(fragment, GL_COMPILE_STATUS, &compiled);
if (!compiled) return Note(r, "the fragment shader did not compile"), false;
program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked) return Note(r, "the program did not link"), false;
glGenVertexArrays(1, &vertexArray);
glGenTextures(1, &texture);
Fill(rgba);
return true;
}
void Fill(Uint32 rgba) const {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &rgba);
}
void Draw() const {
glViewport(0, 0, 8, 8);
glUseProgram(program);
glUniform1i(glGetUniformLocation(program, "image"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(vertexArray);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
};
Uint32 ServerPid() {
auto* session = Remote::Client::ClientSession::Active();
return session != nullptr ? session->PeerServerPid() : 0;
}
// The wire handle of texture `name` of `context`, through the client's own books.
MobileGL::MG_Pipe::MGPipeHandle TextureHandle(EGLContext context, GLuint name) {
const auto glState = MobileGL::MG_State::pEGLContext->GetContextGLState(context);
if (!glState) return MobileGL::MG_Pipe::kMGPipeNullHandle;
const auto& texture = glState->GetTextureObject(name);
if (!texture) return MobileGL::MG_Pipe::kMGPipeNullHandle;
return MobileGL::MG_Pipe::MGPipeSlots().FindByLifetimeId(MobileGL::MG_Pipe::MGPipeKind::Texture,
texture->GetLifetimeId());
}
void NoteTextureBooks(MobileGL::MG_Pipe::MGPipeHandle handle, Int32* published, Int32* owed) {
using namespace MobileGL::MG_Pipe;
*published = MGPipeHandleIsPublished(MGPipeKind::Texture, handle) ? 1 : 0;
*owed = MGPipeTextureEmitterInstance().OwesUpload(handle) ? 1 : 0;
}
Uint64 PublishedSeq() {
auto* session = Remote::Client::ClientSession::Active();
return session != nullptr ? session->LastPublishedSeq() : 0;
}
struct Egl {
EGLDisplay dpy = EGL_NO_DISPLAY;
EGLConfig config = nullptr;
EGLSurface surface = EGL_NO_SURFACE;
Bool Bring(RecoveryReport& r) {
dpy = EGL::GetDisplay(EGL_DEFAULT_DISPLAY);
EGLint major = 0;
EGLint minor = 0;
if (dpy == EGL_NO_DISPLAY || EGL::Initialize(dpy, &major, &minor) != EGL_TRUE)
return Note(r, "eglInitialize failed (no spawn session?)"), false;
(void)EGL::BindAPI(EGL_OPENGL_API);
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_NONE};
EGLint count = 0;
if (EGL::ChooseConfig(dpy, configAttribs, &config, 1, &count) != EGL_TRUE || count < 1)
return Note(r, "no config"), false;
const EGLint pbufferAttribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE};
surface = EGL::CreatePbufferSurface(dpy, config, pbufferAttribs);
if (surface == EGL_NO_SURFACE) return Note(r, "no pbuffer"), false;
return true;
}
EGLContext Context() const {
const EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE};
return EGL::CreateContext(dpy, config, EGL_NO_CONTEXT, attribs);
}
};
// The client, in a fork: everything an application does around a loss, through the entry points.
void RunRecoveringClient(Route route, RecoveryReport& r) {
Egl egl;
if (!egl.Bring(r)) return;
const EGLContext lost = egl.Context();
if (lost == EGL_NO_CONTEXT || EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, lost) != EGL_TRUE)
return Note(r, "the first context did not go current");
r.initialized = 1;
r.serverPidBefore = ServerPid();
// The objects of the context that is about to be lost, and device check #1 in the server
// process: a textured draw, read back.
TexturedDraw old;
if (!old.Build(r, kGreen)) return;
old.Draw();
r.firstPixel = ReadPixel();
// Device check #2: the device is "lost" there. The session latches, the server ends it.
r.lostPixel = ClearAndRead(0.0f, 0.0f, 1.0f);
(void)WaitFor([] { return glGetGraphicsResetStatus() != GL_NO_ERROR; }, 5000);
// THE LOSS, REPORTED.
r.resetStatusLost = glGetGraphicsResetStatus();
r.errorLost = glGetError();
r.errorAfterLost = glGetError();
// What an application does before it has looked: more work on the lost context - a draw,
// and then an upload that no later verb of the lost context will ever carry (it is owed to
// a server that is gone). None of it may surface on the fresh session.
old.Draw();
const Uint32 white = kWhite;
glBindTexture(GL_TEXTURE_2D, old.texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &white);
const auto oldHandle = TextureHandle(lost, old.texture);
NoteTextureBooks(oldHandle, &r.oldTexturePublishedBefore, &r.oldTextureOwedBefore);
r.swapLost = EGL::SwapBuffers(egl.dpy, egl.surface);
r.swapLostError = EGL::GetError();
r.releaseOk = EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) == EGL_TRUE;
r.oldCurrent = EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, lost);
r.oldCurrentError = EGL::GetError();
if (r.oldCurrent == EGL_TRUE) (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
EGLContext fresh = EGL_NO_CONTEXT;
if (route == Route::NewContext) {
// A NEW CONTEXT FIRST (Chrome's order: the replacement exists before the old one goes),
// then the old one is destroyed - which must put nothing on the new session's wire.
fresh = egl.Context();
r.newContextOk = fresh != EGL_NO_CONTEXT;
r.newContextError = EGL::GetError();
// The fresh session is up: the lost texture is not on its server, and nothing is owed.
NoteTextureBooks(oldHandle, &r.oldTexturePublishedAfter, &r.oldTextureOwedAfter);
const Uint64 before = PublishedSeq();
r.destroyOldOk = EGL::DestroyContext(egl.dpy, lost) == EGL_TRUE;
r.recordsByOldDestroy = static_cast<Int64>(PublishedSeq()) - static_cast<Int64>(before);
} else {
r.destroyOldOk = EGL::DestroyContext(egl.dpy, lost) == EGL_TRUE;
(void)EGL::DestroySurface(egl.dpy, egl.surface);
(void)EGL::Terminate(egl.dpy);
Egl again;
if (!again.Bring(r)) return;
egl = again;
NoteTextureBooks(oldHandle, &r.oldTexturePublishedAfter, &r.oldTextureOwedAfter);
fresh = egl.Context();
r.newContextOk = fresh != EGL_NO_CONTEXT;
r.newContextError = EGL::GetError();
}
if (fresh == EGL_NO_CONTEXT) return Note(r, "no context after the loss");
r.newCurrentOk = EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, fresh) == EGL_TRUE;
r.newCurrentError = EGL::GetError();
if (!r.newCurrentOk) return Note(r, "the new context did not go current");
r.serverPidAfter = ServerPid();
r.resetStatusNew = glGetGraphicsResetStatus();
r.oldNameIsTexture = glIsTexture(old.texture) == GL_TRUE ? 1 : 0;
// THE NEW CONTEXT RENDERS: the same textured draw with a new program and texture, a clear of
// the surface's own framebuffer, then a texture-backed framebuffer.
TexturedDraw fresher;
if (!fresher.Build(r, kRed)) return;
fresher.Draw();
r.drawnPixel = ReadPixel();
r.secondPixel = ClearAndRead(0.0f, 0.0f, 1.0f);
GLuint target = 0;
GLuint framebuffer = 0;
glGenTextures(1, &target);
glBindTexture(GL_TEXTURE_2D, target);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target, 0);
r.fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
r.fboPixel = ClearAndRead(1.0f, 1.0f, 0.0f);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
r.swapNew = EGL::SwapBuffers(egl.dpy, egl.surface);
r.resetStatusEnd = glGetGraphicsResetStatus();
(void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
(void)EGL::DestroyContext(egl.dpy, fresh);
(void)EGL::DestroySurface(egl.dpy, egl.surface);
(void)EGL::Terminate(egl.dpy);
}
// A thread of the client that runs what it is handed, one job at a time, and keeps whatever
// EGL context it made current between jobs - an application's other GL thread.
class Worker {
public:
Worker() : m_thread([this] { Loop(); }) {}
~Worker() {
{
const std::lock_guard<std::mutex> lock(m_mutex);
m_quit = true;
}
m_wake.notify_all();
m_thread.join();
}
void Run(const std::function<void()>& job) {
std::unique_lock<std::mutex> lock(m_mutex);
m_job = job;
m_wake.notify_all();
m_wake.wait(lock, [this] { return !m_job; });
}
private:
void Loop() {
std::unique_lock<std::mutex> lock(m_mutex);
for (;;) {
m_wake.wait(lock, [this] { return m_quit || static_cast<bool>(m_job); });
if (m_quit) return;
lock.unlock();
m_job();
lock.lock();
m_job = nullptr;
m_wake.notify_all();
}
}
std::mutex m_mutex;
std::condition_variable m_wake;
std::function<void()> m_job;
Bool m_quit = false;
std::thread m_thread;
};
// THE OTHER THREAD. The context that is lost stays current on a second thread of the
// application while its main thread recovers and renders on a fresh session; then the second
// thread - which has not looked yet - draws, uploads and reads on the lost context. All of it
// must be refused as lost (reset status, nothing read back) and none of it may reach the fresh
// session: no record published while it runs, no latch on the server (a binding that named the
// lost context's token there would be Fatal{ProtocolCorruption}), and the main thread's
// rendering is unaffected afterwards.
struct StaleThreadReport {
Int32 initialized = 0;
Uint32 firstPixel = 0;
Int32 newCurrentOk = 0;
Uint32 drawnPixel = 0;
Uint32 staleResetStatus = 0;
Uint32 stalePixel = 0;
Int32 staleSwap = -1;
Int32 staleSwapError = 0;
Int32 staleRelease = 0;
Int64 recordsByStaleThread = -1;
Uint32 pixelAfterStale = 0;
Uint32 resetStatusAfterStale = 0xFFFFFFFFu;
Int32 destroyOldOk = 0;
char note[256] = {};
};
void Note(StaleThreadReport& r, const char* text) {
if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text);
}
void RunStaleThreadClient(StaleThreadReport& r) {
Egl egl;
RecoveryReport bring{};
if (!egl.Bring(bring)) return Note(r, bring.note);
const EGLContext lost = egl.Context();
Worker second;
TexturedDraw old;
second.Run([&] {
if (lost == EGL_NO_CONTEXT || EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, lost) != EGL_TRUE)
return Note(r, "the first context did not go current on the second thread");
RecoveryReport built{};
if (!old.Build(built, kGreen)) return Note(r, built.note);
r.initialized = 1;
old.Draw();
r.firstPixel = ReadPixel(); // device check #1
(void)ClearAndRead(0.0f, 0.0f, 1.0f); // device check #2: lost
(void)WaitFor([] { return glGetGraphicsResetStatus() != GL_NO_ERROR; }, 5000);
});
if (!r.initialized) return;
// The main thread recovers: a surface and a context of its own, on a fresh session.
const EGLint pbufferAttribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE};
const EGLSurface mine = EGL::CreatePbufferSurface(egl.dpy, egl.config, pbufferAttribs);
const EGLContext fresh = egl.Context();
r.newCurrentOk = fresh != EGL_NO_CONTEXT && mine != EGL_NO_SURFACE &&
EGL::MakeCurrent(egl.dpy, mine, mine, fresh) == EGL_TRUE;
if (!r.newCurrentOk) return Note(r, "no fresh context went current on the main thread");
TexturedDraw fresher;
RecoveryReport built{};
if (!fresher.Build(built, kRed)) return Note(r, built.note);
fresher.Draw();
r.drawnPixel = ReadPixel();
// The second thread, still on the lost context, gets to it now.
const Uint64 before = PublishedSeq();
second.Run([&] {
r.staleResetStatus = glGetGraphicsResetStatus();
old.Draw();
old.Fill(kWhite);
r.stalePixel = ClearAndRead(1.0f, 1.0f, 1.0f);
r.staleSwap = EGL::SwapBuffers(egl.dpy, egl.surface);
r.staleSwapError = EGL::GetError();
r.staleRelease = EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) == EGL_TRUE;
});
r.recordsByStaleThread = static_cast<Int64>(PublishedSeq()) - static_cast<Int64>(before);
r.destroyOldOk = EGL::DestroyContext(egl.dpy, lost) == EGL_TRUE;
// The main thread renders on, unaffected.
fresher.Draw();
r.pixelAfterStale = ReadPixel();
r.resetStatusAfterStale = glGetGraphicsResetStatus();
(void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
(void)EGL::DestroyContext(egl.dpy, fresh);
(void)EGL::DestroySurface(egl.dpy, mine);
(void)EGL::DestroySurface(egl.dpy, egl.surface);
(void)EGL::Terminate(egl.dpy);
}
// Runs `body` in a forked client (the environment a deployment sets, then the entry points)
// and hands back the report it filled.
template <class Report, class Body>
Report RunInClient(const ServerProcess& server, const std::string& clientLogBase, Body body) {
Report report{};
int fds[2] = {-1, -1};
if (::pipe(fds) != 0) return report;
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid == 0) {
::close(fds[0]);
::setenv("MOBILEGL_LOG_FILE_PATH", clientLogBase.c_str(), 1);
::setenv("MOBILEGL_TRANSPORT", "spawn", 1);
::setenv("MOBILEGL_IPC_CONTROL", server.endpoint.c_str(), 1);
::unsetenv("MOBILEGL_IPC_SURFACE");
::unsetenv("MOBILEGL_BACKEND_TYPE");
::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_AT");
Report r{};
body(r);
const ssize_t wrote = ::write(fds[1], &r, sizeof(r));
(void)wrote;
std::fflush(nullptr);
::_exit(0);
}
::close(fds[1]);
if (pid < 0) {
::close(fds[0]);
return report;
}
pollfd pfd{fds[0], POLLIN, 0};
if (::poll(&pfd, 1, 90000) > 0) {
if (::read(fds[0], &report, sizeof(report)) != static_cast<ssize_t>(sizeof(report))) {
report = Report{};
Note(report, "the client died before reporting");
}
} else {
Note(report, "the client timed out");
::kill(pid, SIGKILL);
}
::close(fds[0]);
int status = 0;
::waitpid(pid, &status, 0);
return report;
}
RecoveryReport RunClient(const ServerProcess& server, Route route, const std::string& clientLogBase) {
return RunInClient<RecoveryReport>(server, clientLogBase,
[route](RecoveryReport& r) { RunRecoveringClient(route, r); });
}
struct ClientLog {
std::string base;
explicit ClientLog(const std::string& label)
: base("/tmp/mgl-recover-client-" + label + "-" + std::to_string(::getpid()) + ".log") {
Debug::TruncateRoleLogs(base.c_str());
}
std::string Text() const { return ReadFile(Debug::RoleLogPath(base.c_str(), Debug::LogRole::Client)); }
~ClientLog() {
if (::testing::Test::HasFailure() || std::getenv("MOBILEGL_TEST_KEEP_LOGS") != nullptr) return;
std::error_code ec;
std::filesystem::remove(Debug::RoleLogPath(base.c_str(), Debug::LogRole::Client), ec);
std::filesystem::remove(Debug::RoleLogPath(base.c_str(), Debug::LogRole::Server), ec);
std::filesystem::remove(base, ec);
}
};
class ClientRecovery : public ::testing::Test {
protected:
void SetUp() override { ::signal(SIGPIPE, SIG_IGN); }
// The server, with the loss armed for the second device check in its process.
bool Launch(ServerProcess* server, const std::string& label) {
::setenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_AT", "2", 1);
const bool launched = LaunchUnixInProcessServer(server, label);
::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_AT");
return launched;
}
// What every route has to show from the application's side, before and after the loss.
static void ExpectLossReportedAndRecovered(const RecoveryReport& r, const std::string& diagnostics) {
ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics;
ASSERT_EQ(r.firstPixel, kGreen) << "the first draw (device check #1) did not read back: " << std::hex
<< r.firstPixel << "\n" << r.note << "\n" << diagnostics;
EXPECT_NE(r.lostPixel, kBlue) << "a read-back on the lost device returned a frame";
// The loss is reported.
EXPECT_TRUE(r.resetStatusLost == GL_UNKNOWN_CONTEXT_RESET || r.resetStatusLost == GL_GUILTY_CONTEXT_RESET)
<< "glGetGraphicsResetStatus after the loss: 0x" << std::hex << r.resetStatusLost;
EXPECT_EQ(r.errorLost, static_cast<Uint32>(GL_CONTEXT_LOST))
<< "glGetError after the loss: 0x" << std::hex << r.errorLost;
EXPECT_EQ(r.errorAfterLost, static_cast<Uint32>(GL_NO_ERROR))
<< "GL_CONTEXT_LOST must be reported once, not forever (a drain loop must end)";
EXPECT_EQ(r.swapLost, EGL_FALSE) << "eglSwapBuffers on the lost context succeeded";
EXPECT_EQ(r.swapLostError, EGL_CONTEXT_LOST) << "eglSwapBuffers on the lost context: 0x" << std::hex
<< r.swapLostError;
EXPECT_EQ(r.releaseOk, 1) << "releasing the lost context failed";
EXPECT_EQ(r.oldCurrent, EGL_FALSE) << "the lost context was made current again";
EXPECT_EQ(r.oldCurrentError, EGL_CONTEXT_LOST) << "eglMakeCurrent of the lost context: 0x" << std::hex
<< r.oldCurrentError;
EXPECT_EQ(r.destroyOldOk, 1) << "the lost context could not be destroyed";
// A fresh session, and the new context renders.
ASSERT_EQ(r.newContextOk, 1) << "no context could be created after the loss (EGL error 0x" << std::hex
<< r.newContextError << ")\n" << r.note << "\n" << diagnostics;
ASSERT_EQ(r.newCurrentOk, 1) << "the new context did not go current (EGL error 0x" << std::hex
<< r.newCurrentError << ")\n" << r.note << "\n" << diagnostics;
EXPECT_EQ(r.serverPidAfter, r.serverPidBefore) << "the new session is not on the same server process";
EXPECT_EQ(r.resetStatusNew, static_cast<Uint32>(GL_NO_ERROR)) << "the new context reports a reset";
EXPECT_EQ(r.oldNameIsTexture, 0) << "the old context's texture name resolves in the new context";
// The client's books on the lost texture: before the recovery the lost session had its
// record (the arm proof that there was something to forget), and the upload made on the
// lost context was queued for nobody; after it, neither - its death and its texels must
// not reach the fresh server.
EXPECT_EQ(r.oldTexturePublishedBefore, 1) << "the lost texture was never published (nothing to forget)";
EXPECT_EQ(r.oldTextureOwedBefore, 0) << "an upload on the lost context was queued; the next context's "
"verb would carry it to whatever session is current";
EXPECT_EQ(r.oldTexturePublishedAfter, 0) << "the fresh session's client still believes it holds the "
"lost texture's record";
EXPECT_EQ(r.oldTextureOwedAfter, 0) << "an upload owed to the lost session is queued for the fresh one";
EXPECT_EQ(r.drawnPixel, kRed) << "the new context's textured draw did not read back: " << std::hex
<< r.drawnPixel << "\n" << r.note << "\n" << diagnostics;
EXPECT_EQ(r.secondPixel, kBlue) << "the new context's clear did not read back: " << std::hex
<< r.secondPixel;
EXPECT_EQ(r.fboStatus, static_cast<Uint32>(GL_FRAMEBUFFER_COMPLETE)) << std::hex << r.fboStatus;
EXPECT_EQ(r.fboPixel, kYellow) << "the new context's texture framebuffer did not read back: " << std::hex
<< r.fboPixel;
EXPECT_EQ(r.swapNew, EGL_TRUE) << "eglSwapBuffers on the new context failed";
EXPECT_EQ(r.resetStatusEnd, static_cast<Uint32>(GL_NO_ERROR)) << "the new session was lost too";
}
// The server's side: the first session latched on the injected loss and nothing else did; a
// second session started and ended cleanly; nothing from the first reached it.
static void ExpectOneLossAndACleanSecondSession(ServerProcess& server) {
ASSERT_TRUE(WaitFor([&] { return Count(server.Log(), "in-process unix session #2 started") == 1; }, 5000))
<< "the client never opened a second session:\n" << server.Log();
// Both sessions' ends: the lost one's latched exit and the fresh one's clean one.
ASSERT_TRUE(WaitFor([&] { return Count(server.Log(), "reaped exit=") == 2; }, 10000))
<< "the second session did not end:\n" << server.Log();
const std::string log = server.Log();
EXPECT_NE(log.find("SessionLatch{BackendDeviceLost}"), std::string::npos) << log;
EXPECT_EQ(Count(log, "reaped exit=75"), 1u) << "the lost session did not end latched:\n" << log;
EXPECT_EQ(Count(log, "reaped exit=0"), 1u) << "the fresh session did not end cleanly:\n" << log;
// A fresh session latches the moment the client carries anything of the lost one it
// checks: the applier-reset serial it counts from 0, a context binding naming a token it
// never created, a create or bind it cannot parse - each Fatal{ProtocolCorruption}.
EXPECT_EQ(Count(log, "SessionLatch{"), 1u) << "the recovered session latched too:\n" << log;
EXPECT_EQ(log.find("Fatal{ProtocolCorruption"), std::string::npos)
<< "the recovered session was sent something it never created:\n" << log;
EXPECT_TRUE(server.pid > 0 && ::kill(server.pid, 0) == 0) << "the server died";
}
};
} // namespace
// THE HEADLINE: Chrome's route. The lost context is reported lost, a new one is created on a fresh
// session and renders into the surface the application kept, and destroying the old one is silent.
TEST_F(ClientRecovery, ANewContextAfterADeviceLossRendersOnAFreshSession) {
ServerProcess server;
ASSERT_TRUE(Launch(&server, "newctx")) << server.Log();
ClientLog client("newctx");
const RecoveryReport r = RunClient(server, Route::NewContext, client.base);
const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text();
ExpectLossReportedAndRecovered(r, diagnostics);
EXPECT_EQ(r.recordsByOldDestroy, 0) << "destroying the lost context published records on the new session";
ExpectOneLossAndACleanSecondSession(server);
server.Stop();
}
// The other route back: everything destroyed, eglTerminate, eglInitialize, and a fresh start.
TEST_F(ClientRecovery, TerminateAndInitializeAfterADeviceLossBringsUpAFreshSession) {
ServerProcess server;
ASSERT_TRUE(Launch(&server, "reinit")) << server.Log();
ClientLog client("reinit");
const RecoveryReport r = RunClient(server, Route::TerminateInitialize, client.base);
const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text();
ExpectLossReportedAndRecovered(r, diagnostics);
ExpectOneLossAndACleanSecondSession(server);
server.Stop();
}
// THE OTHER THREAD: the lost context is still current on a second thread while the main thread
// recovers. Everything that thread does afterwards is refused as lost and reaches nothing of the
// fresh session - without the per-thread answer its first GL call would bind the lost context's
// token on the fresh session, which latches it Fatal{ProtocolCorruption, "BindContext..."}.
TEST_F(ClientRecovery, AThreadStillOnTheLostContextReachesNothingOfTheFreshSession) {
ServerProcess server;
ASSERT_TRUE(Launch(&server, "stale")) << server.Log();
ClientLog client("stale");
const StaleThreadReport r = RunInClient<StaleThreadReport>(server, client.base, &RunStaleThreadClient);
const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text();
ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics;
ASSERT_EQ(r.firstPixel, kGreen) << std::hex << r.firstPixel << "\n" << diagnostics;
ASSERT_EQ(r.newCurrentOk, 1) << r.note << "\n" << diagnostics;
EXPECT_EQ(r.drawnPixel, kRed) << "the fresh context did not render: " << std::hex << r.drawnPixel;
EXPECT_TRUE(r.staleResetStatus == GL_UNKNOWN_CONTEXT_RESET || r.staleResetStatus == GL_GUILTY_CONTEXT_RESET)
<< "the thread on the lost context sees no reset: 0x" << std::hex << r.staleResetStatus;
EXPECT_NE(r.stalePixel, kWhite) << "a read-back on the lost context returned a frame";
EXPECT_EQ(r.staleSwap, EGL_FALSE);
EXPECT_EQ(r.staleSwapError, EGL_CONTEXT_LOST) << std::hex << r.staleSwapError;
EXPECT_EQ(r.staleRelease, 1) << "the second thread could not release the lost context";
EXPECT_EQ(r.recordsByStaleThread, 0) << "the thread on the lost context published records on the fresh session";
EXPECT_EQ(r.destroyOldOk, 1);
EXPECT_EQ(r.pixelAfterStale, kRed) << "the fresh context stopped rendering: " << std::hex << r.pixelAfterStale
<< "\n" << diagnostics;
EXPECT_EQ(r.resetStatusAfterStale, static_cast<Uint32>(GL_NO_ERROR)) << "the fresh session was lost too";
ExpectOneLossAndACleanSecondSession(server);
server.Stop();
}
+3 -58
View File
@@ -52,64 +52,9 @@ using namespace P12;
namespace {
// ---- the server: this test's own fork, the display server entry on a thread, a UNIX endpoint
// (the TCP in-process shape keeps its one-session-at-a-time supervisor; this slice changed the
// unix one, which is the shape anland's display server uses).
[[noreturn]] void RunUnixInProcessServerProcess(const std::string& endpoint, const std::string& logBase) {
::setenv("MOBILEGL_LOG_FILE_PATH", logBase.c_str(), 1);
::setenv("MOBILEGL_IPC_ROLE", "server", 1);
::setenv("MOBILEGL_IPC_DIAL", "no", 1);
for (const char* name : {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB",
"MOBILEGL_IPC_STAGE_MB", "MOBILEGL_IPC_CONTROL", "MOBILEGL_IPC_SURFACE",
"MOBILEGL_IPC_INPROC_MAX_SESSIONS", "MOBILEGL_BACKEND_TYPE"})
::unsetenv(name);
sigset_t stop;
sigemptyset(&stop);
sigaddset(&stop, SIGTERM);
pthread_sigmask(SIG_BLOCK, &stop, nullptr);
std::atomic<bool> returned{false};
std::atomic<int> served{-1};
std::thread server([&] {
served.store(mobilegl_server_serve_inprocess(endpoint.c_str()));
returned.store(true);
});
bool stopping = false;
while (!returned.load()) {
timespec slice{0, 100 * 1000 * 1000};
if (sigtimedwait(&stop, nullptr, &slice) == SIGTERM && !stopping) {
stopping = true;
mobilegl_server_stop_inprocess();
}
}
server.join();
std::fflush(nullptr);
::_exit(stopping && served.load() == 0 ? 0 : 100 + (served.load() & 0x7f));
}
bool LaunchUnixInProcessServer(ServerProcess* server, const std::string& label) {
server->endpoint = "@mgl-multi-" + label + "-" + std::to_string(::getpid());
server->logBase = "/tmp/mgl-multi-" + label + "-" + std::to_string(::getpid()) + ".log";
::setenv("MOBILEGL_LOG_FILE_PATH", server->logBase.c_str(), 1);
PinHeadlessEgl();
Debug::TruncateRoleLogs(server->logBase.c_str());
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid < 0) return false;
if (pid == 0) RunUnixInProcessServerProcess(server->endpoint, server->logBase);
server->pid = pid;
bool listening = false;
(void)WaitFor(
[&] {
if (server->Log().find("listening on") != std::string::npos) return listening = true;
return server->WaitExit(0) && server->pid <= 0;
},
10000);
// The listener takes the native address; the client's control selector includes its
// transport prefix (bare @name is not a supported MOBILEGL_IPC_CONTROL spelling).
if (listening) server->endpoint = "unix:" + server->endpoint;
return listening;
}
// The server is the rig's LaunchUnixInProcessServer: this test's own fork running the display
// server entry on a thread, on a UNIX endpoint (the TCP in-process shape keeps its
// one-session-at-a-time supervisor; this slice changed the unix one, which is anland's).
// ---- a peer this test drives STEP BY STEP, so a case can interleave two sessions: everything
// the peer does is decided by the parent over a pipe, and every step answers with a report.
+64
View File
@@ -20,6 +20,7 @@
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/FatalFunnel.h>
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Server/InProcessServer.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Remote/Server/ServerSpawn.h>
#include <MG_Remote/Transport/ILink.h>
@@ -31,6 +32,7 @@
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdint>
@@ -47,6 +49,7 @@
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -196,6 +199,67 @@ namespace {
return false;
}
// ------------------------------------------------------------------------------------
// The in-process display server (MultiSessionTest, ClientRecoveryTest): a fork of this test
// running mobilegl_server_serve_inprocess on a thread, on a UNIX endpoint. One process serves
// every session, so a session that ends - a device loss among them - leaves it serving.
// ------------------------------------------------------------------------------------
[[noreturn]] inline void RunUnixInProcessServerProcess(const std::string& endpoint, const std::string& logBase) {
::setenv("MOBILEGL_LOG_FILE_PATH", logBase.c_str(), 1);
::setenv("MOBILEGL_IPC_ROLE", "server", 1);
::setenv("MOBILEGL_IPC_DIAL", "no", 1);
for (const char* name : {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB",
"MOBILEGL_IPC_STAGE_MB", "MOBILEGL_IPC_CONTROL", "MOBILEGL_IPC_SURFACE",
"MOBILEGL_IPC_INPROC_MAX_SESSIONS", "MOBILEGL_BACKEND_TYPE"})
::unsetenv(name);
sigset_t stop;
sigemptyset(&stop);
sigaddset(&stop, SIGTERM);
pthread_sigmask(SIG_BLOCK, &stop, nullptr);
std::atomic<bool> returned{false};
std::atomic<int> served{-1};
std::thread server([&] {
served.store(mobilegl_server_serve_inprocess(endpoint.c_str()));
returned.store(true);
});
bool stopping = false;
while (!returned.load()) {
timespec slice{0, 100 * 1000 * 1000};
if (sigtimedwait(&stop, nullptr, &slice) == SIGTERM && !stopping) {
stopping = true;
mobilegl_server_stop_inprocess();
}
}
server.join();
std::fflush(nullptr);
::_exit(stopping && served.load() == 0 ? 0 : 100 + (served.load() & 0x7f));
}
inline bool LaunchUnixInProcessServer(ServerProcess* server, const std::string& label) {
server->endpoint = "@mgl-multi-" + label + "-" + std::to_string(::getpid());
server->logBase = "/tmp/mgl-multi-" + label + "-" + std::to_string(::getpid()) + ".log";
::setenv("MOBILEGL_LOG_FILE_PATH", server->logBase.c_str(), 1);
PinHeadlessEgl();
Debug::TruncateRoleLogs(server->logBase.c_str());
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid < 0) return false;
if (pid == 0) RunUnixInProcessServerProcess(server->endpoint, server->logBase);
server->pid = pid;
bool listening = false;
(void)WaitFor(
[&] {
if (server->Log().find("listening on") != std::string::npos) return listening = true;
return server->WaitExit(0) && server->pid <= 0;
},
10000);
// The listener takes the native address; the client's control selector includes its
// transport prefix (bare @name is not a supported MOBILEGL_IPC_CONTROL spelling).
if (listening) server->endpoint = "unix:" + server->endpoint;
return listening;
}
// ------------------------------------------------------------------------------------
// Forked peers (PeerLatchTest's RunPeer, trimmed)
// ------------------------------------------------------------------------------------
@@ -30,4 +30,4 @@
### 15.3 Linux / Windows / 崩溃
- Linux / WSL / CI 永不开窗(`EGL_PLATFORM=surfaceless`);Wayland 不支持。Windows / 桌面 client 在 TCP 终局下是真实形态(Windows 是第二批,开放问题 22);macOS 不拆分;Windows 机器不是正确性门。
- server 死:client 读到挂断 → **device-lost 闩**(GL 调用 no-op、`eglSwapBuffers` 返回 `EGL_CONTEXT_LOST`、reset status `GL_UNKNOWN_CONTEXT_RESET`);`Session::Fail` 漏斗向对端发 `SessionFault` 帧命名家族(`FatalFamilies.def`)。**不重启**:`MOBILEGL_IPC_RESPAWN` 今天是具名拒绝。client 死:server 读到 EOF 立即销毁 context 并退出(supervisor 继续服务下一连接)。
- server 死:client 读到挂断 → **device-lost 闩**(GL 调用 no-op、`eglSwapBuffers` 返回 `EGL_CONTEXT_LOST`、reset status `GL_UNKNOWN_CONTEXT_RESET`);`Session::Fail` 漏斗向对端发 `SessionFault` 帧命名家族(`FatalFamilies.def`)。闩之前创建的 context 永久丢失(`eglMakeCurrent` / `eglSwapBuffers` 返回 `EGL_CONTEXT_LOST`,`glGetError` 报一次 `GL_CONTEXT_LOST`,销毁时不往新 session 发任何记录)。**进程可恢复**(`MOBILEGL_IPC_RECOVER`,默认 1):应用下一次 `eglCreateContext` / `eglCreate*Surface` / `eglInitialize` 拆掉丢失的 session、重新拨号得到新 session,client 侧描述旧 server 的缓存全部清空(发布闩、CSO/sampler 缓存、emitter 闩、create window、applier-reset 序号、persistent map),wire epoch 前进使旧 GLContext 一律视为丢失;surface 保留并在下次 make-current 时于新 session 重建(EGL 对 power-management 事件的语义),Wayland 窗口的 shared image 在新 session 重新分配。in-process server 与 `fd:` 对不可重拨,仍是永久丢失。`MOBILEGL_IPC_RESPAWN`(server 重启后全量重推)仍是具名拒绝。client 死:server 读到 EOF 立即销毁 context 并退出(supervisor 继续服务下一连接)。
@@ -62,6 +62,7 @@ CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_
| `MOBILEGL_IPC_ALLOW_ADOPT_T0` | 1 | server 自有:`0` = 永不批准 T0(会话走 T2),经 `ServerSpawn` 保留 |
| `MOBILEGL_IPC_STRICT_ERRORS` / `MOBILEGL_IPC_AUDIT` / `MOBILEGL_IPC_ROLE_SPLIT_STATE` | 0 / 0 / 0 | 残余输入读升级为 Fatal / 退休 staging 填 `0xDD` / 双块演练(P5f) |
| `MOBILEGL_IPC_SPIN_US` / `MOBILEGL_IPC_SERVER_AFFINITY` | 50 / `auto` | park 前自旋预算 / apply 线程亲和(Redmi 内核忽略) |
| `MOBILEGL_IPC_RECOVER` | 1 | session 因 device loss 结束后,应用下一次 `eglCreateContext` / `eglCreate*Surface` / `eglInitialize` 重新拨号得到新 session(旧 context 保持丢失,surface 在新 session 重建);`0` = 丢失对整个进程永久 |
| `MOBILEGL_IPC_RESPAWN` | — | 具名拒绝:没有阶段实现 server 重启后的全量重推 |
显式不设立:`MOBILEGL_IPC_PROGRAM`(没有 relink 档)、`MOBILEGL_IPC_VALIDATE_SERVER`。计划中未接线:`MOBILEGL_IPC_IDLE_EXIT_S`(未解析)。