mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Remote, MG_Test, scripts] (P12): the server's own window - ServerDisplay (an opaque window with hooks, a lease, a bounded Detach), the ServerOwned arm with the one-mode-per-session latch and the named no-display refusal, the lost-window release on the apply thread and the ServerWindowLost family
This commit is contained in:
@@ -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 -
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <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::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.
|
||||
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<unsigned long long>(generation));
|
||||
}
|
||||
|
||||
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) {
|
||||
ServerDisplayHooks hooks{};
|
||||
Uint64 interrupts = 0;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!m_installed) return ServerWindowAcquire::NoDisplay;
|
||||
if (m_leaseHolder != nullptr && m_leaseHolder != holder) return ServerWindowAcquire::LeasedElsewhere;
|
||||
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<std::mutex> 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<void*> releases;
|
||||
ServerDisplayHooks hooks{};
|
||||
{
|
||||
const std::lock_guard<std::mutex> 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<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;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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 <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);
|
||||
// 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<void*> 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
|
||||
@@ -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<std::mutex> 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<Uint32>(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<Uint8>(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<unsigned long long>(frame.seq), frame.width, frame.height,
|
||||
SessionSurfaceModeName(m_surfaceMode));
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
frame.ok = backend->CreateEGLPbufferSurface(HandleFromToken<EGLSurface>(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 "<unknown SessionSurfaceMode>";
|
||||
}
|
||||
|
||||
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<ServerLoop*>(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<const ServerLoop*>(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<Uint8>(SurfaceRefusalCode::None);
|
||||
const Uint32 wantWidth = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
|
||||
const Uint32 wantHeight = frame.height > 0 ? static_cast<Uint32>(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<Uint8>(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<unsigned long long>(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<Uint8>(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<EGLSurface>(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<unsigned long long>(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<Int>(lease.width);
|
||||
frame.height = static_cast<Int>(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<unsigned long long>(frame.seq), lease.window,
|
||||
static_cast<unsigned long long>(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<SurfaceRefusalCode>(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<Int>(width);
|
||||
frame.height = static_cast<Int>(height);
|
||||
const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame);
|
||||
ServerOwnedWindowReply reply;
|
||||
reply.transport = rc;
|
||||
reply.ok = rc == MOBILEGL_OK && frame.ok;
|
||||
reply.refusal = static_cast<SurfaceRefusalCode>(frame.refusal);
|
||||
reply.width = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
|
||||
reply.height = frame.height > 0 ? static_cast<Uint32>(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
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#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<Bool> m_windowLostRequested{false};
|
||||
std::atomic<Uint64> 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 -
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <MG_Remote/Server/ServerDisplay.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> events;
|
||||
std::atomic<int> acquires{0};
|
||||
std::atomic<int> releases{0};
|
||||
std::atomic<int> geometryRequests{0};
|
||||
Uint32 lastWidth = 0;
|
||||
Uint32 lastHeight = 0;
|
||||
|
||||
void Note(const std::string& event) {
|
||||
const std::lock_guard<std::mutex> lock(mutex);
|
||||
events.push_back(event);
|
||||
}
|
||||
std::vector<std::string> Events() {
|
||||
const std::lock_guard<std::mutex> lock(mutex);
|
||||
return events;
|
||||
}
|
||||
};
|
||||
|
||||
void FakeAcquire(void* user, void*) {
|
||||
auto* platform = static_cast<FakePlatform*>(user);
|
||||
++platform->acquires;
|
||||
platform->Note("acquire");
|
||||
}
|
||||
void FakeRelease(void* user, void*) {
|
||||
auto* platform = static_cast<FakePlatform*>(user);
|
||||
++platform->releases;
|
||||
platform->Note("release");
|
||||
}
|
||||
void FakeGeometry(void* user, Uint32 width, Uint32 height) {
|
||||
auto* platform = static_cast<FakePlatform*>(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<bool> lostRequested{false};
|
||||
std::atomic<int> 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<FakeSession*>(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<bool> 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());
|
||||
}
|
||||
@@ -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<ServerWindowProbe*>(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<int> acquires{0};
|
||||
static inline std::atomic<int> 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) {
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"RetirementWaitFailed",
|
||||
"RingOverrun",
|
||||
"RoleViolation",
|
||||
"ServerWindowLost",
|
||||
"StageSnapshotTooNarrow",
|
||||
"UncarriedInitialBytes",
|
||||
"UnexpectedMapAccept",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user