mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user