diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 7e971493..0bf20075 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -370,8 +370,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const auto suspended = m_suspendedWindowSurfaces.find(m_activatingSurface); if (suspended != m_suspendedWindowSurfaces.end()) { if (UsesSurfaceTargets() && pVulkanRenderer) { - ActivateSuspendedPlaceholder(m_activatingSurface, suspended->second); - return true; + return ActivateSuspendedPlaceholder(m_activatingSurface, suspended->second); } return InitPbufferSurface(static_cast(suspended->second.Width), static_cast(suspended->second.Height)); @@ -386,8 +385,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VulkanRendererConfig config; config.SwapInterval = GetRequestedSwapInterval(); if (UsesSurfaceTargets() && pVulkanRenderer) { - pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), nativeWindow, config); - return true; + return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), nativeWindow, config); } // Any renderer instance this assignment replaces is destroyed here; // fence/timer-query handles stamped with the old generation go stale. @@ -404,8 +402,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { config.SurfaceWidth = static_cast(std::max(width, 1)); config.SurfaceHeight = static_cast(std::max(height, 1)); if (UsesSurfaceTargets() && pVulkanRenderer) { - pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), NativeWindowType{}, config); - return true; + return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), NativeWindowType{}, + config); } // Any renderer instance this assignment replaces is destroyed here; // fence/timer-query handles stamped with the old generation go stale. @@ -511,7 +509,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VulkanRendererConfig config; config.SurfaceWidth = static_cast(state->Width); config.SurfaceHeight = static_cast(state->Height); - pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config); + (void)pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config); } return true; } @@ -610,7 +608,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { BackendObject::ReleaseEGLResources(); } - void BackendObject_DirectVulkan::ActivateSuspendedPlaceholder(EGLSurface surface, + Bool 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 @@ -618,7 +616,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VulkanRendererConfig config; config.SurfaceWidth = std::max(extent.Width, 1); config.SurfaceHeight = std::max(extent.Height, 1); - pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config); + return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config); } Bool BackendObject_DirectVulkan::SuspendServerWindow(void* window) { @@ -648,7 +646,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // swapchain and VkSurfaceKHR - nothing references the window after this. pVulkanRenderer->DestroySurfaceTarget(key); m_suspendedWindowSurfaces[client] = extent; - if (active) ActivateSuspendedPlaceholder(client, extent); + // A placeholder that cannot be built is a suspend that failed: the window is already let + // go of, and the caller ends the session by name instead (its EGL resources go). + if (active && !ActivateSuspendedPlaceholder(client, extent)) return false; } else { m_suspendedWindowSurfaces[client] = extent; } @@ -686,7 +686,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { config.SurfaceWidth = std::max(width, 1); config.SurfaceHeight = std::max(height, 1); config.SwapInterval = GetRequestedSwapInterval(); - pVulkanRenderer->ActivateSurfaceTarget(key, reinterpret_cast(window), config); + if (!pVulkanRenderer->ActivateSurfaceTarget(key, reinterpret_cast(window), config)) { + return false; + } } MGLOG_I("DirectVulkan: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(), window, width, height); diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index 5d5741ef..17a1f553 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -81,8 +81,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 Height = 1; }; UnorderedMap m_suspendedWindowSurfaces; - // The placeholder pbuffer target of a suspended surface, made active. - void ActivateSuspendedPlaceholder(EGLSurface surface, const SuspendedWindowSurface& extent); + // The placeholder pbuffer target of a suspended surface, made active. False: it could not be built. + Bool ActivateSuspendedPlaceholder(EGLSurface surface, const SuspendedWindowSurface& extent); }; // Single-source-of-truth helpers shared with the driver POST diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.h new file mode 100644 index 00000000..ab9ed903 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.h @@ -0,0 +1,37 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// How an offscreen (pbuffer) surface target gets its VkSurfaceKHR on Android, decided from what the +// INSTANCE has, not from what the loader could offer. A renderer created for a window enabled +// VK_KHR_android_surface and nothing headless, so its later offscreen targets (a second client +// surface, or the placeholder a suspended server window draws into) must take the AImageReader +// route; asking that instance for vkCreateHeadlessSurfaceEXT answers null. Pure, so a host case pins +// it without a driver. + +#pragma once +#include + +namespace MobileGL::MG_Backend::DirectVulkan { + + enum class OffscreenSurfaceRoute : Uint8 { + Headless, // vkCreateHeadlessSurfaceEXT + AndroidImageReader, // vkCreateAndroidSurfaceKHR on an AImageReader's window nobody displays + Unavailable, // neither: the target cannot be built (the caller refuses, by name) + }; + + // `headlessEnabled`: the instance was created with VK_EXT_headless_surface. + // `headlessEntryPoint`: vkGetInstanceProcAddr answered vkCreateHeadlessSurfaceEXT. + // `androidSurfaceEnabled`: the instance was created with VK_KHR_android_surface. + constexpr OffscreenSurfaceRoute ChooseOffscreenSurfaceRoute(Bool headlessEnabled, Bool headlessEntryPoint, + Bool androidSurfaceEnabled) { + if (headlessEnabled && headlessEntryPoint) return OffscreenSurfaceRoute::Headless; + if (androidSurfaceEnabled) return OffscreenSurfaceRoute::AndroidImageReader; + return OffscreenSurfaceRoute::Unavailable; + } + +} // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index c236c4bd..400332c6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -8,6 +8,7 @@ #include "VulkanRenderer.h" #include "SubmitFencePrefix.h" +#include "OffscreenSurfaceRoute.h" #include "MG_Backend/DirectVulkan/SubgroupSupportPolicy.h" #include "MG_Backend/DirectGLES/Utils.h" @@ -3267,7 +3268,7 @@ void main() { MagmaPipeValidateSubsystemConfiguration(); #endif CreateInstance(); - CreateSurface(); + if (!CreateSurface()) throw RuntimeError("DirectVulkan: the initial surface could not be created"); PickPhysicalDevice(); CreateLogicalDeviceAndQueues(); CreateAllocator(); @@ -14807,6 +14808,7 @@ void main() { MGLOG_I("%s not available; falling back to an AImageReader %s surface for the pbuffer context.", VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); exts.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); + m_androidSurfaceEnabled = true; } #elif defined VK_USE_PLATFORM_XLIB_KHR // An offscreen surface has ZERO window-system dependence, by design and on @@ -14833,6 +14835,7 @@ void main() { } else { #ifdef VK_USE_PLATFORM_ANDROID_KHR exts.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); + m_androidSurfaceEnabled = true; #elif defined VK_USE_PLATFORM_WIN32_KHR exts.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME); #elif defined VK_USE_PLATFORM_METAL_EXT @@ -16438,42 +16441,66 @@ void main() { MGLOG_I("Command pool created"); } - void VulkanRenderer::CreateSurface() { + Bool VulkanRenderer::CreateSurface() { if (!m_window) { #if defined VK_USE_PLATFORM_METAL_EXT m_window = reinterpret_cast( CreateInternalMetalLayer(m_config.SurfaceWidth, m_config.SurfaceHeight, &m_platformDisplay)); m_platformLibrary = reinterpret_cast(m_window); #elif defined VK_USE_PLATFORM_ANDROID_KHR - if (m_headlessSurfaceSupported) { - auto* createHeadlessSurface = reinterpret_cast( - vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT")); - MOBILEGL_ASSERT(createHeadlessSurface != nullptr, - "VK_EXT_headless_surface is not available for DirectVulkan pbuffer surface"); + // EVERY ENTRY POINT HERE IS OPTIONAL, and each refusal is a `return false`, not + // MOBILEGL_ASSERT: that macro is compiled out at the INFO level every shipping build uses, + // and a renderer created for a WINDOW never enabled the headless extension - asking its + // instance for vkCreateHeadlessSurfaceEXT answers null, and calling that crashed the server + // the first time such a renderer needed an offscreen target. + auto* createHeadlessSurface = + m_headlessSurfaceSupported ? reinterpret_cast( + vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT")) + : nullptr; + const OffscreenSurfaceRoute route = ChooseOffscreenSurfaceRoute( + m_headlessSurfaceSupported, createHeadlessSurface != nullptr, m_androidSurfaceEnabled); + if (route == OffscreenSurfaceRoute::Headless) { VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT}; - VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface), - "vkCreateHeadlessSurfaceEXT failed"); - return; + const VkResult result = createHeadlessSurface(m_instance, &sci, nullptr, &m_surface); + if (result != VK_SUCCESS) { + MGLOG_E("DirectVulkan: vkCreateHeadlessSurfaceEXT failed (%d) for an offscreen target", + static_cast(result)); + m_surface = VK_NULL_HANDLE; + return false; + } + return true; + } + if (route == OffscreenSurfaceRoute::Unavailable) { + MGLOG_E("DirectVulkan: no offscreen surface route on this instance (neither %s nor %s enabled)", + VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); + return false; } // Windowless context on a driver without VK_EXT_headless_surface: give // the WSI an AImageReader's ANativeWindow. It is a real, valid producer // surface that is attached to no display and whose images this code never // acquires, which is exactly the "drawable nobody sees" the Xlib fallback // below builds out of an unmapped window. libmediandk is dlopen'd rather - // than linked so a device without it degrades to the old error instead of + // than linked so a device without it degrades to a refusal instead of // failing to load the library at all. { void* mediaLib = dlopen("libmediandk.so", RTLD_NOW | RTLD_LOCAL); - MOBILEGL_ASSERT(mediaLib != nullptr, - "VK_EXT_headless_surface is unavailable and libmediandk.so could not be loaded " - "for the pbuffer surface fallback"); + if (mediaLib == nullptr) { + MGLOG_E("DirectVulkan: libmediandk.so could not be loaded for an offscreen target"); + return false; + } using AImageReaderNewFn = int (*)(int32_t, int32_t, int32_t, int32_t, void**); using AImageReaderGetWindowFn = int (*)(void*, void**); + using AImageReaderDeleteFn = void (*)(void*); auto* imageReaderNew = reinterpret_cast(dlsym(mediaLib, "AImageReader_new")); auto* imageReaderGetWindow = reinterpret_cast(dlsym(mediaLib, "AImageReader_getWindow")); - MOBILEGL_ASSERT(imageReaderNew != nullptr && imageReaderGetWindow != nullptr, - "libmediandk.so is missing AImageReader_new/AImageReader_getWindow"); + auto* imageReaderDelete = + reinterpret_cast(dlsym(mediaLib, "AImageReader_delete")); + if (imageReaderNew == nullptr || imageReaderGetWindow == nullptr || imageReaderDelete == nullptr) { + MGLOG_E("DirectVulkan: libmediandk.so is missing AImageReader_new/getWindow/delete"); + dlclose(mediaLib); + return false; + } constexpr int32_t kAndroidFormatRgba8888 = 0x1; // AIMAGE_FORMAT_RGBA_8888 const int32_t width = static_cast(std::max(m_config.SurfaceWidth, 1)); @@ -16482,12 +16509,16 @@ void main() { // maxImages must cover the swapchain's images; the reader never // acquires any, so this only sizes its buffer queue. const int status = imageReaderNew(width, height, kAndroidFormatRgba8888, 8, &reader); - MOBILEGL_ASSERT(status == 0 && reader != nullptr, - "AImageReader_new failed (%d) for the pbuffer surface fallback", status); void* nativeWindow = nullptr; - const int windowStatus = imageReaderGetWindow(reader, &nativeWindow); - MOBILEGL_ASSERT(windowStatus == 0 && nativeWindow != nullptr, - "AImageReader_getWindow failed (%d) for the pbuffer surface fallback", windowStatus); + const int windowStatus = + (status == 0 && reader != nullptr) ? imageReaderGetWindow(reader, &nativeWindow) : -1; + if (windowStatus != 0 || nativeWindow == nullptr) { + MGLOG_E("DirectVulkan: no AImageReader window for an offscreen target (new %d, getWindow %d)", + status, windowStatus); + if (reader != nullptr) imageReaderDelete(reader); + dlclose(mediaLib); + return false; + } m_fallbackImageReader = reader; m_platformLibrary = mediaLib; m_window = reinterpret_cast(nativeWindow); @@ -16510,7 +16541,7 @@ void main() { VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT}; VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface), "vkCreateHeadlessSurfaceEXT failed"); - return; + return true; #else auto* createHeadlessSurface = reinterpret_cast( @@ -16526,7 +16557,7 @@ void main() { VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT}; VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface), "vkCreateHeadlessSurfaceEXT failed"); - return; + return true; #endif } #if defined VK_USE_PLATFORM_ANDROID_KHR @@ -16535,7 +16566,12 @@ void main() { VkAndroidSurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR}; sci.window = nativeWindow; - VK_VERIFY(vkCreateAndroidSurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateAndroidSurfaceKHR failed"); + const VkResult androidSurfaceResult = vkCreateAndroidSurfaceKHR(m_instance, &sci, nullptr, &m_surface); + if (androidSurfaceResult != VK_SUCCESS) { + MGLOG_E("DirectVulkan: vkCreateAndroidSurfaceKHR failed (%d)", static_cast(androidSurfaceResult)); + m_surface = VK_NULL_HANDLE; + return false; + } #elif defined VK_USE_PLATFORM_WIN32_KHR auto hwnd = static_cast(m_window); MOBILEGL_ASSERT(hwnd, "HWND is null"); @@ -16606,6 +16642,7 @@ void main() { MGLOG_W("VulkanRenderer::Initialize called on a platform which is not supported yet"); // TODO: support more // platforms #endif + return true; } Vector VulkanRenderer::GetQueueFamilyFromPhysicalDevice(VkPhysicalDevice device) { @@ -16947,9 +16984,9 @@ void main() { InvalidateSetupDrawSnapshots(); } - void VulkanRenderer::ActivateSurfaceTarget(Uint64 key, NativeWindowType window, + Bool VulkanRenderer::ActivateSurfaceTarget(Uint64 key, NativeWindowType window, const VulkanRendererConfig& surfaceConfig) { - if (key == m_activeTargetKey && m_surface != VK_NULL_HANDLE) return; + if (key == m_activeTargetKey && m_surface != VK_NULL_HANDLE) return true; ParkActiveTarget(); auto parked = m_parkedTargets.find(key); if (parked != m_parkedTargets.end()) { @@ -16967,7 +17004,17 @@ void main() { m_swapchainSwapInterval.reset(); m_swapchainResizeRequested = false; m_presentSuspended = false; - CreateSurface(); + if (!CreateSurface()) { + // Nothing of the half-built target stays active: its image reader and library go + // with it, and the renderer has no target until the caller activates another. + SurfaceTarget failed; + ExchangeActiveTarget(failed); + DestroyParkedTarget(failed); + m_activeTargetKey = 0; + MGLOG_E("DirectVulkan: surface target for key %llu could not be built (%s)", + static_cast(key), window != NativeWindowType{} ? "window" : "pbuffer"); + return false; + } CreateSwapchain(); m_defaultFramebufferImageIndex = 0; VK_VERIFY(m_frameContext.InitializeSwapchainSemaphores( @@ -16988,6 +17035,7 @@ void main() { } m_activeTargetKey = key; OnActiveTargetChanged(); + return true; } void VulkanRenderer::DestroyParkedTarget(SurfaceTarget& target) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 71ec63e3..425aa3b0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -453,7 +453,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Activating another key parks the active target - its recorded work submitted, its // pending acquire consumed - and brings the other back, or builds it on first use. void SetActiveSurfaceTargetKey(Uint64 key) { m_activeTargetKey = key; } - void ActivateSurfaceTarget(Uint64 key, NativeWindowType window, const VulkanRendererConfig& surfaceConfig); + // False when a NEW target's surface could not be built (logged); the renderer is then left + // with no active target - every present drops its frame - and the caller refuses by name. + Bool 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 @@ -944,7 +946,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { // a substituted window. (Android is the one exception and has its own path - // no Mali/Adreno driver seen so far exposes the extension, so a windowless // context is given an AImageReader ANativeWindow that is never displayed.) - Bool m_headlessSurfaceSupported = true; + // ENABLED ON THIS INSTANCE, not merely offered: a renderer created for a window never asks + // for the extension, and its later offscreen targets must not call an entry point the + // instance does not have (OffscreenSurfaceRoute.h). Hence false until CreateInstance says so. + Bool m_headlessSurfaceSupported = false; + // VK_KHR_android_surface was enabled on this instance (the AImageReader route needs it). + Bool m_androidSurfaceEnabled = false; // Android has the same shortfall: no Mali/Adreno driver seen so far exposes // VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an // AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever @@ -1963,7 +1970,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkResult SetupDebugReportCallback(); void DestroyDebugReportCallback(); VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo(); - void CreateSurface(); + // False (logged) when the target's surface cannot be built; the renderer then has no surface. + Bool CreateSurface(); void PickPhysicalDevice(); void CreateLogicalDeviceAndQueues(); void CreateAllocator(); diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index fcc480f1..673e1873 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -3733,6 +3734,30 @@ TEST(ServerLoopSuspendTest, ABackendThatCannotSuspendStillLatchesServerWindowLos // Mid-stream: answered within the stream (DrainRing's per-pop check) and the stream goes on. Red with // the suspend arm deleted: the stream is cut at the latch (bits 4|8|16). +// MAGMA'S PLACEHOLDER ON A RENDERER CREATED FOR A WINDOW (the device crash: SIGSEGV at pc 0 in +// VulkanRenderer::CreateSurface on screen-off). The on-screen session's renderer was built for the +// server's window, so its instance enabled the Android surface extension and nothing headless; the +// suspend's offscreen placeholder must take the AImageReader route - the way a windowless renderer's +// pbuffers are built on drivers without the headless extension - and never call a headless entry point +// the instance does not have. With no route at all the target is refused (and the suspend fails, so the +// session latches as the cannot-suspend case below shows) instead of called through a null pointer. +// Red with the route keyed on what the LOADER offers rather than what the instance enabled (the first +// two expectations answer Headless). +TEST(ServerLoopSuspendTest, AnOffscreenTargetNeverCallsAHeadlessEntryPointTheInstanceDoesNotHave) { + using MG_Backend::DirectVulkan::ChooseOffscreenSurfaceRoute; + using MG_Backend::DirectVulkan::OffscreenSurfaceRoute; + // A renderer created for a window: Android surfaces enabled, the headless extension never enabled - + // whether or not the loader would answer its entry point. + EXPECT_EQ(ChooseOffscreenSurfaceRoute(false, false, true), OffscreenSurfaceRoute::AndroidImageReader); + EXPECT_EQ(ChooseOffscreenSurfaceRoute(false, true, true), OffscreenSurfaceRoute::AndroidImageReader); + // Enabled, but the entry point is absent: still never called. + EXPECT_EQ(ChooseOffscreenSurfaceRoute(true, false, true), OffscreenSurfaceRoute::AndroidImageReader); + EXPECT_EQ(ChooseOffscreenSurfaceRoute(true, false, false), OffscreenSurfaceRoute::Unavailable); + EXPECT_EQ(ChooseOffscreenSurfaceRoute(false, false, false), OffscreenSurfaceRoute::Unavailable); + // A windowless renderer on a driver that has the extension. + EXPECT_EQ(ChooseOffscreenSurfaceRoute(true, true, false), OffscreenSurfaceRoute::Headless); +} + 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 "