[Feature] (P12): suspend the on-screen session when the server window goes, resume it on the next, and republish a resized window's extent

This commit is contained in:
2026-10-02 11:14:27 -04:00
parent 15f9b27a42
commit 5a7b23f661
16 changed files with 1192 additions and 91 deletions
+19
View File
@@ -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;
@@ -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<void*>(m_activatingSurface));
return false;
}
m_nativeSurfaces[m_activatingSurface] = placeholder;
return true;
}
// Only use EGL for now
auto nativeWindow = reinterpret_cast<NativeWindowType>(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<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Vector<EGLSurface> 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<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Vector<EGLSurface> 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<EGLint>(std::max<Uint32>(width, 1));
state->second.Height = static_cast<EGLint>(std::max<Uint32>(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<NativeWindowType>(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;
@@ -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<EGLSurface, EGLSurface> m_nativeSurfaces;
// Window surfaces whose window is gone (SuspendServerWindow): their native surface is a
// placeholder pbuffer until ResumeServerWindow.
Vector<EGLSurface> m_suspendedWindowSurfaces;
MG_External::EGLFunctionsTable m_EGLFunctions;
MG_External::GLESFunctionsTable m_GLESFunctions;
MG_External::GLESCapabilities m_GLESCapabilities;
+174 -36
View File
@@ -36,6 +36,7 @@
#include <MG_Remote/Server/SharedImageRegistry.h>
#if defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <android/native_window.h>
#include <unistd.h>
#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<Uint32>(extentWidth);
info.Height = static_cast<Uint32>(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<std::mutex> 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<const void*>(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<ANativeWindow*>(window);
outWidth = static_cast<Int>(ANativeWindow_getWidth(nativeWindow));
outHeight = static_cast<Int>(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<Int>(width);
outHeight = static_cast<Int>(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<NativeWindowType>(0);
for (const auto& entry : session.Surfaces) {
if (entry.Surface != drawSurface) continue;
window = entry.Window;
break;
}
if (reinterpret_cast<const void*>(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<void*>(windowSurface), static_cast<unsigned>(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<void*>(window), static_cast<unsigned>(error));
return EGL_NO_SURFACE;
}
NativeSessionState& session = ActiveNativeSession();
{
const std::lock_guard<std::mutex> 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<void*>(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<std::mutex> 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<const void*>(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();
@@ -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.
@@ -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<EGLint>(suspended->second.Width),
static_cast<EGLint>(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<Uint32>(extent.Width, 1);
config.SurfaceHeight = std::max<Uint32>(extent.Height, 1);
pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config);
}
Bool BackendObject_DirectVulkan::SuspendServerWindow(void* window) {
const std::lock_guard<std::recursive_mutex> 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<EGLSurface> 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<Uint32>(std::max<EGLint>(state->second.Width, 1));
extent.Height = static_cast<Uint32>(std::max<EGLint>(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<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr || !UsesSurfaceTargets()) return false;
UnorderedMap<EGLSurface, SuspendedWindowSurface> 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<EGLint>(std::max<Uint32>(width, 1));
state->second.Height = static_cast<EGLint>(std::max<Uint32>(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<Uint32>(width, 1);
config.SurfaceHeight = std::max<Uint32>(height, 1);
config.SwapInterval = GetRequestedSwapInterval();
pVulkanRenderer->ActivateSurfaceTarget(key, reinterpret_cast<NativeWindowType>(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));
}
@@ -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<MagmaSession> m_magmaSession;
// The binding each client context was last made current with, for OnClientContextBound.
UnorderedMap<EGLContext, EGLCurrentState> 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<EGLSurface, SuspendedWindowSurface> 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
@@ -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;
}
@@ -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
+77 -17
View File
@@ -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<Uint32>(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<unsigned long long>(generation));
MGLOG_I("MG_Remote server: server window %p attached at %ux%u (generation %llu)%s", window, width, height,
static_cast<unsigned long long>(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<void*> releases;
ServerDisplayHooks hooks{};
{
const std::lock_guard<std::mutex> lock(m_mutex);
if (holder == nullptr || m_leaseHolder != holder) return;
m_leaseHolder = nullptr;
m_onLost = nullptr;
m_lostRequested = false;
releases.swap(m_releaseAfterLease);
hooks = m_hooks;
// An Uninstall that ran while this lease still owed releases kept the hooks for them.
if (!m_installed) m_hooks = ServerDisplayHooks{};
}
m_cv.notify_all();
Bool ServerDisplay::EndLeaseLocked(void* holder, std::vector<void*>* 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<void*>& 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<void*> releases;
ServerDisplayHooks hooks{};
{
const std::lock_guard<std::mutex> 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<void*> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(m_mutex);
return static_cast<Uint32>(m_suspended.size());
}
void ServerDisplay::Interrupt() {
{
const std::lock_guard<std::mutex> lock(m_mutex);
+33 -3
View File
@@ -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 <Includes.h>
@@ -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<void*>* releases, ServerDisplayHooks* hooks);
void ReleaseAfterLease(const ServerDisplayHooks& hooks, const std::vector<void*>& 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<void*> m_releaseAfterLease;
// SuspendLease's registrations, told about the next new window.
struct SuspendedHolder {
void* holder = nullptr;
ServerWindowAttachedHook onAttached = nullptr;
};
std::vector<SuspendedHolder> m_suspended;
};
// The process's one display (leaked at exit, like every MG_Remote singleton).
+163 -27
View File
@@ -316,6 +316,14 @@ namespace MobileGL::MG_Remote::Server {
MG_Backend::BackendObject* ServerLoop::Backend() { return m_backend.get(); }
MobileGLResult ServerLoop::InstallBackendForTesting(UniquePtr<MG_Backend::BackendObject> 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<std::mutex> 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<ServerLoop*>(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<Uint8>(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<Uint8>(SurfaceRefusalCode::None);
const Uint32 wantWidth = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
const Uint32 wantHeight = frame.height > 0 ? static_cast<Uint32>(frame.height) : 0u;
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<unsigned long long>(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<Uint8>(refusal);
return acquired;
}
m_serverWindowWantWidth = wantWidth;
m_serverWindowWantHeight = wantHeight;
frame.ok = backend->ResizeEGLWindowSurface(HandleFromToken<EGLSurface>(frame.surface), lease.width, lease.height);
frame.width = static_cast<Int>(lease.width);
frame.height = static_cast<Int>(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<unsigned long long>(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<unsigned long long>(lease.generation),
lease.sizeAsRequested ? "" : ", NOT the size it asked for", static_cast<unsigned long long>(resumes));
return true;
}
// ---------------------------------------------------------------------------------
// The twelve forwarders: pack the frame, post it, unpack the reply
// ---------------------------------------------------------------------------------
+49 -7
View File
@@ -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<MG_Backend::BackendObject> 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<Bool> 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<Uint32> m_windowRequests{0};
std::atomic<Uint64> m_serverWindowsLost{0};
// Written by the apply thread, read by tests.
std::atomic<Bool> m_serverWindowSuspended{false};
std::atomic<Uint64> m_serverWindowsSuspended{0};
std::atomic<Uint64> 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.
@@ -3076,6 +3076,94 @@ TEST(RemoteRunAhead, AnEventBurstBehindAnUnwaitedSequenceFlowControlsInsteadOfAb
ExpectChildSuccess(child);
}
#include <MG_State/EGLState/Core.h>
// 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<MG_State::EGLState::EGLContext>();
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<MG_State::GLState::TextureObject2D>(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>(
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<Uint32>(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<MG_State::GLState::TextureObject2D*>(defaultFramebuffer->colorAttachment.get())->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0);
const IntVec3 depth =
static_cast<MG_State::GLState::TextureObject2D*>(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
// ===========================================================================================
@@ -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<int> attached{0};
attached.store(0);
const auto onAttached = [](void*) { attached.fetch_add(1); };
static std::atomic<int> 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);
}
+303 -1
View File
@@ -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<void*> window{nullptr};
static inline std::atomic<void*> suspendedOn{nullptr};
static inline std::atomic<int> creates{0};
static inline std::atomic<int> suspends{0};
static inline std::atomic<int> resumes{0};
static inline std::atomic<int> releases{0};
static inline std::atomic<int> presents{0};
static inline std::atomic<int> presentsWithoutWindow{0};
static inline std::atomic<Bool> suspendOnApplyThread{false};
static inline std::atomic<Bool> resumeOnApplyThread{false};
static inline std::atomic<Uint32> resumeWidth{0};
static inline std::atomic<Uint32> 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<EGLSurface>(static_cast<std::uintptr_t>(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<WindowBackendDouble>(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<Uint64>(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<unsigned long long>(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<int> g_suspendStreamRecords{0};
std::thread g_suspendStreamDetachThread;
std::atomic<int> g_suspendStreamDetachResult{-1};
std::atomic<long long> 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::milliseconds>(
std::chrono::steady_clock::now() - started)
.count(),
std::memory_order_release);
g_suspendStreamDetachResult.store(static_cast<int>(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<int>(Server::ServerWindowDetach::ReleasedBySession))
failed |= kSuspendStreamDetach;
if (g_suspendStreamDetachMs.load() >= 1000) failed |= kSuspendStreamSlow;
if (loop.DrainedRecords() - drainedBefore != static_cast<Uint64>(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<Server::ServerWindowDetach>(g_suspendStreamDetachResult.load())),
g_suspendStreamDetachMs.load(),
static_cast<unsigned long long>(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