[Fix] (Server): a lost session's window lease is waited for by the fresh session, and a window wait holds no backend turn.

This commit is contained in:
2026-10-03 09:05:54 -04:00
parent f52a546707
commit f22ae625cb
10 changed files with 1005 additions and 14 deletions
+10
View File
@@ -30,12 +30,18 @@ namespace MobileGL::MG_Remote {
// that forks a child and reads it back sees the increment even though the child is gone -
// it reads its OWN process's counter for the funnel it drove, not the child's.
std::atomic<std::uint64_t> g_sessionFaultCount{0};
// SetSessionLatchObserver's; read once per latched session.
std::atomic<SessionLatchObserver> g_sessionLatchObserver{nullptr};
} // namespace
std::uint64_t SessionFaultCount() {
return g_sessionFaultCount.load(std::memory_order_relaxed);
}
void SetSessionLatchObserver(SessionLatchObserver observer) {
g_sessionLatchObserver.store(observer, std::memory_order_release);
}
namespace {
// THE PROJECTION FROM MG_Pipe's TWO-WORD BOUNDARY ENUM ONTO THE .def's VOCABULARY, as a
// switch with a named default rather than a cast. MGPipeFatalFamily is deliberately not
@@ -241,6 +247,10 @@ namespace MobileGL::MG_Remote {
"closes (exit %d) for the supervisor to serve the next connection (PH-1, "
"ID-P7-1)",
FatalFamilyName(family), kSessionLatchedExitCode);
// Before the peer hears of it: what the observer records must already hold when the client
// reacts (a lost compositor re-creates its window surface on a fresh session at once).
if (const SessionLatchObserver observer = g_sessionLatchObserver.load(std::memory_order_acquire))
observer();
PublishSessionFault(family, line);
return false;
}
+8
View File
@@ -140,6 +140,14 @@ namespace MobileGL::MG_Remote {
#endif
SessionLatch(MGFatalFamily family, const char* fmt, ...);
// THE FIRST LATCHED FAULT OF A SESSION, OBSERVED. Called on the latching thread (its latch
// domain bound) once the latch is raised and BEFORE the SessionFault frame goes to the peer, so
// what it records about the session is in place before the client can act on the loss. One
// process-wide observer, replaced by the next install; it must not block. ServerLoop's marks the
// session's lease on the server window as ending (ServerDisplay::MarkLeaseEnding).
using SessionLatchObserver = void (*)();
void SetSessionLatchObserver(SessionLatchObserver observer);
// P7 wave 0. Points MG_Pipe's MGPipeSessionFail seam (MG_Pipe/PipeSessionFail.h) at
// SessionFail, which is what makes the three Magma wire funnels in MG_Backend's renderer
// publish a SessionFault, bump SessionFaultCount() and appear in the census like every other
+40 -4
View File
@@ -204,7 +204,8 @@ namespace MobileGL::MG_Remote::Server {
{
const std::lock_guard<std::mutex> lock(m_mutex);
if (!m_installed) return ServerWindowAcquire::NoDisplay;
if (m_leaseHolder != nullptr && m_leaseHolder != holder) return ServerWindowAcquire::LeasedElsewhere;
if (m_leaseHolder != nullptr && m_leaseHolder != holder && !m_leaseEnding)
return ServerWindowAcquire::LeasedElsewhere;
hooks = m_hooks;
interrupts = m_interrupts;
previousWidth = m_requestedWidth;
@@ -231,14 +232,27 @@ namespace MobileGL::MG_Remote::Server {
std::unique_lock<std::mutex> lock(m_mutex);
const auto deadline = Clock::now() + std::chrono::milliseconds(timeoutMs);
Bool attachedSeen = false;
Bool waitedForEndingHolder = false;
auto geometryDeadline = deadline;
for (;;) {
if (!m_installed) return ServerWindowAcquire::NoDisplay;
if (m_interrupts != interrupts) return ServerWindowAcquire::Interrupted;
if (cancel != nullptr && cancel(cancelUser)) return ServerWindowAcquire::Cancelled;
if (m_leaseHolder != nullptr && m_leaseHolder != holder) return ServerWindowAcquire::LeasedElsewhere;
// ANOTHER HOLDER: refused at once while its session goes on, waited for while it ends
// (its EndLease wakes this). The window is judged only once the lease is free.
const Bool heldElsewhere = m_leaseHolder != nullptr && m_leaseHolder != holder;
if (heldElsewhere && !m_leaseEnding) return ServerWindowAcquire::LeasedElsewhere;
if (heldElsewhere && !waitedForEndingHolder) {
waitedForEndingHolder = true;
MGLOG_I("MG_Remote server: the server window is still leased by a session that is ending; this "
"session waits for that lease to end (at most %u ms)",
timeoutMs);
}
const auto now = Clock::now();
if (m_window != nullptr) {
if (heldElsewhere) {
attachedSeen = false;
geometryDeadline = deadline;
} else if (m_window != nullptr) {
if (!attachedSeen) {
attachedSeen = true;
geometryDeadline = std::min(deadline, now + std::chrono::milliseconds(kGeometryGraceMs));
@@ -249,6 +263,7 @@ namespace MobileGL::MG_Remote::Server {
m_onLost = onLost;
m_onResized = onResized;
m_lostRequested = false;
m_leaseEnding = false;
// A suspended holder that leased a window again waits for no other.
m_suspended.erase(std::remove_if(m_suspended.begin(), m_suspended.end(),
[holder](const SuspendedHolder& entry) {
@@ -273,13 +288,18 @@ namespace MobileGL::MG_Remote::Server {
"at that size and the client is told so",
m_width, m_height, kGeometryGraceMs);
}
if (waitedForEndingHolder) {
MGLOG_I("MG_Remote server: the ending session's lease ended; the server window is leased to "
"this session");
}
return ServerWindowAcquire::Acquired;
}
} else {
attachedSeen = false;
geometryDeadline = deadline;
}
if (now >= deadline) return ServerWindowAcquire::NoWindow;
if (now >= deadline)
return heldElsewhere ? ServerWindowAcquire::LeasedElsewhere : ServerWindowAcquire::NoWindow;
const auto wake = std::min({deadline, geometryDeadline, now + kCancelPollInterval});
m_cv.wait_until(lock, wake);
}
@@ -291,6 +311,7 @@ namespace MobileGL::MG_Remote::Server {
m_onLost = nullptr;
m_onResized = nullptr;
m_lostRequested = false;
m_leaseEnding = false;
releases->swap(m_releaseAfterLease);
*hooks = m_hooks;
// An Uninstall that ran while this lease still owed releases kept the hooks for them.
@@ -317,6 +338,16 @@ namespace MobileGL::MG_Remote::Server {
ReleaseAfterLease(hooks, releases);
}
void ServerDisplay::MarkLeaseEnding(void* holder) {
{
const std::lock_guard<std::mutex> lock(m_mutex);
if (holder == nullptr || m_leaseHolder != holder || m_leaseEnding) return;
m_leaseEnding = true;
}
// A request already refused is not waiting; one made from here on waits for the EndLease.
m_cv.notify_all();
}
void ServerDisplay::SuspendLease(void* holder, ServerWindowAttachedHook onAttached) {
if (holder == nullptr) return;
std::vector<void*> releases;
@@ -379,6 +410,11 @@ namespace MobileGL::MG_Remote::Server {
return m_leaseHolder != nullptr;
}
Bool ServerDisplay::LeaseEnding() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_leaseHolder != nullptr && m_leaseEnding;
}
Uint64 ServerDisplay::Generation() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_generation;
+20 -1
View File
@@ -35,6 +35,14 @@
// reference is kept and released when the lease finally ends - and Detach returns TimedOut, by
// name, so the UI thread is never held longer than its bound.
//
// A LEASE THAT IS ENDING IS WAITED FOR, NOT REFUSED. A holder whose session is over - it latched a
// fault (a device loss among them) or is stopping - says so (MarkLeaseEnding) before its client can
// learn of it; its lease still lasts until its backend has let go of the window. A client that loses
// its session re-creates its window surface on a fresh session at once, typically while the old
// session's apply thread is still tearing its backend down: that request waits for the old lease
// to end, bounded by its own timeout, instead of being refused as LeasedElsewhere. A holder whose
// session goes on keeps the window exclusively, as before.
//
// SUSPEND, NOT END (screen off / lock). A holder whose backend can let go of the window and keep
// everything else - its contexts, objects and device - ends its lease with SuspendLease instead of
// EndLease and names a second hook: the next Attach of a NEW window calls it, so the holder can
@@ -96,7 +104,9 @@ namespace MobileGL::MG_Remote::Server {
Interrupted, // Interrupt() (the in-process server is stopping)
// P14 S2: NOT A BUG ANY MORE. Several sessions share one process and the display's window
// is one window, so a second ON-SCREEN session asking for it is refused by name here
// (ServerLoop logs the holder); offscreen sessions take no lease and are unaffected.
// (ServerLoop logs the holder); offscreen sessions take no lease and are unaffected. A holder
// whose session is ending (MarkLeaseEnding) is waited for instead, and this is the answer
// only when its lease outlasted the wait.
LeasedElsewhere, // another session in this process holds the window: see the note above
};
@@ -156,6 +166,11 @@ namespace MobileGL::MG_Remote::Server {
// whole backend). Wakes a waiting Detach; releases a reference Detach left behind on timeout.
// A holder that holds no lease is a no-op.
void EndLease(void* holder);
// `holder`'s session is ending (it latched, or it is stopping): its lease ends once its
// backend has let go of the window, and another holder's AcquireFor waits for that rather
// than being refused (see the header block). A no-op for a holder that holds no lease; the
// mark ends with the lease.
void MarkLeaseEnding(void* holder);
// EndLease for a holder that only SUSPENDED its use of the window (its backend let go of the
// window and kept everything else). Also registers `onAttached`, which the next Attach of a
// new window calls - at once when a window is already attached. The registration ends when
@@ -169,6 +184,8 @@ namespace MobileGL::MG_Remote::Server {
// Diagnostics, for logs and tests.
Bool Attached() const;
Bool Leased() const;
// The lease is held by a holder whose session is ending (MarkLeaseEnding).
Bool LeaseEnding() const;
Uint64 Generation() const;
// How many holders wait for a window to resume on (SuspendLease).
Uint32 SuspendedHolders() const;
@@ -207,6 +224,8 @@ namespace MobileGL::MG_Remote::Server {
ServerWindowLostHook m_onLost = nullptr;
ServerWindowResizedHook m_onResized = nullptr;
Bool m_lostRequested = false;
// The holder's session is ending (MarkLeaseEnding); cleared with the lease.
Bool m_leaseEnding = false;
// References a timed-out Detach could not release yet; EndLease releases them.
std::vector<void*> m_releaseAfterLease;
// SuspendLease's registrations, told about the next new window.
+75 -8
View File
@@ -79,6 +79,24 @@ namespace MobileGL::MG_Remote::Server {
++m_serving;
m_turn.notify_all();
}
// Gives the turn up however deep the calling thread holds it, for a wait that does no
// backend work; answers the depth Resume restores (0: the thread did not hold it).
Uint32 YieldIfHeld() {
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_owner != std::this_thread::get_id()) return 0;
const Uint32 depth = m_depth;
m_depth = 0;
m_owner = std::thread::id{};
++m_serving;
m_turn.notify_all();
return depth;
}
void Resume(Uint32 depth) {
if (depth == 0) return;
Lock();
const std::lock_guard<std::mutex> lock(m_mutex);
m_depth = depth;
}
private:
std::mutex m_mutex;
@@ -103,6 +121,31 @@ namespace MobileGL::MG_Remote::Server {
ScopedBackendTurn& operator=(const ScopedBackendTurn&) = delete;
};
// A WAIT FOR THE SERVER WINDOW HOLDS NO TURN. It can last seconds - a window that is not up
// yet, or the lease of a session that is ending - and the other sessions' apply threads need
// the turn meanwhile: the ending one to tear its backend down, which is what ends its lease.
// Nothing of the backend is touched while it is given up; it is taken back (a handoff, when
// another thread had it) before the waiter goes on.
class ScopedBackendTurnYield {
public:
ScopedBackendTurnYield() : m_depth(BackendTurn().YieldIfHeld()) {}
~ScopedBackendTurnYield() { BackendTurn().Resume(m_depth); }
ScopedBackendTurnYield(const ScopedBackendTurnYield&) = delete;
ScopedBackendTurnYield& operator=(const ScopedBackendTurnYield&) = delete;
private:
const Uint32 m_depth;
};
// The process-wide test seam (ServerLoop.h).
std::atomic<ServerLoop::ServerOwnedSurfaceHook> g_serverOwnedSurfaceHook{nullptr};
// FatalFunnel's latch observer. The latching thread is one of the session's own (its scope
// resolves ServerLoopInstance to that session's loop): a session that latched is over, and
// its lease on the server window, if it holds one, ends once its apply thread has torn the
// backend down - which a fresh session of the same client waits for (MarkLeaseEnding).
void MarkLatchedSessionsLeaseEnding() { ServerDisplayInstance().MarkLeaseEnding(&ServerLoopInstance()); }
// The bounded join. InProcessTransportTest.cpp:344 uses five seconds for the same
// reason: a lost wakeup must be a RED TEST and not a hung CI job.
constexpr Uint32 kJoinTimeoutMs = 5000;
@@ -333,6 +376,10 @@ namespace MobileGL::MG_Remote::Server {
return MOBILEGL_OK;
}
void ServerLoop::SetServerOwnedSurfaceHookForTesting(ServerOwnedSurfaceHook hook) {
g_serverOwnedSurfaceHook.store(hook, std::memory_order_release);
}
// ---------------------------------------------------------------------------------
// Start / the loop / Stop
// ---------------------------------------------------------------------------------
@@ -347,6 +394,8 @@ namespace MobileGL::MG_Remote::Server {
return MOBILEGL_ERR_NOT_INITIALIZED;
}
m_session = &session;
// A session that latches marks its window lease as ending before its client hears of it.
SetSessionLatchObserver(&MarkLatchedSessionsLeaseEnding);
m_stopRequested.store(false, std::memory_order_release);
m_abandonQueue.store(false, std::memory_order_release);
// PH-6 fix round: the session's copy of the request, for ReserveEventOrBlock's wait. Lowered
@@ -735,6 +784,11 @@ namespace MobileGL::MG_Remote::Server {
// AND AFTER AbandonQueuedRecords (P12 review fix), for the forfeit's reason: the server is
// stopping under a client that may still be streaming, and draining what it keeps queuing
// is the unbounded hold Stop()'s bounded join turns into an abort.
//
// THE SESSION IS ENDING, whatever ended it: its lease on the server window (if it holds one)
// ends below, once the backend is gone, and a fresh session of the same client that asks for
// the window meanwhile waits for that instead of being refused.
ServerDisplayInstance().MarkLeaseEnding(this);
std::optional<ScopedBackendTurn> exitTurn(std::in_place);
const Bool forfeited = session.ReverseChannelForfeited();
const Bool abandoned = m_abandonQueue.load(std::memory_order_acquire);
@@ -1293,6 +1347,9 @@ namespace MobileGL::MG_Remote::Server {
m_running.store(false, std::memory_order_release);
return;
}
// As on the apply thread's own way out: the window lease ends once the thread has torn the
// backend down, and a fresh session waits for it meanwhile.
ServerDisplayInstance().MarkLeaseEnding(this);
m_stopRequested.store(true, std::memory_order_release);
if (m_session != nullptr) {
// PH-6 fix round: the apply thread may be parked in ReserveEventOrBlock rather than in
@@ -1820,9 +1877,12 @@ namespace MobileGL::MG_Remote::Server {
ServerDisplay& display = ServerDisplayInstance();
ServerWindowLease local;
ServerWindowLease* const lease = out != nullptr ? out : &local;
const ServerWindowAcquire acquired = display.AcquireFor(width, height, timeoutMs, this, &ServerWindowLostThunk,
&ServerWindowWaitCancelled, this, lease,
&ServerWindowResizedThunk);
ServerWindowAcquire acquired = ServerWindowAcquire::NoDisplay;
{
const ScopedBackendTurnYield yield;
acquired = display.AcquireFor(width, height, timeoutMs, this, &ServerWindowLostThunk,
&ServerWindowWaitCancelled, this, lease, &ServerWindowResizedThunk);
}
switch (acquired) {
case ServerWindowAcquire::Acquired:
m_holdsWindowLease = true;
@@ -1902,7 +1962,10 @@ namespace MobileGL::MG_Remote::Server {
// Review fix: the backend is told which window is the server's own, so its surface init
// reports the extent for this window and for no window a client named (MGPipeServerOwnedWindow).
NoteServerWindowExtent(lease.window, lease.width, lease.height);
frame.ok = backend->CreateEGLWindowSurface(HandleFromToken<EGLSurface>(frame.surface), window);
const ServerOwnedSurfaceHook surfaceHook = g_serverOwnedSurfaceHook.load(std::memory_order_acquire);
const EGLSurface ownedSurface = HandleFromToken<EGLSurface>(frame.surface);
frame.ok = surfaceHook != nullptr ? surfaceHook(backend, ownedSurface, window)
: backend->CreateEGLWindowSurface(ownedSurface, window);
if (!frame.ok) {
MGLOG_E("MG_Remote server: the backend could not create a window surface on the server window %p "
"(%ux%u) for ServerOwned seq %llu",
@@ -2084,10 +2147,14 @@ namespace MobileGL::MG_Remote::Server {
// have survived it). Bounded by the geometry grace: the window IS attached - that is what woke
// this - so only its size is waited for.
ServerWindowLease lease;
const ServerWindowAcquire acquired =
ServerDisplayInstance().AcquireFor(m_serverWindowWantWidth, m_serverWindowWantHeight,
ServerDisplay::kGeometryGraceMs, this, &ServerWindowLostThunk,
&ServerWindowWaitCancelled, this, &lease, &ServerWindowResizedThunk);
ServerWindowAcquire acquired = ServerWindowAcquire::NoDisplay;
{
const ScopedBackendTurnYield yield;
acquired = ServerDisplayInstance().AcquireFor(m_serverWindowWantWidth, m_serverWindowWantHeight,
ServerDisplay::kGeometryGraceMs, this, &ServerWindowLostThunk,
&ServerWindowWaitCancelled, this, &lease,
&ServerWindowResizedThunk);
}
if (acquired != ServerWindowAcquire::Acquired) {
// Still registered: the next new window wakes this again.
MGLOG_I("MG_Remote server: a suspended on-screen session did not get the new server window (%s); it "
+9
View File
@@ -449,6 +449,15 @@ namespace MobileGL::MG_Remote::Server {
// suspend, for the window-loss cases). Before Start(); fails when one is already installed.
MobileGLResult InstallBackendForTesting(UniquePtr<MG_Backend::BackendObject> backend);
// PROCESS-WIDE TEST SEAM for a server forked from a test (every session's loop reads it; null -
// the default - is the production path): the backend surface behind a ServerOwned window
// surface. A host has no window a backend can draw into, so a case that serves a stand-in
// window gives the backend a surface of the window's extent another way (a pbuffer). Called on
// the apply thread with the lease held, in place of CreateEGLWindowSurface.
using ServerOwnedSurfaceHook = Bool (*)(MG_Backend::BackendObject* backend, EGLSurface surface,
const MG_Backend::WindowHandle& window);
static void SetServerOwnedSurfaceHookForTesting(ServerOwnedSurfaceHook hook);
private:
RemoteControlSink m_remoteSink = nullptr;
void* m_remoteSinkUser = nullptr;
+7 -1
View File
@@ -83,6 +83,11 @@ if (NOT WIN32)
# 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)
# A compositor outlives its lost session: a client on the server's own window (ServerOwned) that
# samples another client's shared image loses its session, re-creates its surface on a fresh one -
# waiting for the lost session's window lease - and imports the image again; the image's owner
# is untouched. Forks the in-process display server (with a stand-in window) and its clients.
list(APPEND MOBILEGL_WIRE_TESTS CompositorRecoveryTest)
# Several contexts on several threads of one client, in one share group or one each, drawing at
# once against the in-process display server (Qt Quick's shape, and its shape after a loss).
list(APPEND MOBILEGL_WIRE_TESTS ConcurrentContextsTest)
@@ -114,7 +119,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 "GlamorEglSequenceTest" OR
test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest" OR test STREQUAL "ServerProbeTest" OR
test STREQUAL "ClientRecoveryTest" OR test STREQUAL "ConcurrentContextsTest")
test STREQUAL "ClientRecoveryTest" OR test STREQUAL "ConcurrentContextsTest" OR
test STREQUAL "CompositorRecoveryTest")
# 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,686 @@
// MobileGL - MobileGL/MG_Test/Wire/CompositorRecoveryTest.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 COMPOSITOR OUTLIVES ITS LOST SESSION. The compositor of the anland desktop renders into the
// SERVER's window (MOBILEGL_IPC_SURFACE=server: a ServerOwned window surface, which its session holds
// the display's window lease for) and samples its clients' frames as shared images it imports from
// their dma-bufs. ClientRecoveryTest is an application's recovery; this is the compositor's, against
// the in-process display server with a stand-in window (a host has no window a backend can draw into,
// so the server gives the ServerOwned surface a pbuffer of the window's extent - the seam is
// ServerLoop's). The hand-over of the window lease from a lost session that is still tearing down is
// ServerDisplayTest's and ServerLoopTest's: the client hears of a loss it is blocked in only when the
// lost session hangs up, which is after that session ended its lease.
//
// What it asserts, from each side:
// * the compositor's session is lost (the pid-targeted knob) and the compositor, recovering the
// way KWin does - its contexts recreated, naming the lost share context, and the surface it kept
// made current again - gets the server's window again on a FRESH session, asked for at the size
// the compositor asked for (none: the window's own), not fixed at the extent it had;
// * the surface renders on the fresh session (a clear, read back) and swaps;
// * the other client's shared image survives the loss and is imported again; the lost session's
// EGLImage of it can no longer be bound, and destroying it - after the same image was imported
// again under the same id - puts nothing on the fresh session's wire (it must not drop the new
// reference);
// * the image's owner is untouched: its session is not lost, it renders, and the server still
// identifies its image.
#include "P12ServerRig.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Remote/Server/ServerDisplay.h>
#include <fcntl.h>
#include <sys/uio.h>
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 glFlush(void);
}
using namespace MobileGL;
using namespace P12;
namespace {
namespace EGL = MobileGL::MG_Impl::EGLImpl;
namespace Server = MobileGL::MG_Remote::Server;
constexpr Uint32 kGreen = 0xFF00FF00u;
constexpr Uint32 kBlue = 0xFFFF0000u;
constexpr Uint32 kRed = 0xFF0000FFu;
constexpr Uint32 kUntouched = 0x5A5A5A5Au;
constexpr Uint32 kXrgb8888 = 0x34325258u;
constexpr Uint32 kWindowWidth = 64;
constexpr Uint32 kWindowHeight = 48;
constexpr Uint32 kImageWidth = 32;
constexpr Uint32 kImageHeight = 16;
// KWin's context shape: desktop GL 3.1 that asks to be told of a reset.
const EGLint kRobustAttribs[] = {EGL_CONTEXT_MAJOR_VERSION,
3,
EGL_CONTEXT_MINOR_VERSION,
1,
EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY,
EGL_LOSE_CONTEXT_ON_RESET,
EGL_NONE};
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();
}
Uint64 PublishedSeq() {
auto* session = Remote::Client::ClientSession::Active();
return session != nullptr ? session->LastPublishedSeq() : 0;
}
// ---- the server process -----------------------------------------------------------------
int g_window = 0;
std::string g_geometryPath;
void NoWindowReference(void*, void*) {}
// Every geometry the display is asked for, one "W H" line each, for the parent to read.
void RecordGeometry(void*, Uint32 width, Uint32 height) {
const int fd = ::open(g_geometryPath.c_str(), O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
if (fd < 0) return;
const std::string line = std::to_string(width) + " " + std::to_string(height) + "\n";
const ssize_t wrote = ::write(fd, line.data(), line.size());
(void)wrote;
::close(fd);
}
Bool PbufferForTheServerWindow(MG_Backend::BackendObject* backend, EGLSurface surface,
const MG_Backend::WindowHandle& window) {
return backend->CreateEGLPbufferSurface(surface, static_cast<EGLint>(window.Width),
static_cast<EGLint>(window.Height));
}
// RunUnixInProcessServerProcess (P12ServerRig.h), with a display and its stand-in window.
[[noreturn]] void RunDisplayServerProcess(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);
g_geometryPath = logBase + ".geometry";
Server::ServerDisplayHooks hooks;
hooks.acquire = &NoWindowReference;
hooks.release = &NoWindowReference;
hooks.requestGeometry = &RecordGeometry;
Server::ServerDisplayInstance().Install(hooks);
Server::ServerDisplayInstance().Attach(&g_window, kWindowWidth, kWindowHeight);
Server::ServerLoop::SetServerOwnedSurfaceHookForTesting(&PbufferForTheServerWindow);
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 LaunchDisplayServer(ServerProcess* server, const std::string& label) {
server->endpoint = "@mgl-comprec-" + label + "-" + std::to_string(::getpid());
server->logBase = "/tmp/mgl-comprec-" + label + "-" + std::to_string(::getpid()) + ".log";
::setenv("MOBILEGL_LOG_FILE_PATH", server->logBase.c_str(), 1);
PinHeadlessEgl();
Debug::TruncateRoleLogs(server->logBase.c_str());
std::error_code ec;
std::filesystem::remove(server->logBase + ".geometry", ec);
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid < 0) return false;
if (pid == 0) RunDisplayServerProcess(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);
if (listening) server->endpoint = "unix:" + server->endpoint;
return listening;
}
// ---- the descriptor between the two clients ---------------------------------------------
struct SharedImageMessage {
Uint32 width = 0;
Uint32 height = 0;
Uint32 fourcc = 0;
Uint32 stride = 0;
Uint32 offset = 0;
Uint64 id = 0;
};
bool SendFd(int sock, int fd, const SharedImageMessage& message) {
iovec iov{const_cast<SharedImageMessage*>(&message), sizeof(message)};
alignas(cmsghdr) char control[CMSG_SPACE(sizeof(int))] = {};
msghdr msg{};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
std::memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
return ::sendmsg(sock, &msg, 0) == static_cast<ssize_t>(sizeof(message));
}
bool ReceiveFd(int sock, int* fd, SharedImageMessage* message, int timeoutMs) {
pollfd pfd{sock, POLLIN, 0};
if (::poll(&pfd, 1, timeoutMs) <= 0) return false;
iovec iov{message, sizeof(*message)};
alignas(cmsghdr) char control[CMSG_SPACE(sizeof(int))] = {};
msghdr msg{};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
if (::recvmsg(sock, &msg, 0) != static_cast<ssize_t>(sizeof(*message))) return false;
cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg == nullptr || cmsg->cmsg_type != SCM_RIGHTS) return false;
std::memcpy(fd, CMSG_DATA(cmsg), sizeof(int));
return true;
}
bool WaitForByte(int sock, int timeoutMs) {
pollfd pfd{sock, POLLIN, 0};
char byte = 0;
return ::poll(&pfd, 1, timeoutMs) > 0 && ::read(sock, &byte, 1) == 1;
}
// ---- the clients --------------------------------------------------------------------------
void SetClientEnvironment(const std::string& endpoint, const std::string& logBase, bool serverWindow) {
::setenv("MOBILEGL_LOG_FILE_PATH", logBase.c_str(), 1);
::setenv("MOBILEGL_TRANSPORT", "spawn", 1);
::setenv("MOBILEGL_IPC_CONTROL", endpoint.c_str(), 1);
if (serverWindow) ::setenv("MOBILEGL_IPC_SURFACE", "server", 1);
else ::unsetenv("MOBILEGL_IPC_SURFACE");
::unsetenv("MOBILEGL_BACKEND_TYPE");
::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_AT");
::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID");
}
EGLImage ImportImage(EGLDisplay dpy, int fd, const SharedImageMessage& image) {
const EGLAttrib attribs[] = {EGL_WIDTH,
static_cast<EGLAttrib>(image.width),
EGL_HEIGHT,
static_cast<EGLAttrib>(image.height),
EGL_LINUX_DRM_FOURCC_EXT,
static_cast<EGLAttrib>(image.fourcc),
EGL_DMA_BUF_PLANE0_FD_EXT,
fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT,
static_cast<EGLAttrib>(image.offset),
EGL_DMA_BUF_PLANE0_PITCH_EXT,
static_cast<EGLAttrib>(image.stride),
EGL_NONE};
return EGL::CreateImage(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs);
}
// A new texture with `image` as its level 0 (glEGLImageTargetTexture2DOES); the GL error it raised.
Uint32 BindImageToNewTexture(EGLImage image) {
using TargetFn = void (*)(GLenum, void*);
const auto target = reinterpret_cast<TargetFn>(EGL::GetProcAddress("glEGLImageTargetTexture2DOES"));
if (target == nullptr) return 0xFFFFFFFFu;
while (glGetError() != GL_NO_ERROR) {
}
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
target(GL_TEXTURE_2D, image);
return glGetError();
}
// THE IMAGE'S OWNER (a Wayland client of the compositor): an offscreen session that allocates a
// shared image and hands its descriptor over, then - once the compositor has recovered - checks
// that nothing of the compositor's loss reached it.
struct OwnerReport {
Int32 initialized = 0;
Int32 allocated = 0;
Int32 sent = 0;
Int32 sawDone = 0;
Uint32 statusAfter = 0xFFFFFFFFu;
Uint32 pixelAfter = 0;
Int32 stillIdentified = -1;
Uint64 imageId = 0;
char note[256] = {};
};
void Note(OwnerReport& r, const char* text) {
if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text);
}
void RunOwner(int sock, OwnerReport& r) {
const EGLDisplay 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, "no display");
(void)EGL::BindAPI(EGL_OPENGL_API);
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_NONE};
EGLConfig config = nullptr;
EGLint count = 0;
if (EGL::ChooseConfig(dpy, configAttribs, &config, 1, &count) != EGL_TRUE || count < 1)
return Note(r, "no config");
const EGLint pbufferAttribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE};
const EGLSurface surface = EGL::CreatePbufferSurface(dpy, config, pbufferAttribs);
const EGLContext context = EGL::CreateContext(dpy, config, EGL_NO_CONTEXT, kRobustAttribs);
if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT ||
EGL::MakeCurrent(dpy, surface, surface, context) != EGL_TRUE)
return Note(r, "the owner's context did not go current");
r.initialized = 1;
auto* backend = MG_Backend::pActiveBackendObject.get();
MG_Backend::SharedImageExport image;
if (backend == nullptr || !backend->AllocateSharedImage(kImageWidth, kImageHeight, kXrgb8888, &image) ||
image.Fd < 0)
return Note(r, "no shared image was allocated");
r.allocated = 1;
r.imageId = image.Id;
// A frame of the client's own first (its session is as busy as a client's is).
(void)ClearAndRead(0.0f, 1.0f, 0.0f);
glFlush();
SharedImageMessage message;
message.width = image.Width;
message.height = image.Height;
message.fourcc = image.Fourcc;
message.stride = image.Stride;
message.offset = image.Offset;
message.id = image.Id;
r.sent = SendFd(sock, image.Fd, message) ? 1 : 0;
if (!r.sent) return Note(r, "the descriptor could not be sent");
r.sawDone = WaitForByte(sock, 60000) ? 1 : 0;
r.statusAfter = glGetGraphicsResetStatus();
r.pixelAfter = ClearAndRead(1.0f, 0.0f, 0.0f);
Uint64 again = 0;
r.stillIdentified =
backend->ImportSharedImage(image.Fd, image.Width, image.Height, image.Fourcc, &again) && again == image.Id
? 1
: 0;
if (again != 0) (void)backend->ReleaseSharedImage(again);
::close(image.Fd);
(void)backend->ReleaseSharedImage(image.Id);
(void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
(void)EGL::DestroyContext(dpy, context);
(void)EGL::DestroySurface(dpy, surface);
(void)EGL::Terminate(dpy);
}
// THE COMPOSITOR: KWin's EGL shape on the server's window, its loss and its restart.
struct CompositorReport {
Int32 received = 0;
Int32 surfaceOk = 0;
Int32 width = 0;
Int32 height = 0;
Int32 firstCurrent = 0;
Int32 imported = 0;
Uint64 importedId = 0;
Uint32 firstBindError = 0xFFFFFFFFu;
Uint32 firstPixel = 0;
Int32 firstSwap = -1;
Int32 lossSeen = 0;
Uint32 resetStatus = 0;
Int32 lostSwap = -1;
Int32 lostSwapError = 0;
Int32 newContextOk = 0;
Int32 newCurrentOk = 0;
Int32 newCurrentError = 0;
Int64 rehomeMs = -1;
Uint32 resetStatusNew = 0xFFFFFFFFu;
Int32 widthAfter = 0;
Int32 heightAfter = 0;
Uint32 staleBindError = 0;
Int32 reimported = 0;
Uint64 reimportedId = 0;
Int64 recordsByStaleDestroy = -1;
Uint32 freshBindError = 0xFFFFFFFFu;
Uint32 freshPixel = 0;
Int32 freshSwap = -1;
Uint32 resetStatusEnd = 0xFFFFFFFFu;
char note[256] = {};
};
void Note(CompositorReport& r, const char* text) {
if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text);
}
void RunCompositor(int sock, CompositorReport& r) {
int fd = -1;
SharedImageMessage image;
r.received = ReceiveFd(sock, &fd, &image, 30000) ? 1 : 0;
if (!r.received) return Note(r, "no descriptor from the owner");
// AnlandBackend::initialize: the display, an RGBA window config, the server's window.
const EGLDisplay 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, "no display");
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
EGL_NONE};
EGLConfig config = nullptr;
EGLint count = 0;
if (EGL::ChooseConfig(dpy, configAttribs, &config, 1, &count) != EGL_TRUE || count < 1)
return Note(r, "no window config");
const EGLSurface surface = EGL::CreateWindowSurface(dpy, config, NativeWindowType{}, nullptr);
r.surfaceOk = surface != EGL_NO_SURFACE ? 1 : 0;
if (!r.surfaceOk) return Note(r, "the server's window surface was not created");
(void)EGL::QuerySurface(dpy, surface, EGL_WIDTH, &r.width);
(void)EGL::QuerySurface(dpy, surface, EGL_HEIGHT, &r.height);
// EglBackend::createContext: a global share context, and the compositor's sharing with it.
(void)EGL::BindAPI(EGL_OPENGL_API);
const EGLContext share = EGL::CreateContext(dpy, config, EGL_NO_CONTEXT, kRobustAttribs);
const EGLContext lost = EGL::CreateContext(dpy, config, share, kRobustAttribs);
r.firstCurrent = share != EGL_NO_CONTEXT && lost != EGL_NO_CONTEXT &&
EGL::MakeCurrent(dpy, surface, surface, lost) == EGL_TRUE
? 1
: 0;
if (!r.firstCurrent) return Note(r, "the compositor's context did not go current");
// A client's buffer, imported and bound (the scene's surface texture), and a frame.
const EGLImage stale = ImportImage(dpy, fd, image);
r.imported = stale != EGL_NO_IMAGE ? 1 : 0;
if (!r.imported) return Note(r, "the client's image was not imported");
EGLint ignored = 0;
(void)EGL::LookupSharedImage(stale, &r.importedId, &ignored, &ignored);
r.firstBindError = BindImageToNewTexture(stale);
r.firstPixel = ClearAndRead(0.0f, 1.0f, 0.0f);
r.firstSwap = EGL::SwapBuffers(dpy, surface);
// Compositing until the session is lost (the knob targets this pid).
const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(10);
while (std::chrono::steady_clock::now() < until) {
(void)ClearAndRead(0.0f, 1.0f, 0.0f);
r.lostSwap = EGL::SwapBuffers(dpy, surface);
r.lostSwapError = EGL::GetError();
if (glGetGraphicsResetStatus() != GL_NO_ERROR) {
r.lossSeen = 1;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
if (!r.lossSeen) return Note(r, "the compositor's session was never lost");
r.resetStatus = glGetGraphicsResetStatus();
// The swap that ran into the loss, if this frame's did not: one more, which must say so.
r.lostSwap = EGL::SwapBuffers(dpy, surface);
r.lostSwapError = EGL::GetError();
// Compositor::reinitialize: the scene and the context go, a new context is created (still
// naming the lost share context) and made current on the surface the compositor kept.
(void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
(void)EGL::DestroyContext(dpy, lost);
const auto restart = std::chrono::steady_clock::now();
const EGLContext fresh = EGL::CreateContext(dpy, config, share, kRobustAttribs);
r.newContextOk = fresh != EGL_NO_CONTEXT ? 1 : 0;
if (!r.newContextOk) return Note(r, "no context after the loss");
r.newCurrentOk = EGL::MakeCurrent(dpy, surface, surface, fresh) == EGL_TRUE ? 1 : 0;
r.newCurrentError = EGL::GetError();
r.rehomeMs =
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - restart).count();
if (!r.newCurrentOk) return Note(r, "the kept surface did not go current on the fresh session");
r.resetStatusNew = glGetGraphicsResetStatus();
(void)EGL::QuerySurface(dpy, surface, EGL_WIDTH, &r.widthAfter);
(void)EGL::QuerySurface(dpy, surface, EGL_HEIGHT, &r.heightAfter);
// The lost session's image names nothing the fresh one holds; the buffer is imported again
// (the same server image, so the same id) - and only then is the stale one destroyed.
r.staleBindError = BindImageToNewTexture(stale);
const EGLImage again = ImportImage(dpy, fd, image);
r.reimported = again != EGL_NO_IMAGE ? 1 : 0;
if (!r.reimported) return Note(r, "the client's image was not imported again");
(void)EGL::LookupSharedImage(again, &r.reimportedId, &ignored, &ignored);
const Uint64 before = PublishedSeq();
(void)EGL::DestroyImage(dpy, stale);
r.recordsByStaleDestroy = static_cast<Int64>(PublishedSeq()) - static_cast<Int64>(before);
r.freshBindError = BindImageToNewTexture(again);
r.freshPixel = ClearAndRead(0.0f, 0.0f, 1.0f);
r.freshSwap = EGL::SwapBuffers(dpy, surface);
r.resetStatusEnd = glGetGraphicsResetStatus();
// The owner looks at its own session now.
const char done = 1;
const ssize_t wrote = ::write(sock, &done, 1);
(void)wrote;
(void)EGL::DestroyImage(dpy, again);
::close(fd);
(void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
(void)EGL::DestroyContext(dpy, fresh);
(void)EGL::DestroyContext(dpy, share);
(void)EGL::DestroySurface(dpy, surface);
(void)EGL::Terminate(dpy);
}
// A forked client that waits for the server's endpoint before it does anything (the knob names
// the compositor's pid, so the clients exist before the server starts).
template <class Report>
struct HeldChild {
pid_t pid = -1;
int go = -1;
int report = -1;
template <class Body>
bool Fork(Body body) {
int goFds[2] = {-1, -1};
int reportFds[2] = {-1, -1};
if (::pipe(goFds) != 0 || ::pipe(reportFds) != 0) return false;
std::fflush(nullptr);
pid = ::fork();
if (pid == 0) {
::close(goFds[1]);
::close(reportFds[0]);
char endpoint[512] = {};
const ssize_t got = ::read(goFds[0], endpoint, sizeof(endpoint) - 1);
if (got <= 0) ::_exit(0);
Report r{};
body(std::string(endpoint), r);
const ssize_t wrote = ::write(reportFds[1], &r, sizeof(r));
(void)wrote;
std::fflush(nullptr);
::_exit(0);
}
::close(goFds[0]);
::close(reportFds[1]);
go = goFds[1];
report = reportFds[0];
return pid > 0;
}
void Go(const std::string& endpoint) {
const ssize_t wrote = ::write(go, endpoint.c_str(), endpoint.size());
(void)wrote;
::close(go);
go = -1;
}
Report Wait() {
Report r{};
pollfd pfd{report, POLLIN, 0};
if (::poll(&pfd, 1, 90000) > 0) {
if (::read(report, &r, sizeof(r)) != static_cast<ssize_t>(sizeof(r))) {
r = Report{};
Note(r, "the client died before reporting");
}
} else {
Note(r, "the client timed out");
::kill(pid, SIGKILL);
}
::close(report);
int status = 0;
::waitpid(pid, &status, 0);
return r;
}
~HeldChild() {
if (go >= 0) ::close(go); // a child never released ends itself
}
};
struct ClientLog {
std::string base;
explicit ClientLog(const std::string& label)
: base("/tmp/mgl-comprec-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);
}
};
} // namespace
TEST(CompositorRecovery, ACompositorOnTheServerWindowRecoversOnAFreshSessionAndReimportsItsClientsImages) {
::signal(SIGPIPE, SIG_IGN);
int pair[2] = {-1, -1};
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair), 0);
ClientLog ownerLog("owner");
ClientLog compositorLog("compositor");
HeldChild<OwnerReport> owner;
ASSERT_TRUE(owner.Fork([&](const std::string& endpoint, OwnerReport& r) {
::close(pair[1]);
SetClientEnvironment(endpoint, ownerLog.base, /*serverWindow=*/false);
RunOwner(pair[0], r);
}));
HeldChild<CompositorReport> compositor;
ASSERT_TRUE(compositor.Fork([&](const std::string& endpoint, CompositorReport& r) {
::close(pair[0]);
SetClientEnvironment(endpoint, compositorLog.base, /*serverWindow=*/true);
RunCompositor(pair[1], r);
}));
::close(pair[0]);
::close(pair[1]);
ServerProcess server;
::setenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID", std::to_string(compositor.pid).c_str(), 1);
const bool launched = LaunchDisplayServer(&server, "kwin");
::unsetenv("MOBILEGL_DEBUG_INJECT_DEVICE_LOST_PID");
ASSERT_TRUE(launched) << server.Log();
owner.Go(server.endpoint);
compositor.Go(server.endpoint);
const CompositorReport c = compositor.Wait();
const OwnerReport o = owner.Wait();
const std::string diagnostics = "server:\n" + server.Log() + "\ncompositor:\n" + compositorLog.Text() +
"\nowner:\n" + ownerLog.Text();
// The compositor, before the loss: the server's window, an imported client image, a frame.
ASSERT_EQ(c.received, 1) << c.note << "\n" << o.note << "\n" << diagnostics;
ASSERT_EQ(c.surfaceOk, 1) << c.note << "\n" << diagnostics;
EXPECT_EQ(c.width, static_cast<Int32>(kWindowWidth));
EXPECT_EQ(c.height, static_cast<Int32>(kWindowHeight));
ASSERT_EQ(c.firstCurrent, 1) << c.note << "\n" << diagnostics;
ASSERT_EQ(c.imported, 1) << c.note << "\n" << diagnostics;
EXPECT_EQ(c.importedId, o.imageId) << "the compositor imported some other image";
EXPECT_EQ(c.firstBindError, static_cast<Uint32>(GL_NO_ERROR)) << std::hex << c.firstBindError;
EXPECT_EQ(c.firstPixel, kGreen) << std::hex << c.firstPixel << "\n" << diagnostics;
EXPECT_EQ(c.firstSwap, EGL_TRUE);
// The loss, reported.
ASSERT_EQ(c.lossSeen, 1) << c.note << "\n" << diagnostics;
EXPECT_EQ(c.resetStatus, static_cast<Uint32>(GL_UNKNOWN_CONTEXT_RESET)) << std::hex << c.resetStatus;
EXPECT_EQ(c.lostSwap, EGL_FALSE);
EXPECT_EQ(c.lostSwapError, EGL_CONTEXT_LOST) << std::hex << c.lostSwapError;
// The restart: a fresh session, the server's window again - after the lost session let go of it.
ASSERT_EQ(c.newContextOk, 1) << c.note << "\n" << diagnostics;
ASSERT_EQ(c.newCurrentOk, 1) << "the kept server-window surface did not go current on the fresh session (EGL "
"error 0x"
<< std::hex << c.newCurrentError << ")\n"
<< c.note << "\n"
<< diagnostics;
EXPECT_EQ(c.resetStatusNew, static_cast<Uint32>(GL_NO_ERROR));
EXPECT_EQ(c.widthAfter, static_cast<Int32>(kWindowWidth));
EXPECT_EQ(c.heightAfter, static_cast<Int32>(kWindowHeight));
// The client's image: the lost session's EGLImage binds nothing any more, the buffer imports
// again as the same image, and destroying the stale EGLImage after that is silent.
EXPECT_EQ(c.staleBindError, static_cast<Uint32>(GL_INVALID_OPERATION))
<< "an EGLImage of the lost session was bound on the fresh one: 0x" << std::hex << c.staleBindError;
ASSERT_EQ(c.reimported, 1) << c.note << "\n" << diagnostics;
EXPECT_EQ(c.reimportedId, o.imageId);
EXPECT_EQ(c.recordsByStaleDestroy, 0) << "destroying the lost session's EGLImage released the fresh import's "
"reference on the new session";
EXPECT_EQ(c.freshBindError, static_cast<Uint32>(GL_NO_ERROR))
<< "the image imported again could not be bound: 0x" << std::hex << c.freshBindError;
EXPECT_EQ(c.freshPixel, kBlue) << std::hex << c.freshPixel << "\n" << diagnostics;
EXPECT_EQ(c.freshSwap, EGL_TRUE);
EXPECT_EQ(c.resetStatusEnd, static_cast<Uint32>(GL_NO_ERROR)) << "the fresh session was lost too";
// The owner: untouched.
ASSERT_EQ(o.initialized, 1) << o.note << "\n" << diagnostics;
ASSERT_EQ(o.allocated, 1) << o.note;
EXPECT_EQ(o.sent, 1);
EXPECT_EQ(o.sawDone, 1) << "the compositor never finished";
EXPECT_EQ(o.statusAfter, static_cast<Uint32>(GL_NO_ERROR)) << "the owner's session was lost with the compositor's";
EXPECT_EQ(o.pixelAfter, kRed) << std::hex << o.pixelAfter;
EXPECT_EQ(o.stillIdentified, 1) << "the owner's image is gone from the server";
// The server's side: the window went to the compositor twice, and only ever at the size the
// compositor asked for (the window's own).
ASSERT_TRUE(WaitFor([&] { return Count(server.Log(), "reaped exit=") == 3; }, 10000)) << server.Log();
const std::string log = server.Log();
EXPECT_EQ(Count(log, "owner=server (ServerOwned"), 2u) << log;
EXPECT_EQ(Count(log, "SessionLatch{"), 1u) << log;
EXPECT_NE(log.find("injected:pid=" + std::to_string(compositor.pid)), std::string::npos) << log;
EXPECT_EQ(log.find("Fatal{ProtocolCorruption"), std::string::npos) << log;
EXPECT_EQ(Count(log, "reaped exit=75"), 1u) << log;
EXPECT_EQ(Count(log, "reaped exit=0"), 2u) << log;
const std::string geometry = ReadFile(server.logBase + ".geometry");
EXPECT_EQ(Count(geometry, "\n"), 2u) << "geometry requests:\n" << geometry;
EXPECT_EQ(Count(geometry, "0 0\n"), Count(geometry, "\n"))
<< "the window was asked for a fixed size - re-creating the surface pinned the window's extent:\n"
<< geometry;
server.Stop();
std::error_code ec;
if (!::testing::Test::HasFailure()) std::filesystem::remove(server.logBase + ".geometry", ec);
}
@@ -395,6 +395,90 @@ TEST(ServerDisplayTest, AWindowLeasedToOneHolderIsNotLeasedToAnother) {
display.EndLease(&first);
}
// A LEASE WHOSE SESSION IS ENDING IS WAITED FOR. A compositor whose session was lost re-creates its
// window surface on a fresh session at once, while the lost session's apply thread is still tearing
// its backend down: the fresh session's request waits for that lease to end and then gets the window,
// where it used to be refused (LeasedElsewhere) - and the compositor could never draw again. Red with
// AcquireFor ignoring the mark: the second holder is refused at once.
TEST(ServerDisplayTest, ALeaseWhoseSessionIsEndingIsWaitedForAndThenHandedOver) {
FakePlatform platform;
ServerDisplay display;
display.Install(HooksFor(platform));
display.Attach(FakeWindow(0), 64, 48);
int lost = 0;
int fresh = 0;
ServerWindowLease lease;
ASSERT_EQ(display.AcquireFor(0, 0, 1000, &lost, nullptr, nullptr, nullptr, &lease), ServerWindowAcquire::Acquired);
display.MarkLeaseEnding(&lost);
EXPECT_TRUE(display.LeaseEnding());
std::thread teardown([&] {
// The lost session's backend lets go of the window, then its lease ends.
std::this_thread::sleep_for(std::chrono::milliseconds(300));
display.EndLease(&lost);
});
const auto started = std::chrono::steady_clock::now();
ServerWindowLease freshLease;
EXPECT_EQ(display.AcquireFor(0, 0, 5000, &fresh, nullptr, nullptr, nullptr, &freshLease),
ServerWindowAcquire::Acquired);
const auto waited =
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - started).count();
teardown.join();
EXPECT_GE(waited, 250) << "the window was handed over before the ending lease ended";
EXPECT_EQ(freshLease.window, FakeWindow(0));
EXPECT_EQ(freshLease.width, 64u);
EXPECT_EQ(freshLease.height, 48u);
EXPECT_TRUE(display.Leased());
EXPECT_FALSE(display.LeaseEnding()) << "the new holder's lease inherited the old one's mark";
// The new lease is exclusive again: a third holder is refused at once.
int third = 0;
const auto refusedAt = std::chrono::steady_clock::now();
EXPECT_EQ(display.AcquireFor(0, 0, 2000, &third, nullptr, nullptr, nullptr, &lease),
ServerWindowAcquire::LeasedElsewhere);
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - refusedAt)
.count(),
500);
display.EndLease(&fresh);
EXPECT_EQ(platform.releases.load(), 0) << "a lease that changed hands released the window";
}
// The wait for an ending lease is bounded by the request's own timeout and then refused by name; a
// mark from a holder that holds no lease changes nothing; the mark goes with the lease it was on.
TEST(ServerDisplayTest, AnEndingLeaseIsWaitedForOnlyAsLongAsTheRequestWaitsAndTheMarkGoesWithIt) {
FakePlatform platform;
ServerDisplay display;
display.Install(HooksFor(platform));
display.Attach(FakeWindow(0), 10, 10);
int first = 0;
int second = 0;
ServerWindowLease lease;
ASSERT_EQ(display.AcquireFor(0, 0, 1000, &first, nullptr, nullptr, nullptr, &lease), ServerWindowAcquire::Acquired);
display.MarkLeaseEnding(&second);
EXPECT_FALSE(display.LeaseEnding()) << "a holder without the lease marked it ending";
display.MarkLeaseEnding(&first);
const auto started = std::chrono::steady_clock::now();
EXPECT_EQ(display.AcquireFor(0, 0, 200, &second, nullptr, nullptr, nullptr, &lease),
ServerWindowAcquire::LeasedElsewhere);
EXPECT_GE(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - started)
.count(),
150)
<< "the ending lease was not waited for";
display.EndLease(&first);
EXPECT_FALSE(display.Leased());
EXPECT_FALSE(display.LeaseEnding());
ASSERT_EQ(display.AcquireFor(0, 0, 1000, &first, nullptr, nullptr, nullptr, &lease), ServerWindowAcquire::Acquired);
EXPECT_FALSE(display.LeaseEnding()) << "the mark outlived the lease it was on";
const auto refusedAt = std::chrono::steady_clock::now();
EXPECT_EQ(display.AcquireFor(0, 0, 2000, &second, nullptr, nullptr, nullptr, &lease),
ServerWindowAcquire::LeasedElsewhere);
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - refusedAt)
.count(),
500);
display.EndLease(&first);
}
// The two ways a wait ends early, by name: the in-process server stopping (Interrupt), and the
// caller's own cancel predicate (ServerLoop's: the loop stopping, the session latched).
TEST(ServerDisplayTest, InterruptAndTheCancelPredicateEndAWaitByName) {
+66
View File
@@ -3335,6 +3335,72 @@ TEST(ServerLoopLatchTest, ALostServerWindowIsReleasedOnTheApplyThreadBeforeDetac
"latch; 16 = ServerWindowsLost() != 1; 32 = the apply thread stayed (64+ = setup)";
}
namespace {
// A SESSION THAT LATCHES WHILE IT HOLDS THE SERVER WINDOW (a compositor's device loss). Its client
// hears of the loss at once and re-creates its window surface on a fresh session, which asks the
// display for the window while the lost session still holds it - its apply thread has the
// backend to tear down first. The latch itself marks the lease ending (before the peer is told),
// so the fresh session's request waits for the lease and gets the window when the session ends.
enum : int {
kEndingNotLeased = 1, // the probe could not take the lease
kEndingNotMarked = 2, // the latch did not mark the lease ending
kEndingRefused = 4, // the fresh holder was refused instead of waiting
kEndingNotHandedOver = 8, // the fresh holder did not get the window once the session ended
kEndingReleased = 16, // the window's reference was released in the hand-over
};
int g_freshServerWindowHolder = 0;
[[noreturn]] void LatchWhileHoldingTheServerWindowAndExit() {
Remote::ArmSessionLatch();
ServerFixture fixture;
if (!fixture.Handshake() || !fixture.StartLoop()) ::_exit(64);
if (!fixture.WaitUntilTrulyParked()) ::_exit(65);
Server::ServerLoop& loop = Server::ServerLoopInstance();
Server::ServerDisplay& display = Server::ServerDisplayInstance();
display.Install(CountingWindowHooks::Hooks());
display.Attach(&g_fakeServerWindow, 64, 48);
ServerWindowProbe probe;
probe.width = 64;
probe.height = 48;
if (loop.RunProbeOnApplyThreadForTesting(&AcquireServerWindowOnTheApplyThread, &probe) != MOBILEGL_OK) ::_exit(66);
int failed = 0;
if (probe.rc != MOBILEGL_OK || probe.lease.window != &g_fakeServerWindow) failed |= kEndingNotLeased;
if (!fixture.WaitUntilTrulyParked()) ::_exit(67);
// The loss, latched by a thread of the session while its apply thread is parked.
(void)Remote::SessionLatch(Remote::MGFatalFamily::BackendDeviceLost,
"MGPipe: Fatal{BackendDeviceLost, \"test\"} - the session's device is lost while it "
"holds the server window");
if (!display.LeaseEnding()) failed |= kEndingNotMarked;
std::atomic<int> fresh{-1};
std::thread freshSession([&] {
Server::ServerWindowLease lease;
fresh.store(static_cast<int>(display.AcquireFor(0, 0, 5000, &g_freshServerWindowHolder, nullptr, nullptr,
nullptr, &lease)));
});
std::this_thread::sleep_for(std::chrono::milliseconds(200));
if (fresh.load() == static_cast<int>(Server::ServerWindowAcquire::LeasedElsewhere)) failed |= kEndingRefused;
// The session ends: its backend goes, then its lease.
fixture.Stop();
freshSession.join();
if (fresh.load() != static_cast<int>(Server::ServerWindowAcquire::Acquired)) failed |= kEndingNotHandedOver;
if (CountingWindowHooks::releases.load() != 0) failed |= kEndingReleased;
display.EndLease(&g_freshServerWindowHolder);
::_exit(failed);
}
} // namespace
// Red with the latch observer not installed (bits 2|4: the fresh session is refused at once, and a
// compositor whose session was lost could never draw again).
TEST(ServerLoopLatchTest, ALatchedSessionsWindowLeaseIsWaitedForAndHandedToAFreshSession) {
EXPECT_EXIT(LatchWhileHoldingTheServerWindowAndExit(), ::testing::ExitedWithCode(0), ".*")
<< "bits: 1 = the lease was not taken; 2 = the latch did not mark it ending; 4 = the fresh session was "
"refused; 8 = it did not get the window after the session ended; 16 = the window was released "
"(64+ = setup)";
}
namespace {
// P12 review fix (major): THE LOST WINDOW IS ANSWERED IN THE MIDDLE OF A BATCH. A client streaming
// frames keeps the command ring from emptying, and the first version answered a window-lost request