diff --git a/CMakeLists.txt b/CMakeLists.txt index 98d887b3..ddb29bf4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -655,6 +655,10 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam). MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp + # P12 (on-screen server window): the process-wide holder of the server's own window + # (the display Activity's SurfaceView on Android; nobody installs one on the host, so a + # ServerOwned request is refused by name there). + MobileGL/MG_Remote/Server/ServerDisplay.cpp # P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve # arms and the probe that measures the first against the second, shared by the # renderer's bring-up and the Vulkan POST. Wire-only - the monolith records neither arm - diff --git a/MobileGL/MG_Remote/FatalFamilies.def b/MobileGL/MG_Remote/FatalFamilies.def index fc41ae4f..6008a30b 100644 --- a/MobileGL/MG_Remote/FatalFamilies.def +++ b/MobileGL/MG_Remote/FatalFamilies.def @@ -51,6 +51,12 @@ // could not obtain a resource" value and adding one would move `wireFingerprint`, which is a // protocol change no wave-0 instrument is entitled to make (recorded for the integrator). // +// P12 (on-screen server window) ADDED ONE ROW, ServerWindowLost: the display server's window was +// destroyed (surfaceDestroyed) under a live on-screen session. The session latches by name after +// its surface is released, and the WAITING client is told what ServerCrashed already says - the +// server-side resource its session had to have is gone - so it reads a clean device loss. Not +// DeviceLost: that code is the latch's own, never a family's projection (see its bucket above). +// // Two words were NOT given rows, they were collapsed instead: `InitialBytesNotCarried` // (MG_Impl/Pipe/PipeFill.cpp) said the same thing as `UncarriedInitialBytes` (Client/ // WireTables.cpp) about the same record - a respecify that crossed with no initial bytes - and @@ -106,6 +112,7 @@ X(RetirementWaitFailed, ServerCrashed, "a wait for the server to retire staging did not complete") \ X(ApplyThreadJoinTimeout, ServerCrashed, "the apply thread did not join within its bound at teardown") \ X(ApplyThreadNotRunning, ServerCrashed, "an operation needed the apply thread and it was not running") \ - X(ResourceUnavailable, ServerCrashed, "the server could not obtain a native GL/Vulkan resource it had to have") + X(ResourceUnavailable, ServerCrashed, "the server could not obtain a native GL/Vulkan resource it had to have") \ + X(ServerWindowLost, ServerCrashed, "the server's own display window was destroyed under a live on-screen session (P12)") // clang-format on diff --git a/MobileGL/MG_Remote/Server/ServerDisplay.cpp b/MobileGL/MG_Remote/Server/ServerDisplay.cpp new file mode 100644 index 00000000..1cc526e2 --- /dev/null +++ b/MobileGL/MG_Remote/Server/ServerDisplay.cpp @@ -0,0 +1,298 @@ +// MobileGL - MobileGL/MG_Remote/Server/ServerDisplay.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 + +// P12 (on-screen server window), D3/D6. See ServerDisplay.h. + +#include "ServerDisplay.h" + +#include + +#include +#include +#include + +#if defined(__ANDROID__) +#include +#endif + +namespace MobileGL::MG_Remote::Server { + + namespace { + using Clock = std::chrono::steady_clock; + + // How often a waiting AcquireFor re-asks its cancel predicate. The predicate is the apply + // thread's own stop / latch state, which nobody signals through this object's condition. + constexpr auto kCancelPollInterval = std::chrono::milliseconds(50); + } // namespace + + const char* ServerWindowAcquireName(ServerWindowAcquire result) { + switch (result) { + case ServerWindowAcquire::Acquired: return "Acquired"; + case ServerWindowAcquire::NoDisplay: return "NoDisplay"; + case ServerWindowAcquire::NoWindow: return "NoWindow"; + case ServerWindowAcquire::Cancelled: return "Cancelled"; + case ServerWindowAcquire::Interrupted: return "Interrupted"; + case ServerWindowAcquire::LeasedElsewhere: return "LeasedElsewhere"; + } + return ""; + } + + const char* ServerWindowDetachName(ServerWindowDetach result) { + switch (result) { + case ServerWindowDetach::NoWindow: return "NoWindow"; + case ServerWindowDetach::Released: return "Released"; + case ServerWindowDetach::ReleasedBySession: return "ReleasedBySession"; + case ServerWindowDetach::TimedOut: return "TimedOut"; + } + return ""; + } + + void ServerDisplay::ReleaseOutsideLock(const ServerDisplayHooks& hooks, void* window) const { + if (window != nullptr && hooks.release != nullptr) hooks.release(hooks.user, window); + } + + void ServerDisplay::Install(const ServerDisplayHooks& hooks) { + { + const std::lock_guard lock(m_mutex); + m_hooks = hooks; + m_installed = true; + } + m_cv.notify_all(); + MGLOG_I("MG_Remote server: this process owns a display (ServerDisplay installed); a ServerOwned " + "window surface is served on its window"); + } + + void ServerDisplay::Uninstall(Uint32 detachTimeoutMs) { + (void)Detach(detachTimeoutMs); + { + const std::lock_guard lock(m_mutex); + m_installed = false; + // The hooks stay until every reference they took is released: a timed-out Detach may + // still owe EndLease a release through them. + if (m_releaseAfterLease.empty()) m_hooks = ServerDisplayHooks{}; + } + m_cv.notify_all(); + MGLOG_I("MG_Remote server: ServerDisplay uninstalled; this process owns no display"); + } + + Bool ServerDisplay::HasDisplay() const { + const std::lock_guard lock(m_mutex); + return m_installed; + } + + void ServerDisplay::Attach(void* window, Uint32 width, Uint32 height) { + if (window == nullptr) return; + std::unique_lock lock(m_mutex); + if (window == m_window) { + // surfaceChanged: the same window at a new extent. This is also how a geometry request + // is answered, so a waiting AcquireFor is woken. + m_width = width; + m_height = height; + lock.unlock(); + m_cv.notify_all(); + MGLOG_I("MG_Remote server: server window %p is now %ux%u", window, width, height); + return; + } + if (m_window != nullptr) { + // A new window while one is still attached - the platform skipped the destroy. The old + // one is detached exactly as surfaceDestroyed would have done it, bounded. + lock.unlock(); + const ServerWindowDetach detached = Detach(kDefaultDetachTimeoutMs); + MGLOG_W("MG_Remote server: server window %p attached while another was still attached; the " + "old one was detached first (%s)", + window, ServerWindowDetachName(detached)); + lock.lock(); + } + if (m_hooks.acquire != nullptr) m_hooks.acquire(m_hooks.user, window); + m_window = window; + m_width = width; + m_height = height; + ++m_generation; + const Uint64 generation = m_generation; + lock.unlock(); + m_cv.notify_all(); + MGLOG_I("MG_Remote server: server window %p attached at %ux%u (generation %llu)", window, width, height, + static_cast(generation)); + } + + ServerWindowDetach ServerDisplay::Detach(Uint32 timeoutMs) { + std::unique_lock lock(m_mutex); + if (m_window == nullptr) return ServerWindowDetach::NoWindow; + void* const window = m_window; + m_window = nullptr; + m_width = 0; + m_height = 0; + ++m_generation; + ServerWindowDetach result = ServerWindowDetach::Released; + if (m_leaseHolder != nullptr) { + // A live on-screen session renders into this window. Ask it to let go - the hook runs + // under this lock, so the holder cannot end its lease (and go away) between being read + // here and being called - then wait for EndLease, bounded. + if (!m_lostRequested) { + m_lostRequested = true; + if (m_onLost != nullptr) m_onLost(m_leaseHolder); + } + const Bool released = m_cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), + [this] { return m_leaseHolder == nullptr; }); + if (!released) { + // NOT RELEASED UNDER IT. The holder may still reference the window (Magma keeps the + // raw pointer until the swapchain is built), so the reference stays until it ends + // its lease. The UI thread is not held past its bound either way. + m_releaseAfterLease.push_back(window); + lock.unlock(); + m_cv.notify_all(); + MGLOG_E("MG_Remote server: surfaceDestroyed - the on-screen session did not release server " + "window %p within %u ms; its reference is kept until it does (Detach TimedOut)", + window, timeoutMs); + return ServerWindowDetach::TimedOut; + } + result = ServerWindowDetach::ReleasedBySession; + } + const ServerDisplayHooks hooks = m_hooks; + lock.unlock(); + m_cv.notify_all(); + ReleaseOutsideLock(hooks, window); + MGLOG_I("MG_Remote server: server window %p detached (%s)", window, ServerWindowDetachName(result)); + return result; + } + + ServerWindowAcquire ServerDisplay::AcquireFor(Uint32 width, Uint32 height, Uint32 timeoutMs, void* holder, + ServerWindowLostHook onLost, ServerWindowWaitCancel cancel, + void* cancelUser, ServerWindowLease* out) { + ServerDisplayHooks hooks{}; + Uint64 interrupts = 0; + { + const std::lock_guard lock(m_mutex); + if (!m_installed) return ServerWindowAcquire::NoDisplay; + if (m_leaseHolder != nullptr && m_leaseHolder != holder) return ServerWindowAcquire::LeasedElsewhere; + hooks = m_hooks; + interrupts = m_interrupts; + } + // The geometry request is an up-call into the platform (Java), made without the lock: its + // answer comes back as an Attach() of the same window, which needs the lock. + if (hooks.requestGeometry != nullptr) hooks.requestGeometry(hooks.user, width, height); + const Bool sizeRequested = width != 0 && height != 0; + + std::unique_lock lock(m_mutex); + const auto deadline = Clock::now() + std::chrono::milliseconds(timeoutMs); + Bool attachedSeen = 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; + const auto now = Clock::now(); + if (m_window != nullptr) { + if (!attachedSeen) { + attachedSeen = true; + geometryDeadline = std::min(deadline, now + std::chrono::milliseconds(kGeometryGraceMs)); + } + const Bool sizeOk = !sizeRequested || (m_width == width && m_height == height); + if (sizeOk || now >= geometryDeadline) { + m_leaseHolder = holder; + m_onLost = onLost; + m_lostRequested = false; + if (out != nullptr) { + out->window = m_window; + out->width = m_width; + out->height = m_height; + out->generation = m_generation; + out->sizeAsRequested = sizeOk; + } + if (!sizeOk) { + MGLOG_W("MG_Remote server: the server window is %ux%u, not the %ux%u requested, after " + "%u ms of geometry grace; the surface is created at the window's size and the " + "client is told so", + m_width, m_height, width, height, kGeometryGraceMs); + } + return ServerWindowAcquire::Acquired; + } + } else { + attachedSeen = false; + geometryDeadline = deadline; + } + if (now >= deadline) return ServerWindowAcquire::NoWindow; + const auto wake = std::min({deadline, geometryDeadline, now + kCancelPollInterval}); + m_cv.wait_until(lock, wake); + } + } + + void ServerDisplay::EndLease(void* holder) { + std::vector releases; + ServerDisplayHooks hooks{}; + { + const std::lock_guard lock(m_mutex); + if (holder == nullptr || m_leaseHolder != holder) return; + m_leaseHolder = nullptr; + m_onLost = nullptr; + m_lostRequested = false; + releases.swap(m_releaseAfterLease); + hooks = m_hooks; + // An Uninstall that ran while this lease still owed releases kept the hooks for them. + if (!m_installed) m_hooks = ServerDisplayHooks{}; + } + m_cv.notify_all(); + for (void* window : releases) { + ReleaseOutsideLock(hooks, window); + MGLOG_I("MG_Remote server: server window %p released after its session let go (a late Detach)", + window); + } + } + + void ServerDisplay::Interrupt() { + { + const std::lock_guard lock(m_mutex); + ++m_interrupts; + } + m_cv.notify_all(); + } + + Bool ServerDisplay::Attached() const { + const std::lock_guard lock(m_mutex); + return m_window != nullptr; + } + + Bool ServerDisplay::Leased() const { + const std::lock_guard lock(m_mutex); + return m_leaseHolder != nullptr; + } + + Uint64 ServerDisplay::Generation() const { + const std::lock_guard lock(m_mutex); + return m_generation; + } + + ServerDisplay& ServerDisplayInstance() { + // ID-8: leak at exit, like every MG_Remote singleton. + static ServerDisplay& instance = *new ServerDisplay{}; + return instance; + } + +#if defined(__ANDROID__) + namespace { + void AcquireAndroidWindow(void*, void* window) { + ANativeWindow_acquire(static_cast(window)); + } + void ReleaseAndroidWindow(void*, void* window) { + ANativeWindow_release(static_cast(window)); + } + } // namespace + + ServerDisplayHooks AndroidNativeWindowHooks(void (*requestGeometry)(void* user, Uint32 width, Uint32 height), + void* user) { + ServerDisplayHooks hooks; + hooks.acquire = &AcquireAndroidWindow; + hooks.release = &ReleaseAndroidWindow; + hooks.requestGeometry = requestGeometry; + hooks.user = user; + return hooks; + } +#endif + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/ServerDisplay.h b/MobileGL/MG_Remote/Server/ServerDisplay.h new file mode 100644 index 00000000..f761db61 --- /dev/null +++ b/MobileGL/MG_Remote/Server/ServerDisplay.h @@ -0,0 +1,175 @@ +// MobileGL - MobileGL/MG_Remote/Server/ServerDisplay.h +// 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 + +// P12 (on-screen server window), D3/D6. THE SERVER'S OWN WINDOW, AS ONE PROCESS-WIDE HOLDER. +// +// WHAT IT IS FOR. A client that sets MOBILEGL_IPC_SURFACE=server is headless: it asks the server to +// create its window surface on the SERVER's window (WindowKind::ServerOwned) and learns the +// surface's geometry back from the server. That window belongs to the in-process display server - +// on Android the display Activity's SurfaceView, handed in through JNI (the next stage's glue) - +// and this class is where it is held between the UI thread that owns the window's lifecycle +// (surfaceCreated / surfaceChanged / surfaceDestroyed) and the apply thread that renders into it. +// +// WHO HAS A DISPLAY. Nobody, until Install(): the Linux host server, the exec'd supervisor and +// every session child it forks never call it, so HasDisplay() is false there and a ServerOwned +// request is refused by name (ServerLoop.cpp's ServerOwned arm, SurfaceRefusal::NoServerDisplay). +// Only the in-process display server installs one. +// +// THE WINDOW IS OPAQUE HERE. It is a `void*` with three hooks - acquire and release (Android: +// ANativeWindow_acquire / ANativeWindow_release) and a geometry request (the native -> Java +// up-call: SurfaceHolder.setFixedSize(w, h) for w, h > 0, setSizeFromLayout() for 0/0). That is +// what lets the host unit tests drive attach / wait / detach with a fake handle, and it keeps this +// file free of any platform header. +// +// THE LEASE. The apply thread that substitutes the window into a backend takes a LEASE on it +// (AcquireFor) and names a hook that asks it to let go. Detach() - the surfaceDestroyed path - +// calls that hook and then BLOCKS, bounded, until the holder has released its backend surface and +// ended the lease (EndLease); only then is the window's reference released. That ordering is the +// GLSurfaceView contract: after surfaceDestroyed returns nothing may render into the window. A +// holder that does not answer in time does not get the window pulled out from under it - the +// 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. +// +// THREADS. Every method is thread-safe. The hooks are called from the thread that called the +// method: acquire from Attach's caller, release from Detach's / EndLease's caller, requestGeometry +// from the apply thread (AcquireFor), the lost hook from Detach's caller WITH THIS OBJECT'S LOCK +// HELD - so it must not block and must not call back into this object (ServerLoop's sets an atomic +// and rings its doorbell). + +#pragma once +#include + +#include +#include +#include + +namespace MobileGL::MG_Remote::Server { + + struct ServerDisplayHooks { + // Take / drop one reference to `window` (ANativeWindow_acquire / ANativeWindow_release). + void (*acquire)(void* user, void* window) = nullptr; + void (*release)(void* user, void* window) = nullptr; + // Ask the platform for a buffer geometry: w, h > 0 fixes it (setFixedSize), 0/0 hands it + // back to the layout (setSizeFromLayout). Asynchronous: the new size arrives later as an + // Attach() of the same window with the new extent (surfaceChanged). May be null. + void (*requestGeometry)(void* user, Uint32 width, Uint32 height) = nullptr; + void* user = nullptr; + }; + + // Called with the display's lock held: must not block, must not re-enter the display. + using ServerWindowLostHook = void (*)(void* holder); + // Asked about every 50 ms while AcquireFor waits; true ends the wait (Cancelled). + using ServerWindowWaitCancel = Bool (*)(void* user); + + struct ServerWindowLease { + void* window = nullptr; + Uint32 width = 0; // the window's extent when the lease was taken + Uint32 height = 0; + Uint64 generation = 0; + // False when a size was requested and the window had not reached it within the geometry + // grace: the lease is still granted, at the window's own size (logged by name). + Bool sizeAsRequested = true; + }; + + enum class ServerWindowAcquire : Uint8 { + Acquired, + NoDisplay, // this process owns no display (HasDisplay() is false) + NoWindow, // a display, but no window was attached within the wait + Cancelled, // the caller's cancel predicate said so + Interrupted, // Interrupt() (the in-process server is stopping) + LeasedElsewhere, // another holder has the window (sessions are sequential: a bug if seen) + }; + + enum class ServerWindowDetach : Uint8 { + NoWindow, // nothing was attached + Released, // no session held the window; its reference is released + ReleasedBySession, // the holding session released its surface in time, then the reference went + TimedOut, // the holder did not answer within the bound; released when it does + }; + + const char* ServerWindowAcquireName(ServerWindowAcquire result); + const char* ServerWindowDetachName(ServerWindowDetach result); + + class ServerDisplay { + public: + // The apply thread's wait for a window (D3: bounded, ~10 s) and the UI thread's wait for the + // holder to let go (D6: bounded, ~3 s). Once a window is attached, a requested size gets + // kGeometryGraceMs more to arrive before the lease is granted at the window's own size. + static constexpr Uint32 kDefaultAcquireTimeoutMs = 10000; + static constexpr Uint32 kDefaultDetachTimeoutMs = 3000; + static constexpr Uint32 kGeometryGraceMs = 2000; + + ServerDisplay() = default; + ServerDisplay(const ServerDisplay&) = delete; + ServerDisplay& operator=(const ServerDisplay&) = delete; + + // This process owns a display from here on (the in-process display server, once, before it + // serves). Re-installing replaces the hooks. + void Install(const ServerDisplayHooks& hooks); + // The display is gone: detach whatever is attached (bounded, as Detach), wake every waiter + // (NoDisplay), and forget the hooks. + void Uninstall(Uint32 detachTimeoutMs = kDefaultDetachTimeoutMs); + Bool HasDisplay() const; + + // surfaceCreated / surfaceChanged: `window` with its current buffer extent. The SAME window + // again only updates the extent (and wakes a geometry wait); a DIFFERENT window first + // detaches the old one (bounded) and then takes a reference to the new one. + void Attach(void* window, Uint32 width, Uint32 height); + // surfaceDestroyed. See the header block: asks the lease holder to release, waits up to + // `timeoutMs`, then releases the window's reference (or leaves that to EndLease on timeout). + ServerWindowDetach Detach(Uint32 timeoutMs = kDefaultDetachTimeoutMs); + + // The apply thread's side. Requests the geometry (w, h; 0/0 = the window's own), then waits + // until a window is attached - and, for a requested size, has reached it or the geometry + // grace ran out - and leases it to `holder`, whose `onLost` Detach will call. The same holder + // may lease again while it holds the lease (a surface re-created at a new size). + ServerWindowAcquire AcquireFor(Uint32 width, Uint32 height, Uint32 timeoutMs, void* holder, + ServerWindowLostHook onLost, ServerWindowWaitCancel cancel, + void* cancelUser, ServerWindowLease* out); + // The holder has released everything that referenced the window (its backend surface, or the + // 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); + // Wakes every AcquireFor waiter with Interrupted (the in-process server is stopping). + void Interrupt(); + + // Diagnostics, for logs and tests. + Bool Attached() const; + Bool Leased() const; + Uint64 Generation() const; + + private: + void ReleaseOutsideLock(const ServerDisplayHooks& hooks, void* window) const; + + mutable std::mutex m_mutex; + std::condition_variable m_cv; + ServerDisplayHooks m_hooks{}; + Bool m_installed = false; + void* m_window = nullptr; + Uint32 m_width = 0; + Uint32 m_height = 0; + Uint64 m_generation = 0; + Uint64 m_interrupts = 0; + void* m_leaseHolder = nullptr; + ServerWindowLostHook m_onLost = nullptr; + Bool m_lostRequested = false; + // References a timed-out Detach could not release yet; EndLease releases them. + std::vector m_releaseAfterLease; + }; + + // The process's one display (leaked at exit, like every MG_Remote singleton). + ServerDisplay& ServerDisplayInstance(); + +#if defined(__ANDROID__) + // The ANativeWindow half of the hooks (acquire / release); the JNI glue supplies the geometry + // up-call and its user pointer. Android-only: compiled by the APK build, never on the host. + ServerDisplayHooks AndroidNativeWindowHooks(void (*requestGeometry)(void* user, Uint32 width, Uint32 height), + void* user); +#endif + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index 79160095..f3c4e675 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -226,6 +226,12 @@ namespace MobileGL::MG_Remote::Server { m_controlSeq.store(0, std::memory_order_release); m_controlFramesDispatched.store(0, std::memory_order_release); m_haveCurrentTuple = false; + // P12 (D4, D6): a new session decides its own surface mode, holds no window lease yet and + // has no window-lost request pending. The apply thread that reads these is not started yet. + m_surfaceMode = SessionSurfaceMode::None; + m_holdsWindowLease = false; + m_windowLostRequested.store(false, std::memory_order_release); + m_serverWindowsLost.store(0, std::memory_order_release); { const std::lock_guard lock(m_exitMutex); m_exited = false; @@ -415,8 +421,11 @@ namespace MobileGL::MG_Remote::Server { const auto stopOrForfeit = [this, &session] { return m_stopRequested.load(std::memory_order_acquire) || session.ReverseChannelForfeited(); }; + // P12 (D6) ADDS ONE MORE WAKE, BESIDE CONTROL: a window-lost request from ServerDisplay:: + // Detach (the UI thread's surfaceDestroyed, which is blocked until this thread answers). const auto ready = [this, signals, &ring, &stopOrForfeit] { - if (stopOrForfeit() || ControlIsPending()) return true; + if (stopOrForfeit() || ControlIsPending() || m_windowLostRequested.load(std::memory_order_acquire)) + return true; if (signals.EventRingFull->load(std::memory_order_acquire) != 0) return false; return signals.CmdHead->load(std::memory_order_acquire) != ring.LocalTail(); }; @@ -520,6 +529,11 @@ namespace MobileGL::MG_Remote::Server { "which is the context owner"); m_backend.reset(); } + // P12 (D6): the backend - and with it every surface on the server's window - is gone, so the + // session's lease on that window ends here, AFTER the reset. A Detach waiting on it returns + // now; one that arrives later finds no lease and releases the window at once. + m_windowLostRequested.store(false, std::memory_order_release); + EndServerWindowLease(); MG_Pipe::MGPipeServerSetContextLive(false); // N-3: the context died with the backend; a tuple that outlives it would make the next // session's first make-current onto the same (recycled) handle values a RepeatNoOp. @@ -588,6 +602,10 @@ namespace MobileGL::MG_Remote::Server { } Bool ServerLoop::PumpControlRequest() { + // P12 (D6): A LOST SERVER WINDOW IS ANSWERED FIRST, before any frame this pump would run - + // a frame taken now might render into the window the UI thread is waiting to destroy. One + // relaxed-cost load on the idle poll, beside the shadow's. + if (m_windowLostRequested.load(std::memory_order_acquire)) ReleaseLostServerWindow(); // THE IDLE POLL'S FAST NEGATIVE. The loop calls this once per iteration whether or not // anything was posted, and the common answer is "nothing". Taking m_controlMutex to // learn that was the other half of package T's finding; the shadow answers it with a @@ -924,6 +942,8 @@ namespace MobileGL::MG_Remote::Server { // thread called Stop. No context was ever made current from another thread in that // case, which is exactly the condition that makes this safe. if (m_backend != nullptr) m_backend.reset(); + // P12 (D6): as on the apply thread's own exit - no thread runs, so nothing else ends it. + EndServerWindowLease(); MG_Pipe::MGPipeServerSetContextLive(false); // N-3, same reason as ApplyThreadMain's exit: no thread runs, so the apply-thread-only // rule on the tuple has no other writer to race. @@ -1124,6 +1144,9 @@ namespace MobileGL::MG_Remote::Server { return MOBILEGL_OK; } case SurfaceControlOp::CreateWindowSurface: { + // P12 (D3): the SERVER's window, asked for by a headless client. Before the unpack, + // which would refuse the frame-local tag as a WindowBackend out of range. + if (frame.windowBackend == kServerOwnedWindowBackend) return ApplyServerOwnedWindowSurface(backend, frame); MG_Backend::WindowHandle window; if (!UnpackWindowHandle(frame, &window)) { frame.ok = false; @@ -1142,12 +1165,29 @@ namespace MobileGL::MG_Remote::Server { static_cast(frame.height)); return MOBILEGL_OK; case SurfaceControlOp::CreatePbufferSurface: + // P12 (D4): a pbuffer in a session that went on-screen is the other mode, refused by + // name and not latched - the session and its window surface carry on. + if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/false)) { + frame.ok = false; + frame.refusal = static_cast(SurfaceRefusalCode::SurfaceModeMismatch); + MGLOG_E("MG_Remote server: SurfaceModeMismatch - CreatePbufferSurface (seq %llu, %dx%d) in a " + "session whose surface mode is %s: its first surface was a server-owned window, and " + "only one rendering path is active per session. Refused, the session is not latched", + static_cast(frame.seq), frame.width, frame.height, + SessionSurfaceModeName(m_surfaceMode)); + return MOBILEGL_ERR_INVALID_ARGUMENT; + } frame.ok = backend->CreateEGLPbufferSurface(HandleFromToken(frame.surface), frame.width, frame.height); // N-3: as for the window surface - a (re)creation may have destroyed the context the // held tuple named. The surface's own creation binds natively, so the client's // make-current that follows is forwarded and deduped one layer down (ID-54). - if (frame.ok) ForgetCurrentTuple(); + if (frame.ok) { + ForgetCurrentTuple(); + if (m_surfaceMode == SessionSurfaceMode::None) m_surfaceMode = SessionSurfaceMode::Offscreen; + // P12: THE ARM PROOF (ID-124), once per surface: which of the two paths ran. + MGLOG_I("MG_Remote server: surface=pbuffer %dx%d", frame.width, frame.height); + } return MOBILEGL_OK; case SurfaceControlOp::MakeCurrent: { // C7 / ID-54: the apply thread binds the native context ONCE per context lifetime @@ -1282,6 +1322,160 @@ namespace MobileGL::MG_Remote::Server { return MOBILEGL_ERR_PROTOCOL_MISMATCH; } + // --------------------------------------------------------------------------------- + // P12 (on-screen server window): the ServerOwned arm, the surface mode, the lost window + // --------------------------------------------------------------------------------- + + const char* SessionSurfaceModeName(SessionSurfaceMode mode) { + switch (mode) { + case SessionSurfaceMode::None: return "undecided"; + case SessionSurfaceMode::OnScreen: return "on-screen"; + case SessionSurfaceMode::Offscreen: return "offscreen"; + } + return ""; + } + + Bool SessionSurfaceModeAdmits(SessionSurfaceMode current, Bool serverOwnedWindow) { + switch (current) { + case SessionSurfaceMode::None: return true; + case SessionSurfaceMode::OnScreen: return serverOwnedWindow; + case SessionSurfaceMode::Offscreen: return !serverOwnedWindow; + } + return false; + } + + void ServerLoop::ServerWindowLostThunk(void* self) { + auto* loop = static_cast(self); + // Publish, then ring (the park predicate reads the flag; Doorbell.h's order). Called under + // the display's lock while this loop holds its lease - and the lease ends before the apply + // thread exits and before Stop() lets go of m_session - so the session is still here. + loop->m_windowLostRequested.store(true, std::memory_order_release); + if (loop->m_session != nullptr && loop->m_session->DataLink() != nullptr) { + loop->m_session->DataLink()->ConsumerBell().Notify(); + } + } + + Bool ServerLoop::ServerWindowWaitCancelled(void* self) { + const auto* loop = static_cast(self); + // A window-lost request also ends the wait: this thread holds a lease on a window that is + // being destroyed, and the UI thread is blocked until it answers (PumpControlRequest). + return loop->m_stopRequested.load(std::memory_order_acquire) || SessionLatched() || + loop->m_windowLostRequested.load(std::memory_order_acquire); + } + + MobileGLResult ServerLoop::AcquireServerWindow(Uint32 width, Uint32 height, Uint32 timeoutMs, + ServerWindowLease* out, SurfaceRefusalCode* refusal) { + ServerDisplay& display = ServerDisplayInstance(); + const ServerWindowAcquire acquired = display.AcquireFor(width, height, timeoutMs, this, &ServerWindowLostThunk, + &ServerWindowWaitCancelled, this, out); + switch (acquired) { + case ServerWindowAcquire::Acquired: + m_holdsWindowLease = true; + return MOBILEGL_OK; + case ServerWindowAcquire::NoDisplay: + // A CONFIGURATION ANSWER, NOT A LATCH: the client asked an offscreen server (the exec'd + // supervisor's session child, or any host server) for a window it does not have. + if (refusal != nullptr) *refusal = SurfaceRefusalCode::NoServerDisplay; + MGLOG_E("MG_Remote server: Refuse ServerOwned: this server owns no display - a client " + "(MOBILEGL_IPC_SURFACE=server) asked for a %ux%u window surface on the server's own window, " + "and this process is an offscreen server. Start the on-screen display server, or run the " + "client offscreen. Refused (NoServerDisplay); the session is not latched", + width, height); + return MOBILEGL_ERR_UNSUPPORTED; + case ServerWindowAcquire::NoWindow: + case ServerWindowAcquire::Cancelled: + case ServerWindowAcquire::Interrupted: + case ServerWindowAcquire::LeasedElsewhere: + break; + } + if (refusal != nullptr) *refusal = SurfaceRefusalCode::NoServerWindow; + MGLOG_E("MG_Remote server: Refuse ServerOwned: this server owns a display but no window for a %ux%u surface " + "(%s after at most %u ms) - the display's surface is not up. Refused (NoServerWindow); the " + "session is not latched", + width, height, ServerWindowAcquireName(acquired), timeoutMs); + return MOBILEGL_ERR_TIMEOUT; + } + + MobileGLResult ServerLoop::ApplyServerOwnedWindowSurface(MG_Backend::BackendObject* backend, + SurfaceControlFrame& frame) { + frame.ok = false; + frame.refusal = static_cast(SurfaceRefusalCode::None); + const Uint32 wantWidth = frame.width > 0 ? static_cast(frame.width) : 0u; + const Uint32 wantHeight = frame.height > 0 ? static_cast(frame.height) : 0u; + // D4 FIRST: an offscreen session may not go on-screen, display or no display. + if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/true)) { + frame.refusal = static_cast(SurfaceRefusalCode::SurfaceModeMismatch); + MGLOG_E("MG_Remote server: SurfaceModeMismatch - a ServerOwned CreateWindowSurface (seq %llu, %ux%u) in a " + "session whose surface mode is %s: its first surface was a pbuffer, and only one rendering " + "path is active per session. Refused, the session is not latched", + static_cast(frame.seq), wantWidth, wantHeight, + SessionSurfaceModeName(m_surfaceMode)); + return MOBILEGL_ERR_INVALID_ARGUMENT; + } + ServerWindowLease lease; + SurfaceRefusalCode refusal = SurfaceRefusalCode::None; + const MobileGLResult acquired = AcquireServerWindow(wantWidth, wantHeight, kServerWindowWaitMs, &lease, &refusal); + if (acquired != MOBILEGL_OK) { + frame.refusal = static_cast(refusal); + return acquired; + } + // THE SUBSTITUTION. The client named no window; the server's own goes into the SAME backend + // call monolith Android makes (Register -> Activate -> SetWindowHandle -> InitWindowSurface), + // so nothing below this line knows the window was not the client's. Android is the only + // platform that installs a display (the host never does), hence the backend tag. + MG_Backend::WindowHandle window; + window.Backend = MG_Backend::WindowBackend::Android; + window.Handle = lease.window; + window.Width = lease.width; + window.Height = lease.height; + frame.ok = backend->CreateEGLWindowSurface(HandleFromToken(frame.surface), 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", + lease.window, lease.width, lease.height, static_cast(frame.seq)); + // A session that never got a surface on the window has nothing to release: the lease goes. + if (m_surfaceMode != SessionSurfaceMode::OnScreen) EndServerWindowLease(); + return MOBILEGL_OK; + } + // N-3: as for any window surface - a (re)creation may have destroyed the held context. + ForgetCurrentTuple(); + m_surfaceMode = SessionSurfaceMode::OnScreen; + // The reply's geometry (SurfaceReply.width/height): the window's real extent, which is what + // the headless client's eglQuerySurface answers from here on. + frame.width = static_cast(lease.width); + frame.height = static_cast(lease.height); + // THE ARM PROOF (ID-124), once per surface: which of the two paths ran, and at what size. + MGLOG_I("MG_Remote server: surface=window %ux%u owner=server (ServerOwned seq %llu, window %p generation %llu%s)", + lease.width, lease.height, static_cast(frame.seq), lease.window, + static_cast(lease.generation), + lease.sizeAsRequested ? "" : ", NOT the size the client asked for"); + return MOBILEGL_OK; + } + + void ServerLoop::EndServerWindowLease() { + if (!m_holdsWindowLease) return; + m_holdsWindowLease = false; + ServerDisplayInstance().EndLease(this); + } + + void ServerLoop::ReleaseLostServerWindow() { + if (!m_windowLostRequested.exchange(false, std::memory_order_acq_rel)) return; + if (!m_holdsWindowLease) return; // raced with the session's own end; nothing is held + // THE ORDER IS THE CONTRACT (D6): the backend lets go of the window first - Espryt destroys + // its EGL surface and context, Magma its swapchain and VkSurface - then the session latches + // by name, and only then does the lease end, which is what lets surfaceDestroyed return. + if (m_backend != nullptr) m_backend->ReleaseEGLResources(); + MG_Pipe::MGPipeServerSetContextLive(false); + ForgetCurrentTuple(); + m_serverWindowsLost.fetch_add(1, std::memory_order_acq_rel); + (void)SessionLatch(MGFatalFamily::ServerWindowLost, + "MGPipe: Fatal{ServerWindowLost, \"surfaceDestroyed\"} - the server's own display " + "window was destroyed under this on-screen session; its surface was released on " + "mgl-srv-apply before the window went, and the session ends so the client reads a " + "clean device loss. The display server keeps listening"); + EndServerWindowLease(); + } + // --------------------------------------------------------------------------------- // The twelve forwarders: pack the frame, post it, unpack the reply // --------------------------------------------------------------------------------- @@ -1320,16 +1514,38 @@ namespace MobileGL::MG_Remote::Server { return rc == MOBILEGL_OK && frame.ok; } - Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) { + Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height, SurfaceRefusalCode* refusal) { SurfaceControlFrame frame; frame.kind = SurfaceControlOp::CreatePbufferSurface; frame.surface = TokenFromHandle(surface); frame.width = width; frame.height = height; const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + if (refusal != nullptr) *refusal = static_cast(frame.refusal); return rc == MOBILEGL_OK && frame.ok; } + ServerOwnedWindowReply ServerCreateServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) { + // P12 (D1/D2): ONE frame, and no SetWindowHandle in front of it - the window is the + // server's, so there is nothing of the client's to name. The frame-local tag is what the + // codec turns into WindowKind::ServerOwned with token 0. + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::CreateWindowSurface; + frame.surface = TokenFromHandle(surface); + frame.windowBackend = kServerOwnedWindowBackend; + frame.nativeToken = 0; + frame.width = static_cast(width); + frame.height = static_cast(height); + const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + ServerOwnedWindowReply reply; + reply.transport = rc; + reply.ok = rc == MOBILEGL_OK && frame.ok; + reply.refusal = static_cast(frame.refusal); + reply.width = frame.width > 0 ? static_cast(frame.width) : 0u; + reply.height = frame.height > 0 ? static_cast(frame.height) : 0u; + return reply; + } + Bool ServerMakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { SurfaceControlFrame frame; // The release request (the three NO_* markers, ClassifyEglMakeCurrent's ClientRelease diff --git a/MobileGL/MG_Remote/Server/ServerLoop.h b/MobileGL/MG_Remote/Server/ServerLoop.h index 3cef426b..e0699296 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.h +++ b/MobileGL/MG_Remote/Server/ServerLoop.h @@ -70,6 +70,7 @@ #pragma once #include +#include "ServerDisplay.h" #include "ServerSession.h" #include "SurfaceControlFrame.h" @@ -187,6 +188,22 @@ namespace MobileGL::MG_Remote::Server { } } // namespace Detail + // --------------------------------------------------------------------------------- + // P12 (on-screen server window), D4: ONE SURFACE MODE PER SESSION + // --------------------------------------------------------------------------------- + // + // "Only one of the two rendering paths is active at a time, decided at context creation": a + // session's mode latches at its FIRST successful surface creation - a ServerOwned window makes + // it on-screen, a pbuffer makes it offscreen - and a later surface of the OTHER kind in the same + // session is refused by name (SurfaceRefusal::SurfaceModeMismatch: reply ok=false, logged, the + // session NOT latched). Windows the client names itself (the X11/Win32/None tokens CONTRACT-P6 + // D8 has yet to close) are neither kind and neither latch nor are refused: nothing about them + // changes here. Reset by ServerLoop::Start, i.e. per session. + enum class SessionSurfaceMode : Uint8 { None, OnScreen, Offscreen }; + const char* SessionSurfaceModeName(SessionSurfaceMode mode); + // The decision, pure, so a unit case can drive all six pairs without a backend. + Bool SessionSurfaceModeAdmits(SessionSurfaceMode current, Bool serverOwnedWindow); + class ServerLoop { public: // Creates the apply thread, names it mgl-srv-apply, applies @@ -407,10 +424,50 @@ namespace MobileGL::MG_Remote::Server { // construction cannot be inside that window. Bool ControlIsPending() const; + // ---- P12 (on-screen server window), D3/D6 ------------------------------------------- + // + // How long a ServerOwned creation waits for the display server's window (D3: ~10 s). The + // control pump's SurfaceProgress heartbeat keeps the client's reply budget alive meanwhile. + static constexpr Uint32 kServerWindowWaitMs = ServerDisplay::kDefaultAcquireTimeoutMs; + + // APPLY THREAD ONLY. Leases the process display's window for this session (ServerDisplay:: + // AcquireFor with this loop as the holder), refusing by name when there is no display + // (NoServerDisplay, NOT a latch - a configuration answer) or no window within `timeoutMs` + // (NoServerWindow). The ServerOwned arm of the dispatch is its production caller; a test + // reaches it through RunProbeOnApplyThreadForTesting. The lease is held until the backend + // has let go of the window: ServerDisplay::Detach's lost hook (D6) or the session's end. + MobileGLResult AcquireServerWindow(Uint32 width, Uint32 height, Uint32 timeoutMs, + ServerWindowLease* out, SurfaceRefusalCode* refusal); + // APPLY THREAD ONLY: this session's surface mode (D4). + SessionSurfaceMode SurfaceMode() const { return m_surfaceMode; } + // How many times this loop released a lost server window and latched ServerWindowLost (D6). + Uint64 ServerWindowsLost() const { return m_serverWindowsLost.load(std::memory_order_acquire); } + private: RemoteControlSink m_remoteSink = nullptr; void* m_remoteSinkUser = nullptr; + // P12. The ServerOwned arm of the CreateWindowSurface dispatch (D3, D4). + MobileGLResult ApplyServerOwnedWindowSurface(MG_Backend::BackendObject* backend, SurfaceControlFrame& frame); + // P12 (D6), apply thread: the lost window's backend surface goes, the lease ends, and the + // session latches ServerWindowLost. Runs from PumpControlRequest when Detach asked. + void ReleaseLostServerWindow(); + // Ends this loop's display lease if it holds one (after the backend let go of the window). + void EndServerWindowLease(); + // ServerDisplay's lost hook: called under the display's lock from Detach's thread. Sets the + // request and rings the apply thread's bell; never blocks. + static void ServerWindowLostThunk(void* self); + // AcquireFor's cancel predicate: the loop is stopping, or the session latched. + static Bool ServerWindowWaitCancelled(void* self); + // D4: latched at the session's first successful surface creation; reset by Start(). + SessionSurfaceMode m_surfaceMode = SessionSurfaceMode::None; + // Apply thread only: this loop holds the display's lease. + Bool m_holdsWindowLease = false; + // Set by ServerWindowLostThunk (any thread), taken by the apply thread. Part of the park + // predicate, so a parked apply thread wakes for it. + std::atomic m_windowLostRequested{false}; + std::atomic m_serverWindowsLost{0}; + void ApplyThreadMain(); // Runs a posted control frame, if there is one. Returns true if it ran one. Bool PumpControlRequest(); @@ -558,7 +615,22 @@ namespace MobileGL::MG_Remote::Server { Bool ServerInitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor); Bool ServerCreateEGLWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle); Bool ServerResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height); - Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height); + // P12: `refusal`, when given, receives the server's named refusal (SurfaceModeMismatch for a + // pbuffer in an on-screen session), None otherwise. + Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height, + SurfaceRefusalCode* refusal = nullptr); + // P12 (on-screen server window), D1/D2. CreateWindowSurface on the SERVER's window: one frame, + // WindowKind::ServerOwned with nativeToken 0 on the wire, no SetWindowHandle. `width`/`height` + // is the size the client asked for (0/0 = the server window's own size); the reply carries the + // window's REAL extent, and the server's named refusal when it declined. + struct ServerOwnedWindowReply { + Bool ok = false; + MobileGLResult transport = MOBILEGL_OK; // the frame channel's own answer + SurfaceRefusalCode refusal = SurfaceRefusalCode::None; + Uint32 width = 0; + Uint32 height = 0; + }; + ServerOwnedWindowReply ServerCreateServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height); // Also RE-PUBLISHES THE CAPS SNAPSHOT on success (R-12). BackendObject::MakeEGLCurrent runs // InitCapabilities() on the first make-current per surface (BackendObject.cpp:341-347), so // this is the moment the server's answers stop being the empty ones Accept() published - diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index be9bedf4..af043317 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -14,6 +14,9 @@ set(MOBILEGL_WIRE_TESTS SessionTest # P6 dl: the Fatal-family table and its total projection onto the wire FatalCode. FatalFamilyTest + # P12 (on-screen server window): the server's own window holder, driven with a fake + # window handle (no GL, no process, no platform window). + ServerDisplayTest ) if (NOT WIN32) diff --git a/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp b/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp new file mode 100644 index 00000000..967e2376 --- /dev/null +++ b/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp @@ -0,0 +1,316 @@ +// MobileGL - MobileGL/MG_Test/Wire/ServerDisplayTest.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 + +// P12 (on-screen server window), D3/D6: the server's own window, held by ServerDisplay, driven here +// with a FAKE window handle and counting hooks - the host has no ANativeWindow, and that is exactly +// why the holder takes the window as an opaque handle with callbacks. +// +// What is proven: a process that installed nothing has no display (the host server, the exec'd +// supervisor); a wait for a window is bounded and wakes on the attach and on the requested size; +// the geometry request reaches the platform hook with the size asked for (0/0 = the window's own); +// Detach of a window a session holds asks the holder to let go, BLOCKS until it did, and releases +// the reference only after it; a holder that never answers does not have the window released under +// it; Detach of a window nobody holds releases it at once; Interrupt and a cancel predicate end a +// wait by name. + +#include + +#include + +#include +#include +#include +#include +#include +#include + +using namespace MobileGL; +using namespace MobileGL::MG_Remote::Server; + +namespace { + + // The platform half, faked: every acquire/release/geometry request is recorded, in order. + struct FakePlatform { + std::mutex mutex; + std::vector events; + std::atomic acquires{0}; + std::atomic releases{0}; + std::atomic geometryRequests{0}; + Uint32 lastWidth = 0; + Uint32 lastHeight = 0; + + void Note(const std::string& event) { + const std::lock_guard lock(mutex); + events.push_back(event); + } + std::vector Events() { + const std::lock_guard lock(mutex); + return events; + } + }; + + void FakeAcquire(void* user, void*) { + auto* platform = static_cast(user); + ++platform->acquires; + platform->Note("acquire"); + } + void FakeRelease(void* user, void*) { + auto* platform = static_cast(user); + ++platform->releases; + platform->Note("release"); + } + void FakeGeometry(void* user, Uint32 width, Uint32 height) { + auto* platform = static_cast(user); + platform->lastWidth = width; + platform->lastHeight = height; + ++platform->geometryRequests; + platform->Note("geometry"); + } + + ServerDisplayHooks HooksFor(FakePlatform& platform) { + ServerDisplayHooks hooks; + hooks.acquire = &FakeAcquire; + hooks.release = &FakeRelease; + hooks.requestGeometry = &FakeGeometry; + hooks.user = &platform; + return hooks; + } + + // The session side of a lease: what ServerLoop's lost hook does is set a flag its apply thread + // reads; here a "session" thread does the same and ends the lease when asked. + struct FakeSession { + ServerDisplay* display = nullptr; + FakePlatform* platform = nullptr; + std::atomic lostRequested{false}; + std::atomic lostCalls{0}; + bool answer = true; // false: never ends its lease (a wedged apply thread) + std::thread thread; + + static void OnLost(void* self) { + auto* session = static_cast(self); + ++session->lostCalls; + session->lostRequested.store(true); + } + void Serve() { + thread = std::thread([this] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!lostRequested.load() && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + if (!answer || !lostRequested.load()) return; + // "The backend lets go of the window" - then the lease ends. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + platform->Note("session released its surface"); + display->EndLease(this); + }); + } + ~FakeSession() { + if (thread.joinable()) thread.join(); + } + }; + + int g_windowStorage[4]; + void* FakeWindow(int index) { return &g_windowStorage[index]; } + +} // namespace + +// Nobody installed a display: HasDisplay is false and a wait for a window answers NoDisplay at once +// - the host server's and the exec'd supervisor's shape, and what makes a ServerOwned request a named +// refusal there. +TEST(ServerDisplayTest, AProcessThatInstalledNothingHasNoDisplay) { + ServerDisplay display; + EXPECT_FALSE(display.HasDisplay()); + ServerWindowLease lease; + const auto started = std::chrono::steady_clock::now(); + EXPECT_EQ(display.AcquireFor(64, 48, 5000, this, nullptr, nullptr, nullptr, &lease), ServerWindowAcquire::NoDisplay); + EXPECT_LT(std::chrono::steady_clock::now() - started, std::chrono::milliseconds(1000)) + << "a process with no display must not wait for a window"; + // And the process singleton is that process: nothing on the host installs one. + EXPECT_FALSE(ServerDisplayInstance().HasDisplay()); +} + +// D3: the wait for a window is BOUNDED, and the platform was asked for the size first. +TEST(ServerDisplayTest, AWaitForAWindowThatNeverComesEndsByNameAfterItsBound) { + FakePlatform platform; + ServerDisplay display; + display.Install(HooksFor(platform)); + EXPECT_TRUE(display.HasDisplay()); + ServerWindowLease lease; + const auto started = std::chrono::steady_clock::now(); + EXPECT_EQ(display.AcquireFor(640, 480, 200, this, nullptr, nullptr, nullptr, &lease), ServerWindowAcquire::NoWindow); + const auto waited = std::chrono::steady_clock::now() - started; + EXPECT_GE(waited, std::chrono::milliseconds(150)); + EXPECT_LT(waited, std::chrono::milliseconds(3000)); + EXPECT_EQ(platform.geometryRequests.load(), 1); + EXPECT_EQ(platform.lastWidth, 640u); + EXPECT_EQ(platform.lastHeight, 480u); + EXPECT_FALSE(display.Leased()); +} + +// D3: a window attached while the apply thread waits wakes it; 0/0 takes the window at its own size +// (setSizeFromLayout), and the reference was taken by the attach. +TEST(ServerDisplayTest, AnAttachWakesTheWaitAndTheLeaseIsTheWindowsOwnSizeForZeroByZero) { + FakePlatform platform; + ServerDisplay display; + display.Install(HooksFor(platform)); + std::thread attacher([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + display.Attach(FakeWindow(0), 1080, 2400); + }); + ServerWindowLease lease; + EXPECT_EQ(display.AcquireFor(0, 0, 5000, this, nullptr, nullptr, nullptr, &lease), ServerWindowAcquire::Acquired); + attacher.join(); + EXPECT_EQ(lease.window, FakeWindow(0)); + EXPECT_EQ(lease.width, 1080u); + EXPECT_EQ(lease.height, 2400u); + EXPECT_TRUE(lease.sizeAsRequested); + EXPECT_EQ(platform.lastWidth, 0u) << "0/0 is the request for the layout's own size"; + EXPECT_EQ(platform.acquires.load(), 1); + EXPECT_TRUE(display.Leased()); + display.EndLease(this); + EXPECT_FALSE(display.Leased()); + EXPECT_EQ(platform.releases.load(), 0) << "ending a lease is not detaching the window"; +} + +// D3: a requested size is waited for - the surfaceChanged that answers the geometry request is an +// Attach of the SAME window at the new size, and it is what grants the lease. +TEST(ServerDisplayTest, ARequestedSizeIsWaitedForUntilTheWindowReportsIt) { + FakePlatform platform; + ServerDisplay display; + display.Install(HooksFor(platform)); + display.Attach(FakeWindow(0), 1080, 2400); + std::thread resizer([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + display.Attach(FakeWindow(0), 640, 480); + }); + ServerWindowLease lease; + const auto started = std::chrono::steady_clock::now(); + EXPECT_EQ(display.AcquireFor(640, 480, 5000, this, nullptr, nullptr, nullptr, &lease), + ServerWindowAcquire::Acquired); + resizer.join(); + EXPECT_GE(std::chrono::steady_clock::now() - started, std::chrono::milliseconds(100)) + << "the lease was granted before the window reached the requested size"; + EXPECT_EQ(lease.width, 640u); + EXPECT_EQ(lease.height, 480u); + EXPECT_TRUE(lease.sizeAsRequested); + EXPECT_EQ(platform.acquires.load(), 1) << "the same window re-attached must not take a second reference"; + display.EndLease(this); +} + +// D6, THE CONTRACT: Detach of a window a session holds asks the holder to let go, WAITS until it did, +// and only then releases the window's reference. Red with Detach's wait deleted: the release is +// recorded before the session released its surface (and Detach answers Released, not +// ReleasedBySession). +TEST(ServerDisplayTest, DetachOfALeasedWindowWaitsForTheSessionAndReleasesOnlyAfterIt) { + FakePlatform platform; + ServerDisplay display; + display.Install(HooksFor(platform)); + display.Attach(FakeWindow(1), 800, 600); + FakeSession session; + session.display = &display; + session.platform = &platform; + ServerWindowLease lease; + ASSERT_EQ(display.AcquireFor(800, 600, 1000, &session, &FakeSession::OnLost, nullptr, nullptr, &lease), + ServerWindowAcquire::Acquired); + session.Serve(); + EXPECT_EQ(display.Detach(3000), ServerWindowDetach::ReleasedBySession); + EXPECT_EQ(session.lostCalls.load(), 1); + EXPECT_EQ(platform.releases.load(), 1); + const auto events = platform.Events(); + ASSERT_GE(events.size(), 3u); + EXPECT_EQ(events[events.size() - 2], "session released its surface"); + EXPECT_EQ(events.back(), "release") << "the window was released before the session let go of it"; + EXPECT_FALSE(display.Attached()); + EXPECT_FALSE(display.Leased()); +} + +// D6: a holder that does not answer within the bound does NOT have the window released under it - +// Detach returns TimedOut by name and the reference goes when the lease finally ends. +TEST(ServerDisplayTest, ADetachThatTimesOutKeepsTheReferenceUntilTheLeaseEnds) { + FakePlatform platform; + ServerDisplay display; + display.Install(HooksFor(platform)); + display.Attach(FakeWindow(2), 800, 600); + FakeSession session; + session.display = &display; + session.platform = &platform; + session.answer = false; + ServerWindowLease lease; + ASSERT_EQ(display.AcquireFor(800, 600, 1000, &session, &FakeSession::OnLost, nullptr, nullptr, &lease), + ServerWindowAcquire::Acquired); + const auto started = std::chrono::steady_clock::now(); + EXPECT_EQ(display.Detach(150), ServerWindowDetach::TimedOut); + EXPECT_LT(std::chrono::steady_clock::now() - started, std::chrono::milliseconds(2000)); + EXPECT_EQ(platform.releases.load(), 0) << "the window was released under a session still holding it"; + display.EndLease(&session); + EXPECT_EQ(platform.releases.load(), 1) << "the late EndLease did not release the kept reference"; +} + +// D6: "a session that never had a window is unaffected" - Detach of a window nobody holds releases it +// at once and calls no lost hook. +TEST(ServerDisplayTest, DetachOfAWindowNobodyHoldsReleasesItAtOnce) { + FakePlatform platform; + ServerDisplay display; + display.Install(HooksFor(platform)); + EXPECT_EQ(display.Detach(), ServerWindowDetach::NoWindow); + display.Attach(FakeWindow(3), 10, 10); + EXPECT_EQ(display.Detach(3000), ServerWindowDetach::Released); + EXPECT_EQ(platform.acquires.load(), 1); + EXPECT_EQ(platform.releases.load(), 1); +} + +// A second holder cannot take a window another session holds (sessions are sequential; seeing this +// is a bug, and it is named rather than shared). +TEST(ServerDisplayTest, AWindowLeasedToOneHolderIsNotLeasedToAnother) { + 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); + EXPECT_EQ(display.AcquireFor(0, 0, 1000, &first, nullptr, nullptr, nullptr, &lease), ServerWindowAcquire::Acquired) + << "the holder itself may lease again (a surface re-created at a new size)"; + EXPECT_EQ(display.AcquireFor(0, 0, 100, &second, nullptr, nullptr, nullptr, &lease), + ServerWindowAcquire::LeasedElsewhere); + 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) { + FakePlatform platform; + ServerDisplay display; + display.Install(HooksFor(platform)); + std::thread interrupter([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + display.Interrupt(); + }); + ServerWindowLease lease; + EXPECT_EQ(display.AcquireFor(0, 0, 5000, this, nullptr, nullptr, nullptr, &lease), ServerWindowAcquire::Interrupted); + interrupter.join(); + + static std::atomic cancelled{false}; + cancelled.store(false); + std::thread canceller([] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + cancelled.store(true); + }); + const auto cancel = [](void*) -> Bool { return cancelled.load(); }; + EXPECT_EQ(display.AcquireFor(0, 0, 5000, this, nullptr, cancel, nullptr, &lease), ServerWindowAcquire::Cancelled); + canceller.join(); + + // And Uninstall: the display is gone, the waiter hears NoDisplay. + std::thread uninstaller([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + display.Uninstall(); + }); + EXPECT_EQ(display.AcquireFor(0, 0, 5000, this, nullptr, nullptr, nullptr, &lease), ServerWindowAcquire::NoDisplay); + uninstaller.join(); + EXPECT_FALSE(display.HasDisplay()); +} diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index 6c84d8d3..8a38ea7c 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -3211,6 +3211,135 @@ TEST(ServerLoopLatchTest, ALatchFromAnotherThreadBetweenTwoRecordsStopsTheNextPo "appliedSeq moved past the first record; 16 = the between-records hook did not run exactly " "once (64+ = setup)"; } + +// ===================================================================================== +// P12 (on-screen server window): the surface mode (D4), the display-less refusal (D3) and the lost +// window (D6), on a real apply thread with no GL context +// ===================================================================================== + +namespace { + // The apply-thread side of AcquireServerWindow, through the probe seam. + struct ServerWindowProbe { + Uint32 width = 0; + Uint32 height = 0; + Uint32 timeoutMs = 1000; + Server::ServerWindowLease lease; + Server::SurfaceRefusalCode refusal = Server::SurfaceRefusalCode::None; + MobileGLResult rc = MOBILEGL_ERR_INVALID_ARGUMENT; + Bool onApplyThread = false; + }; + + MobileGLResult AcquireServerWindowOnTheApplyThread(void* user) { + auto& probe = *static_cast(user); + probe.onApplyThread = Server::ServerLoop::OnApplyThread(); + probe.rc = Server::ServerLoopInstance().AcquireServerWindow(probe.width, probe.height, probe.timeoutMs, + &probe.lease, &probe.refusal); + return MOBILEGL_OK; + } + + struct CountingWindowHooks { + static inline std::atomic acquires{0}; + static inline std::atomic releases{0}; + static void Acquire(void*, void*) { ++acquires; } + static void Release(void*, void*) { ++releases; } + static Server::ServerDisplayHooks Hooks() { + Server::ServerDisplayHooks hooks; + hooks.acquire = &Acquire; + hooks.release = &Release; + return hooks; + } + }; + + int g_fakeServerWindow = 0; + + // D6 ON THE REAL APPLY THREAD. The session holds the display's window (a lease taken on the apply + // thread, as the ServerOwned arm takes it); the UI thread's surfaceDestroyed (Detach) must get the + // apply thread to release it, see the session latch ServerWindowLost, and only then release the + // window's reference - all while the apply thread was PARKED when the request came. + enum : int { + kLostNotLeased = 1, // the probe could not take the lease + kLostDetach = 2, // Detach did not answer ReleasedBySession + kLostReleases = 4, // the window's reference was not released exactly once + kLostNotLatched = 8, // the session did not latch ServerWindowLost + kLostCounter = 16, // ServerWindowsLost() != 1 + kLostThreadStayed = 32, // the apply thread was still running 3 s after the latch + }; + + [[noreturn]] void LoseTheServerWindowAndExit() { + 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 || !probe.onApplyThread) + failed |= kLostNotLeased; + if (!fixture.WaitUntilTrulyParked()) ::_exit(67); + const Server::ServerWindowDetach detached = display.Detach(3000); + if (detached != Server::ServerWindowDetach::ReleasedBySession) failed |= kLostDetach; + if (CountingWindowHooks::releases.load() != 1) failed |= kLostReleases; + if (!Remote::SessionLatched() || Remote::SessionLatchedFamily() != Remote::MGFatalFamily::ServerWindowLost) + failed |= kLostNotLatched; + if (loop.ServerWindowsLost() != 1) failed |= kLostCounter; + if (!PollUntil([&] { return !loop.Running(); }, 3000)) failed |= kLostThreadStayed; + fixture.Stop(); + ::_exit(failed); + } +} // namespace + +// D6. Red with PumpControlRequest's lost-window arm deleted: nobody answers the request, Detach waits +// out its bound and says TimedOut, the reference is kept and no latch is raised (bits 2|4|8|16|32); +// red with the park predicate's lost-window wake deleted: the apply thread sleeps through it the same +// way. +TEST(ServerLoopLatchTest, ALostServerWindowIsReleasedOnTheApplyThreadBeforeDetachReturnsAndLatchesByName) { + EXPECT_EXIT(LoseTheServerWindowAndExit(), ::testing::ExitedWithCode(0), ".*") + << "bits: 1 = the lease was not taken on the apply thread; 2 = Detach did not answer " + "ReleasedBySession; 4 = the reference was not released exactly once; 8 = no ServerWindowLost " + "latch; 16 = ServerWindowsLost() != 1; 32 = the apply thread stayed (64+ = setup)"; +} + +// D3. A server that owns no display - the host, the exec'd supervisor's children - refuses a request +// for its window BY NAME and does NOT latch: it is the client's configuration, not corrupt bytes. Red +// with the refusal turned into a latch: this unarmed process aborts. +TEST(ServerLoopTest, AServerWindowRequestOnAServerWithNoDisplayIsRefusedByNameWithoutALatch) { + ServerFixture fixture; + ASSERT_TRUE(fixture.Handshake()); + ASSERT_TRUE(fixture.StartLoop()); + ASSERT_FALSE(Server::ServerDisplayInstance().HasDisplay()); + ServerWindowProbe probe; + probe.width = 640; + probe.height = 480; + ASSERT_EQ(Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(&AcquireServerWindowOnTheApplyThread, &probe), + MOBILEGL_OK); + EXPECT_EQ(probe.rc, MOBILEGL_ERR_UNSUPPORTED); + EXPECT_EQ(probe.refusal, Server::SurfaceRefusalCode::NoServerDisplay); + EXPECT_FALSE(MobileGL::MG_Remote::SessionLatched()); + EXPECT_TRUE(Server::ServerLoopInstance().Running()) << "a refusal must not stop the session"; + fixture.Stop(); + EXPECT_NE(ReadLog().find("Refuse ServerOwned: this server owns no display"), std::string::npos) << ReadLog(); +} + +// D4. The decision the dispatch asks, pure: the first surface decides, and the OTHER kind is refused +// afterwards. Red with SessionSurfaceModeAdmits answering true for everything (the check deleted). +TEST(ServerLoopTest, SessionSurfaceModeAdmitsOnlyTheModeTheFirstSurfaceChose) { + using Server::SessionSurfaceMode; + EXPECT_TRUE(Server::SessionSurfaceModeAdmits(SessionSurfaceMode::None, /*serverOwnedWindow=*/true)); + EXPECT_TRUE(Server::SessionSurfaceModeAdmits(SessionSurfaceMode::None, false)); + EXPECT_TRUE(Server::SessionSurfaceModeAdmits(SessionSurfaceMode::OnScreen, true)) + << "an on-screen session may re-create its window surface (a resize)"; + EXPECT_FALSE(Server::SessionSurfaceModeAdmits(SessionSurfaceMode::OnScreen, false)) + << "a pbuffer in an on-screen session is SurfaceModeMismatch"; + EXPECT_TRUE(Server::SessionSurfaceModeAdmits(SessionSurfaceMode::Offscreen, false)); + EXPECT_FALSE(Server::SessionSurfaceModeAdmits(SessionSurfaceMode::Offscreen, true)) + << "a ServerOwned window in an offscreen session is SurfaceModeMismatch"; +} #endif int main(int argc, char** argv) { diff --git a/scripts/ci/fatal_census_baseline.json b/scripts/ci/fatal_census_baseline.json index fcd7e00e..7da07dee 100644 --- a/scripts/ci/fatal_census_baseline.json +++ b/scripts/ci/fatal_census_baseline.json @@ -54,6 +54,7 @@ "RetirementWaitFailed", "RingOverrun", "RoleViolation", + "ServerWindowLost", "StageSnapshotTooNarrow", "UncarriedInitialBytes", "UnexpectedMapAccept", diff --git a/scripts/ci/ph_latch_sites.py b/scripts/ci/ph_latch_sites.py index a6807e48..5d146c13 100644 --- a/scripts/ci/ph_latch_sites.py +++ b/scripts/ci/ph_latch_sites.py @@ -67,6 +67,12 @@ UNREACHABLE = [ "rows) before a frame reaches the dispatch"), ("ServerLoop.cpp", '"SurfaceOp.kind"', "SurfaceOpCodec refuses an unknown wire kind (SurfaceOpUnknownKind) before the dispatch"), + # P12 (D6): not a peer's byte at all - the server's OWN display window was destroyed + # (ServerDisplay::Detach from the display Activity's surfaceDestroyed). Its control is + # ServerLoopTest's ALostServerWindowIsReleasedOnTheApplyThreadBeforeDetachReturnsAndLatchesByName. + ("ServerLoop.cpp", '"surfaceDestroyed"', + "raised by the server's own display (surfaceDestroyed -> ServerDisplay::Detach), never by the " + "peer's bytes; ServerLoopTest's lost-window case is its negative control"), ] REMOTE = ROOT / "MobileGL" / "MG_Remote"