diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index d8eaa176..fc70eb52 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -692,6 +692,25 @@ namespace MobileGL { virtual void ReleaseEGLSurface(EGLSurface surface); virtual void ReleaseEGLResources(); + // ---- the server's own display window going away and coming back ------------------- + // + // `window` (a window surface's native window) is about to be destroyed - the display's + // screen went off. Release ONLY what is bound to it: every window surface on it keeps its + // client handle and draws into a placeholder from here on, and every context, object and + // device stays. Called on the thread that runs this backend; nothing may reference the + // window once it returns true. False: not supported (the caller ends the session instead). + virtual Bool SuspendServerWindow(void* window) { + (void)window; + return false; + } + // The surfaces SuspendServerWindow parked are rebuilt on `window` (a new native window, + // `width`x`height`), and their new extent is published as a creation publishes it. False: + // they could not be (the caller ends the session). + virtual Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) { + (void)window, (void)width, (void)height; + return false; + } + void SetWindowHandle(const WindowHandle& handle); virtual const RendererInfo& GetRendererInfo() const = 0; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 2be53a57..11f0bf11 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -936,6 +936,19 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool BackendObject_DirectGLES::InitWindowSurface() { if (RebindNativeSurfaceFor(m_activatingSurface)) return true; + // A suspended surface whose placeholder went: another placeholder - never the window, which + // is gone. + if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), m_activatingSurface) != + m_suspendedWindowSurfaces.end()) { + const EGLSurface placeholder = DirectGLES::CreateSessionPbuffer(1, 1); + if (placeholder == EGL_NO_SURFACE || !DirectGLES::BindSessionSurface(placeholder)) { + MGLOG_E("DirectGLES: no placeholder for the suspended window surface %p", + reinterpret_cast(m_activatingSurface)); + return false; + } + m_nativeSurfaces[m_activatingSurface] = placeholder; + return true; + } // Only use EGL for now auto nativeWindow = reinterpret_cast(m_windowHandle.Handle); if (!DirectGLES::InitWindowSurface(nativeWindow)) { @@ -1223,9 +1236,71 @@ namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_BUILD_DISAGGREGATED NoteNativeContextGone(); #endif + m_suspendedWindowSurfaces.clear(); BackendObject::ReleaseEGLResources(); } + Bool BackendObject_DirectGLES::SuspendServerWindow(void* window) { + const std::lock_guard lock(m_eglStateMutex); + if (window == nullptr) return false; + Vector onWindow; + for (const auto& [client, state] : m_eglSurfaces) { + if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue; + if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) != + m_suspendedWindowSurfaces.end()) + continue; + onWindow.push_back(client); + } + for (const EGLSurface client : onWindow) { + // A surface never activated has no native surface: only its record names the window. + const auto native = m_nativeSurfaces.find(client); + if (native != m_nativeSurfaces.end()) { + const EGLSurface placeholder = DirectGLES::SuspendSessionWindowSurface(native->second); + if (placeholder == EGL_NO_SURFACE) return false; + native->second = placeholder; + } + m_suspendedWindowSurfaces.push_back(client); + } +#if MOBILEGL_BUILD_DISAGGREGATED + // The native binding moved onto the placeholder: the next make-current binds for real. + NoteNativeContextGone(); +#endif + MGLOG_I("DirectGLES: %zu window surface(s) on server window %p now draw into placeholder pbuffers; the " + "contexts and their objects stay", + onWindow.size(), window); + return true; + } + + Bool BackendObject_DirectGLES::ResumeServerWindow(void* window, Uint32 width, Uint32 height) { + const std::lock_guard lock(m_eglStateMutex); + if (window == nullptr) return false; + Vector suspended; + suspended.swap(m_suspendedWindowSurfaces); + for (const EGLSurface client : suspended) { + const auto state = m_eglSurfaces.find(client); + if (state == m_eglSurfaces.end()) continue; // released while suspended + state->second.Window.Handle = window; + state->second.Window.Width = width; + state->second.Window.Height = height; + state->second.Width = static_cast(std::max(width, 1)); + state->second.Height = static_cast(std::max(height, 1)); + if (m_eglSurface == client) m_windowHandle = state->second.Window; + // Never activated: its first activation creates it on the new window. + const auto native = m_nativeSurfaces.find(client); + if (native == m_nativeSurfaces.end()) continue; + const EGLSurface surface = + DirectGLES::ResumeSessionWindowSurface(native->second, reinterpret_cast(window)); + if (surface == EGL_NO_SURFACE) return false; + native->second = surface; + } +#if MOBILEGL_BUILD_DISAGGREGATED + NoteNativeContextGone(); +#endif + MGLOG_I("DirectGLES: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(), + window, width, height); + return true; + } + void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) { // P14: releasing a surface releases THAT SURFACE (OnEGLSurfaceForgotten), never the // contexts. This used to be DestroyEGLContext() - every native context of the session, with @@ -1238,6 +1313,9 @@ namespace MobileGL::MG_Backend::DirectGLES { } void BackendObject_DirectGLES::OnEGLSurfaceForgotten(EGLSurface surface) { + m_suspendedWindowSurfaces.erase( + std::remove(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), surface), + m_suspendedWindowSurfaces.end()); const auto it = m_nativeSurfaces.find(surface); if (it == m_nativeSurfaces.end()) return; const EGLSurface native = it->second; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index 92c50e5d..c0cabc51 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -47,6 +47,10 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image) override; void ReleaseEGLSurface(EGLSurface surface) override; void ReleaseEGLResources() override; + // The server window going and coming back: each window surface on it swaps its native EGL + // window surface for a placeholder pbuffer and back (DirectGLES::Suspend/ResumeSessionWindowSurface). + Bool SuspendServerWindow(void* window) override; + Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override; const RendererInfo& GetRendererInfo() const override; String GetBackendAPIVersionString() const override; @@ -75,6 +79,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // on every later one. A multi-window client switches surfaces every frame; making a new // native surface each time leaked them and lost what the window had drawn. UnorderedMap m_nativeSurfaces; + // Window surfaces whose window is gone (SuspendServerWindow): their native surface is a + // placeholder pbuffer until ResumeServerWindow. + Vector m_suspendedWindowSurfaces; MG_External::EGLFunctionsTable m_EGLFunctions; MG_External::GLESFunctionsTable m_GLESFunctions; MG_External::GLESCapabilities m_GLESCapabilities; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index d58fb659..777a41d5 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -36,6 +36,7 @@ #include #if defined(__ANDROID__) #include +#include #include #endif #endif @@ -16275,6 +16276,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // re-makes current, and what Present/eglSwapBuffers names. EGLSurface Draw = EGL_NO_SURFACE; EGLSurface Read = EGL_NO_SURFACE; + // The server window's extent this session last published to its client (0x0: none yet). + // Present compares the window against it (RepublishServerWindowExtentIfMoved). + Int PublishedWidth = 0; + Int PublishedHeight = 0; }; static std::mutex g_nativeRegistryMutex; @@ -16516,6 +16521,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // framebuffer's shape, so that publish - and no pbuffer's - may carry the surface's extent. // Split builds only: the pull build's publish is unchanged (G1). static Bool g_publishWindowExtent = false; + // The extent that publish carries when the caller already knows it (a republish from the window's + // own size, which the surface's buffers follow from the next frame on); 0x0 = ask the surface. + static Int g_publishExtentWidth = 0; + static Int g_publishExtentHeight = 0; #endif static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) { @@ -16680,11 +16689,15 @@ namespace MobileGL::MG_Backend::DirectGLES { // (ClientSession.cpp ApplySurfaceChangedToClient), so it is deliberately NOT published // for a pbuffer, nor under inproc / monolith: those keep the format-only shape above. if (g_publishWindowExtent && MG_Config::Transport == MG_Config::TransportMode::Spawn) { - Int extentWidth = 0; - Int extentHeight = 0; - if (QueryCurrentSurfaceSize(extentWidth, extentHeight)) { + Int extentWidth = g_publishExtentWidth; + Int extentHeight = g_publishExtentHeight; + if ((extentWidth > 0 && extentHeight > 0) || QueryCurrentSurfaceSize(extentWidth, extentHeight)) { info.Width = static_cast(extentWidth); info.Height = static_cast(extentHeight); + // What Present compares the window against from here on. + NativeSessionState& session = ActiveNativeSession(); + session.PublishedWidth = extentWidth; + session.PublishedHeight = extentHeight; } } #endif @@ -17209,6 +17222,157 @@ namespace MobileGL::MG_Backend::DirectGLES { void ForgetRequestedSwapInterval() { g_requestedSwapInterval = -1; } #endif + // A crashed predecessor session may have left its surface bound to this window (the + // server-owned display window is one ANativeWindow for the process's lifetime, and + // several drivers refuse a second eglCreateWindowSurface on it with EGL_BAD_NATIVE_WINDOW). + // Teardown is keyed on the session the surface belongs to, so a session that died without + // one leaks it; find and destroy those before asking the driver. + static void DestroyStaleSurfacesOnWindow(NativeWindowType window) { + const std::lock_guard lock(g_nativeRegistryMutex); + for (auto* session : NativeSessions()) { + auto& entries = session->Surfaces; + for (auto it = entries.begin(); it != entries.end();) { + if (it->Window == window) { + if (g_EGLFuncs.eglDestroySurface) g_EGLFuncs.eglDestroySurface(g_Display, it->Surface); + if (session->Draw == it->Surface) session->Draw = EGL_NO_SURFACE; + if (session->Read == it->Surface) session->Read = EGL_NO_SURFACE; + it = entries.erase(it); + } else { + ++it; + } + } + } + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + } + + // The default framebuffer's shape after a window surface was bound - with its extent when the + // window is the server's own (see the push arm of the publish). + static void PublishWindowSurfaceShape(NativeWindowType window) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P12 (D3): this publish may carry the window's extent (see the push arm of the publish) - + // review fix: only when the window is the SERVER's own (a headless client's ServerOwned + // surface). A window the client named keeps the format-only publish without the knob. + g_publishWindowExtent = MG_Pipe::MGPipeServerOwnedWindow() != nullptr && + reinterpret_cast(window) == MG_Pipe::MGPipeServerOwnedWindow(); + PublishDefaultFramebufferDepthStencilFormat(); + g_publishWindowExtent = false; +#else + (void)window; + PublishDefaultFramebufferDepthStencilFormat(); +#endif + } + +#if MOBILEGL_BUILD_DISAGGREGATED + // The window's own extent, which the surface's buffers take from the next frame on - not the + // surface's, which on some drivers still reports the buffer just presented. + static Bool CurrentWindowExtent(NativeWindowType window, EGLSurface surface, Int& outWidth, Int& outHeight) { +#if defined(__ANDROID__) + (void)surface; + auto* nativeWindow = reinterpret_cast(window); + outWidth = static_cast(ANativeWindow_getWidth(nativeWindow)); + outHeight = static_cast(ANativeWindow_getHeight(nativeWindow)); +#else + (void)window; + EGLint width = 0; + EGLint height = 0; + if (!g_EGLFuncs.eglQuerySurface || !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &width) || + !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &height)) + return false; + outWidth = static_cast(width); + outHeight = static_cast(height); +#endif + return outWidth > 0 && outHeight > 0; + } + + // THE SERVER WINDOW CHANGED SIZE UNDER ITS SURFACE (the app's layout moved the view: a bar shown + // or hidden, a rotation). Its extent used to be published once, at the surface's creation, so the + // client kept sizing its viewport and default framebuffer for the old one while the window's + // buffers took the new one. Asked at every present of the session's server-owned window surface - + // one size query - and republished, as the creation publishes it, when it moved. + static void RepublishServerWindowExtentIfMoved(EGLSurface drawSurface) { + const void* const serverWindow = MG_Pipe::MGPipeServerOwnedWindow(); + if (serverWindow == nullptr || drawSurface == EGL_NO_SURFACE || + MG_Config::Transport != MG_Config::TransportMode::Spawn) + return; + NativeSessionState& session = ActiveNativeSession(); + NativeWindowType window = static_cast(0); + for (const auto& entry : session.Surfaces) { + if (entry.Surface != drawSurface) continue; + window = entry.Window; + break; + } + if (reinterpret_cast(window) != serverWindow) return; + Int width = 0; + Int height = 0; + if (!CurrentWindowExtent(window, drawSurface, width, height)) return; + if (width == session.PublishedWidth && height == session.PublishedHeight) return; + MGLOG_I("DirectGLES: the server window is now %dx%d (last published %dx%d); its extent is republished to " + "the client", + width, height, session.PublishedWidth, session.PublishedHeight); + g_publishExtentWidth = width; + g_publishExtentHeight = height; + PublishWindowSurfaceShape(window); + g_publishExtentWidth = 0; + g_publishExtentHeight = 0; + // Asked once per size, whatever the publish managed: a publish that could not run is not + // retried - and logged - every frame. + session.PublishedWidth = width; + session.PublishedHeight = height; + } +#endif + + EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface) { + if (windowSurface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return EGL_NO_SURFACE; + if (!SessionOwnsSurface(ActiveNativeSession(), windowSurface)) return EGL_NO_SURFACE; + // 1x1: nothing is shown while the window is gone, and nothing is published for it - the + // client keeps the extent it has until the resume publishes the new window's. + const EGLSurface placeholder = CreateSessionPbuffer(1, 1); + if (placeholder == EGL_NO_SURFACE) { + const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; + MGLOG_E("DirectGLES: no placeholder pbuffer could stand in for the window surface %p (EGL error 0x%x)", + reinterpret_cast(windowSurface), static_cast(error)); + return EGL_NO_SURFACE; + } + ReplaceSessionSurface(windowSurface, placeholder); + // The rebind onto the placeholder failed and the window surface is still current here - EGL + // destroys a current surface only once it is not, and the window goes when this returns. + if (t_boundNativeDraw == windowSurface) (void)ReleaseCurrent(); + return placeholder; + } + + EGLSurface ResumeSessionWindowSurface(EGLSurface placeholder, NativeWindowType window) { + if (!window || g_Display == EGL_NO_DISPLAY) return EGL_NO_SURFACE; + NativeContextTuple& tuple = ActiveNativeContext(); + if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return EGL_NO_SURFACE; + DestroyStaleSurfacesOnWindow(window); + const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr); + if (surface == EGL_NO_SURFACE) { + const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; + MGLOG_E("DirectGLES: eglCreateWindowSurface(window=%p) failed with EGL error 0x%x on resume", + reinterpret_cast(window), static_cast(error)); + return EGL_NO_SURFACE; + } + NativeSessionState& session = ActiveNativeSession(); + { + const std::lock_guard lock(g_nativeRegistryMutex); + session.Surfaces.push_back({window, surface}); + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + } + if (placeholder != EGL_NO_SURFACE && SessionOwnsSurface(session, placeholder)) { + ReplaceSessionSurface(placeholder, surface); + } + // Bound, because the publish below reads the default framebuffer. + if (!BindSessionSurface(surface) || t_boundNativeDraw != surface) { + MGLOG_E("DirectGLES: the resumed window surface %p could not be made current; its extent is published " + "at its next bind", + reinterpret_cast(surface)); + return surface; + } + ApplyRequestedSwapInterval(); + PublishWindowSurfaceShape(window); + return surface; + } + Bool InitWindowSurface(NativeWindowType window) { if (!window) return false; @@ -17217,28 +17381,7 @@ namespace MobileGL::MG_Backend::DirectGLES { NativeContextTuple& tuple = ActiveNativeContext(); if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return false; - // A crashed predecessor session may have left its surface bound to this window (the - // server-owned display window is one ANativeWindow for the process's lifetime, and - // several drivers refuse a second eglCreateWindowSurface on it with EGL_BAD_NATIVE_WINDOW). - // Teardown is keyed on the session the surface belongs to, so a session that died without - // one leaks it; find and destroy those before asking the driver. - { - const std::lock_guard lock(g_nativeRegistryMutex); - for (auto* session : NativeSessions()) { - auto& entries = session->Surfaces; - for (auto it = entries.begin(); it != entries.end();) { - if (it->Window == window) { - if (g_EGLFuncs.eglDestroySurface) g_EGLFuncs.eglDestroySurface(g_Display, it->Surface); - if (session->Draw == it->Surface) session->Draw = EGL_NO_SURFACE; - if (session->Read == it->Surface) session->Read = EGL_NO_SURFACE; - it = entries.erase(it); - } else { - ++it; - } - } - } - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - } + DestroyStaleSurfacesOnWindow(window); const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr); if (surface == EGL_NO_SURFACE) { @@ -17257,17 +17400,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!MakeCurrent()) return false; ApplyRequestedSwapInterval(); -#if MOBILEGL_BUILD_DISAGGREGATED - // P12 (D3): this publish may carry the window's extent (see the push arm of the publish) - - // review fix: only when the window is the SERVER's own (a headless client's ServerOwned - // surface). A window the client named keeps the format-only publish without the knob. - g_publishWindowExtent = MG_Pipe::MGPipeServerOwnedWindow() != nullptr && - reinterpret_cast(window) == MG_Pipe::MGPipeServerOwnedWindow(); - PublishDefaultFramebufferDepthStencilFormat(); - g_publishWindowExtent = false; -#else - PublishDefaultFramebufferDepthStencilFormat(); -#endif + PublishWindowSurfaceShape(window); MGLOG_D("EGL window surface created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display, surface, tuple.Context, window); @@ -18002,6 +18135,11 @@ namespace MobileGL::MG_Backend::DirectGLES { if (canFence) PollFrameFences(); +#if MOBILEGL_BUILD_DISAGGREGATED + // The frame is out; the next one is drawn into the window's current buffers. + RepublishServerWindowExtentIfMoved(drawSurface); +#endif + // After the watermark advanced: retire grown-away ring stores and record the // frame's ring high-water marks for slot reclamation. BufferImpl::UboRingOnPresent(); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 3e30a4b3..b53cf69e 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -130,6 +130,16 @@ namespace MobileGL::MG_Backend::DirectGLES { // Every context of the session that drew to `previous` draws to `replacement` instead, and // `previous` is destroyed (a pbuffer-backed window's resize). void ReplaceSessionSurface(EGLSurface previous, EGLSurface replacement); + // The server window going away (screen off): every context of the session that drew to the + // window surface `windowSurface` draws to a new 1x1 placeholder pbuffer instead, and the window + // surface is destroyed - no longer current anywhere once this returns. The contexts and their + // objects stay. Returns the placeholder, or EGL_NO_SURFACE (nothing changed) when none could be + // made. + EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface); + // The way back: a window surface on `window` replaces `placeholder` (EGL_NO_SURFACE: none to + // replace) for every context that drew to it, is bound, and its extent is published as a + // creation publishes it. Returns the new surface, or EGL_NO_SURFACE. + EGLSurface ResumeSessionWindowSurface(EGLSurface placeholder, NativeWindowType window); Bool MakeCurrent(); Bool ReleaseCurrent(); // True when the backend ES context is current on the calling thread, i.e. diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index f66bfd7c..7e971493 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -366,6 +366,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {} Bool BackendObject_DirectVulkan::InitWindowSurface() { + // A suspended surface activates its placeholder - never the window, which is gone. + const auto suspended = m_suspendedWindowSurfaces.find(m_activatingSurface); + if (suspended != m_suspendedWindowSurfaces.end()) { + if (UsesSurfaceTargets() && pVulkanRenderer) { + ActivateSuspendedPlaceholder(m_activatingSurface, suspended->second); + return true; + } + return InitPbufferSurface(static_cast(suspended->second.Width), + static_cast(suspended->second.Height)); + } if (!m_windowHandle.Handle) { MGLOG_E("Cannot initialize DirectVulkan window surface: native window handle is null"); return false; @@ -596,14 +606,99 @@ namespace MobileGL::MG_Backend::DirectVulkan { // The reflection cache is file-scope, not renderer-owned; without this the // deleted programs' reflection strings survive full context teardown. ClearProgramResourceCaches(); + m_suspendedWindowSurfaces.clear(); BackendObject::ReleaseEGLResources(); } + void BackendObject_DirectVulkan::ActivateSuspendedPlaceholder(EGLSurface surface, + const SuspendedWindowSurface& extent) { + // A pbuffer target at the window's extent: every default-framebuffer path (render passes, + // read-back, the shared-image blit, the acquire and the present) keeps a live swapchain to + // work on, and the extent it publishes is the one the client already has. + VulkanRendererConfig config; + config.SurfaceWidth = std::max(extent.Width, 1); + config.SurfaceHeight = std::max(extent.Height, 1); + pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config); + } + + Bool BackendObject_DirectVulkan::SuspendServerWindow(void* window) { + const std::lock_guard lock(m_eglStateMutex); + // The monolith's one renderer IS its window; only surface targets can be swapped under it. + if (window == nullptr || !UsesSurfaceTargets()) return false; + Vector onWindow; + for (const auto& [client, state] : m_eglSurfaces) { + if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue; + if (m_suspendedWindowSurfaces.find(client) != m_suspendedWindowSurfaces.end()) continue; + onWindow.push_back(client); + } + for (const EGLSurface client : onWindow) { + const auto state = m_eglSurfaces.find(client); + SuspendedWindowSurface extent; + extent.Width = static_cast(std::max(state->second.Width, 1)); + extent.Height = static_cast(std::max(state->second.Height, 1)); + if (pVulkanRenderer) { + const Uint64 key = SurfaceTargetKey(client); + const VkExtent2D built = pVulkanRenderer->SurfaceTargetExtent(key); + if (built.width != 0 && built.height != 0) { + extent.Width = built.width; + extent.Height = built.height; + } + const Bool active = pVulkanRenderer->IsActiveSurfaceTarget(key); + // Flushes, waits the device idle, purges the render passes on the target and drops its + // swapchain and VkSurfaceKHR - nothing references the window after this. + pVulkanRenderer->DestroySurfaceTarget(key); + m_suspendedWindowSurfaces[client] = extent; + if (active) ActivateSuspendedPlaceholder(client, extent); + } else { + m_suspendedWindowSurfaces[client] = extent; + } + } + MGLOG_I("DirectVulkan: %zu window surface(s) on server window %p now draw into placeholder pbuffer targets; " + "the renderer, the device and every object stay", + onWindow.size(), window); + return true; + } + + Bool BackendObject_DirectVulkan::ResumeServerWindow(void* window, Uint32 width, Uint32 height) { + const std::lock_guard lock(m_eglStateMutex); + if (window == nullptr || !UsesSurfaceTargets()) return false; + UnorderedMap suspended; + suspended.swap(m_suspendedWindowSurfaces); + for (const auto& [client, extent] : suspended) { + (void)extent; + const auto state = m_eglSurfaces.find(client); + if (state == m_eglSurfaces.end()) continue; // released while suspended + state->second.Window.Handle = window; + state->second.Window.Width = width; + state->second.Window.Height = height; + state->second.Width = static_cast(std::max(width, 1)); + state->second.Height = static_cast(std::max(height, 1)); + if (m_eglSurface == client) m_windowHandle = state->second.Window; + if (!pVulkanRenderer) continue; + const Uint64 key = SurfaceTargetKey(client); + const Bool active = pVulkanRenderer->IsActiveSurfaceTarget(key); + pVulkanRenderer->DestroySurfaceTarget(key); // the placeholder + // A parked surface is built on the new window when it is next activated (InitWindowSurface). + if (!active) continue; + // The creation's own path: a new VkSurfaceKHR and swapchain with the surface's current + // pre-transform, a new target serial (render passes rebuilt), and the extent published. + VulkanRendererConfig config; + config.SurfaceWidth = std::max(width, 1); + config.SurfaceHeight = std::max(height, 1); + config.SwapInterval = GetRequestedSwapInterval(); + pVulkanRenderer->ActivateSurfaceTarget(key, reinterpret_cast(window), config); + } + MGLOG_I("DirectVulkan: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(), + window, width, height); + return true; + } + Bool BackendObject_DirectVulkan::UsesSurfaceTargets() const { return MG_Config::Transport != MG_Config::TransportMode::Monolith; } void BackendObject_DirectVulkan::OnEGLSurfaceForgotten(EGLSurface surface) { + m_suspendedWindowSurfaces.erase(surface); if (UsesSurfaceTargets() && pVulkanRenderer) pVulkanRenderer->DestroySurfaceTarget(SurfaceTargetKey(surface)); } diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index 7811d861..5d5741ef 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -41,6 +41,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image) override; void ReleaseEGLSurface(EGLSurface surface) override; void ReleaseEGLResources() override; + // The server window going and coming back: the window surface's target (VkSurfaceKHR, + // swapchain) is destroyed and a pbuffer target of the same extent stands in for it, and back. + Bool SuspendServerWindow(void* window) override; + Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override; const RendererInfo& GetRendererInfo() const override; String GetBackendAPIVersionString() const override; @@ -69,6 +73,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { UniquePtr m_magmaSession; // The binding each client context was last made current with, for OnClientContextBound. UnorderedMap m_contextBindings; + // Window surfaces whose window is gone (SuspendServerWindow), with the extent their + // placeholder pbuffer target is built at - the window's, so the default framebuffer the + // client knows keeps its size. + struct SuspendedWindowSurface { + Uint32 Width = 1; + Uint32 Height = 1; + }; + UnorderedMap m_suspendedWindowSurfaces; + // The placeholder pbuffer target of a suspended surface, made active. + void ActivateSuspendedPlaceholder(EGLSurface surface, const SuspendedWindowSurface& extent); }; // Single-source-of-truth helpers shared with the driver POST diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 931a5101..c236c4bd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -16791,9 +16791,17 @@ void main() { DestroyDeferredDepthMipmapCleanup(); m_deferredDepthMipmapCleanup.assign(m_frameContext.GetFrameCount(), {}); + const VkExtent2D previousExtent = m_swapchainObject.GetSurfaceExtent(); ShutdownSwapchain(); + // The new swapchain publishes its extent (SwapchainObject::Create), which is how a client + // sizing its viewport from eglQuerySurface learns that the window moved. CreateSwapchain(); + const VkExtent2D extent = m_swapchainObject.GetSurfaceExtent(); + if (extent.width != previousExtent.width || extent.height != previousExtent.height) { + MGLOG_I("DirectVulkan: swapchain rebuilt at %ux%u (was %ux%u); the new extent is published", extent.width, + extent.height, previousExtent.width, previousExtent.height); + } // Every image of the fresh swapchain holds garbage, so the default framebuffer restarts // at index 0: the next write into it re-points the GL-visible index at whatever the next // acquire returns (see m_defaultFramebufferImageIndex). Without this the index could name @@ -17038,6 +17046,18 @@ void main() { m_parkedTargets.erase(parked); } + VkExtent2D VulkanRenderer::SurfaceTargetExtent(Uint64 key) const { + if (key == m_activeTargetKey && m_surface != VK_NULL_HANDLE) { + return m_swapchainObject.GetHandle() != VK_NULL_HANDLE ? m_swapchainObject.GetSurfaceExtent() + : VkExtent2D{0, 0}; + } + const auto parked = m_parkedTargets.find(key); + if (parked == m_parkedTargets.end() || parked->second.swapchain.GetHandle() == VK_NULL_HANDLE) { + return VkExtent2D{0, 0}; + } + return parked->second.swapchain.GetSurfaceExtent(); + } + VkInstance VulkanRenderer::GetInstance() const { return m_instance; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 76d1bc06..71ec63e3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -456,6 +456,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { void ActivateSurfaceTarget(Uint64 key, NativeWindowType window, const VulkanRendererConfig& surfaceConfig); // Drops a surface's target, active or parked. A no-op for a key that has none. void DestroySurfaceTarget(Uint64 key); + // The default framebuffer's extent (surface space, what PublishDefaultFramebufferInfo + // publishes) of `key`'s target, active or parked; 0x0 when it has none or no swapchain. + VkExtent2D SurfaceTargetExtent(Uint64 key) const; + // Whether `key` names the active target. + Bool IsActiveSurfaceTarget(Uint64 key) const { return key == m_activeTargetKey && m_surface != VK_NULL_HANDLE; } private: // The presentation half of the renderer, parked while another surface is active. Its diff --git a/MobileGL/MG_Remote/Server/ServerDisplay.cpp b/MobileGL/MG_Remote/Server/ServerDisplay.cpp index 0ce67084..20e357c6 100644 --- a/MobileGL/MG_Remote/Server/ServerDisplay.cpp +++ b/MobileGL/MG_Remote/Server/ServerDisplay.cpp @@ -125,10 +125,20 @@ namespace MobileGL::MG_Remote::Server { NoteExtentReportLocked(width, height); ++m_generation; const Uint64 generation = m_generation; + // A suspended session (screen off, lock) rebuilds its surface on this window. + const Uint32 suspended = static_cast(m_suspended.size()); + NotifySuspendedLocked(); 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(generation)); + MGLOG_I("MG_Remote server: server window %p attached at %ux%u (generation %llu)%s", window, width, height, + static_cast(generation), + suspended != 0 ? "; a suspended session is told to resume on it" : ""); + } + + void ServerDisplay::NotifySuspendedLocked() const { + for (const SuspendedHolder& entry : m_suspended) { + if (entry.onAttached != nullptr) entry.onAttached(entry.holder); + } } ServerWindowDetach ServerDisplay::Detach(Uint32 timeoutMs) { @@ -231,6 +241,12 @@ namespace MobileGL::MG_Remote::Server { m_leaseHolder = holder; m_onLost = onLost; m_lostRequested = false; + // A suspended holder that leased a window again waits for no other. + m_suspended.erase(std::remove_if(m_suspended.begin(), m_suspended.end(), + [holder](const SuspendedHolder& entry) { + return entry.holder == holder; + }), + m_suspended.end()); if (out != nullptr) { out->window = m_window; out->width = m_width; @@ -261,21 +277,19 @@ namespace MobileGL::MG_Remote::Server { } } - void ServerDisplay::EndLease(void* holder) { - std::vector releases; - ServerDisplayHooks hooks{}; - { - const std::lock_guard 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(); + Bool ServerDisplay::EndLeaseLocked(void* holder, std::vector* releases, ServerDisplayHooks* hooks) { + if (holder == nullptr || m_leaseHolder != holder) return false; + 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{}; + return true; + } + + void ServerDisplay::ReleaseAfterLease(const ServerDisplayHooks& hooks, const std::vector& releases) const { for (void* window : releases) { ReleaseOutsideLock(hooks, window); MGLOG_I("MG_Remote server: server window %p released after its session let go (a late Detach)", @@ -283,6 +297,52 @@ namespace MobileGL::MG_Remote::Server { } } + void ServerDisplay::EndLease(void* holder) { + std::vector releases; + ServerDisplayHooks hooks{}; + { + const std::lock_guard lock(m_mutex); + if (!EndLeaseLocked(holder, &releases, &hooks)) return; + } + m_cv.notify_all(); + ReleaseAfterLease(hooks, releases); + } + + void ServerDisplay::SuspendLease(void* holder, ServerWindowAttachedHook onAttached) { + if (holder == nullptr) return; + std::vector releases; + ServerDisplayHooks hooks{}; + { + // ONE LOCK HOLD for the lease's end and the registration, so no Attach can fall between + // them and go unheard. + const std::lock_guard lock(m_mutex); + (void)EndLeaseLocked(holder, &releases, &hooks); + Bool registered = false; + for (SuspendedHolder& entry : m_suspended) { + if (entry.holder != holder) continue; + entry.onAttached = onAttached; + registered = true; + } + if (!registered) m_suspended.push_back(SuspendedHolder{holder, onAttached}); + // A window that is already up (a Detach that raced an Attach) is the one to resume on. + if (m_window != nullptr && onAttached != nullptr) onAttached(holder); + } + m_cv.notify_all(); + ReleaseAfterLease(hooks, releases); + } + + void ServerDisplay::CancelResume(void* holder) { + const std::lock_guard lock(m_mutex); + m_suspended.erase(std::remove_if(m_suspended.begin(), m_suspended.end(), + [holder](const SuspendedHolder& entry) { return entry.holder == holder; }), + m_suspended.end()); + } + + Uint32 ServerDisplay::SuspendedHolders() const { + const std::lock_guard lock(m_mutex); + return static_cast(m_suspended.size()); + } + void ServerDisplay::Interrupt() { { const std::lock_guard lock(m_mutex); diff --git a/MobileGL/MG_Remote/Server/ServerDisplay.h b/MobileGL/MG_Remote/Server/ServerDisplay.h index 8b7ee2b0..4999a25c 100644 --- a/MobileGL/MG_Remote/Server/ServerDisplay.h +++ b/MobileGL/MG_Remote/Server/ServerDisplay.h @@ -35,11 +35,17 @@ // reference is kept and released when the lease finally ends - and Detach returns TimedOut, by // name, so the UI thread is never held longer than its bound. // +// SUSPEND, NOT END (screen off / lock). A holder whose backend can let go of the window and keep +// everything else - its contexts, objects and device - ends its lease with SuspendLease instead of +// EndLease and names a second hook: the next Attach of a NEW window calls it, so the holder can +// lease that window (AcquireFor) and rebuild its surface on it. The registration lasts until that +// AcquireFor succeeds or the holder cancels it (CancelResume, at the session's end). +// // THREADS. Every method is thread-safe. The hooks are called from the thread that called the // method: acquire from Attach's caller, release from Detach's / EndLease's caller, requestGeometry -// from the apply thread (AcquireFor), the lost hook from Detach's caller 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). +// from the apply thread (AcquireFor), the lost hook from Detach's caller and the attached hook from +// Attach's (or SuspendLease's) caller, both WITH THIS OBJECT'S LOCK HELD - so they must not block and +// must not call back into this object (ServerLoop's set an atomic and ring its doorbell). #pragma once #include @@ -63,6 +69,9 @@ namespace MobileGL::MG_Remote::Server { // Called with the display's lock held: must not block, must not re-enter the display. using ServerWindowLostHook = void (*)(void* holder); + // A new window was attached while `holder` was suspended (SuspendLease). Same rules as the lost + // hook: called with the display's lock held. + using ServerWindowAttachedHook = void (*)(void* holder); // Asked about every 50 ms while AcquireFor waits; true ends the wait (Cancelled). using ServerWindowWaitCancel = Bool (*)(void* user); @@ -138,6 +147,13 @@ namespace MobileGL::MG_Remote::Server { // whole backend). Wakes a waiting Detach; releases a reference Detach left behind on timeout. // A holder that holds no lease is a no-op. void EndLease(void* holder); + // EndLease for a holder that only SUSPENDED its use of the window (its backend let go of the + // window and kept everything else). Also registers `onAttached`, which the next Attach of a + // new window calls - at once when a window is already attached. The registration ends when + // the same holder's AcquireFor succeeds, or at CancelResume. + void SuspendLease(void* holder, ServerWindowAttachedHook onAttached); + // The holder is going away: no attached hook is called for it once this returns. + void CancelResume(void* holder); // Wakes every AcquireFor waiter with Interrupted (the in-process server is stopping). void Interrupt(); @@ -145,9 +161,17 @@ namespace MobileGL::MG_Remote::Server { Bool Attached() const; Bool Leased() const; Uint64 Generation() const; + // How many holders wait for a window to resume on (SuspendLease). + Uint32 SuspendedHolders() const; private: void ReleaseOutsideLock(const ServerDisplayHooks& hooks, void* window) const; + // m_mutex held: ends `holder`'s lease (false when it holds none); the references a timed-out + // Detach left behind are moved to `releases`, to be released outside the lock with `hooks`. + Bool EndLeaseLocked(void* holder, std::vector* releases, ServerDisplayHooks* hooks); + void ReleaseAfterLease(const ServerDisplayHooks& hooks, const std::vector& releases) const; + // m_mutex held: a new window is attached; every suspended holder is told. + void NotifySuspendedLocked() const; // m_mutex held: an Attach reported `width`x`height` (see m_extentReports). void NoteExtentReportLocked(Uint32 width, Uint32 height); @@ -175,6 +199,12 @@ namespace MobileGL::MG_Remote::Server { Bool m_lostRequested = false; // References a timed-out Detach could not release yet; EndLease releases them. std::vector m_releaseAfterLease; + // SuspendLease's registrations, told about the next new window. + struct SuspendedHolder { + void* holder = nullptr; + ServerWindowAttachedHook onAttached = nullptr; + }; + std::vector m_suspended; }; // The process's one display (leaked at exit, like every MG_Remote singleton). diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index 2f562b88..0c7fdf20 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -316,6 +316,14 @@ namespace MobileGL::MG_Remote::Server { MG_Backend::BackendObject* ServerLoop::Backend() { return m_backend.get(); } + MobileGLResult ServerLoop::InstallBackendForTesting(UniquePtr backend) { + if (m_backend != nullptr || backend == nullptr || m_running.load(std::memory_order_acquire)) { + return MOBILEGL_ERR_INVALID_ARGUMENT; + } + m_backend = std::move(backend); + return MOBILEGL_OK; + } + // --------------------------------------------------------------------------------- // Start / the loop / Stop // --------------------------------------------------------------------------------- @@ -354,9 +362,15 @@ namespace MobileGL::MG_Remote::Server { // in the same process starts here too and neither's first surface decides the other's. m_surfaceMode = SessionSurfaceMode::None; m_holdsWindowLease = false; + m_leasedWindow = nullptr; m_serverOwnedSurfaces.clear(); - m_windowLostRequested.store(false, std::memory_order_release); + m_windowRequests.store(0, std::memory_order_release); m_serverWindowsLost.store(0, std::memory_order_release); + m_serverWindowSuspended.store(false, std::memory_order_release); + m_serverWindowsSuspended.store(0, std::memory_order_release); + m_serverWindowsResumed.store(0, std::memory_order_release); + m_serverWindowWantWidth = 0; + m_serverWindowWantHeight = 0; m_testLogFloodLines = ApplyLogFloodLinesForTest(); { const std::lock_guard lock(m_exitMutex); @@ -580,10 +594,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). + // P12 (D6) ADDS ONE MORE WAKE, BESIDE CONTROL: a window request - lost from ServerDisplay:: + // Detach (the UI thread's surfaceDestroyed, which is blocked until this thread answers), or + // attached for a suspended session (a new window to resume on). const auto ready = [this, signals, &ring, &stopOrForfeit] { - if (stopOrForfeit() || ControlIsPending() || m_windowLostRequested.load(std::memory_order_acquire)) + if (stopOrForfeit() || ControlIsPending() || m_windowRequests.load(std::memory_order_acquire) != 0) return true; if (signals.EventRingFull->load(std::memory_order_acquire) != 0) return false; return signals.CmdHead->load(std::memory_order_acquire) != ring.LocalTail(); @@ -726,9 +741,10 @@ namespace MobileGL::MG_Remote::Server { exitTurn.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(); + // now; one that arrives later finds no lease and releases the window at once. A suspended + // session stops waiting for a window to resume on. + m_windowRequests.store(0, std::memory_order_release); + ForgetServerWindow(); 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. @@ -804,8 +820,9 @@ 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(); + // relaxed-cost load on the idle poll, beside the shadow's. A suspended session's resume on + // a new window is the same word's other bit. + if (m_windowRequests.load(std::memory_order_acquire) != 0) HandleServerWindowRequests(); // 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 @@ -929,8 +946,9 @@ namespace MobileGL::MG_Remote::Server { // streaming frames keeps it from emptying for seconds: surfaceDestroyed (ServerDisplay:: // Detach) waited that long on the UI thread - past its 3 s bound, so it gave up and the // session went on presenting into the destroyed window. Checked AFTER the latch check (the - // MECHANICS row in ph_latch_sites.py pins that one as the loop's first statement); the - // release latches ServerWindowLost, so `continue` takes the latch check's way out. One more + // MECHANICS row in ph_latch_sites.py pins that one as the loop's first statement). A backend + // that cannot suspend latches ServerWindowLost, so `continue` takes the latch check's way out; + // a suspended session (and a resumed one - the request word's other bit) pops on. One more // acquire load per pop; the empty-ring answer pays it once. ServerSession& session = *m_session; Transport::SessionConsumer& consumer = session.Consumer(); @@ -962,8 +980,8 @@ namespace MobileGL::MG_Remote::Server { Uint64 applied = 0; for (;;) { if (SessionLatched()) break; - if (m_windowLostRequested.load(std::memory_order_acquire)) { - ReleaseLostServerWindow(); + if (m_windowRequests.load(std::memory_order_acquire) != 0) { + HandleServerWindowRequests(); continue; } bool corrupt = false; @@ -1241,7 +1259,7 @@ namespace MobileGL::MG_Remote::Server { // 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(); + ForgetServerWindow(); 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. @@ -1713,7 +1731,18 @@ namespace MobileGL::MG_Remote::Server { // 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); + loop->m_windowRequests.fetch_or(kWindowLost, std::memory_order_acq_rel); + if (loop->m_session != nullptr && loop->m_session->DataLink() != nullptr) { + loop->m_session->DataLink()->ConsumerBell().Notify(); + } + } + + void ServerLoop::ServerWindowAttachedThunk(void* self) { + auto* loop = static_cast(self); + // As the lost hook. Called under the display's lock while this loop is registered as suspended, + // and the registration is cancelled (under that lock) before the apply thread exits and before + // Stop() lets go of m_session. + loop->m_windowRequests.fetch_or(kWindowAttached, std::memory_order_acq_rel); if (loop->m_session != nullptr && loop->m_session->DataLink() != nullptr) { loop->m_session->DataLink()->ConsumerBell().Notify(); } @@ -1724,17 +1753,20 @@ namespace MobileGL::MG_Remote::Server { // 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); + (loop->m_windowRequests.load(std::memory_order_acquire) & kWindowLost) != 0; } MobileGLResult ServerLoop::AcquireServerWindow(Uint32 width, Uint32 height, Uint32 timeoutMs, ServerWindowLease* out, SurfaceRefusalCode* refusal) { ServerDisplay& display = ServerDisplayInstance(); + ServerWindowLease local; + ServerWindowLease* const lease = out != nullptr ? out : &local; const ServerWindowAcquire acquired = display.AcquireFor(width, height, timeoutMs, this, &ServerWindowLostThunk, - &ServerWindowWaitCancelled, this, out); + &ServerWindowWaitCancelled, this, lease); switch (acquired) { case ServerWindowAcquire::Acquired: m_holdsWindowLease = true; + m_leasedWindow = lease->window; return MOBILEGL_OK; case ServerWindowAcquire::NoDisplay: // A CONFIGURATION ANSWER, NOT A LATCH: the client asked an offscreen server (the exec'd @@ -1792,6 +1824,12 @@ namespace MobileGL::MG_Remote::Server { frame.refusal = static_cast(refusal); return acquired; } + m_serverWindowWantWidth = wantWidth; + m_serverWindowWantHeight = wantHeight; + // A SUSPENDED session that creates a surface on the new window first gets back the ones it had. + if (m_serverWindowSuspended.load(std::memory_order_acquire) && !ResumeServerWindowOnLease(lease)) { + return MOBILEGL_OK; // latched by name + } // 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 @@ -1852,6 +1890,19 @@ namespace MobileGL::MG_Remote::Server { frame.refusal = static_cast(SurfaceRefusalCode::None); const Uint32 wantWidth = frame.width > 0 ? static_cast(frame.width) : 0u; const Uint32 wantHeight = frame.height > 0 ? static_cast(frame.height) : 0u; + if (m_serverWindowSuspended.load(std::memory_order_acquire)) { + // NO WINDOW TO RESIZE, AND NONE TO WAIT FOR: the screen is off. The size is what the resume + // asks the next window for, and the extent the client adopts then is published by the + // backend's rebuild. Answered at once - waiting here would hold the apply thread for the + // whole of AcquireServerWindow's bound and refuse anyway. + m_serverWindowWantWidth = wantWidth; + m_serverWindowWantHeight = wantHeight; + frame.ok = true; + MGLOG_I("MG_Remote server: surface=window resize to %ux%u while suspended (ServerOwned seq %llu) - kept " + "for the next window", + wantWidth, wantHeight, static_cast(frame.seq)); + return MOBILEGL_OK; + } ServerWindowLease lease; SurfaceRefusalCode refusal = SurfaceRefusalCode::None; const MobileGLResult acquired = AcquireServerWindow(wantWidth, wantHeight, kServerWindowWaitMs, &lease, &refusal); @@ -1859,6 +1910,8 @@ namespace MobileGL::MG_Remote::Server { frame.refusal = static_cast(refusal); return acquired; } + m_serverWindowWantWidth = wantWidth; + m_serverWindowWantHeight = wantHeight; frame.ok = backend->ResizeEGLWindowSurface(HandleFromToken(frame.surface), lease.width, lease.height); frame.width = static_cast(lease.width); frame.height = static_cast(lease.height); @@ -1871,28 +1924,111 @@ namespace MobileGL::MG_Remote::Server { void ServerLoop::EndServerWindowLease() { if (!m_holdsWindowLease) return; m_holdsWindowLease = false; + m_leasedWindow = nullptr; MG_Pipe::MGPipeServerSetOwnedWindow(nullptr); 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. + void ServerLoop::ForgetServerWindow() { + EndServerWindowLease(); + // Under the display's lock: no attached hook names this loop once it returns. + ServerDisplayInstance().CancelResume(this); + m_serverWindowSuspended.store(false, std::memory_order_release); + } + + void ServerLoop::HandleServerWindowRequests() { + // Both bits at once: a lost request is answered first (the UI thread is blocked on it), and a + // new window after it. A request posted after this exchange is the next check's. + const Uint32 requests = m_windowRequests.exchange(0, std::memory_order_acq_rel); + if ((requests & kWindowLost) != 0) ReleaseLostServerWindow(); + if ((requests & kWindowAttached) != 0 && !SessionLatched()) ResumeSuspendedServerWindow(); + } + + void ServerLoop::LatchServerWindowLost(const char* why) { if (m_backend != nullptr) m_backend->ReleaseEGLResources(); MG_Pipe::MGPipeServerSetContextLive(false); ForgetCurrentTuple(); + m_serverWindowSuspended.store(false, std::memory_order_release); 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"); + "MGPipe: Fatal{ServerWindowLost, \"surfaceDestroyed\"} - %s, and the session ends so the " + "client reads a clean device loss. The display server keeps listening", + why); EndServerWindowLease(); } + void ServerLoop::ReleaseLostServerWindow() { + 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, and only then does + // the lease end, which is what lets surfaceDestroyed return. + // + // SUSPEND, NOT END. The window going is the screen going off (or the lock screen), not the + // session's end: a backend that can drops only what is bound to the window - Espryt its EGL + // window surfaces, Magma the swapchain and VkSurfaceKHR - keeps its contexts, objects and + // device, and the session waits for the display's next window with every record still applied. + void* const window = m_leasedWindow; + if (m_backend != nullptr && m_backend->SuspendServerWindow(window)) { + // N-3: the native binding moved off the window's surface. + ForgetCurrentTuple(); + m_serverWindowSuspended.store(true, std::memory_order_release); + const Uint64 suspends = m_serverWindowsSuspended.fetch_add(1, std::memory_order_acq_rel) + 1; + m_holdsWindowLease = false; + m_leasedWindow = nullptr; + MG_Pipe::MGPipeServerSetOwnedWindow(nullptr); + MGLOG_I("MG_Remote server: server window %p lost - the on-screen session is SUSPENDED, not ended: what " + "was bound to the window was released on mgl-srv-apply, its surfaces draw into an offscreen " + "placeholder, and it resumes on the next window (suspend #%llu)", + window, static_cast(suspends)); + // Ends the lease - surfaceDestroyed returns - and waits for the next window, in one step. + ServerDisplayInstance().SuspendLease(this, &ServerWindowAttachedThunk); + return; + } + LatchServerWindowLost("the server's own display window was destroyed under this on-screen session, whose " + "backend cannot suspend; its EGL resources were released on mgl-srv-apply before the " + "window went"); + } + + void ServerLoop::ResumeSuspendedServerWindow() { + if (!m_serverWindowSuspended.load(std::memory_order_acquire) || m_holdsWindowLease) return; + // The geometry the session last asked for, asked again (the window is new; a fixed size may not + // have survived it). Bounded by the geometry grace: the window IS attached - that is what woke + // this - so only its size is waited for. + ServerWindowLease lease; + const ServerWindowAcquire acquired = + ServerDisplayInstance().AcquireFor(m_serverWindowWantWidth, m_serverWindowWantHeight, + ServerDisplay::kGeometryGraceMs, this, &ServerWindowLostThunk, + &ServerWindowWaitCancelled, this, &lease); + if (acquired != ServerWindowAcquire::Acquired) { + // Still registered: the next new window wakes this again. + MGLOG_I("MG_Remote server: a suspended on-screen session did not get the new server window (%s); it " + "stays suspended until the next one", + ServerWindowAcquireName(acquired)); + return; + } + m_holdsWindowLease = true; + m_leasedWindow = lease.window; + (void)ResumeServerWindowOnLease(lease); + } + + Bool ServerLoop::ResumeServerWindowOnLease(const ServerWindowLease& lease) { + // Before the backend: its republished extent is the server window's (MGPipeServerOwnedWindow). + MG_Pipe::MGPipeServerSetOwnedWindow(lease.window); + if (m_backend == nullptr || !m_backend->ResumeServerWindow(lease.window, lease.width, lease.height)) { + LatchServerWindowLost("the suspended on-screen session's surfaces could not be rebuilt on the new server " + "window"); + return false; + } + // N-3: the native binding moved onto the new window's surface. + ForgetCurrentTuple(); + m_serverWindowSuspended.store(false, std::memory_order_release); + const Uint64 resumes = m_serverWindowsResumed.fetch_add(1, std::memory_order_acq_rel) + 1; + MGLOG_I("MG_Remote server: the on-screen session RESUMED on server window %p at %ux%u (generation %llu%s, " + "resume #%llu); its surfaces were rebuilt there and the extent republished", + lease.window, lease.width, lease.height, static_cast(lease.generation), + lease.sizeAsRequested ? "" : ", NOT the size it asked for", static_cast(resumes)); + return true; + } + // --------------------------------------------------------------------------------- // The twelve forwarders: pack the frame, post it, unpack the reply // --------------------------------------------------------------------------------- diff --git a/MobileGL/MG_Remote/Server/ServerLoop.h b/MobileGL/MG_Remote/Server/ServerLoop.h index b157eb90..fed001dc 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.h +++ b/MobileGL/MG_Remote/Server/ServerLoop.h @@ -431,8 +431,20 @@ namespace MobileGL::MG_Remote::Server { 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). + // How many times this loop released a lost server window and latched ServerWindowLost (D6) - + // the path of a backend that cannot suspend. Uint64 ServerWindowsLost() const { return m_serverWindowsLost.load(std::memory_order_acquire); } + // SUSPEND, NOT END. How many times a lost server window SUSPENDED this session (its backend let + // go of the window and kept everything else) and how many times a new window resumed it. A + // suspended session holds no lease and keeps applying records; its window surfaces draw into + // a placeholder until the display attaches a window again. + Uint64 ServerWindowsSuspended() const { return m_serverWindowsSuspended.load(std::memory_order_acquire); } + Uint64 ServerWindowsResumed() const { return m_serverWindowsResumed.load(std::memory_order_acquire); } + Bool ServerWindowSuspended() const { return m_serverWindowSuspended.load(std::memory_order_acquire); } + + // TEST SEAM: the backend this loop owns, instead of CreateBackend's (a fake that can or cannot + // suspend, for the window-loss cases). Before Start(); fails when one is already installed. + MobileGLResult InstallBackendForTesting(UniquePtr backend); private: RemoteControlSink m_remoteSink = nullptr; @@ -440,11 +452,26 @@ namespace MobileGL::MG_Remote::Server { // 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. + // P12 (D6), apply thread: the lost window is let go of before the lease ends. A backend that + // can suspend releases only what is bound to the window and the session carries on, waiting + // for the next window (SuspendLease); otherwise the backend's EGL resources go and the session + // latches ServerWindowLost. Runs from PumpControlRequest / DrainRing when Detach asked. void ReleaseLostServerWindow(); + // The old ending: the backend's EGL resources go, the session latches ServerWindowLost by + // name with `why`, the lease ends. + void LatchServerWindowLost(const char* why); + // Apply thread: the window requests ServerWindowLostThunk / ServerWindowAttachedThunk posted. + void HandleServerWindowRequests(); + // Apply thread: a suspended session leases the display's new window and its backend + // rebuilds the suspended surfaces on it. + void ResumeSuspendedServerWindow(); + // Apply thread, holding a fresh lease on `lease.window`: the backend rebuilds what it + // suspended. False: it could not, and the session latched (LatchServerWindowLost). + Bool ResumeServerWindowOnLease(const ServerWindowLease& lease); // Ends this loop's display lease if it holds one (after the backend let go of the window). void EndServerWindowLease(); + // The session's end: the lease, and any wait for a window to resume on. + void ForgetServerWindow(); // Review fix: the ResizeWindowSurface arm for a server-owned surface - a geometry request to // the display, the backend surface at the window's real extent, that extent in the reply. MobileGLResult ApplyServerOwnedWindowResize(MG_Backend::BackendObject* backend, SurfaceControlFrame& frame); @@ -455,6 +482,8 @@ namespace MobileGL::MG_Remote::Server { // 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); + // ServerDisplay's attached hook for a suspended session: the same discipline as the lost one. + static void ServerWindowAttachedThunk(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(). Per @@ -462,12 +491,25 @@ namespace MobileGL::MG_Remote::Server { SessionSurfaceMode m_surfaceMode = SessionSurfaceMode::None; // P14 S2: the session this loop serves, when the process serves several; see SetRuntime. SessionRuntime* m_runtime = nullptr; - // Apply thread only: this loop holds the display's lease. + // Apply thread only: this loop holds the display's lease, on this window. 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 m_windowLostRequested{false}; + void* m_leasedWindow = nullptr; + // The window requests: kWindowLost set by ServerWindowLostThunk, kWindowAttached by + // ServerWindowAttachedThunk (any thread), taken by the apply thread. ONE word, so the idle + // poll and DrainRing's per-pop check stay one load. Part of the park predicate, so a parked + // apply thread wakes for it. + static constexpr Uint32 kWindowLost = 1u; + static constexpr Uint32 kWindowAttached = 2u; + std::atomic m_windowRequests{0}; std::atomic m_serverWindowsLost{0}; + // Written by the apply thread, read by tests. + std::atomic m_serverWindowSuspended{false}; + std::atomic m_serverWindowsSuspended{0}; + std::atomic m_serverWindowsResumed{0}; + // Apply thread only: the geometry this session last asked the display for (0/0 = the + // layout's), which a resume asks for again. + Uint32 m_serverWindowWantWidth = 0; + Uint32 m_serverWindowWantHeight = 0; void ApplyThreadMain(); // Runs a posted control frame, if there is one. Returns true if it ran one. diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 39b2ef5c..c2bc49e5 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -3076,6 +3076,94 @@ TEST(RemoteRunAhead, AnEventBurstBehindAnUnwaitedSequenceFlowControlsInsteadOfAb ExpectChildSuccess(child); } +#include + +// THE SERVER WINDOW'S EXTENT AFTER THE CREATION. A server whose window changed size republishes its +// extent - Espryt at the present after the change (RepublishServerWindowExtentIfMoved), Magma when its +// swapchain is rebuilt for it - as one surface-changed event carrying the new size. The client's drain +// must then make eglQuerySurface(EGL_WIDTH/EGL_HEIGHT) answer it for the server-owned window surface +// (what a headless compositor sizes its output from) and reallocate the default framebuffer's +// attachments at it; a format-only event must change no size. Red with the drain's +// ApplyServerOwnedSurfaceExtent call deleted (exit 93: the query keeps 1920x1080) and with +// ApplySurfaceChangedToClient's extent arm deleted (exit 94: the colour stays at the placeholder's). +TEST(RemoteRunAhead, ARepublishedServerWindowExtentResizesTheServerOwnedSurfaceAndTheDefaultFramebuffer) { + const auto child = RunInChild([] { + StartRunAheadSession(); + // The client's EGL state with one server-owned window surface, as its creation leaves it. + MG_State::pEGLContext = MakeUnique(); + const EGLDisplay dpy = MG_State::pEGLContext->GetDisplay(EGL_DEFAULT_DISPLAY); + EGLint major = 0; + EGLint minor = 0; + (void)MG_State::pEGLContext->InitializeDisplay(dpy, &major, &minor); + const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE}; + EGLConfig config = nullptr; + EGLint count = 0; + (void)MG_State::pEGLContext->ChooseConfig(dpy, configAttribs, &config, 1, &count); + const EGLSurface surface = + MG_State::pEGLContext->CreateServerOwnedWindowSurface(dpy, config, nullptr, 1920, 1080, false); + if (surface == EGL_NO_SURFACE) ::_exit(90); + ClientSessionInstance().NoteServerOwnedWindowSurface(surface, 1920, 1080); + // The default framebuffer's placeholder attachments (MG_Impl::Init's shape). + const auto placeholder = [](TextureInternalFormat format) { + auto texture = MakeShared(0); + texture->SetInternalFormat(format); + texture->AllocateStorage(TextureUploadTarget::Texture2D, 0, {{512, 512, 1}, 0}); + return texture; + }; + auto& defaultFramebuffer = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo; + defaultFramebuffer = MakeUnique( + MG_Impl::GLImpl::FramebufferImpl::DefaultFramebufferInfo{ + nullptr, placeholder(TextureInternalFormat::RGBA8), + placeholder(TextureInternalFormat::Depth32FStencil8), + placeholder(TextureInternalFormat::Depth32FStencil8)}); + const auto query = [&](EGLint attribute) { + EGLint value = -1; + (void)MG_State::pEGLContext->QuerySurface(dpy, surface, attribute, &value); + return value; + }; + if (query(EGL_WIDTH) != 1920 || query(EGL_HEIGHT) != 1080) ::_exit(91); + + // The server's republish, from the apply thread like the backend's own. + static MG_Pipe::MGPSurfaceInfo s_info{}; + s_info = MG_Pipe::MGPSurfaceInfo{}; + s_info.Width = 2560; + s_info.Height = 1600; + s_info.InternalFormat = static_cast(TextureInternalFormat::Depth24Stencil8); + s_info.Samples = 1; + s_info.Layers = 1; + s_info.IsDefault = 1; + const auto republish = [] { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( + +[](void*) -> MobileGLResult { + MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&s_info); + return MOBILEGL_OK; + }, + nullptr); + }; + republish(); + if (ClientSessionInstance().DrainPublishedEvents() < 1u) ::_exit(92); + if (query(EGL_WIDTH) != 2560 || query(EGL_HEIGHT) != 1600) ::_exit(93); + const IntVec3 colour = + static_cast(defaultFramebuffer->colorAttachment.get())->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0); + const IntVec3 depth = + static_cast(defaultFramebuffer->depthAttachment.get())->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0); + if (colour.x() != 2560 || colour.y() != 1600 || depth.x() != 2560 || depth.y() != 1600) ::_exit(94); + + // A format-only event (a pbuffer's, a client-named window's) says nothing about the size. + s_info.Width = 0; + s_info.Height = 0; + republish(); + if (ClientSessionInstance().DrainPublishedEvents() < 1u) ::_exit(95); + if (query(EGL_WIDTH) != 2560 || query(EGL_HEIGHT) != 1600) ::_exit(96); + + defaultFramebuffer.reset(); + ClientSessionInstance().ForgetServerOwnedWindowSurface(surface); + MG_State::pEGLContext.reset(); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); +} + // =========================================================================================== // PH-6 (ID-P7-2): a client that does not drain SEG_EVENT FORFEITS the reverse channel // =========================================================================================== diff --git a/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp b/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp index d4ef8f6e..2214aa27 100644 --- a/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp @@ -428,3 +428,60 @@ TEST(ServerDisplayTest, InterruptAndTheCancelPredicateEndAWaitByName) { uninstaller.join(); EXPECT_FALSE(display.HasDisplay()); } + +// SUSPEND, NOT END. A holder that suspends (its backend let go of the window and kept the rest) ends +// its lease - a waiting Detach returns, the reference is released - and is told about the next NEW +// window: not about a geometry change of the same one, once per new window, until it leases a window +// again or cancels. Red with Attach not calling the hooks (attached stays 0), with AcquireFor keeping +// the registration (SuspendedHolders stays 1), or with CancelResume a no-op (the last window +// calls it again). +TEST(ServerDisplayTest, ASuspendedHolderIsToldAboutTheNextNewWindowUntilItLeasesOneOrCancels) { + FakePlatform platform; + ServerDisplay display; + display.Install(HooksFor(platform)); + display.Attach(FakeWindow(0), 64, 48); + static std::atomic attached{0}; + attached.store(0); + const auto onAttached = [](void*) { attached.fetch_add(1); }; + static std::atomic lost{0}; + lost.store(0); + const auto onLost = [](void*) { lost.fetch_add(1); }; + int holder = 0; + ServerWindowLease lease; + ASSERT_EQ(display.AcquireFor(64, 48, 100, &holder, onLost, nullptr, nullptr, &lease), ServerWindowAcquire::Acquired); + + // The screen goes off; the "apply thread" suspends instead of ending. + std::thread session([&] { + while (lost.load() == 0) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + display.SuspendLease(&holder, onAttached); + }); + EXPECT_EQ(display.Detach(3000), ServerWindowDetach::ReleasedBySession); + session.join(); + EXPECT_EQ(platform.releases.load(), 1); + EXPECT_FALSE(display.Leased()); + EXPECT_EQ(display.SuspendedHolders(), 1u); + EXPECT_EQ(attached.load(), 0) << "no window is up: nothing to resume on yet"; + + // The screen comes back: a new window, then its size settling (the same window again). + display.Attach(FakeWindow(1), 64, 48); + EXPECT_EQ(attached.load(), 1); + display.Attach(FakeWindow(1), 80, 60); + EXPECT_EQ(attached.load(), 1) << "a geometry change of the same window is not a new window"; + + // The holder leases it: the registration is over. + ASSERT_EQ(display.AcquireFor(0, 0, 100, &holder, onLost, nullptr, nullptr, &lease), ServerWindowAcquire::Acquired); + EXPECT_EQ(lease.window, FakeWindow(1)); + EXPECT_EQ(display.SuspendedHolders(), 0u); + + // Suspended again while a window is ALREADY up (a Detach that raced an Attach): told at once. + display.SuspendLease(&holder, onAttached); + EXPECT_EQ(attached.load(), 2); + EXPECT_FALSE(display.Leased()); + // The session ends: no hook after CancelResume. + display.CancelResume(&holder); + EXPECT_EQ(display.SuspendedHolders(), 0u); + EXPECT_EQ(display.Detach(100), ServerWindowDetach::Released); + display.Attach(FakeWindow(2), 64, 48); + EXPECT_EQ(attached.load(), 2); + display.Uninstall(100); +} diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index e120223e..fcc480f1 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -3282,7 +3282,9 @@ namespace { // 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. + // window's reference - all while the apply thread was PARKED when the request came. This session + // has NO backend, so nothing can suspend it: the latch is the path of a backend that cannot (the + // suspend contract's own cases are below, with a backend double). enum : int { kLostNotLeased = 1, // the probe could not take the lease kLostDetach = 2, // Detach did not answer ReleasedBySession @@ -3437,6 +3439,306 @@ TEST(ServerLoopLatchTest, ALostServerWindowIsReleasedMidBatchWhileTheClientStrea "latch; 32 = the apply thread stayed (64+ = setup)"; } +// ===================================================================================== +// SUSPEND, NOT END: the server window going away (screen off, lock) and coming back +// ===================================================================================== + +namespace { + // A backend double for the window cases. It takes the server's window the way a real backend + // does (CreateEGLWindowSurface on the substituted handle) and, when it CAN suspend, answers + // SuspendServerWindow / ResumeServerWindow the way Espryt and Magma do: by letting go of the + // window - presents from then on reach no window - and by taking the new one. Its Present counts + // the frames and the ones that came while no window was held. Statics, because the loop owns + // (and destroys) the object and each case runs in its own death-test child. + class WindowBackendDouble final : public MG_Backend::BackendObject { + public: + explicit WindowBackendDouble(Bool canSuspend) : m_canSuspend(canSuspend) { m_functions.Present = &Present; } + + void Initialize() override {} + Bool InitCapabilities() override { return true; } + Bool InitWindowSurface() override { return true; } + Bool CreateEGLWindowSurface(EGLSurface, const MG_Backend::WindowHandle& handle) override { + window.store(handle.Handle, std::memory_order_release); + creates.fetch_add(1, std::memory_order_acq_rel); + return true; + } + Bool SuspendServerWindow(void* lost) override { + if (!m_canSuspend) return false; + suspendedOn.store(lost, std::memory_order_release); + suspendOnApplyThread.store(Server::ServerLoop::OnApplyThread(), std::memory_order_release); + window.store(nullptr, std::memory_order_release); + suspends.fetch_add(1, std::memory_order_acq_rel); + return true; + } + Bool ResumeServerWindow(void* fresh, Uint32 width, Uint32 height) override { + resumeOnApplyThread.store(Server::ServerLoop::OnApplyThread(), std::memory_order_release); + resumeWidth.store(width, std::memory_order_release); + resumeHeight.store(height, std::memory_order_release); + window.store(fresh, std::memory_order_release); + resumes.fetch_add(1, std::memory_order_acq_rel); + return true; + } + void ReleaseEGLResources() override { + releases.fetch_add(1, std::memory_order_acq_rel); + BackendObject::ReleaseEGLResources(); + } + const RendererInfo& GetRendererInfo() const override { return m_info; } + String GetBackendAPIVersionString() const override { return "window double"; } + const MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override { return m_functions; } + const MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override { return m_params; } + BackendType GetBackendType() const override { return BackendType::Unknown; } + + static void Present() { + presents.fetch_add(1, std::memory_order_acq_rel); + if (window.load(std::memory_order_acquire) == nullptr) presentsWithoutWindow.fetch_add(1, std::memory_order_acq_rel); + } + + static inline std::atomic window{nullptr}; + static inline std::atomic suspendedOn{nullptr}; + static inline std::atomic creates{0}; + static inline std::atomic suspends{0}; + static inline std::atomic resumes{0}; + static inline std::atomic releases{0}; + static inline std::atomic presents{0}; + static inline std::atomic presentsWithoutWindow{0}; + static inline std::atomic suspendOnApplyThread{false}; + static inline std::atomic resumeOnApplyThread{false}; + static inline std::atomic resumeWidth{0}; + static inline std::atomic resumeHeight{0}; + + private: + Bool m_canSuspend = false; + MG_Backend::GlobalBackendFunctionsTable m_functions{}; + MG_Backend::DynamicBackendParameters m_params{}; + RendererInfo m_info{.RendererName = "Test", + .BackendName = "WindowBackendDouble", + .ExtraVendor = Nullopt, + .RendererGLInfo = {.TargetGLVersion = {3, 3, 0}, + .TargetGLSLVersion = {4, 6, 0}, + .Extensions = {}, + .IsCompatibilityProfile = false}, + .StaticBackendCapability = {.AllowVSOnlyPrograms = false}}; + }; + + int g_secondFakeServerWindow = 0; + const EGLSurface kServerOwnedSurface = reinterpret_cast(static_cast(0x51)); + + // Handshake, the double installed, the loop started and parked, a display with a window, and the + // session's first surface created on that window through the ServerOwned arm (as a headless + // client's eglCreateWindowSurface reaches it). False on any setup failure. + bool BringUpAnOnScreenSession(ServerFixture& fixture, Bool canSuspend) { + if (!fixture.Handshake()) return false; + if (Server::ServerLoopInstance().InstallBackendForTesting(MakeUnique(canSuspend)) != + MOBILEGL_OK) + return false; + if (!fixture.StartLoop() || !fixture.WaitUntilTrulyParked()) return false; + Server::ServerDisplay& display = Server::ServerDisplayInstance(); + display.Install(CountingWindowHooks::Hooks()); + display.Attach(&g_fakeServerWindow, 64, 48); + const Server::ServerOwnedWindowReply reply = + Server::ServerCreateServerOwnedWindowSurface(kServerOwnedSurface, 64, 48); + return reply.ok && reply.width == 64 && reply.height == 48 && + WindowBackendDouble::window.load() == &g_fakeServerWindow && display.Leased() && + fixture.WaitUntilTrulyParked(); + } + + // `count` presents under ONE publish, serials from `firstSerial`; true once all are applied. + bool PresentAndWait(ServerFixture& fixture, Uint64 firstSerial, int count, Uint32 timeoutMs = 5000) { + Uint64 last = Codec::kInvalidSeq; + for (int i = 0; i < count; ++i) { + MG_Pipe::MGPPresent present{}; + present.FrameSerial = firstSerial + static_cast(i); + last = fixture.encoder.EncodeRecord(MG_Pipe::MGPWireOp::Present, &present, sizeof(present)); + if (last == Codec::kInvalidSeq) return false; + } + fixture.encoder.Publish(); + fixture.producer.PublishAndNotify(last); + return fixture.producer.WaitForApplied(last, timeoutMs) == Transport::SessionWait::Reached; + } + + enum : int { + kSuspendDetach = 1, // Detach did not answer ReleasedBySession + kSuspendLatched = 2, // the session latched, or the apply thread left + kSuspendBackend = 4, // the backend was not asked to suspend exactly once, on the apply thread, for the lost window + kSuspendState = 8, // the loop's counters / the display's lease and wait are not the suspended state + kSuspendRecords = 16, // presents after the loss were not applied (all of them, none reaching a window) + kSuspendResume = 32, // a new window did not resume the session on it, on the apply thread + kSuspendAgain = 64, // the resumed session did not suspend again on the next loss + kSuspendNotCancelled = 128, // the session's end left it waiting for a window + }; + + [[noreturn]] void SuspendResumeAndExit() { + Remote::ArmSessionLatch(); + ServerFixture fixture; + if (!BringUpAnOnScreenSession(fixture, /*canSuspend=*/true)) ::_exit(250); + Server::ServerLoop& loop = Server::ServerLoopInstance(); + Server::ServerDisplay& display = Server::ServerDisplayInstance(); + int failed = 0; + + // The screen goes off: surfaceDestroyed. + if (display.Detach(3000) != Server::ServerWindowDetach::ReleasedBySession) failed |= kSuspendDetach; + if (CountingWindowHooks::releases.load() != 1) failed |= kSuspendDetach; + if (Remote::SessionLatched() || !loop.Running()) failed |= kSuspendLatched; + if (WindowBackendDouble::suspends.load() != 1 || WindowBackendDouble::suspendedOn.load() != &g_fakeServerWindow || + !WindowBackendDouble::suspendOnApplyThread.load() || WindowBackendDouble::releases.load() != 0) + failed |= kSuspendBackend; + if (loop.ServerWindowsSuspended() != 1 || loop.ServerWindowsLost() != 0 || !loop.ServerWindowSuspended() || + display.Leased() || display.SuspendedHolders() != 1) + failed |= kSuspendState; + + // The client keeps drawing: its frames are applied - and their credit returned - with no window. + if (!PresentAndWait(fixture, 1, 3) || WindowBackendDouble::presents.load() != 3 || + WindowBackendDouble::presentsWithoutWindow.load() != 3) + failed |= kSuspendRecords; + if (Remote::SessionLatched() || !loop.Running()) failed |= kSuspendLatched; + + // The screen comes back: a NEW window. + display.Attach(&g_secondFakeServerWindow, 64, 48); + if (!PollUntil([&] { return loop.ServerWindowsResumed() == 1; }, 3000) || + WindowBackendDouble::resumes.load() != 1 || WindowBackendDouble::window.load() != &g_secondFakeServerWindow || + WindowBackendDouble::resumeWidth.load() != 64 || WindowBackendDouble::resumeHeight.load() != 48 || + !WindowBackendDouble::resumeOnApplyThread.load() || loop.ServerWindowSuspended() || !display.Leased() || + display.SuspendedHolders() != 0) + failed |= kSuspendResume; + if (!PresentAndWait(fixture, 4, 1) || WindowBackendDouble::presentsWithoutWindow.load() != 3) + failed |= kSuspendRecords; + + // And again: the cycle holds. + if (!fixture.WaitUntilTrulyParked()) ::_exit(251); + if (display.Detach(3000) != Server::ServerWindowDetach::ReleasedBySession || + WindowBackendDouble::suspends.load() != 2 || + WindowBackendDouble::suspendedOn.load() != &g_secondFakeServerWindow || loop.ServerWindowsSuspended() != 2 || + Remote::SessionLatched()) + failed |= kSuspendAgain; + + fixture.Stop(); + if (display.SuspendedHolders() != 0) failed |= kSuspendNotCancelled; + std::fprintf(stderr, "[suspend] suspends=%d resumes=%d presents=%d (%d without a window) lost=%llu\n", + WindowBackendDouble::suspends.load(), WindowBackendDouble::resumes.load(), + WindowBackendDouble::presents.load(), WindowBackendDouble::presentsWithoutWindow.load(), + static_cast(loop.ServerWindowsLost())); + ::_exit(failed); + } + + // THE NEGATIVE CONTROL: the same session over a backend that cannot suspend (the BackendObject + // default) still ends the old way - its EGL resources released, ServerWindowLost latched by name. + enum : int { + kNoSuspendDetach = 1, // Detach did not answer ReleasedBySession + kNoSuspendNotLatched = 2, // no ServerWindowLost latch + kNoSuspendRelease = 4, // the backend's EGL resources were not released exactly once + kNoSuspendCounters = 8, // ServerWindowsLost() != 1 or a suspend was counted + kNoSuspendStayed = 16, // the apply thread was still running 3 s after the latch + }; + + [[noreturn]] void ABackendThatCannotSuspendLatchesAndExit() { + Remote::ArmSessionLatch(); + ServerFixture fixture; + if (!BringUpAnOnScreenSession(fixture, /*canSuspend=*/false)) ::_exit(250); + Server::ServerLoop& loop = Server::ServerLoopInstance(); + int failed = 0; + if (Server::ServerDisplayInstance().Detach(3000) != Server::ServerWindowDetach::ReleasedBySession) + failed |= kNoSuspendDetach; + if (!Remote::SessionLatched() || Remote::SessionLatchedFamily() != Remote::MGFatalFamily::ServerWindowLost) + failed |= kNoSuspendNotLatched; + if (WindowBackendDouble::releases.load() != 1) failed |= kNoSuspendRelease; + if (loop.ServerWindowsLost() != 1 || loop.ServerWindowsSuspended() != 0 || loop.ServerWindowSuspended()) + failed |= kNoSuspendCounters; + if (!PollUntil([&] { return !loop.Running(); }, 3000)) failed |= kNoSuspendStayed; + fixture.Stop(); + ::_exit(failed); + } + + // The mid-stream case's suspend twin: the loss lands in the middle of a 4 s stream of presents, + // Detach is answered within the stream, and the stream is then applied to its end - not cut. + std::atomic g_suspendStreamRecords{0}; + std::thread g_suspendStreamDetachThread; + std::atomic g_suspendStreamDetachResult{-1}; + std::atomic g_suspendStreamDetachMs{-1}; + + void StreamOnePresentAndLoseTheWindowAfterTheFirst() { + if (g_suspendStreamRecords.fetch_add(1, std::memory_order_acq_rel) == 0) { + g_suspendStreamDetachThread = std::thread([] { + const auto started = std::chrono::steady_clock::now(); + const Server::ServerWindowDetach detached = Server::ServerDisplayInstance().Detach(3000); + g_suspendStreamDetachMs.store(std::chrono::duration_cast( + std::chrono::steady_clock::now() - started) + .count(), + std::memory_order_release); + g_suspendStreamDetachResult.store(static_cast(detached), std::memory_order_release); + }); + (void)PollUntil([] { return !Server::ServerDisplayInstance().Attached(); }, 1000); + } + std::this_thread::sleep_for(std::chrono::milliseconds(kStreamRecordMs)); + } + + enum : int { + kSuspendStreamDetach = 1, // Detach did not answer ReleasedBySession + kSuspendStreamSlow = 2, // Detach took 1 s or more + kSuspendStreamCut = 4, // the stream was not applied to its end + kSuspendStreamLatched = 8, // the session latched or the apply thread left + kSuspendStreamNotSuspended = 16, // the session is not suspended exactly once + }; + + [[noreturn]] void SuspendMidStreamAndExit() { + Remote::ArmSessionLatch(); + ServerFixture fixture; + if (!BringUpAnOnScreenSession(fixture, /*canSuspend=*/true)) ::_exit(250); + Server::ServerLoop& loop = Server::ServerLoopInstance(); + loop.SetBetweenRecordsHookForTesting(&StreamOnePresentAndLoseTheWindowAfterTheFirst); + const Uint64 drainedBefore = loop.DrainedRecords(); + int failed = 0; + if (!PresentAndWait(fixture, 1, kStreamRecords, 15000)) failed |= kSuspendStreamCut; + if (!PollUntil([] { return g_suspendStreamDetachResult.load(std::memory_order_acquire) >= 0; }, 8000)) ::_exit(251); + if (g_suspendStreamDetachThread.joinable()) g_suspendStreamDetachThread.join(); + if (g_suspendStreamDetachResult.load() != static_cast(Server::ServerWindowDetach::ReleasedBySession)) + failed |= kSuspendStreamDetach; + if (g_suspendStreamDetachMs.load() >= 1000) failed |= kSuspendStreamSlow; + if (loop.DrainedRecords() - drainedBefore != static_cast(kStreamRecords) || + WindowBackendDouble::presents.load() != kStreamRecords) + failed |= kSuspendStreamCut; + if (Remote::SessionLatched() || !loop.Running()) failed |= kSuspendStreamLatched; + if (loop.ServerWindowsSuspended() != 1 || !loop.ServerWindowSuspended()) failed |= kSuspendStreamNotSuspended; + std::fprintf(stderr, "[suspend-stream] Detach answered %s in %lld ms; %llu of %d presents applied\n", + Server::ServerWindowDetachName( + static_cast(g_suspendStreamDetachResult.load())), + g_suspendStreamDetachMs.load(), + static_cast(loop.DrainedRecords() - drainedBefore), kStreamRecords); + loop.SetBetweenRecordsHookForTesting(nullptr); + fixture.Stop(); + ::_exit(failed); + } +} // namespace + +// THE CONTRACT. Red with ReleaseLostServerWindow's suspend arm deleted (the old latch for every +// backend): the session latches and its loop is gone before the second loss (exit 251); red with +// Attach no longer calling the attached hooks AND ForgetServerWindow's CancelResume deleted: nothing +// resumes and the ended session is still registered (bits 16|32|64|128). +TEST(ServerLoopSuspendTest, ALostServerWindowSuspendsTheSessionAndTheNextWindowResumesIt) { + EXPECT_EXIT(SuspendResumeAndExit(), ::testing::ExitedWithCode(0), ".*") + << "bits: 1 = Detach did not answer ReleasedBySession; 2 = the session latched or left; 4 = the " + "backend was not suspended once, on the apply thread, for the lost window; 8 = the suspended state " + "(counters, lease, wait) is wrong; 16 = presents after the loss were not applied; 32 = the new window " + "did not resume the session; 64 = the resumed session did not suspend again; 128 = the ended session " + "still waits for a window (250+ = setup)"; +} + +// The negative control: a backend that cannot suspend keeps today's ending. Red with the suspend arm +// taken whatever the backend answers: no latch (bits 2|4|8|16). +TEST(ServerLoopSuspendTest, ABackendThatCannotSuspendStillLatchesServerWindowLost) { + EXPECT_EXIT(ABackendThatCannotSuspendLatchesAndExit(), ::testing::ExitedWithCode(0), ".*") + << "bits: 1 = Detach did not answer ReleasedBySession; 2 = no ServerWindowLost latch; 4 = the EGL " + "resources were not released once; 8 = the counters are wrong; 16 = the apply thread stayed (250+ = " + "setup)"; +} + +// Mid-stream: answered within the stream (DrainRing's per-pop check) and the stream goes on. Red with +// the suspend arm deleted: the stream is cut at the latch (bits 4|8|16). +TEST(ServerLoopSuspendTest, ALostServerWindowMidStreamSuspendsPromptlyAndTheStreamCarriesOn) { + EXPECT_EXIT(SuspendMidStreamAndExit(), ::testing::ExitedWithCode(0), ".*") + << "bits: 1 = Detach did not answer ReleasedBySession; 2 = Detach took 1 s or more; 4 = the stream was " + "cut; 8 = the session latched or left; 16 = not suspended exactly once (250+ = setup)"; +} + namespace { // P12 review fix: A STOPPING SERVER UNDER A STREAMING CLIENT. 200 records at 40 ms each is an 8 s // stream, longer than Stop()'s 5 s bounded join. The display server's stop marks the queue