mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user