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

This commit is contained in:
2026-10-02 11:14:27 -04:00
parent 15f9b27a42
commit 5a7b23f661
16 changed files with 1192 additions and 91 deletions
@@ -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));
}