diff --git a/MobileGL/Config.h b/MobileGL/Config.h index c28548d4..3015cd67 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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); diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 4809329c..3db4ab3a 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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 diff --git a/MobileGL/Init.h b/MobileGL/Init.h index d5c380fc..5b41d9cc 100644 --- a/MobileGL/Init.h +++ b/MobileGL/Init.h @@ -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 diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index 8d25f1ad..c72fa137 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -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 +// 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 +#include +#include +#include +#include #endif #include #include @@ -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 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 lostDefault = MG_State::ReplaceProcessDefaultGLContext(); + UniquePtr 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 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_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 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 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 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 destroyLock(EGLOperationMutex()); + const Bool lost = ContextIsLost(state, ctx); + std::optional 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 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; { diff --git a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp index aee6095f..8943ee8c 100644 --- a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp +++ b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp @@ -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; diff --git a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h index 181ad21c..04e7ac69 100644 --- a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h +++ b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h @@ -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: diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index bbc1dbf0..a9081482 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -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; diff --git a/MobileGL/MG_Impl/Init.cpp b/MobileGL/MG_Impl/Init.cpp index 927d78b6..326af2cc 100644 --- a/MobileGL/MG_Impl/Init.cpp +++ b/MobileGL/MG_Impl/Init.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include +#include #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(); + 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(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 diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 1817ce21..afdefecc 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -1828,6 +1828,12 @@ namespace MobileGL::MG_Pipe { *entry = Entry{}; } + // A fresh server holds no record at all (MGPipeForgetEndedSession). + void ForgetAll() { + for (Vector& 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( 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 diff --git a/MobileGL/MG_Impl/Pipe/ResourceTracker.h b/MobileGL/MG_Impl/Pipe/ResourceTracker.h index b77a57cb..51792381 100644 --- a/MobileGL/MG_Impl/Pipe/ResourceTracker.h +++ b/MobileGL/MG_Impl/Pipe/ResourceTracker.h @@ -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 diff --git a/MobileGL/MG_Impl/Pipe/SamplerEmit.h b/MobileGL/MG_Impl/Pipe/SamplerEmit.h index e8150111..e08694d2 100644 --- a/MobileGL/MG_Impl/Pipe/SamplerEmit.h +++ b/MobileGL/MG_Impl/Pipe/SamplerEmit.h @@ -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. diff --git a/MobileGL/MG_Impl/Pipe/TextureEmit.h b/MobileGL/MG_Impl/Pipe/TextureEmit.h index cc208ada..764d3a4d 100644 --- a/MobileGL/MG_Impl/Pipe/TextureEmit.h +++ b/MobileGL/MG_Impl/Pipe/TextureEmit.h @@ -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. diff --git a/MobileGL/MG_Pipe/PipeMutation.h b/MobileGL/MG_Pipe/PipeMutation.h index 4382c15b..a08a158a 100644 --- a/MobileGL/MG_Pipe/PipeMutation.h +++ b/MobileGL/MG_Pipe/PipeMutation.h @@ -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); diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index a989b7a3..6289d23b 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -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 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 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 lock(m_eglStateMutex); + m_homedSurfaces.clear(); + } MG_Backend::BackendObject::ReleaseEGLResources(); } + void BackendObject_Remote::OnSessionReplaced() { + const std::lock_guard 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 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(reply.width), + static_cast(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(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(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 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 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 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; diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.h b/MobileGL/MG_Remote/Client/BackendObject_Remote.h index 68aca0a8..18eb7cf8 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.h +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.h @@ -51,6 +51,9 @@ #include +#include +#include + 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 lock(m_eglStateMutex); + m_homedSurfaces.insert(surface); + } + Bool HoldsSharedImage(Uint64 id) const { + const std::lock_guard 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 m_homedSurfaces; + std::unordered_map m_sessionSharedImages; // id -> references taken + // The ids a replaced session held: releasing one is a success with nothing sent. + std::unordered_set m_lostSessionSharedImages; }; } // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index c4b60536..82b61ddc 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -24,12 +24,15 @@ #include "../Transport/InProcessTransport.h" #include "WireTables.h" #include "EmitTables.h" +#include "PersistentMapTracker.h" +#include "ServerProbe.h" #include #include #include #include #include +#include #include #include #include @@ -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 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& 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& LostSessionT0Stores() { + static auto* stores = new Vector(); + 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(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(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(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(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() : "", + 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 diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index 8be79199..55bc9064 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -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 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 diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index abfcbf80..2ac3e6b6 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -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> g_fenceProxies; @@ -1754,6 +1775,7 @@ namespace MobileGL::MG_Remote::Client { ClientSession& session = RequireSession("ClientWaitSync"); const std::lock_guard 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 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 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(MG_Pipe::MGPipeKind::Fence), 0}; session->EmitAndWait(MG_Pipe::MGPWireOp::FenceDestroy, &desc, sizeof(desc), nullptr, 0, nullptr, 0, nullptr); diff --git a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp index 8fa63e8b..4f43d178 100644 --- a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp +++ b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "PersistentMapTracker.h" +#include "ClientSession.h" #include #include @@ -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(); } diff --git a/MobileGL/MG_Remote/Client/PersistentMapTracker.h b/MobileGL/MG_Remote/Client/PersistentMapTracker.h index 165518bf..75b6850d 100644 --- a/MobileGL/MG_Remote/Client/PersistentMapTracker.h +++ b/MobileGL/MG_Remote/Client/PersistentMapTracker.h @@ -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 diff --git a/MobileGL/MG_Remote/Client/QueryEmit.inc b/MobileGL/MG_Remote/Client/QueryEmit.inc index a506e0b1..6b54c7a1 100644 --- a/MobileGL/MG_Remote/Client/QueryEmit.inc +++ b/MobileGL/MG_Remote/Client/QueryEmit.inc @@ -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> 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 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 lock(g_queryProxyMutex); + if (QueryFromEndedSession(handle)) return false; const MG_Pipe::MGPHandleOnly request{QueryDesc(handle).Query, static_cast(MG_Pipe::MGPipeKind::Query), 0}; const auto result = ReadQueryReply(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 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 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(MG_Pipe::MGPipeKind::Query), 0}; session->EmitAndWait(MG_Pipe::MGPWireOp::QueryDestroy, &request, sizeof(request), nullptr, 0, nullptr, 0, nullptr); diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index 4d9511e8..fc46c9b5 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -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 diff --git a/MobileGL/MG_Remote/Client/WireTables.h b/MobileGL/MG_Remote/Client/WireTables.h index eb1ccfb0..77e3345b 100644 --- a/MobileGL/MG_Remote/Client/WireTables.h +++ b/MobileGL/MG_Remote/Client/WireTables.h @@ -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 diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index 3eb441b0..d5b46c3b 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -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); diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 51e1b16b..720d1779 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -29,8 +29,27 @@ namespace MobileGL::MG_State { static SharedPtr* slot = new SharedPtr(); return *slot; } + +#if MOBILEGL_BUILD_DISAGGREGATED + std::atomic 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 ReplaceProcessDefaultGLContext() { + auto& slot = DefaultGLContextSlot(); + SharedPtr old = slot; + const Bool callerOnOld = old && pGLContext == old; + DefaultFramebufferSlot().reset(); + slot = MakeShared(); + if (callerOnOld || !pGLContext) pGLContext = slot; + return old; + } +#endif + const SharedPtr& 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()) {} GLContext::GLContext(SharedPtr shareGroup) diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 53c7e76b..aec10b8d 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -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& 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 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 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 diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index b2bb5bcb..5e584f1b 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -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.) diff --git a/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp b/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp new file mode 100644 index 00000000..6c2a319a --- /dev/null +++ b/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp @@ -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 +#include +#include +#include +#include + +#include +#include + +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(PublishedSeq()) - static_cast(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 lock(m_mutex); + m_quit = true; + } + m_wake.notify_all(); + m_thread.join(); + } + void Run(const std::function& job) { + std::unique_lock lock(m_mutex); + m_job = job; + m_wake.notify_all(); + m_wake.wait(lock, [this] { return !m_job; }); + } + + private: + void Loop() { + std::unique_lock lock(m_mutex); + for (;;) { + m_wake.wait(lock, [this] { return m_quit || static_cast(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 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(PublishedSeq()) - static_cast(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 + 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(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(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(GL_CONTEXT_LOST)) + << "glGetError after the loss: 0x" << std::hex << r.errorLost; + EXPECT_EQ(r.errorAfterLost, static_cast(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(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(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(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(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(GL_NO_ERROR)) << "the fresh session was lost too"; + ExpectOneLossAndACleanSecondSession(server); + server.Stop(); +} diff --git a/MobileGL/MG_Test/Wire/MultiSessionTest.cpp b/MobileGL/MG_Test/Wire/MultiSessionTest.cpp index 73af3027..84fa0439 100644 --- a/MobileGL/MG_Test/Wire/MultiSessionTest.cpp +++ b/MobileGL/MG_Test/Wire/MultiSessionTest.cpp @@ -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 returned{false}; - std::atomic 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. diff --git a/MobileGL/MG_Test/Wire/P12ServerRig.h b/MobileGL/MG_Test/Wire/P12ServerRig.h index 75b7321f..56b89fc0 100644 --- a/MobileGL/MG_Test/Wire/P12ServerRig.h +++ b/MobileGL/MG_Test/Wire/P12ServerRig.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,7 @@ #include +#include #include #include #include @@ -47,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -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 returned{false}; + std::atomic 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) // ------------------------------------------------------------------------------------ diff --git a/docs/Disaggregated/design/08-runtime-and-platform.md b/docs/Disaggregated/design/08-runtime-and-platform.md index a67eb01d..aecdfd92 100644 --- a/docs/Disaggregated/design/08-runtime-and-platform.md +++ b/docs/Disaggregated/design/08-runtime-and-platform.md @@ -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 继续服务下一连接)。 diff --git a/docs/Disaggregated/design/09-build-switches-counters.md b/docs/Disaggregated/design/09-build-switches-counters.md index df1e92e1..0d0f54b4 100644 --- a/docs/Disaggregated/design/09-build-switches-counters.md +++ b/docs/Disaggregated/design/09-build-switches-counters.md @@ -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`(未解析)。