Files
MobileGL/MobileGL/MG_Remote/Server/ServerDisplay.h
T

231 lines
14 KiB
C++

// 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.
//
// 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
// lease that window (AcquireFor) and rebuild its surface on it. The registration lasts until that
// AcquireFor succeeds or the holder cancels it (CancelResume, at the session's end).
//
// 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 and the attached hook from
// Attach's (or SuspendLease's) caller, both WITH THIS OBJECT'S LOCK HELD - so they must not block and
// must not call back into this object (ServerLoop's set an atomic and ring its doorbell).
#pragma once
#include <Includes.h>
#include <condition_variable>
#include <mutex>
#include <vector>
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);
// A new window was attached while `holder` was suspended (SuspendLease). Same rules as the lost
// hook: called with the display's lock held.
using ServerWindowAttachedHook = void (*)(void* holder);
// The leased window changed size (surfaceChanged of the SAME window: the app's layout moved it).
// Same rules: called with the display's lock held; the holder reads the new extent with WindowExtent.
using ServerWindowResizedHook = 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)
// 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.
LeasedElsewhere, // another session in this process holds the window: see the note above
};
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). `onResized`, when
// given, is called whenever the leased window changes size while the lease lasts.
ServerWindowAcquire AcquireFor(Uint32 width, Uint32 height, Uint32 timeoutMs, void* holder,
ServerWindowLostHook onLost, ServerWindowWaitCancel cancel,
void* cancelUser, ServerWindowLease* out,
ServerWindowResizedHook onResized = nullptr);
// THE WINDOW'S EXTENT AS THE PLATFORM LAST REPORTED IT (surfaceCreated / surfaceChanged): the one
// source of truth for the size a session publishes to its client. False unless `holder` holds
// the lease on an attached window.
Bool WindowExtent(void* holder, void** window, Uint32* width, Uint32* height) const;
// 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);
// 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
// the same holder's AcquireFor succeeds, or at CancelResume.
void SuspendLease(void* holder, ServerWindowAttachedHook onAttached);
// The holder is going away: no attached hook is called for it once this returns.
void CancelResume(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;
// How many holders wait for a window to resume on (SuspendLease).
Uint32 SuspendedHolders() const;
private:
void ReleaseOutsideLock(const ServerDisplayHooks& hooks, void* window) const;
// m_mutex held: ends `holder`'s lease (false when it holds none); the references a timed-out
// Detach left behind are moved to `releases`, to be released outside the lock with `hooks`.
Bool EndLeaseLocked(void* holder, std::vector<void*>* releases, ServerDisplayHooks* hooks);
void ReleaseAfterLease(const ServerDisplayHooks& hooks, const std::vector<void*>& releases) const;
// m_mutex held: a new window is attached; every suspended holder is told.
void NotifySuspendedLocked() const;
// m_mutex held: an Attach reported `width`x`height` (see m_extentReports).
void NoteExtentReportLocked(Uint32 width, Uint32 height);
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;
// P12 review fix (stale size). The geometry the last AcquireFor asked the platform for (0/0 =
// the layout's), the extent the window last reported while the layout owned its size (0/0 =
// not seen yet), and a count of every extent report. A 0/0 request that FOLLOWS a fixed one
// is a change of size too - setSizeFromLayout answers asynchronously, like setFixedSize - so
// it waits for the layout's extent instead of leasing the previous session's fixed one.
Uint32 m_requestedWidth = 0;
Uint32 m_requestedHeight = 0;
Uint32 m_layoutWidth = 0;
Uint32 m_layoutHeight = 0;
Uint64 m_extentReports = 0;
void* m_leaseHolder = nullptr;
ServerWindowLostHook m_onLost = nullptr;
ServerWindowResizedHook m_onResized = nullptr;
Bool m_lostRequested = 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.
struct SuspendedHolder {
void* holder = nullptr;
ServerWindowAttachedHook onAttached = nullptr;
};
std::vector<SuspendedHolder> m_suspended;
};
// 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