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