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

459 lines
20 KiB
C++

// 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 <MG_Util/Debug/Log.h>
#include <algorithm>
#include <chrono>
#include <utility>
#if defined(__ANDROID__)
#include <android/native_window.h>
#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 "<unknown ServerWindowAcquire>";
}
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 "<unknown ServerWindowDetach>";
}
void ServerDisplay::ReleaseOutsideLock(const ServerDisplayHooks& hooks, void* window) const {
if (window != nullptr && hooks.release != nullptr) hooks.release(hooks.user, window);
}
void ServerDisplay::NoteExtentReportLocked(Uint32 width, Uint32 height) {
++m_extentReports;
// Only a report made while the layout owns the size says what the layout's size is.
if (m_requestedWidth == 0 && m_requestedHeight == 0) {
m_layoutWidth = width;
m_layoutHeight = height;
}
}
void ServerDisplay::Install(const ServerDisplayHooks& hooks) {
{
const std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(m_mutex);
return m_installed;
}
void ServerDisplay::Attach(void* window, Uint32 width, Uint32 height) {
if (window == nullptr) return;
std::unique_lock<std::mutex> 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.
const Bool resized = width != m_width || height != m_height;
m_width = width;
m_height = height;
NoteExtentReportLocked(width, height);
// The session rendering into it publishes the new extent to its client NOW - not at its
// next present, which a compositor with nothing to redraw never makes.
const Bool told = resized && m_leaseHolder != nullptr && m_onResized != nullptr;
if (told) m_onResized(m_leaseHolder);
lock.unlock();
m_cv.notify_all();
MGLOG_I("MG_Remote server: server window %p is now %ux%u%s", window, width, height,
told ? "; its session is told" : "");
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;
NoteExtentReportLocked(width, height);
++m_generation;
const Uint64 generation = m_generation;
// A suspended session (screen off, lock) rebuilds its surface on this window.
const Uint32 suspended = static_cast<Uint32>(m_suspended.size());
NotifySuspendedLocked();
lock.unlock();
m_cv.notify_all();
MGLOG_I("MG_Remote server: server window %p attached at %ux%u (generation %llu)%s", window, width, height,
static_cast<unsigned long long>(generation),
suspended != 0 ? "; a suspended session is told to resume on it" : "");
}
void ServerDisplay::NotifySuspendedLocked() const {
for (const SuspendedHolder& entry : m_suspended) {
if (entry.onAttached != nullptr) entry.onAttached(entry.holder);
}
}
ServerWindowDetach ServerDisplay::Detach(Uint32 timeoutMs) {
std::unique_lock<std::mutex> 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,
ServerWindowResizedHook onResized) {
ServerDisplayHooks hooks{};
Uint64 interrupts = 0;
const Bool sizeRequested = width != 0 && height != 0;
// P12 review fix (stale size): a 0/0 request after a fixed one hands the size BACK to the
// layout, and until the platform has done so the window still reports the fixed size.
Bool backToLayout = false;
Uint32 previousWidth = 0;
Uint32 previousHeight = 0;
Uint64 reportsAtRequest = 0;
{
const std::lock_guard<std::mutex> lock(m_mutex);
if (!m_installed) return ServerWindowAcquire::NoDisplay;
if (m_leaseHolder != nullptr && m_leaseHolder != holder && !m_leaseEnding)
return ServerWindowAcquire::LeasedElsewhere;
hooks = m_hooks;
interrupts = m_interrupts;
previousWidth = m_requestedWidth;
previousHeight = m_requestedHeight;
backToLayout = !sizeRequested && (previousWidth != 0 || previousHeight != 0);
m_requestedWidth = sizeRequested ? width : 0u;
m_requestedHeight = sizeRequested ? height : 0u;
reportsAtRequest = m_extentReports;
}
// 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);
// What "the size asked for" means on each path. A fixed size: the window reports it. The
// layout's, after a fixed one: the window reports the layout's extent it reported before, or
// - the layout moved meanwhile (a rotation), or was never seen - reports anything but the old
// fixed size after this request. The layout's, with no fixed size in effect: whatever it is.
const auto sizeReached = [&] {
if (sizeRequested) return m_width == width && m_height == height;
if (!backToLayout) return true;
if (m_layoutWidth != 0 && m_width == m_layoutWidth && m_height == m_layoutHeight) return true;
return m_extentReports != reportsAtRequest && (m_width != previousWidth || m_height != previousHeight);
};
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;
// 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 (heldElsewhere) {
attachedSeen = false;
geometryDeadline = deadline;
} else if (m_window != nullptr) {
if (!attachedSeen) {
attachedSeen = true;
geometryDeadline = std::min(deadline, now + std::chrono::milliseconds(kGeometryGraceMs));
}
const Bool sizeOk = sizeReached();
if (sizeOk || now >= geometryDeadline) {
m_leaseHolder = holder;
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) { return entry.holder == holder; }),
m_suspended.end());
if (out != nullptr) {
out->window = m_window;
out->width = m_width;
out->height = m_height;
out->generation = m_generation;
out->sizeAsRequested = sizeOk;
}
if (!sizeOk && sizeRequested) {
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);
} else if (!sizeOk) {
MGLOG_W("MG_Remote server: the server window still reports %ux%u - the previous fixed "
"size - %u ms after its size was handed back to the layout; the surface is created "
"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 heldElsewhere ? ServerWindowAcquire::LeasedElsewhere : ServerWindowAcquire::NoWindow;
const auto wake = std::min({deadline, geometryDeadline, now + kCancelPollInterval});
m_cv.wait_until(lock, wake);
}
}
Bool ServerDisplay::EndLeaseLocked(void* holder, std::vector<void*>* releases, ServerDisplayHooks* hooks) {
if (holder == nullptr || m_leaseHolder != holder) return false;
m_leaseHolder = nullptr;
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.
if (!m_installed) m_hooks = ServerDisplayHooks{};
return true;
}
void ServerDisplay::ReleaseAfterLease(const ServerDisplayHooks& hooks, const std::vector<void*>& releases) const {
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::EndLease(void* holder) {
std::vector<void*> releases;
ServerDisplayHooks hooks{};
{
const std::lock_guard<std::mutex> lock(m_mutex);
if (!EndLeaseLocked(holder, &releases, &hooks)) return;
}
m_cv.notify_all();
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;
ServerDisplayHooks hooks{};
{
// ONE LOCK HOLD for the lease's end and the registration, so no Attach can fall between
// them and go unheard.
const std::lock_guard<std::mutex> lock(m_mutex);
(void)EndLeaseLocked(holder, &releases, &hooks);
Bool registered = false;
for (SuspendedHolder& entry : m_suspended) {
if (entry.holder != holder) continue;
entry.onAttached = onAttached;
registered = true;
}
if (!registered) m_suspended.push_back(SuspendedHolder{holder, onAttached});
// A window that is already up (a Detach that raced an Attach) is the one to resume on.
if (m_window != nullptr && onAttached != nullptr) onAttached(holder);
}
m_cv.notify_all();
ReleaseAfterLease(hooks, releases);
}
void ServerDisplay::CancelResume(void* holder) {
const std::lock_guard<std::mutex> lock(m_mutex);
m_suspended.erase(std::remove_if(m_suspended.begin(), m_suspended.end(),
[holder](const SuspendedHolder& entry) { return entry.holder == holder; }),
m_suspended.end());
}
Bool ServerDisplay::WindowExtent(void* holder, void** window, Uint32* width, Uint32* height) const {
const std::lock_guard<std::mutex> lock(m_mutex);
if (holder == nullptr || m_leaseHolder != holder || m_window == nullptr) return false;
if (window != nullptr) *window = m_window;
if (width != nullptr) *width = m_width;
if (height != nullptr) *height = m_height;
return true;
}
Uint32 ServerDisplay::SuspendedHolders() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return static_cast<Uint32>(m_suspended.size());
}
void ServerDisplay::Interrupt() {
{
const std::lock_guard<std::mutex> lock(m_mutex);
++m_interrupts;
}
m_cv.notify_all();
}
Bool ServerDisplay::Attached() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_window != nullptr;
}
Bool ServerDisplay::Leased() const {
const std::lock_guard<std::mutex> lock(m_mutex);
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;
}
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<ANativeWindow*>(window));
}
void ReleaseAndroidWindow(void*, void* window) {
ANativeWindow_release(static_cast<ANativeWindow*>(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