[Fix] (DirectVulkan): build offscreen targets of a window-created renderer through AImageReader instead of a null headless entry point

This commit is contained in:
2026-10-02 11:42:14 -04:00
parent 51c37bcbc9
commit 16b07cf4b5
6 changed files with 163 additions and 43 deletions
@@ -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<EGLint>(suspended->second.Width),
static_cast<EGLint>(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<Uint32>(std::max<EGLint>(width, 1));
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(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<Uint32>(state->Width);
config.SurfaceHeight = static_cast<Uint32>(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<Uint32>(extent.Width, 1);
config.SurfaceHeight = std::max<Uint32>(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<Uint32>(width, 1);
config.SurfaceHeight = std::max<Uint32>(height, 1);
config.SwapInterval = GetRequestedSwapInterval();
pVulkanRenderer->ActivateSurfaceTarget(key, reinterpret_cast<NativeWindowType>(window), config);
if (!pVulkanRenderer->ActivateSurfaceTarget(key, reinterpret_cast<NativeWindowType>(window), config)) {
return false;
}
}
MGLOG_I("DirectVulkan: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
window, width, height);
@@ -81,8 +81,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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);
// 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
@@ -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 <Includes.h>
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
@@ -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<NativeWindowType>(
CreateInternalMetalLayer(m_config.SurfaceWidth, m_config.SurfaceHeight, &m_platformDisplay));
m_platformLibrary = reinterpret_cast<void*>(m_window);
#elif defined VK_USE_PLATFORM_ANDROID_KHR
if (m_headlessSurfaceSupported) {
auto* createHeadlessSurface = reinterpret_cast<PFN_vkCreateHeadlessSurfaceEXT>(
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<PFN_vkCreateHeadlessSurfaceEXT>(
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<int>(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<AImageReaderNewFn>(dlsym(mediaLib, "AImageReader_new"));
auto* imageReaderGetWindow =
reinterpret_cast<AImageReaderGetWindowFn>(dlsym(mediaLib, "AImageReader_getWindow"));
MOBILEGL_ASSERT(imageReaderNew != nullptr && imageReaderGetWindow != nullptr,
"libmediandk.so is missing AImageReader_new/AImageReader_getWindow");
auto* imageReaderDelete =
reinterpret_cast<AImageReaderDeleteFn>(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<int32_t>(std::max<Uint32>(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<NativeWindowType>(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<PFN_vkCreateHeadlessSurfaceEXT>(
@@ -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<int>(androidSurfaceResult));
m_surface = VK_NULL_HANDLE;
return false;
}
#elif defined VK_USE_PLATFORM_WIN32_KHR
auto hwnd = static_cast<HWND>(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<VkQueueFamilyProperties> 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<unsigned long long>(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) {
@@ -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();
+25
View File
@@ -38,6 +38,7 @@
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
#include <MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Pipe/MGPipe.h>
@@ -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 "