[MG_Remote, MG_Impl, MG_State, Config] (P12): MOBILEGL_IPC_SURFACE=server - headless window surfaces (NULL accepted), one ServerOwned CreateWindowSurface and no SetWindowHandle, the server's geometry into the EGL state before the create returns, and every refusal named on the client

This commit is contained in:
2026-09-23 23:55:23 -04:00
parent 844b217661
commit d481393f8f
10 changed files with 627 additions and 3 deletions
+22
View File
@@ -479,6 +479,12 @@ namespace MobileGL::MG_Config {
// the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM,
// INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs
// to P6 or later.
//
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE's two values. See IpcTable::Surface.
enum class IpcSurface : Uint8 {
Offscreen = 0, // a window surface is the client's own window, as before (the default)
Server = 1, // a window surface is the SERVER's window: WindowKind::ServerOwned
};
struct IpcTable {
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
@@ -618,8 +624,24 @@ namespace MobileGL::MG_Config {
// because the resolved mask is logged by whoever starts the apply thread, and the
// string is what an operator typed.
String ServerAffinity = "auto";
// MOBILEGL_IPC_SURFACE (P12, on-screen server window) = offscreen | server. `server` makes
// the client a HEADLESS one: eglCreateWindowSurface / eglCreatePlatformWindowSurface accept
// any native window including NULL, send ONE CreateWindowSurface naming
// WindowKind::ServerOwned (token 0, the size from EGL_WIDTH/EGL_HEIGHT, 0/0 = the server's
// own) and no SetWindowHandle, and take the surface's real geometry back from the server.
// Meaningful only when this client talks to a remote server (MOBILEGL_TRANSPORT=spawn, the
// fork or tcp:// control); under monolith / inproc it is parsed, logged as ignored, and
// changes nothing. `offscreen` (the default) is today's behaviour byte for byte.
// Pbuffers stay pbuffers in both modes (D4: one mode per session, decided by the first).
IpcSurface Surface = IpcSurface::Offscreen;
};
extern IpcTable Ipc;
// P12: the one predicate every client-side consumer of MOBILEGL_IPC_SURFACE asks - a window
// surface is the server's when the knob says so AND there is a remote server to own it.
inline Bool ServerOwnedWindowSurfaces() {
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
}
#else
// The whole point: in a build without MG_Remote this folds at compile time, so
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
+38 -2
View File
@@ -16,6 +16,11 @@
#include <cerrno>
#include <cstdlib>
#if MOBILEGL_BUILD_DISAGGREGATED
// std::strcmp, for InitIpc's server-role check (P12). Split-only, so the pull build's TU is
// unchanged (G1).
#include <cstring>
#endif
#ifndef _WIN32
extern char** environ;
@@ -470,18 +475,49 @@ namespace MobileGL::MG_ConfigLoader {
}
}
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
// the semantics). An unknown value is named and read as the default, like every knob here.
{
String surface;
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
std::transform(surface.begin(), surface.end(), surface.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (surface.empty() || surface == "offscreen") {
ipc.Surface = MG_Config::IpcSurface::Offscreen;
} else if (surface == "server") {
ipc.Surface = MG_Config::IpcSurface::Server;
} else {
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
"offscreen|server, using offscreen",
surface.c_str());
ipc.Surface = MG_Config::IpcSurface::Offscreen;
}
// (Not said by a server process: a spawned server inherits the client's environment,
// parses this before RunSession sets its own transport, and has no client half anyway.)
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
// Said, not silently honoured: there is no remote server here to own a window.
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
"and this run has none. Window surfaces stay the client's own");
}
}
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// One line, on the arm where these numbers decide behaviour, because every one of
// them is a number a bug report has to quote.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
"persistent-block=%uKiB "
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s'",
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
ipc.PersistentBlockKb, ipc.AdoptTier,
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str());
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
if (ipc.VerbBarrier == 0) {
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
+37
View File
@@ -11,6 +11,11 @@
#include <Init.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/EGLState/Core.h>
#if MOBILEGL_BUILD_DISAGGREGATED
// MG_Config::ServerOwnedWindowSurfaces (P12, MOBILEGL_IPC_SURFACE). Split-only: the pull build's
// translation unit is unchanged (G1).
#include <Config.h>
#endif
#include <mutex>
#include <sstream>
#include <type_traits>
@@ -127,7 +132,16 @@ namespace MobileGL::MG_Impl::EGLImpl {
state->SetError(EGL_BAD_CONFIG);
return EGL_NO_SURFACE;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P12 (on-screen server window), D1. With MOBILEGL_IPC_SURFACE=server and a remote server,
// the window is the SERVER's: a headless client passes none, so NULL is not
// EGL_BAD_NATIVE_WINDOW here. Whatever the client passed never reaches the wire - the
// remote backend sends WindowKind::ServerOwned with token 0 (BackendObject_Remote).
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
if (IsNullNativeHandle(window) && !serverOwnedWindow) {
#else
if (IsNullNativeHandle(window)) {
#endif
state->SetError(EGL_BAD_NATIVE_WINDOW);
return EGL_NO_SURFACE;
}
@@ -139,7 +153,15 @@ namespace MobileGL::MG_Impl::EGLImpl {
.Height = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_HEIGHT, 0), 0)),
};
#if MOBILEGL_BUILD_DISAGGREGATED
EGLSurface surface = serverOwnedWindow
? state->CreateServerOwnedWindowSurface(
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false)
: state->CreateWindowSurface(dpy, config, window, attrib_list);
#else
EGLSurface surface = state->CreateWindowSurface(dpy, config, window, attrib_list);
#endif
if (surface == EGL_NO_SURFACE) {
return EGL_NO_SURFACE;
}
@@ -676,7 +698,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_NO_SURFACE;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P12 (on-screen server window), D1: as CreateWindowSurface - with MOBILEGL_IPC_SURFACE=server
// the window is the server's, and a headless client's NULL is accepted.
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
if (native_window == nullptr && !serverOwnedWindow) {
#else
if (native_window == nullptr) {
#endif
state->SetError(EGL_BAD_NATIVE_WINDOW);
return EGL_NO_SURFACE;
}
@@ -696,7 +725,15 @@ namespace MobileGL::MG_Impl::EGLImpl {
.Height = static_cast<Uint32>(std::max<EGLint>(GetAttribValueAttrib(attrib_list, EGL_HEIGHT, 0), 0)),
};
#if MOBILEGL_BUILD_DISAGGREGATED
EGLSurface surface = serverOwnedWindow
? state->CreateServerOwnedWindowSurface(
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true)
: state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
#else
EGLSurface surface = state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
#endif
if (surface == EGL_NO_SURFACE) {
return EGL_NO_SURFACE;
}
@@ -16,6 +16,7 @@
#include "../Server/ServerLoop.h"
#include <MG_State/EGLState/Core.h>
#include <MG_Util/Debug/Log.h>
// Declared rather than included: MG_Backend/BackendObjects.h drags in both concrete backend
@@ -212,6 +213,8 @@ namespace MobileGL::MG_Remote::Client {
// The handle first: the server's backend has to know which window it is about to make
// a surface for, and ServerSetWindowHandle is the only way to tell it.
WaitForApplyBeforeEglForwarder("CreateEGLWindowSurface");
// P12 (D1): unless the window is the SERVER's - then there is no handle of ours to tell.
if (MG_Config::ServerOwnedWindowSurfaces()) return CreateServerOwnedWindowSurface(surface, handle);
Server::ServerSetWindowHandle(handle);
if (!Server::ServerCreateEGLWindowSurface(surface, handle)) return false;
// The server's surface init published the default framebuffer's shape as a
@@ -236,13 +239,96 @@ namespace MobileGL::MG_Remote::Client {
Bool BackendObject_Remote::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
WaitForApplyBeforeEglForwarder("CreateEGLPbufferSurface");
if (!Server::ServerCreateEGLPbufferSurface(surface, width, height)) return false;
Server::SurfaceRefusalCode refusal = Server::SurfaceRefusalCode::None;
if (!Server::ServerCreateEGLPbufferSurface(surface, width, height, &refusal)) {
if (refusal == Server::SurfaceRefusalCode::SurfaceModeMismatch) {
// P12 (D4), named on THIS side too: the server logged its half.
MGLOG_E("MG_Remote client: SurfaceModeMismatch - eglCreatePbufferSurface (%dx%d) in a session "
"whose surface mode is on-screen: its first surface was the server's own window "
"(MOBILEGL_IPC_SURFACE=server), and only one rendering path is active per session. "
"The server refused it; the session carries on",
width, height);
}
return false;
}
// Same drain as the window surface: InitPbufferSurface publishes the default
// framebuffer's depth/stencil format on SEG_EVENT from inside this very RPC.
if (ClientSession* session = ClientSession::Active()) session->DrainPublishedEvents();
return MG_Backend::BackendObject::CreateEGLPbufferSurface(surface, width, height);
}
namespace {
// THE CLIENT'S OWN RECORD OF A SERVER-OWNED WINDOW NEEDS A NON-NULL HANDLE, and a headless
// client has none: BackendObject::RegisterEGLWindowSurface refuses a null handle, and that
// base class is in the pull build (G1). This address stands in for it. It is never
// dereferenced - the client's InitWindowSurface is a no-op - and never crosses the wire
// (the ServerOwned frame carries token 0).
char g_serverOwnedWindowPlaceholder = 0;
} // namespace
Bool BackendObject_Remote::CreateServerOwnedWindowSurface(EGLSurface surface,
const MG_Backend::WindowHandle& handle) {
const Server::ServerOwnedWindowReply reply =
Server::ServerCreateServerOwnedWindowSurface(surface, handle.Width, handle.Height);
if (!reply.ok) {
// P12: FAILS BY NAME ON THIS SIDE TOO. The server logged its reason; the reply's refusal
// code is what lets this line say the same thing instead of "ok=false".
switch (reply.refusal) {
case Server::SurfaceRefusalCode::NoServerDisplay:
MGLOG_E("MG_Remote client: Refuse ServerOwned (NoServerDisplay) - MOBILEGL_IPC_SURFACE=server asked "
"the server to create this %ux%u window surface on its own window, and the server owns no "
"display: it is an offscreen server. eglCreateWindowSurface fails with "
"EGL_BAD_NATIVE_WINDOW; connect to the on-screen display server, or unset "
"MOBILEGL_IPC_SURFACE",
handle.Width, handle.Height);
break;
case Server::SurfaceRefusalCode::NoServerWindow:
MGLOG_E("MG_Remote client: Refuse ServerOwned (NoServerWindow) - the server owns a display but no "
"window came up for this %ux%u surface within its wait (is the display's surface visible?). "
"eglCreateWindowSurface fails with EGL_BAD_NATIVE_WINDOW",
handle.Width, handle.Height);
break;
case Server::SurfaceRefusalCode::SurfaceModeMismatch:
MGLOG_E("MG_Remote client: SurfaceModeMismatch - eglCreateWindowSurface on the server's window "
"(MOBILEGL_IPC_SURFACE=server) in a session whose surface mode is offscreen: its first "
"surface was a pbuffer, and only one rendering path is active per session. The server "
"refused it; the session carries on");
break;
default:
MGLOG_E("MG_Remote client: the server could not create the server-owned window surface "
"(MOBILEGL_IPC_SURFACE=server, %ux%u requested; channel rc=%d, refusal %s)",
handle.Width, handle.Height, static_cast<int>(reply.transport),
Server::SurfaceRefusalCodeName(reply.refusal));
break;
}
return false;
}
// D1: THE GEOMETRY FLOWS BACK BEFORE THIS RETURNS. The reply carries the server window's real
// extent, and it goes into the EGL state here, so eglQuerySurface(EGL_WIDTH/EGL_HEIGHT)
// answers the server's size the moment eglCreateWindowSurface returns - whatever the backend
// (Magma builds its swapchain, and publishes its extent, only at the first MakeCurrent). The
// session records the surface as the server-owned one, so every later surface-changed event
// that carries an extent (the window resized or rotated) resizes it too; then the same drain
// as the ordinary window path, for the default framebuffer's shape the server's surface init
// may have published from inside this very RPC.
if (MG_State::pEGLContext && reply.width != 0 && reply.height != 0) {
(void)MG_State::pEGLContext->SetSurfaceExtent(surface, static_cast<EGLint>(reply.width),
static_cast<EGLint>(reply.height));
}
if (ClientSession* session = ClientSession::Active()) {
session->NoteServerOwnedWindowSurface(surface, reply.width, reply.height);
session->DrainPublishedEvents();
}
MGLOG_I("MG_Remote client: surface=window %ux%u owner=server (MOBILEGL_IPC_SURFACE=server, %ux%u requested)",
reply.width, reply.height, handle.Width, handle.Height);
MG_Backend::WindowHandle local = handle;
if (local.Backend == MG_Backend::WindowBackend::Unknown) local.Backend = MG_Backend::WindowBackend::Android;
if (local.Handle == nullptr) local.Handle = &g_serverOwnedWindowPlaceholder;
local.Width = reply.width;
local.Height = reply.height;
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, local);
}
Bool BackendObject_Remote::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read,
EGLContext ctx) {
// FORWARD FIRST, THEN RUN THE BASE. The server has to own the context before the base
@@ -298,6 +384,8 @@ namespace MobileGL::MG_Remote::Client {
}
void BackendObject_Remote::ReleaseEGLSurface(EGLSurface surface) {
// P12: a released server-owned surface stops taking the server window's geometry.
if (ClientSession* session = ClientSession::Active()) session->ForgetServerOwnedWindowSurface(surface);
Server::ServerReleaseEGLSurface(surface);
MG_Backend::BackendObject::ReleaseEGLSurface(surface);
}
@@ -88,6 +88,11 @@ namespace MobileGL::MG_Remote::Client {
Bool InitPbufferSurface(EGLint width, EGLint height) override;
private:
// P12 (on-screen server window), D1: MOBILEGL_IPC_SURFACE=server's arm of
// CreateEGLWindowSurface - ONE ServerOwned frame, no SetWindowHandle, the server's real
// geometry adopted before it returns, and every refusal named in this process's log.
Bool CreateServerOwnedWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle);
// The generation of the snapshot m_formatCapabilities was filled from. Exposed only
// through the log line on a refresh: a cache that silently stopped tracking the mirror
// is exactly the shape trap 2 exists to prevent.
@@ -28,6 +28,7 @@
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Pipe/MGPipeCallbacks.h>
#include <MG_State/EGLState/Core.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_State/GLState/TextureState/TextureObject2D.h>
@@ -346,6 +347,24 @@ namespace MobileGL::MG_Remote::Client {
}
}
// P12 (on-screen server window), D1: the session's server-owned window surface - the
// client's EGL handle for it - or EGL_NO_SURFACE. Written on the GL thread by the surface
// RPCs (NoteServerOwnedWindowSurface / ForgetServerOwnedWindowSurface) and read by the
// drain, which runs on the same thread; atomic only because nothing else pins that.
std::atomic<EGLSurface> g_serverOwnedSurface{EGL_NO_SURFACE};
// An event's extent is the SERVER's surface size. For the server-owned window that is the
// size the client renders at, so the EGL state's record of the surface follows it:
// eglQuerySurface(EGL_WIDTH/EGL_HEIGHT) is what a headless client sizes its viewport from.
// A format-only event (Width/Height 0) says nothing about the size and changes nothing.
void ApplyServerOwnedSurfaceExtent(Uint32 width, Uint32 height) {
if (width == 0 || height == 0) return;
const EGLSurface surface = g_serverOwnedSurface.load(std::memory_order_acquire);
if (surface == EGL_NO_SURFACE || !MG_State::pEGLContext) return;
(void)MG_State::pEGLContext->SetSurfaceExtent(surface, static_cast<EGLint>(width),
static_cast<EGLint>(height));
}
Uint32 DrainEventRing(Transport::EventRingConsumer& events) {
if (!events.Valid()) return 0;
Uint32 delivered = 0;
@@ -416,6 +435,9 @@ namespace MobileGL::MG_Remote::Client {
info.Layers = head->Layers;
info.IsDefault = head->IsDefault;
ApplySurfaceChangedToClient(info);
// P12 (D1): and the server-owned window's EGL-state size, beside the
// default framebuffer's reallocation above.
ApplyServerOwnedSurfaceExtent(info.Width, info.Height);
++delivered;
break;
}
@@ -2445,6 +2467,16 @@ namespace MobileGL::MG_Remote::Client {
return DrainEventRing(*m_events);
}
void ClientSession::NoteServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
g_serverOwnedSurface.store(surface, std::memory_order_release);
ApplyServerOwnedSurfaceExtent(width, height);
}
void ClientSession::ForgetServerOwnedWindowSurface(EGLSurface surface) {
EGLSurface expected = surface;
(void)g_serverOwnedSurface.compare_exchange_strong(expected, EGL_NO_SURFACE, std::memory_order_acq_rel);
}
Uint64 ClientSession::EventRingCapacityBytes() const { return m_link->Memory().EventRingCapacity(); }
Uint64 ClientSession::StageCapacityBytes() const { return m_link->Memory().StageBytes(); }
+11
View File
@@ -401,6 +401,17 @@ namespace MobileGL::MG_Remote::Client {
// blit-incompatible with the real thing.
Uint32 DrainPublishedEvents();
// P12 (on-screen server window), D1: THE GEOMETRY FLOWS BACK. `surface` (the client's EGL
// handle) is this session's server-owned window surface, `width`x`height` the server
// window's real extent from the CreateWindowSurface reply (0 = not known yet). The extent
// goes into the EGL state at once - eglQuerySurface answers it before eglCreateWindowSurface
// returns - and every later surface-changed event that carries an extent (the server window
// resized or rotated) is applied to the same surface, beside the default-framebuffer
// reallocation the drain already does. One surface at a time: the newest wins, and
// ForgetServerOwnedWindowSurface drops it when the client releases it.
void NoteServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
void ForgetServerOwnedWindowSurface(EGLSurface surface);
// SEG_EVENT's ring capacity, exposed so the readback path can slice a writeback
// request into records that always fit (RingProducer::MaxRecordBytes ==
// capacity/2, Ring.h:288).
+47
View File
@@ -1056,6 +1056,53 @@ namespace MobileGL {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P12 (on-screen server window). See Core.h. Split builds only (G1).
EGLContext::EGLSurfaceHandle EGLContext::CreateServerOwnedWindowSurface(EGLDisplayHandle display,
EGLConfigHandle config,
const void* nativeWindow,
EGLint width, EGLint height,
Bool platformWindow) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (!IsDisplayInitialized(display)) {
SetError(EGL_NOT_INITIALIZED);
return EGL_NO_SURFACE;
}
if (!ValidateConfigOnDisplay(display, config)) {
SetError(EGL_BAD_CONFIG);
return EGL_NO_SURFACE;
}
if (width < 0 || height < 0) {
SetError(EGL_BAD_ATTRIBUTE);
return EGL_NO_SURFACE;
}
// No EGL_BAD_NATIVE_WINDOW for a null window: the window is the server's.
const auto surface = EncodeHandle<EGLSurfaceHandle>(m_nextSurfaceHandle++);
m_surfaces[surface] = SurfaceObject{
.Display = display,
.Config = config,
.Type = platformWindow ? SurfaceType::PlatformWindow : SurfaceType::Window,
.NativeHandleKey = ToNativeKey(nativeWindow),
.ClientBuffer = nullptr,
.BufferType = EGL_NONE,
.Width = width,
.Height = height,
};
return surface;
}
Bool EGLContext::SetSurfaceExtent(EGLSurfaceHandle surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto surfaceIt = m_surfaces.find(surface);
if (surfaceIt == m_surfaces.end() || width < 0 || height < 0) {
return false;
}
surfaceIt->second.Width = width;
surfaceIt->second.Height = height;
return true;
}
#endif
Bool EGLContext::QuerySurface(EGLDisplayHandle display, EGLSurfaceHandle surface, EGLint attribute,
EGLint* value) const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
+16
View File
@@ -87,6 +87,22 @@ namespace MobileGL {
Bool ValidateSurface(EGLSurfaceHandle surface) const;
Bool ValidateSurfaceOnDisplay(EGLDisplayHandle display, EGLSurfaceHandle surface) const;
Bool SwapInterval(EGLDisplayHandle display, EGLint interval);
#if MOBILEGL_BUILD_DISAGGREGATED
// P12 (on-screen server window), MOBILEGL_IPC_SURFACE=server. A window surface whose
// window is the SERVER's: the client's native window may be NULL (a headless client
// has none), and the extent it asked for (EGL_WIDTH/EGL_HEIGHT; 0/0 = the server
// window's own) stands until the server's real one arrives through SetSurfaceExtent.
// `platformWindow` keeps eglCreatePlatformWindowSurface's surface type. Split builds
// only: the pull build's state class is unchanged (G1).
EGLSurfaceHandle CreateServerOwnedWindowSurface(EGLDisplayHandle display, EGLConfigHandle config,
const void* nativeWindow, EGLint width,
EGLint height, Bool platformWindow);
// P12: the server's REAL extent of a surface - a server-owned window's geometry, from
// the CreateWindowSurface reply and from every surface-changed event after it - so
// eglQuerySurface(EGL_WIDTH/EGL_HEIGHT) answers what the server renders at. Unlike
// ResizeSurface it needs no display (the event carries none) and keeps a 0 as 0.
Bool SetSurfaceExtent(EGLSurfaceHandle surface, EGLint width, EGLint height);
#endif
// Current
Bool MakeCurrent(EGLDisplayHandle display, EGLSurfaceHandle draw, EGLSurfaceHandle read,
@@ -23,8 +23,13 @@
// ServerLoopTest's cases (AVoidForwarderCrossesAsOneDispatchedFrame and the whole
// ServerLoopEglTest fixture, which now drives every forwarder through the frame channel).
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Remote/Client/BackendObject_Remote.h>
#include <MG_Remote/FatalFunnel.h>
#include <MG_Remote/Protocol/SurfaceOpCodec.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Server/ServerLoop.h>
@@ -33,10 +38,12 @@
#include <gtest/gtest.h>
#include <csignal>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <type_traits>
#include <vector>
#if defined(_WIN32)
#include <process.h>
@@ -435,6 +442,329 @@ TEST(SurfaceControlFrameTest, ALatchedSessionAnswersAWellFormedSurfaceOpWithoutR
}
#endif
// =====================================================================================
// P12 (on-screen server window): WindowKind::ServerOwned on the wire, the reply's geometry and
// refusal, and MOBILEGL_IPC_SURFACE=server's one-frame create through the real client EGL layer
// =====================================================================================
namespace {
using MobileGL::MG_Remote::SessionLatched;
const ::MobileGL::Wire::SurfaceOp* BuildWireSurfaceOp(flatbuffers::FlatBufferBuilder& builder,
::MobileGL::Wire::SurfaceOpKind kind,
::MobileGL::Wire::WindowKind windowKind, Uint64 token) {
const auto op = ::MobileGL::Wire::CreateSurfaceOp(builder, /*seq=*/31, kind, /*display=*/1, /*surface=*/2,
windowKind, token, 640, 480, 0, 0, 0);
const auto envelope =
::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope);
return ::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_SurfaceOp();
}
} // namespace
// D2. The frame-local tag encodes as WindowKind::ServerOwned with token 0 WHATEVER the frame carried
// (the client's own window value never reaches the wire, Rule G/H), and decodes back to the tag. Red
// with the encoder's ServerOwned arm deleted: the tag names no WindowBackend and the encode fails
// UnknownWindowKind; red with the decoder's arm deleted: the kind decodes as out of range.
TEST(SurfaceControlFrameTest, AServerOwnedCreateEncodesAsItsOwnWindowKindAndNeverCarriesAToken) {
EXPECT_EQ(static_cast<unsigned>(::MobileGL::Wire::WindowKind::ServerOwned), 7u) << "wire ABI: append-only";
SurfaceControlFrame frame;
frame.kind = SurfaceControlOp::CreateWindowSurface;
frame.seq = 5;
frame.surface = 9;
frame.windowBackend = Server::kServerOwnedWindowBackend;
frame.nativeToken = 0xDEADBEEFull;
frame.width = 1280;
frame.height = 720;
flatbuffers::FlatBufferBuilder builder(256);
const ::MobileGL::Wire::SurfaceOp* wireOp = EncodeAndParse(frame, &builder);
ASSERT_NE(wireOp, nullptr) << "the ServerOwned create did not encode";
EXPECT_EQ(wireOp->windowKind(), ::MobileGL::Wire::WindowKind::ServerOwned);
EXPECT_EQ(wireOp->nativeToken(), 0u) << "a client value crossed the wire in a ServerOwned op";
EXPECT_EQ(wireOp->width(), 1280);
EXPECT_EQ(wireOp->height(), 720);
SurfaceControlFrame back;
ASSERT_EQ(DecodeWireSurfaceOp(*wireOp, &back), SurfaceWireError::None);
EXPECT_EQ(back.windowBackend, Server::kServerOwnedWindowBackend);
EXPECT_EQ(back.nativeToken, 0u);
EXPECT_EQ(back.width, 1280);
EXPECT_EQ(back.height, 720);
// ServerOwned is not a window backend: the mapping table has no row for it.
MG_Backend::WindowBackend backend;
EXPECT_FALSE(WindowBackendForWireWindowKind(::MobileGL::Wire::WindowKind::ServerOwned, &backend));
// And the decode bound moved exactly one: the value after it is still unknown.
flatbuffers::FlatBufferBuilder past(256);
const auto* pastOp = BuildWireSurfaceOp(past, ::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface,
static_cast<::MobileGL::Wire::WindowKind>(8), 0);
ASSERT_NE(pastOp, nullptr);
EXPECT_EQ(DecodeWireSurfaceOp(*pastOp, &back), SurfaceWireError::UnknownWindowKind);
}
// D2. SetWindowHandle may not name the server's window: the encoder refuses it before the wire.
TEST(SurfaceControlFrameTest, AServerOwnedSetWindowHandleRefusesToEncode) {
SurfaceControlFrame frame;
frame.kind = SurfaceControlOp::SetWindowHandle;
frame.windowBackend = Server::kServerOwnedWindowBackend;
flatbuffers::FlatBufferBuilder builder(256);
EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::ServerOwnedOnSetWindowHandle);
}
// D2. ... and one arriving from a peer is a NAMED REFUSAL answered in-band - not a latch and, in this
// unarmed process, not a death either. Red with the refusal arm deleted: the decode error falls into
// the latch-or-die path and this process aborts.
TEST(SurfaceControlFrameTest, AServerOwnedSetWindowHandleOnTheWireIsANamedRefusalNotALatch) {
flatbuffers::FlatBufferBuilder builder(256);
const auto* wireOp = BuildWireSurfaceOp(builder, ::MobileGL::Wire::SurfaceOpKind::SetWindowHandle,
::MobileGL::Wire::WindowKind::ServerOwned, 0);
ASSERT_NE(wireOp, nullptr);
SurfaceControlFrame frame;
EXPECT_EQ(DecodeWireSurfaceOp(*wireOp, &frame), SurfaceWireError::ServerOwnedOnSetWindowHandle);
SurfaceControlFrame reply;
EXPECT_EQ(ServerApplyWireSurfaceOp(*wireOp, &reply), MOBILEGL_ERR_UNSUPPORTED);
EXPECT_FALSE(reply.ok);
EXPECT_EQ(reply.seq, 31u) << "the refusal must still name its op";
EXPECT_EQ(reply.refusal, static_cast<Uint8>(Server::SurfaceRefusalCode::ServerOwnedOnSetWindowHandle));
EXPECT_FALSE(SessionLatched());
EXPECT_NE(ReadLog().find("Refuse ServerOwned on SetWindowHandle"), std::string::npos) << ReadLog();
}
#if !defined(_WIN32)
// D2. A ServerOwned create with a non-zero token is the peer's corrupt bytes: unarmed it dies by name.
TEST(SurfaceControlFrameTest, AServerOwnedCreateWithANonZeroTokenIsProtocolCorruptionByName) {
flatbuffers::FlatBufferBuilder builder(256);
const auto* wireOp = BuildWireSurfaceOp(builder, ::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface,
::MobileGL::Wire::WindowKind::ServerOwned, 0x1234);
ASSERT_NE(wireOp, nullptr);
SurfaceControlFrame frame;
EXPECT_EQ(DecodeWireSurfaceOp(*wireOp, &frame), SurfaceWireError::ServerOwnedTokenNotZero);
EXPECT_EXIT(ServerApplyWireSurfaceOp(*wireOp, nullptr), ::testing::KilledBySignal(SIGABRT), ".*");
EXPECT_NE(ReadLog().find("Fatal{ProtocolCorruption, \"SurfaceOp.nativeToken\"}"), std::string::npos)
<< "the abort happened but not for this rule's reason; the log says: " << ReadLog();
}
namespace {
// ... and armed (a session child), the same bytes LATCH: answered PROTOCOL_MISMATCH, not run,
// the first fault named SurfaceOp.nativeToken.
[[noreturn]] void ANonZeroServerOwnedTokenLatchesAndExit() {
MobileGL::MG_Remote::ArmSessionLatch();
flatbuffers::FlatBufferBuilder builder(256);
const auto* wireOp = BuildWireSurfaceOp(builder, ::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface,
::MobileGL::Wire::WindowKind::ServerOwned, 0x1234);
if (wireOp == nullptr) ::_exit(64);
SurfaceControlFrame reply;
reply.ok = true;
const MobileGLResult rc = ServerApplyWireSurfaceOp(*wireOp, &reply);
int failed = 0;
if (rc != MOBILEGL_ERR_PROTOCOL_MISMATCH) failed |= 1;
if (!SessionLatched()) failed |= 2;
if (MobileGL::MG_Remote::SessionLatchedFamily() != MobileGL::MG_Remote::MGFatalFamily::ProtocolCorruption ||
std::strstr(MobileGL::MG_Remote::SessionLatchedLine(), "\"SurfaceOp.nativeToken\"") == nullptr)
failed |= 4;
if (reply.ok) failed |= 8;
::_exit(failed);
}
} // namespace
TEST(SurfaceControlFrameTest, AServerOwnedCreateWithANonZeroTokenLatchesAnArmedSession) {
EXPECT_EXIT(ANonZeroServerOwnedTokenLatchesAndExit(), ::testing::ExitedWithCode(0), ".*")
<< "bits: 1 = not answered PROTOCOL_MISMATCH; 2 = not latched; 4 = the latched fault is not "
"SurfaceOp.nativeToken; 8 = the reply said ok (64 = setup)";
}
#endif
// D1/D3. The reply half carries the surface's REAL geometry and the server's named refusal
// (SurfaceReply.width/height/refusal, revision 3), overwriting what the request carried.
TEST(SurfaceControlFrameTest, TheReplyCarriesTheSurfaceGeometryAndTheNamedRefusal) {
SurfaceControlFrame answered;
answered.kind = SurfaceControlOp::CreateWindowSurface;
answered.seq = 77;
answered.ok = false;
answered.width = 1920;
answered.height = 1080;
answered.refusal = static_cast<Uint8>(Server::SurfaceRefusalCode::NoServerDisplay);
flatbuffers::FlatBufferBuilder builder(256);
EncodeSurfaceReplyFrame(answered, &builder);
const auto* reply = ::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_SurfaceReply();
ASSERT_NE(reply, nullptr);
EXPECT_EQ(reply->refusal(), ::MobileGL::Wire::SurfaceRefusal::NoServerDisplay);
SurfaceControlFrame asked;
asked.width = 64;
asked.height = 48;
DecodeWireSurfaceReply(*reply, &asked);
EXPECT_EQ(asked.seq, 77u);
EXPECT_FALSE(asked.ok);
EXPECT_EQ(asked.width, 1920) << "the reply's geometry is the server's, not the request's";
EXPECT_EQ(asked.height, 1080);
EXPECT_EQ(asked.refusal, static_cast<Uint8>(Server::SurfaceRefusalCode::NoServerDisplay));
}
// ---- D1: the client's half, through the REAL client EGL layer and BackendObject_Remote ----------
//
// The server is replaced by a capturing remote control sink (the seam ClientSession installs under
// spawn/tcp), so what is measured is exactly what the client would put on the wire, and the reply
// the sink gives back is what the client does with the server's answer.
namespace {
struct CapturedControl {
std::vector<SurfaceControlFrame> frames;
Bool answerOk = true;
Uint8 refusal = 0;
Int windowWidth = 1920; // the "server window's" real extent in a ServerOwned reply
Int windowHeight = 1080;
};
CapturedControl g_captured;
MobileGLResult CaptureAndAnswer(void*, SurfaceControlFrame& frame) {
g_captured.frames.push_back(frame);
frame.ok = g_captured.answerOk;
frame.refusal = g_captured.answerOk ? 0 : g_captured.refusal;
if (frame.kind == SurfaceControlOp::InitializeDisplay) {
frame.ok = true;
frame.eglMajor = 1;
frame.eglMinor = 5;
}
if (frame.ok && frame.kind == SurfaceControlOp::CreateWindowSurface &&
frame.windowBackend == Server::kServerOwnedWindowBackend) {
frame.width = g_captured.windowWidth;
frame.height = g_captured.windowHeight;
}
return MOBILEGL_OK;
}
// A client EGL layer with an initialized display and a window-capable config, whose backend is
// the real BackendObject_Remote talking to the capturing sink.
struct RemoteEglClient {
EGLDisplay dpy = EGL_NO_DISPLAY;
EGLConfig config = nullptr;
RemoteEglClient(MG_Config::TransportMode transport, MG_Config::IpcSurface surface) {
MG_Config::Transport = transport;
MG_Config::Ipc.Surface = surface;
g_captured = CapturedControl{};
Server::ServerLoopInstance().SetRemoteControlSink(&CaptureAndAnswer, nullptr);
MG_State::pEGLContext = MakeUnique<MG_State::EGLState::EGLContext>();
MG_Backend::pActiveBackendObject = MakeUnique<MG_Remote::Client::BackendObject_Remote>();
dpy = MG_State::pEGLContext->GetDisplay(EGL_DEFAULT_DISPLAY);
EGLint major = 0;
EGLint minor = 0;
(void)MG_State::pEGLContext->InitializeDisplay(dpy, &major, &minor);
(void)MG_Backend::pActiveBackendObject->InitializeEGLDisplay(dpy, &major, &minor);
const EGLint attribs[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT, EGL_NONE};
EGLint count = 0;
(void)MG_State::pEGLContext->ChooseConfig(dpy, attribs, &config, 1, &count);
g_captured.frames.clear();
}
~RemoteEglClient() {
MG_Backend::pActiveBackendObject.reset();
MG_State::pEGLContext.reset();
Server::ServerLoopInstance().SetRemoteControlSink(nullptr, nullptr);
MG_Config::Transport = MG_Config::TransportMode::Monolith;
MG_Config::Ipc.Surface = MG_Config::IpcSurface::Offscreen;
}
};
NativeWindowType FakeClientWindow() { return (NativeWindowType)(std::uintptr_t)0x1234; }
} // namespace
// THE HEADLINE OF D1: a headless client (NULL window) with MOBILEGL_IPC_SURFACE=server gets a window
// surface, sends ONE CreateWindowSurface naming the server's window (token 0, the EGL_WIDTH/HEIGHT it
// asked for) and no SetWindowHandle, and eglQuerySurface answers the SERVER's size the moment the
// create returns. Red three ways: with EGLImpl's null check restored the create fails
// EGL_BAD_NATIVE_WINDOW; with BackendObject_Remote's arm deleted two frames go (SetWindowHandle first)
// and the null handle is refused; with the reply's geometry not adopted the query answers 640x480.
TEST(SurfaceControlFrameTest, WithTheServerKnobAHeadlessWindowSurfaceIsOneServerOwnedFrameAndTakesTheServersSize) {
RemoteEglClient client(MG_Config::TransportMode::Spawn, MG_Config::IpcSurface::Server);
ASSERT_NE(client.config, nullptr);
const EGLint attribs[] = {EGL_WIDTH, 640, EGL_HEIGHT, 480, EGL_NONE};
const EGLSurface surface =
MG_Impl::EGLImpl::CreateWindowSurface(client.dpy, client.config, NativeWindowType{}, attribs);
ASSERT_NE(surface, EGL_NO_SURFACE) << "a headless client's NULL window was refused under "
"MOBILEGL_IPC_SURFACE=server; eglGetError=0x"
<< std::hex << MG_Impl::EGLImpl::GetError();
ASSERT_EQ(g_captured.frames.size(), 1u) << "exactly ONE control frame - no SetWindowHandle in front";
const SurfaceControlFrame& sent = g_captured.frames.front();
EXPECT_EQ(sent.kind, SurfaceControlOp::CreateWindowSurface);
EXPECT_EQ(sent.windowBackend, Server::kServerOwnedWindowBackend);
EXPECT_EQ(sent.nativeToken, 0u);
EXPECT_EQ(sent.width, 640);
EXPECT_EQ(sent.height, 480);
// What the wire carries for it.
flatbuffers::FlatBufferBuilder builder(256);
const ::MobileGL::Wire::SurfaceOp* wireOp = EncodeAndParse(sent, &builder);
ASSERT_NE(wireOp, nullptr);
EXPECT_EQ(wireOp->windowKind(), ::MobileGL::Wire::WindowKind::ServerOwned);
EXPECT_EQ(wireOp->nativeToken(), 0u);
// The geometry flowed back before the create returned.
EGLint width = 0;
EGLint height = 0;
ASSERT_EQ(MG_Impl::EGLImpl::QuerySurface(client.dpy, surface, EGL_WIDTH, &width), EGL_TRUE);
ASSERT_EQ(MG_Impl::EGLImpl::QuerySurface(client.dpy, surface, EGL_HEIGHT, &height), EGL_TRUE);
EXPECT_EQ(width, 1920) << "eglQuerySurface does not answer the server window's size";
EXPECT_EQ(height, 1080);
EXPECT_NE(ReadLog().find("surface=window 1920x1080 owner=server"), std::string::npos) << ReadLog();
}
// D1: "NULL accepted only in server mode". Offscreen (the default), a NULL window is
// EGL_BAD_NATIVE_WINDOW in the client and nothing reaches the server.
TEST(SurfaceControlFrameTest, WithoutTheServerKnobANullWindowIsRefusedBeforeAnythingIsSent) {
RemoteEglClient client(MG_Config::TransportMode::Spawn, MG_Config::IpcSurface::Offscreen);
const EGLSurface surface =
MG_Impl::EGLImpl::CreateWindowSurface(client.dpy, client.config, NativeWindowType{}, nullptr);
EXPECT_EQ(surface, EGL_NO_SURFACE);
EXPECT_EQ(MG_Impl::EGLImpl::GetError(), EGL_BAD_NATIVE_WINDOW);
EXPECT_TRUE(g_captured.frames.empty()) << "a refused NULL window reached the server";
}
// D1: the knob means nothing without a remote server - under inproc it is ignored and a NULL window
// is refused exactly as before.
TEST(SurfaceControlFrameTest, TheServerKnobIsIgnoredWithoutARemoteServer) {
RemoteEglClient client(MG_Config::TransportMode::InProcess, MG_Config::IpcSurface::Server);
EXPECT_FALSE(MG_Config::ServerOwnedWindowSurfaces());
const EGLSurface surface =
MG_Impl::EGLImpl::CreateWindowSurface(client.dpy, client.config, NativeWindowType{}, nullptr);
EXPECT_EQ(surface, EGL_NO_SURFACE);
EXPECT_EQ(MG_Impl::EGLImpl::GetError(), EGL_BAD_NATIVE_WINDOW);
EXPECT_TRUE(g_captured.frames.empty());
}
// D1: "with offscreen nothing changes, byte for byte" - a client window is still named by
// SetWindowHandle first and then created with its own window kind.
TEST(SurfaceControlFrameTest, WithoutTheServerKnobAClientWindowStillSendsSetWindowHandleFirst) {
RemoteEglClient client(MG_Config::TransportMode::Spawn, MG_Config::IpcSurface::Offscreen);
const EGLSurface surface =
MG_Impl::EGLImpl::CreateWindowSurface(client.dpy, client.config, FakeClientWindow(), nullptr);
EXPECT_NE(surface, EGL_NO_SURFACE);
ASSERT_EQ(g_captured.frames.size(), 2u);
EXPECT_EQ(g_captured.frames[0].kind, SurfaceControlOp::SetWindowHandle);
EXPECT_EQ(g_captured.frames[1].kind, SurfaceControlOp::CreateWindowSurface);
EXPECT_NE(g_captured.frames[1].windowBackend, Server::kServerOwnedWindowBackend);
EXPECT_EQ(g_captured.frames[1].nativeToken, 0x1234u);
}
// D1/D3: a server refusal FAILS BY NAME ON THE CLIENT TOO - the reply's refusal code is what lets the
// client's own log say why, instead of "ok=false".
TEST(SurfaceControlFrameTest, AServerOwnedRefusalIsNamedInTheClientsLog) {
RemoteEglClient client(MG_Config::TransportMode::Spawn, MG_Config::IpcSurface::Server);
g_captured.answerOk = false;
g_captured.refusal = static_cast<Uint8>(Server::SurfaceRefusalCode::NoServerDisplay);
const EGLSurface surface =
MG_Impl::EGLImpl::CreateWindowSurface(client.dpy, client.config, NativeWindowType{}, nullptr);
EXPECT_EQ(surface, EGL_NO_SURFACE);
EXPECT_EQ(MG_Impl::EGLImpl::GetError(), EGL_BAD_NATIVE_WINDOW);
EXPECT_NE(ReadLog().find("Refuse ServerOwned (NoServerDisplay)"), std::string::npos) << ReadLog();
}
// D4, the client's half: a pbuffer the server refused as SurfaceModeMismatch is named here as well.
TEST(SurfaceControlFrameTest, APbufferRefusedAsSurfaceModeMismatchIsNamedInTheClientsLog) {
RemoteEglClient client(MG_Config::TransportMode::Spawn, MG_Config::IpcSurface::Server);
g_captured.answerOk = false;
g_captured.refusal = static_cast<Uint8>(Server::SurfaceRefusalCode::SurfaceModeMismatch);
const EGLint attribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE};
EXPECT_EQ(MG_Impl::EGLImpl::CreatePbufferSurface(client.dpy, client.config, attribs), EGL_NO_SURFACE);
EXPECT_NE(ReadLog().find("SurfaceModeMismatch - eglCreatePbufferSurface"), std::string::npos) << ReadLog();
}
int main(int argc, char** argv) {
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
// write, and caches the FILE*. The name carries this process's pid, because