diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index fc70eb52..79aa2b80 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -710,6 +710,17 @@ namespace MobileGL { (void)window, (void)width, (void)height; return false; } + // `window` changed size to `width`x`height` (the display's report - the one size to publish). + // Its surfaces publish that extent to the client now, without waiting for a present; a + // swapchain that has to follow is rebuilt when no frame is half-recorded, else at the + // present. False: nothing was published (no surface on it, or not supported). + virtual Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) { + (void)window, (void)width, (void)height; + return false; + } + // CLIENT side, from eglQuerySurface(EGL_WIDTH/EGL_HEIGHT): a backend whose surface extents + // change under the application (a remote server's window) applies what it has received. + virtual void RefreshSurfaceExtent(EGLSurface surface) { (void)surface; } void SetWindowHandle(const WindowHandle& handle); diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 11f0bf11..2ae4549c 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1301,6 +1301,31 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } + Bool BackendObject_DirectGLES::ServerWindowResized(void* window, Uint32 width, Uint32 height) { + const std::lock_guard lock(m_eglStateMutex); + if (window == nullptr) return false; + Bool published = false; + for (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; + state.Window.Width = width; + state.Window.Height = height; + state.Width = static_cast(std::max(width, 1)); + state.Height = static_cast(std::max(height, 1)); + if (m_eglSurface == client) m_windowHandle = state.Window; + // The window surface's buffers follow the window by themselves (from the next one the + // driver dequeues); only the client has to be told. + const auto native = m_nativeSurfaces.find(client); + if (native == m_nativeSurfaces.end()) continue; + published = DirectGLES::RepublishWindowSurfaceShape(native->second, + reinterpret_cast(window)) || + published; + } + return published; + } + 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 diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index c0cabc51..2f8d0e64 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -51,6 +51,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // 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; + Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override; const RendererInfo& GetRendererInfo() const override; String GetBackendAPIVersionString() const override; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 777a41d5..db2e7335 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -36,7 +36,6 @@ #include #if defined(__ANDROID__) #include -#include #include #endif #endif @@ -16276,10 +16275,6 @@ 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; @@ -16521,10 +16516,6 @@ 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) { @@ -16689,15 +16680,14 @@ 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 = 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; + // THE DISPLAY'S EXTENT, NEVER THE SURFACE'S: eglQuerySurface answers the buffer last + // dequeued, which after a resize is still the old size - publishing it told the client a + // size the window no longer had. + Uint32 extentWidth = 0; + Uint32 extentHeight = 0; + if (MG_Pipe::MGPipeServerOwnedWindowExtent(&extentWidth, &extentHeight)) { + info.Width = extentWidth; + info.Height = extentHeight; } } #endif @@ -17262,64 +17252,17 @@ namespace MobileGL::MG_Backend::DirectGLES { #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; + // THE SERVER WINDOW CHANGED SIZE (the app's layout moved the view: a bar shown or hidden, a + // rotation). The new extent - the display's, set by the server loop before this runs - is published + // as the creation publishes it, at once: the client sizes its viewport and default framebuffer from + // it, and a compositor with nothing to redraw makes no present to hang this on. + Bool RepublishWindowSurfaceShape(EGLSurface surface, NativeWindowType window) { + if (surface == EGL_NO_SURFACE || !window) return false; + // Bound, because the publish reads the default framebuffer's format. + if (!BindSessionSurface(surface) || t_boundNativeDraw != surface) return false; 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; + return true; } -#endif EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface) { if (windowSurface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return EGL_NO_SURFACE; @@ -18135,11 +18078,6 @@ 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 b53cf69e..3d548840 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -140,6 +140,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // 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); + // The window under `surface` changed size: its extent (the display's) is published again, at once. + Bool RepublishWindowSurfaceShape(EGLSurface surface, 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 0bf20075..7730b93d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -695,6 +695,27 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } + Bool BackendObject_DirectVulkan::ServerWindowResized(void* window, Uint32 width, Uint32 height) { + const std::lock_guard lock(m_eglStateMutex); + if (window == nullptr || !UsesSurfaceTargets()) return false; + Bool followed = false; + for (auto& [client, state] : m_eglSurfaces) { + if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue; + if (m_suspendedWindowSurfaces.find(client) != m_suspendedWindowSurfaces.end()) continue; + state.Window.Width = width; + state.Window.Height = height; + state.Width = static_cast(std::max(width, 1)); + state.Height = static_cast(std::max(height, 1)); + if (m_eglSurface == client) m_windowHandle = state.Window; + // Magma's extent is its swapchain's, published when it is built: the active target follows + // the window now (or at the half-recorded frame's present); a parked one at its next present. + if (pVulkanRenderer && pVulkanRenderer->IsActiveSurfaceTarget(SurfaceTargetKey(client))) { + followed = pVulkanRenderer->FollowActiveWindowResize() || followed; + } + } + return followed; + } + Bool BackendObject_DirectVulkan::UsesSurfaceTargets() const { return MG_Config::Transport != MG_Config::TransportMode::Monolith; } diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index 17a1f553..7456514a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -45,6 +45,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // 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; + Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override; const RendererInfo& GetRendererInfo() const override; String GetBackendAPIVersionString() const override; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 400332c6..c9b22a3a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -17094,6 +17094,30 @@ void main() { m_parkedTargets.erase(parked); } + Bool VulkanRenderer::FollowActiveWindowResize() { + if (m_surface == VK_NULL_HANDLE || !m_presentsToAppWindow) return false; + const auto& frame = m_frameContext.GetCurrent(); + if (frame.isCommandRecording || frame.hasCommandBufferRecorded || frame.isPreCommandRecording || + frame.hasPreCommandBufferRecorded) { + m_swapchainResizeRequested = true; + MGLOG_I("DirectVulkan: the window resized mid-frame; its swapchain is rebuilt at this frame's present"); + return true; + } + // The surface already reports the swapchain's extent: nothing to rebuild. + if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE && !SwapchainIsOutOfDate()) return true; + // The target's own rebuild - the path a surface's re-activation takes: its pending acquire waited + // out, a new VkSurfaceKHR and swapchain at the surface's current extent and pre-transform, a new + // target serial for the render passes, and the extent published. + const Uint64 key = m_activeTargetKey; + const NativeWindowType window = m_window; + VulkanRendererConfig config; + config.SurfaceWidth = m_config.SurfaceWidth; + config.SurfaceHeight = m_config.SurfaceHeight; + config.SwapInterval = m_config.SwapInterval; + DestroySurfaceTarget(key); + return ActivateSurfaceTarget(key, window, config); + } + 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() diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 425aa3b0..5e1c7062 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -461,6 +461,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { // 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; + // The active WINDOW target's window changed size. Between frames (nothing recorded since the last + // present) its swapchain is rebuilt now - which publishes the new extent - so a client that has + // nothing to redraw learns it without presenting; with a frame half-recorded the rebuild waits + // for that frame's present (it would otherwise land half in each swapchain). False: no active + // window target. + Bool FollowActiveWindowResize(); // Whether `key` names the active target. Bool IsActiveSurfaceTarget(Uint64 key) const { return key == m_activeTargetKey && m_surface != VK_NULL_HANDLE; } diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp index 2aedb3b4..8f47f3dd 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp @@ -413,6 +413,23 @@ namespace MobileGL::MG_Pipe { void MGPipeServerSetOwnedWindow(const void* window) { g_serverOwnedWindow = window; } const void* MGPipeServerOwnedWindow() { return g_serverOwnedWindow; } + + namespace { + Uint32 g_serverOwnedWindowWidth = 0; + Uint32 g_serverOwnedWindowHeight = 0; + } // namespace + + void MGPipeServerSetOwnedWindowExtent(Uint32 width, Uint32 height) { + g_serverOwnedWindowWidth = width; + g_serverOwnedWindowHeight = height; + } + + Bool MGPipeServerOwnedWindowExtent(Uint32* width, Uint32* height) { + if (g_serverOwnedWindowWidth == 0 || g_serverOwnedWindowHeight == 0) return false; + if (width != nullptr) *width = g_serverOwnedWindowWidth; + if (height != nullptr) *height = g_serverOwnedWindowHeight; + return true; + } #endif void MGPipeServerBlockNoteIdentity() { diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.h b/MobileGL/MG_Backend/MGPipe/PipeInputs.h index cc914d17..7326404d 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.h +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.h @@ -996,6 +996,11 @@ namespace MobileGL::MG_Pipe { // earlier session relied on. Apply thread only. void MGPipeServerSetOwnedWindow(const void* window); const void* MGPipeServerOwnedWindow(); + // That window's extent as the display last reported it (ServerLoop sets it from ServerDisplay with the + // lease and on every resize; 0x0 with no lease). The ONE size a backend publishes for it: a surface's own + // query can still answer the buffer before the resize. False while unknown. Apply thread only. + void MGPipeServerSetOwnedWindowExtent(Uint32 width, Uint32 height); + Bool MGPipeServerOwnedWindowExtent(Uint32* width, Uint32* height); #else // The push-without-transport build has one role and one block, so the fill side's spelling // folds onto gPipeInputs and PipeFill.cpp reads identically in both build flavours. An diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index 786f5985..86bf3cb8 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -728,6 +728,12 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state) { return EGL_FALSE; } + // A size that changes under the application (a server-owned window resized) arrives from the + // backend: let it bring the surface's record up to date first, so a caller that polls the size + // while it draws nothing still sees the change. + if (attribute == EGL_WIDTH || attribute == EGL_HEIGHT) { + if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) backendObject->RefreshSurfaceExtent(surface); + } return state->QuerySurface(display, surface, attribute, value) ? EGL_TRUE : EGL_FALSE; } diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index 2b050bc7..f19dc584 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -269,6 +269,16 @@ namespace MobileGL::MG_Remote::Client { return MG_Backend::BackendObject::ResizeEGLWindowSurface(surface, width, height); } + void BackendObject_Remote::RefreshSurfaceExtent(EGLSurface surface) { + if (!ClientSession::IsServerOwnedWindowSurface(surface)) return; + ClientSession* session = ClientSession::Active(); + if (session == nullptr) return; + // The known-idle instant every EGL forwarder drains at: the server has applied what this client + // published, so the events it posted for it - a resized window's extent - are all there. + WaitForApplyBeforeEglForwarder("QuerySurface"); + session->DrainPublishedEvents(); + } + Bool BackendObject_Remote::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) { WaitForApplyBeforeEglForwarder("CreateEGLPbufferSurface"); Server::SurfaceRefusalCode refusal = Server::SurfaceRefusalCode::None; diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.h b/MobileGL/MG_Remote/Client/BackendObject_Remote.h index b195f2ff..6580ceff 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.h +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.h @@ -72,6 +72,9 @@ namespace MobileGL::MG_Remote::Client { Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override; Bool CreateEGLWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle) override; Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override; + // A server-owned window surface: the published surface-changed events are applied first (after the + // server caught up), so a client polling eglQuerySurface sees a resize it drew nothing for. + void RefreshSurfaceExtent(EGLSurface surface) override; Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override; Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override; Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override; diff --git a/MobileGL/MG_Remote/Server/ServerDisplay.cpp b/MobileGL/MG_Remote/Server/ServerDisplay.cpp index 20e357c6..faa91c3d 100644 --- a/MobileGL/MG_Remote/Server/ServerDisplay.cpp +++ b/MobileGL/MG_Remote/Server/ServerDisplay.cpp @@ -100,12 +100,18 @@ namespace MobileGL::MG_Remote::Server { if (window == m_window) { // surfaceChanged: the same window at a new extent. This is also how a geometry request // is answered, so a waiting AcquireFor is woken. + const Bool resized = width != m_width || height != m_height; m_width = width; m_height = height; NoteExtentReportLocked(width, height); + // The session rendering into it publishes the new extent to its client NOW - not at its + // next present, which a compositor with nothing to redraw never makes. + const Bool told = resized && m_leaseHolder != nullptr && m_onResized != nullptr; + if (told) m_onResized(m_leaseHolder); lock.unlock(); m_cv.notify_all(); - MGLOG_I("MG_Remote server: server window %p is now %ux%u", window, width, height); + MGLOG_I("MG_Remote server: server window %p is now %ux%u%s", window, width, height, + told ? "; its session is told" : ""); return; } if (m_window != nullptr) { @@ -184,7 +190,8 @@ namespace MobileGL::MG_Remote::Server { ServerWindowAcquire ServerDisplay::AcquireFor(Uint32 width, Uint32 height, Uint32 timeoutMs, void* holder, ServerWindowLostHook onLost, ServerWindowWaitCancel cancel, - void* cancelUser, ServerWindowLease* out) { + void* cancelUser, ServerWindowLease* out, + ServerWindowResizedHook onResized) { ServerDisplayHooks hooks{}; Uint64 interrupts = 0; const Bool sizeRequested = width != 0 && height != 0; @@ -240,6 +247,7 @@ namespace MobileGL::MG_Remote::Server { if (sizeOk || now >= geometryDeadline) { m_leaseHolder = holder; m_onLost = onLost; + m_onResized = onResized; 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(), @@ -281,6 +289,7 @@ namespace MobileGL::MG_Remote::Server { if (holder == nullptr || m_leaseHolder != holder) return false; m_leaseHolder = nullptr; m_onLost = nullptr; + m_onResized = nullptr; m_lostRequested = false; releases->swap(m_releaseAfterLease); *hooks = m_hooks; @@ -338,6 +347,15 @@ namespace MobileGL::MG_Remote::Server { m_suspended.end()); } + Bool ServerDisplay::WindowExtent(void* holder, void** window, Uint32* width, Uint32* height) const { + const std::lock_guard lock(m_mutex); + if (holder == nullptr || m_leaseHolder != holder || m_window == nullptr) return false; + if (window != nullptr) *window = m_window; + if (width != nullptr) *width = m_width; + if (height != nullptr) *height = m_height; + return true; + } + Uint32 ServerDisplay::SuspendedHolders() const { const std::lock_guard lock(m_mutex); return static_cast(m_suspended.size()); diff --git a/MobileGL/MG_Remote/Server/ServerDisplay.h b/MobileGL/MG_Remote/Server/ServerDisplay.h index 4999a25c..3c1ff47b 100644 --- a/MobileGL/MG_Remote/Server/ServerDisplay.h +++ b/MobileGL/MG_Remote/Server/ServerDisplay.h @@ -72,6 +72,9 @@ namespace MobileGL::MG_Remote::Server { // 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); + // The leased window changed size (surfaceChanged of the SAME window: the app's layout moved it). + // Same rules: called with the display's lock held; the holder reads the new extent with WindowExtent. + using ServerWindowResizedHook = void (*)(void* holder); // Asked about every 50 ms while AcquireFor waits; true ends the wait (Cancelled). using ServerWindowWaitCancel = Bool (*)(void* user); @@ -139,10 +142,16 @@ namespace MobileGL::MG_Remote::Server { // The apply thread's side. Requests the geometry (w, h; 0/0 = the window's own), then waits // until a window is attached - and, for a requested size, has reached it or the geometry // grace ran out - and leases it to `holder`, whose `onLost` Detach will call. The same holder - // may lease again while it holds the lease (a surface re-created at a new size). + // may lease again while it holds the lease (a surface re-created at a new size). `onResized`, when + // given, is called whenever the leased window changes size while the lease lasts. ServerWindowAcquire AcquireFor(Uint32 width, Uint32 height, Uint32 timeoutMs, void* holder, ServerWindowLostHook onLost, ServerWindowWaitCancel cancel, - void* cancelUser, ServerWindowLease* out); + void* cancelUser, ServerWindowLease* out, + ServerWindowResizedHook onResized = nullptr); + // THE WINDOW'S EXTENT AS THE PLATFORM LAST REPORTED IT (surfaceCreated / surfaceChanged): the one + // source of truth for the size a session publishes to its client. False unless `holder` holds + // the lease on an attached window. + Bool WindowExtent(void* holder, void** window, Uint32* width, Uint32* height) const; // The holder has released everything that referenced the window (its backend surface, or the // whole backend). Wakes a waiting Detach; releases a reference Detach left behind on timeout. // A holder that holds no lease is a no-op. @@ -196,6 +205,7 @@ namespace MobileGL::MG_Remote::Server { Uint64 m_extentReports = 0; void* m_leaseHolder = nullptr; ServerWindowLostHook m_onLost = nullptr; + ServerWindowResizedHook m_onResized = nullptr; Bool m_lostRequested = false; // References a timed-out Detach could not release yet; EndLease releases them. std::vector m_releaseAfterLease; diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index 0c7fdf20..02b71fe0 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -371,6 +371,9 @@ namespace MobileGL::MG_Remote::Server { m_serverWindowsResumed.store(0, std::memory_order_release); m_serverWindowWantWidth = 0; m_serverWindowWantHeight = 0; + m_publishedWindowWidth = 0; + m_publishedWindowHeight = 0; + m_serverWindowResizes.store(0, std::memory_order_release); m_testLogFloodLines = ApplyLogFloodLinesForTest(); { const std::lock_guard lock(m_exitMutex); @@ -1748,6 +1751,15 @@ namespace MobileGL::MG_Remote::Server { } } + void ServerLoop::ServerWindowResizedThunk(void* self) { + auto* loop = static_cast(self); + // As the lost hook: called under the display's lock while this loop holds the lease. + loop->m_windowRequests.fetch_or(kWindowResized, std::memory_order_acq_rel); + if (loop->m_session != nullptr && loop->m_session->DataLink() != nullptr) { + loop->m_session->DataLink()->ConsumerBell().Notify(); + } + } + Bool ServerLoop::ServerWindowWaitCancelled(void* self) { const auto* loop = static_cast(self); // A window-lost request also ends the wait: this thread holds a lease on a window that is @@ -1762,7 +1774,8 @@ namespace MobileGL::MG_Remote::Server { ServerWindowLease local; ServerWindowLease* const lease = out != nullptr ? out : &local; const ServerWindowAcquire acquired = display.AcquireFor(width, height, timeoutMs, this, &ServerWindowLostThunk, - &ServerWindowWaitCancelled, this, lease); + &ServerWindowWaitCancelled, this, lease, + &ServerWindowResizedThunk); switch (acquired) { case ServerWindowAcquire::Acquired: m_holdsWindowLease = true; @@ -1841,7 +1854,7 @@ namespace MobileGL::MG_Remote::Server { window.Height = lease.height; // Review fix: the backend is told which window is the server's own, so its surface init // reports the extent for this window and for no window a client named (MGPipeServerOwnedWindow). - MG_Pipe::MGPipeServerSetOwnedWindow(lease.window); + NoteServerWindowExtent(lease.window, lease.width, lease.height); frame.ok = backend->CreateEGLWindowSurface(HandleFromToken(frame.surface), window); if (!frame.ok) { MGLOG_E("MG_Remote server: the backend could not create a window surface on the server window %p " @@ -1912,6 +1925,7 @@ namespace MobileGL::MG_Remote::Server { } m_serverWindowWantWidth = wantWidth; m_serverWindowWantHeight = wantHeight; + NoteServerWindowExtent(lease.window, lease.width, lease.height); frame.ok = backend->ResizeEGLWindowSurface(HandleFromToken(frame.surface), lease.width, lease.height); frame.width = static_cast(lease.width); frame.height = static_cast(lease.height); @@ -1925,7 +1939,7 @@ namespace MobileGL::MG_Remote::Server { if (!m_holdsWindowLease) return; m_holdsWindowLease = false; m_leasedWindow = nullptr; - MG_Pipe::MGPipeServerSetOwnedWindow(nullptr); + NoteServerWindowExtent(nullptr, 0, 0); ServerDisplayInstance().EndLease(this); } @@ -1942,6 +1956,35 @@ namespace MobileGL::MG_Remote::Server { const Uint32 requests = m_windowRequests.exchange(0, std::memory_order_acq_rel); if ((requests & kWindowLost) != 0) ReleaseLostServerWindow(); if ((requests & kWindowAttached) != 0 && !SessionLatched()) ResumeSuspendedServerWindow(); + if ((requests & kWindowResized) != 0 && !SessionLatched()) PublishServerWindowResize(); + } + + void ServerLoop::NoteServerWindowExtent(void* window, Uint32 width, Uint32 height) { + MG_Pipe::MGPipeServerSetOwnedWindow(window); + MG_Pipe::MGPipeServerSetOwnedWindowExtent(width, height); + m_publishedWindowWidth = width; + m_publishedWindowHeight = height; + } + + void ServerLoop::PublishServerWindowResize() { + if (!m_holdsWindowLease || m_backend == nullptr) return; + // THE DISPLAY'S EXTENT, READ NOW: the latest report wins over any a burst of them left queued. + void* window = nullptr; + Uint32 width = 0; + Uint32 height = 0; + if (!ServerDisplayInstance().WindowExtent(this, &window, &width, &height) || window != m_leasedWindow || + width == 0 || height == 0) + return; + if (width == m_publishedWindowWidth && height == m_publishedWindowHeight) return; + const Uint32 previousWidth = m_publishedWindowWidth; + const Uint32 previousHeight = m_publishedWindowHeight; + NoteServerWindowExtent(window, width, height); + const Bool published = m_backend->ServerWindowResized(window, width, height); + if (published) m_serverWindowResizes.fetch_add(1, std::memory_order_acq_rel); + MGLOG_I("MG_Remote server: server window %p resized %ux%u -> %ux%u; %s", window, previousWidth, previousHeight, + width, height, + published ? "the new extent is published to the client" + : "the backend published nothing (no surface on the window, or it follows at its present)"); } void ServerLoop::LatchServerWindowLost(const char* why) { @@ -1974,7 +2017,7 @@ namespace MobileGL::MG_Remote::Server { const Uint64 suspends = m_serverWindowsSuspended.fetch_add(1, std::memory_order_acq_rel) + 1; m_holdsWindowLease = false; m_leasedWindow = nullptr; - MG_Pipe::MGPipeServerSetOwnedWindow(nullptr); + NoteServerWindowExtent(nullptr, 0, 0); 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)", @@ -1997,7 +2040,7 @@ namespace MobileGL::MG_Remote::Server { const ServerWindowAcquire acquired = ServerDisplayInstance().AcquireFor(m_serverWindowWantWidth, m_serverWindowWantHeight, ServerDisplay::kGeometryGraceMs, this, &ServerWindowLostThunk, - &ServerWindowWaitCancelled, this, &lease); + &ServerWindowWaitCancelled, this, &lease, &ServerWindowResizedThunk); 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 " @@ -2012,7 +2055,7 @@ namespace MobileGL::MG_Remote::Server { Bool ServerLoop::ResumeServerWindowOnLease(const ServerWindowLease& lease) { // Before the backend: its republished extent is the server window's (MGPipeServerOwnedWindow). - MG_Pipe::MGPipeServerSetOwnedWindow(lease.window); + NoteServerWindowExtent(lease.window, lease.width, lease.height); 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"); diff --git a/MobileGL/MG_Remote/Server/ServerLoop.h b/MobileGL/MG_Remote/Server/ServerLoop.h index fed001dc..e3284e19 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.h +++ b/MobileGL/MG_Remote/Server/ServerLoop.h @@ -440,6 +440,9 @@ namespace MobileGL::MG_Remote::Server { // 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); } + // How many times a resize of the leased window was published to the client (the display's + // surfaceChanged, answered on the apply thread without waiting for a present). + Uint64 ServerWindowResizesPublished() const { return m_serverWindowResizes.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 @@ -484,6 +487,14 @@ namespace MobileGL::MG_Remote::Server { 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); + // ServerDisplay's resized hook for the lease holder: the same discipline again. + static void ServerWindowResizedThunk(void* self); + // Apply thread: the leased window's new extent (the display's) is handed to the backend, which + // publishes it to the client now. + void PublishServerWindowResize(); + // Apply thread: the extent a backend publishes for the server window from here on (MGPipe's), and + // what this session last handed it. + void NoteServerWindowExtent(void* window, Uint32 width, Uint32 height); // 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 @@ -500,6 +511,7 @@ namespace MobileGL::MG_Remote::Server { // apply thread wakes for it. static constexpr Uint32 kWindowLost = 1u; static constexpr Uint32 kWindowAttached = 2u; + static constexpr Uint32 kWindowResized = 4u; std::atomic m_windowRequests{0}; std::atomic m_serverWindowsLost{0}; // Written by the apply thread, read by tests. @@ -510,6 +522,10 @@ namespace MobileGL::MG_Remote::Server { // layout's), which a resume asks for again. Uint32 m_serverWindowWantWidth = 0; Uint32 m_serverWindowWantHeight = 0; + // Apply thread only: the leased window's extent this session last published. + Uint32 m_publishedWindowWidth = 0; + Uint32 m_publishedWindowHeight = 0; + std::atomic m_serverWindowResizes{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 c2bc49e5..ec355cd0 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -3156,6 +3156,21 @@ TEST(RemoteRunAhead, ARepublishedServerWindowExtentResizesTheServerOwnedSurfaceA if (ClientSessionInstance().DrainPublishedEvents() < 1u) ::_exit(95); if (query(EGL_WIDTH) != 2560 || query(EGL_HEIGHT) != 1600) ::_exit(96); + // AN APPLICATION THAT POLLS THE SIZE WHILE IT DRAWS NOTHING (the compositor's timer): the next + // resize is published and nothing drains it - no verb, no make-current - but eglQuerySurface + // itself takes it in. Red with BackendObject_Remote::RefreshSurfaceExtent a no-op (exit 97). + MG_Backend::pActiveBackendObject = MakeUnique(); + s_info.Width = 2560; + s_info.Height = 1412; + republish(); + EGLint polledWidth = -1; + EGLint polledHeight = -1; + if (MG_Impl::EGLImpl::QuerySurface(dpy, surface, EGL_WIDTH, &polledWidth) != EGL_TRUE || + MG_Impl::EGLImpl::QuerySurface(dpy, surface, EGL_HEIGHT, &polledHeight) != EGL_TRUE || + polledWidth != 2560 || polledHeight != 1412) + ::_exit(97); + MG_Backend::pActiveBackendObject.reset(); + defaultFramebuffer.reset(); ClientSessionInstance().ForgetServerOwnedWindowSurface(surface); MG_State::pEGLContext.reset(); diff --git a/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp b/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp index 2214aa27..0ffb764c 100644 --- a/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp @@ -485,3 +485,45 @@ TEST(ServerDisplayTest, ASuspendedHolderIsToldAboutTheNextNewWindowUntilItLeases EXPECT_EQ(attached.load(), 2); display.Uninstall(100); } + +// A RESIZE OF THE LEASED WINDOW REACHES ITS HOLDER AT ONCE (surfaceChanged of the same window), and +// WindowExtent answers the platform's latest report - the one size a session publishes. Not for a +// report of the same size, not for anyone but the holder, not after the lease ended. Red with Attach's +// same-window arm not calling the hook (resized stays 0). +TEST(ServerDisplayTest, AResizeOfTheLeasedWindowIsToldToItsHolderAndWindowExtentAnswersIt) { + FakePlatform platform; + ServerDisplay display; + display.Install(HooksFor(platform)); + display.Attach(FakeWindow(0), 64, 48); + static std::atomic resized{0}; + resized.store(0); + const auto onResized = [](void*) { resized.fetch_add(1); }; + int holder = 0; + int stranger = 0; + ServerWindowLease lease; + ASSERT_EQ(display.AcquireFor(64, 48, 100, &holder, nullptr, nullptr, nullptr, &lease, onResized), + ServerWindowAcquire::Acquired); + void* window = nullptr; + Uint32 width = 0; + Uint32 height = 0; + ASSERT_TRUE(display.WindowExtent(&holder, &window, &width, &height)); + EXPECT_EQ(window, FakeWindow(0)); + EXPECT_EQ(width, 64u); + EXPECT_EQ(height, 48u); + EXPECT_FALSE(display.WindowExtent(&stranger, &window, &width, &height)) << "only the holder reads it"; + + display.Attach(FakeWindow(0), 64, 60); // the app's bar hidden + EXPECT_EQ(resized.load(), 1); + ASSERT_TRUE(display.WindowExtent(&holder, &window, &width, &height)); + EXPECT_EQ(height, 60u); + display.Attach(FakeWindow(0), 64, 60); // the same report again + EXPECT_EQ(resized.load(), 1) << "an unchanged extent is not a resize"; + display.Attach(FakeWindow(0), 64, 48); // and back + EXPECT_EQ(resized.load(), 2); + + display.EndLease(&holder); + display.Attach(FakeWindow(0), 64, 60); + EXPECT_EQ(resized.load(), 2) << "a holder whose lease ended is not told"; + EXPECT_FALSE(display.WindowExtent(&holder, &window, &width, &height)); + display.Uninstall(100); +} diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index 673e1873..a7e846d5 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -3460,6 +3460,7 @@ namespace { Bool InitWindowSurface() override { return true; } Bool CreateEGLWindowSurface(EGLSurface, const MG_Backend::WindowHandle& handle) override { window.store(handle.Handle, std::memory_order_release); + NoteExtentAt(createExtent); creates.fetch_add(1, std::memory_order_acq_rel); return true; } @@ -3475,10 +3476,19 @@ namespace { resumeOnApplyThread.store(Server::ServerLoop::OnApplyThread(), std::memory_order_release); resumeWidth.store(width, std::memory_order_release); resumeHeight.store(height, std::memory_order_release); + NoteExtentAt(resumeExtent); window.store(fresh, std::memory_order_release); resumes.fetch_add(1, std::memory_order_acq_rel); return true; } + Bool ServerWindowResized(void* resizedWindow, Uint32 width, Uint32 height) override { + if (resizedWindow != window.load(std::memory_order_acquire)) return false; + resizedOnApplyThread.store(Server::ServerLoop::OnApplyThread(), std::memory_order_release); + resizedTo.store(Pack(width, height), std::memory_order_release); + NoteExtentAt(resizeExtent); + resizes.fetch_add(1, std::memory_order_acq_rel); + return true; + } void ReleaseEGLResources() override { releases.fetch_add(1, std::memory_order_acq_rel); BackendObject::ReleaseEGLResources(); @@ -3506,6 +3516,21 @@ namespace { static inline std::atomic resumeOnApplyThread{false}; static inline std::atomic resumeWidth{0}; static inline std::atomic resumeHeight{0}; + // The extent the server window had in MGPipe - the one a real backend publishes - when each + // call ran (width << 32 | height; 0 = none). + static inline std::atomic createExtent{0}; + static inline std::atomic resumeExtent{0}; + static inline std::atomic resizeExtent{0}; + static inline std::atomic resizedTo{0}; + static inline std::atomic resizes{0}; + static inline std::atomic resizedOnApplyThread{false}; + static constexpr Uint64 Pack(Uint32 width, Uint32 height) { return (Uint64{width} << 32) | height; } + static void NoteExtentAt(std::atomic& slot) { + Uint32 width = 0; + Uint32 height = 0; + slot.store(MG_Pipe::MGPipeServerOwnedWindowExtent(&width, &height) ? Pack(width, height) : 0, + std::memory_order_release); + } private: Bool m_canSuspend = false; @@ -3734,6 +3759,70 @@ TEST(ServerLoopSuspendTest, ABackendThatCannotSuspendStillLatchesServerWindowLos // 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). +namespace { + enum : int { + kResizeNotPublished = 1, // a resize with no present was not handed to the backend at the new size, on the apply thread + kResizeExtent = 2, // the extent a backend publishes (MGPipe's) did not follow the display + kResizeRepeated = 4, // an unchanged report was published again + kResizePresented = 8, // a present ran (the case is about publishing without one) + kResizeAfterResume = 16, // the resume did not take the new window's extent, or a later resize was lost + }; + + [[noreturn]] void ResizeWithoutAPresentAndExit() { + Remote::ArmSessionLatch(); + ServerFixture fixture; + if (!BringUpAnOnScreenSession(fixture, /*canSuspend=*/true)) ::_exit(250); + Server::ServerLoop& loop = Server::ServerLoopInstance(); + Server::ServerDisplay& display = Server::ServerDisplayInstance(); + using Double = WindowBackendDouble; + int failed = 0; + if (Double::createExtent.load() != Double::Pack(64, 48)) failed |= kResizeExtent; + + // The app's bar is hidden: the SAME window, taller - and the client draws nothing at all. + display.Attach(&g_fakeServerWindow, 64, 60); + if (!PollUntil([&] { return loop.ServerWindowResizesPublished() == 1; }, 3000) || Double::resizes.load() != 1 || + Double::resizedTo.load() != Double::Pack(64, 60) || !Double::resizedOnApplyThread.load()) + failed |= kResizeNotPublished; + if (Double::resizeExtent.load() != Double::Pack(64, 60)) failed |= kResizeExtent; + display.Attach(&g_fakeServerWindow, 64, 60); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (Double::resizes.load() != 1) failed |= kResizeRepeated; + if (Double::presents.load() != 0) failed |= kResizePresented; + + // Screen off; the new window comes back at the session's own size, then the bar moves again. + if (!fixture.WaitUntilTrulyParked()) ::_exit(251); + if (display.Detach(3000) != Server::ServerWindowDetach::ReleasedBySession) failed |= kResizeAfterResume; + if (MG_Pipe::MGPipeServerOwnedWindowExtent(nullptr, nullptr)) failed |= kResizeExtent; // no window, no extent + display.Attach(&g_secondFakeServerWindow, 64, 48); + if (!PollUntil([&] { return loop.ServerWindowsResumed() == 1; }, 3000) || + Double::resumeExtent.load() != Double::Pack(64, 48)) + failed |= kResizeAfterResume; + display.Attach(&g_secondFakeServerWindow, 64, 60); + if (!PollUntil([&] { return Double::resizes.load() == 2; }, 3000) || + Double::resizedTo.load() != Double::Pack(64, 60) || Double::resizeExtent.load() != Double::Pack(64, 60)) + failed |= kResizeAfterResume; + std::fprintf(stderr, "[resize] published=%llu resizes=%d presents=%d\n", + static_cast(loop.ServerWindowResizesPublished()), Double::resizes.load(), + Double::presents.load()); + fixture.Stop(); + ::_exit(failed); + } +} // namespace + +// THE RESIZE NOBODY PRESENTED FOR (device: the bar toggled while the compositor was idle, or just +// before the lock, never reached it). The display's surfaceChanged of the leased window reaches the +// session's apply thread at once, and the backend publishes the DISPLAY's extent - MGPipe's, which +// every publish of the server window reads - with no present in between; after a suspend and resume +// the resume publishes the new window's extent and a later resize is published again. Red with +// ServerDisplay::Attach not telling the holder (exit 23: nothing published), and with the resize path +// handing the backend the size without setting MGPipe's extent (exit 18: bits 2|16). +TEST(ServerLoopSuspendTest, AResizeOfTheServerWindowIsPublishedWithoutWaitingForAPresent) { + EXPECT_EXIT(ResizeWithoutAPresentAndExit(), ::testing::ExitedWithCode(0), ".*") + << "bits: 1 = the resize was not published without a present; 2 = the published extent is not the " + "display's; 4 = an unchanged report was published again; 8 = a present ran; 16 = the resume or a " + "resize after it went wrong (250+ = setup)"; +} + // MAGMA'S PLACEHOLDER ON A RENDERER CREATED FOR A WINDOW (the device crash: SIGSEGV at pc 0 in // VulkanRenderer::CreateSurface on screen-off). The on-screen session's renderer was built for the // server's window, so its instance enabled the Android surface extension and nothing headless; the