diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 130ddceb..88a7be15 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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 diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 1c2ceee2..1389fc8c 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -16,6 +16,11 @@ #include #include +#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 +#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(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(ipc.StrictErrors), static_cast(ipc.Audit), - static_cast(ipc.RoleSplitState), ipc.ServerAffinity.c_str()); + static_cast(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 " diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index 636ff229..962c862a 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -11,6 +11,11 @@ #include #include #include +#if MOBILEGL_BUILD_DISAGGREGATED +// MG_Config::ServerOwnedWindowSurfaces (P12, MOBILEGL_IPC_SURFACE). Split-only: the pull build's +// translation unit is unchanged (G1). +#include +#endif #include #include #include @@ -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(std::max(GetAttribValue(attrib_list, EGL_HEIGHT, 0), 0)), }; +#if MOBILEGL_BUILD_DISAGGREGATED + EGLSurface surface = serverOwnedWindow + ? state->CreateServerOwnedWindowSurface( + dpy, config, windowHandle.Handle, static_cast(windowHandle.Width), + static_cast(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(std::max(GetAttribValueAttrib(attrib_list, EGL_HEIGHT, 0), 0)), }; +#if MOBILEGL_BUILD_DISAGGREGATED + EGLSurface surface = serverOwnedWindow + ? state->CreateServerOwnedWindowSurface( + dpy, config, native_window, static_cast(windowHandle.Width), + static_cast(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; } diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index 72b2e024..943b3b12 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -16,6 +16,7 @@ #include "../Server/ServerLoop.h" +#include #include // 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(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(reply.width), + static_cast(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); } diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.h b/MobileGL/MG_Remote/Client/BackendObject_Remote.h index b74c8f2b..ed252a5d 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.h +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.h @@ -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. diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 93db848a..2344db54 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -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 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(width), + static_cast(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(); } diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index f009a0e8..d5a46ab6 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -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). diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index 6e604182..133d21b8 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -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 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(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 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 lock(m_mutex); diff --git a/MobileGL/MG_State/EGLState/Core.h b/MobileGL/MG_State/EGLState/Core.h index f38a94ca..3c4b24ba 100644 --- a/MobileGL/MG_State/EGLState/Core.h +++ b/MobileGL/MG_State/EGLState/Core.h @@ -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, diff --git a/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp b/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp index bcbcd328..cfccecc8 100644 --- a/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp +++ b/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp @@ -23,8 +23,13 @@ // ServerLoopTest's cases (AVoidForwarderCrossesAsOneDispatchedFrame and the whole // ServerLoopEglTest fixture, which now drives every forwarder through the frame channel). +#include +#include +#include +#include #include #include +#include #include #include #include @@ -33,10 +38,12 @@ #include #include +#include #include #include #include #include +#include #if defined(_WIN32) #include @@ -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(::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(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(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(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 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_Backend::pActiveBackendObject = MakeUnique(); + 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(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(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