mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (MG_Backend/DirectVulkan): offscreen surfaces never touch a window system - delete the hidden-Xlib fallback, make surface and device-enumeration failures loud, guard zero devices
This commit is contained in:
@@ -3012,15 +3012,10 @@ void main() {
|
||||
|
||||
#if defined(VK_USE_PLATFORM_XLIB_KHR)
|
||||
if (m_platformDisplay != nullptr) {
|
||||
if (m_ownsFallbackXlibWindow && m_window != 0 && m_platformLibrary != nullptr) {
|
||||
using XDestroyWindowFn = int (*)(Display*, Window);
|
||||
auto* destroyWindow = reinterpret_cast<XDestroyWindowFn>(dlsym(m_platformLibrary, "XDestroyWindow"));
|
||||
if (destroyWindow) {
|
||||
destroyWindow(static_cast<Display*>(m_platformDisplay), static_cast<Window>(m_window));
|
||||
}
|
||||
m_window = 0;
|
||||
m_ownsFallbackXlibWindow = false;
|
||||
}
|
||||
// No fallback window to destroy any more: the display here is only ever
|
||||
// one this renderer opened for a REAL window surface, and that window is
|
||||
// the caller's to own. The hidden-window pbuffer fallback that used to be
|
||||
// cleaned up here is gone (see CreateSurface).
|
||||
using XCloseDisplayFn = int (*)(Display*);
|
||||
auto* closeDisplay = reinterpret_cast<XCloseDisplayFn>(m_platformCloseDisplay);
|
||||
if (closeDisplay) {
|
||||
@@ -10930,17 +10925,24 @@ void main() {
|
||||
exts.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
|
||||
}
|
||||
#elif defined VK_USE_PLATFORM_XLIB_KHR
|
||||
// An offscreen surface has ZERO window-system dependence, by design and on
|
||||
// every machine - including ones that do have a display. There used to be a
|
||||
// fallback here that requested VK_KHR_xlib_surface and had CreateSurface()
|
||||
// open a hidden, never-mapped X window; it is gone. A pbuffer that quietly
|
||||
// needs an X server is a pbuffer that works on a workstation and dies on a
|
||||
// headless runner, which is exactly what it did: with no DISPLAY, XOpenDisplay
|
||||
// returned null and the next Xlib call segfaulted. If the loader genuinely has
|
||||
// no VK_EXT_headless_surface, that is an honest bring-up failure and is
|
||||
// reported as one below - never papered over with a window.
|
||||
m_headlessSurfaceSupported = IsExtensionSupported(m_extensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
if (m_headlessSurfaceSupported) {
|
||||
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
} else {
|
||||
// Real ICDs (e.g. NVIDIA's proprietary Linux driver) may not implement
|
||||
// VK_EXT_headless_surface at all. CreateSurface() falls back to a
|
||||
// hidden Xlib window in that case, so request that extension instead.
|
||||
MGLOG_I("%s not available; falling back to a hidden %s surface for the pbuffer context.",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
|
||||
exts.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
|
||||
if (!m_headlessSurfaceSupported) {
|
||||
MGLOG_F("%s is not available from this Vulkan loader, so an offscreen (pbuffer) DirectVulkan "
|
||||
"surface cannot be created. Refusing to substitute a window: offscreen surfaces must not "
|
||||
"depend on a window system. Install an ICD that implements it (lavapipe does).",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
throw RuntimeError("VK_EXT_headless_surface is unavailable for an offscreen DirectVulkan surface");
|
||||
}
|
||||
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
#else
|
||||
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
#endif
|
||||
@@ -11079,17 +11081,38 @@ void main() {
|
||||
|
||||
void VulkanRenderer::PickPhysicalDevice() {
|
||||
Uint32 deviceCount = 0;
|
||||
vkEnumeratePhysicalDevices(m_instance, &deviceCount, nullptr);
|
||||
VK_VERIFY(vkEnumeratePhysicalDevices(m_instance, &deviceCount, nullptr));
|
||||
if (deviceCount == 0) {
|
||||
MGLOG_E("No physical devices supporting Vulkan found.");
|
||||
} else {
|
||||
MGLOG_I("Found %d physical device(s).", deviceCount);
|
||||
// A real, reachable configuration, not a broken invariant: an instance can be
|
||||
// created from ICDs that load perfectly and then expose no device at all - a
|
||||
// GPU-less machine with the vendor ICDs installed (RADV/ANV/NVK on a CI runner)
|
||||
// is exactly that. It has to be a bring-up failure the caller can report.
|
||||
//
|
||||
// It used to be MGLOG_E + MOBILEGL_ASSERT, and BOTH are compiled out at the INFO
|
||||
// log level every shipping and CI build uses (Log.h orders DEBUG < WARN < ERROR
|
||||
// < INFO), so the count-zero case fell through in silence to `devices[0]` on an
|
||||
// EMPTY vector below and segfaulted in vkGetPhysicalDeviceProperties.
|
||||
MGLOG_F("No Vulkan physical devices found: the instance loaded ICDs but none of them exposes a "
|
||||
"device. Cannot bring up DirectVulkan. (A software ICD such as lavapipe provides one; "
|
||||
"pin it with VK_ICD_FILENAMES if the machine has no GPU.)");
|
||||
throw RuntimeError("No Vulkan physical devices available for DirectVulkan");
|
||||
}
|
||||
|
||||
MOBILEGL_ASSERT(deviceCount > 0, "No physical devices found.");
|
||||
MGLOG_I("Found %d physical device(s).", deviceCount);
|
||||
|
||||
Vector<VkPhysicalDevice> devices(deviceCount);
|
||||
vkEnumeratePhysicalDevices(m_instance, &deviceCount, devices.data());
|
||||
// Same truncation hazard as the instance-extension enumeration: a VK_INCOMPLETE here
|
||||
// leaves the tail of `devices` default-constructed (VK_NULL_HANDLE), and every one of
|
||||
// those is a null handle waiting to be passed to the driver. Take only what was
|
||||
// actually written.
|
||||
const VkResult enumerateResult = vkEnumeratePhysicalDevices(m_instance, &deviceCount, devices.data());
|
||||
if (enumerateResult != VK_SUCCESS && enumerateResult != VK_INCOMPLETE) {
|
||||
VK_VERIFY(enumerateResult, "vkEnumeratePhysicalDevices failed");
|
||||
}
|
||||
devices.resize(deviceCount);
|
||||
if (devices.empty()) {
|
||||
MGLOG_F("vkEnumeratePhysicalDevices reported devices and then wrote none");
|
||||
throw RuntimeError("No Vulkan physical devices available for DirectVulkan");
|
||||
}
|
||||
for (Int i = 0; i < deviceCount; i++) {
|
||||
if (GetMoreCapablePhysicalDevice(devices[i], m_surface, m_physicalDevice, m_physicalDevice))
|
||||
MGLOG_I("Picked physical device %d.", i);
|
||||
@@ -11924,26 +11947,36 @@ void main() {
|
||||
m_window = reinterpret_cast<NativeWindowType>(nativeWindow);
|
||||
}
|
||||
#elif defined VK_USE_PLATFORM_XLIB_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");
|
||||
VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT};
|
||||
VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface),
|
||||
"vkCreateHeadlessSurfaceEXT failed");
|
||||
return;
|
||||
// No fall-through to Xlib: an offscreen surface never touches a window
|
||||
// system. CreateInstance() has already refused the bring-up if the loader
|
||||
// lacks the extension, so reaching here without it is a broken invariant
|
||||
// rather than a platform limitation - report it and fail, do not continue.
|
||||
auto* createHeadlessSurface =
|
||||
reinterpret_cast<PFN_vkCreateHeadlessSurfaceEXT>(
|
||||
vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT"));
|
||||
if (!m_headlessSurfaceSupported || createHeadlessSurface == nullptr) {
|
||||
MGLOG_F("vkCreateHeadlessSurfaceEXT is unavailable (%s reported as %s) while creating an "
|
||||
"offscreen DirectVulkan surface",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME,
|
||||
m_headlessSurfaceSupported ? "supported" : "unsupported");
|
||||
throw RuntimeError("vkCreateHeadlessSurfaceEXT is unavailable for an offscreen DirectVulkan surface");
|
||||
}
|
||||
// VK_EXT_headless_surface unavailable on this ICD: fall through to the
|
||||
// Xlib branch below, which creates a hidden window since m_window is
|
||||
// still null here.
|
||||
VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT};
|
||||
VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface),
|
||||
"vkCreateHeadlessSurfaceEXT failed");
|
||||
return;
|
||||
#else
|
||||
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");
|
||||
if (createHeadlessSurface == nullptr) {
|
||||
// Same class as the Xlib branch above: a null entry point behind
|
||||
// MOBILEGL_ASSERT is a segv on the next line in every INFO-level build.
|
||||
MGLOG_F("vkCreateHeadlessSurfaceEXT is unavailable while creating an offscreen DirectVulkan "
|
||||
"surface (%s missing from this loader)",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
throw RuntimeError("vkCreateHeadlessSurfaceEXT is unavailable for an offscreen DirectVulkan surface");
|
||||
}
|
||||
VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT};
|
||||
VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface),
|
||||
"vkCreateHeadlessSurfaceEXT failed");
|
||||
@@ -11972,59 +12005,52 @@ void main() {
|
||||
sci.pLayer = reinterpret_cast<const void*>(m_window);
|
||||
VK_VERIFY(vkCreateMetalSurfaceEXT(m_instance, &sci, nullptr, &m_surface), "vkCreateMetalSurfaceEXT failed");
|
||||
#elif defined VK_USE_PLATFORM_XLIB_KHR
|
||||
// m_window may legitimately still be null here: the pbuffer/headless-fallback
|
||||
// path below creates its own window when the caller didn't provide one.
|
||||
// Reached only for a REAL on-screen window surface (a windowed desktop app,
|
||||
// retrace in window mode). Presentation to a window legitimately needs a
|
||||
// window system; offscreen requests returned above and never come here, so
|
||||
// there is no longer any path that opens a display on a caller's behalf.
|
||||
//
|
||||
// Every failure below is a real error return, not MOBILEGL_ASSERT: that macro
|
||||
// is compiled out at the INFO log level every shipping and CI build uses, so
|
||||
// asserting here meant a null Display sailed straight into the next Xlib call
|
||||
// and segfaulted - which is exactly how this presented in CI.
|
||||
if (!m_window) {
|
||||
MGLOG_F("CreateSurface: a window surface was requested with no native window");
|
||||
throw RuntimeError("CreateSurface: no native window for the Vulkan Xlib surface");
|
||||
}
|
||||
|
||||
void* x11Lib = dlopen("libX11.so.6", RTLD_LOCAL | RTLD_NOW);
|
||||
if (!x11Lib) {
|
||||
x11Lib = dlopen("libX11.so", RTLD_LOCAL | RTLD_NOW);
|
||||
}
|
||||
MOBILEGL_ASSERT(x11Lib != nullptr, "Failed to open libX11 while creating Vulkan Xlib surface");
|
||||
if (x11Lib == nullptr) {
|
||||
MGLOG_F("Failed to open libX11 (.so.6 and .so) while creating a Vulkan Xlib window surface: %s",
|
||||
dlerror());
|
||||
throw RuntimeError("libX11 is unavailable for the Vulkan Xlib window surface");
|
||||
}
|
||||
using XOpenDisplayFn = Display* (*)(const char*);
|
||||
using XCloseDisplayFn = int (*)(Display*);
|
||||
auto* xOpenDisplay = reinterpret_cast<XOpenDisplayFn>(dlsym(x11Lib, "XOpenDisplay"));
|
||||
auto* xCloseDisplay = reinterpret_cast<XCloseDisplayFn>(dlsym(x11Lib, "XCloseDisplay"));
|
||||
MOBILEGL_ASSERT(xOpenDisplay != nullptr && xCloseDisplay != nullptr,
|
||||
"Failed to resolve XOpenDisplay/XCloseDisplay while creating Vulkan Xlib surface");
|
||||
if (xOpenDisplay == nullptr || xCloseDisplay == nullptr) {
|
||||
MGLOG_F("Failed to resolve XOpenDisplay/XCloseDisplay while creating a Vulkan Xlib window surface");
|
||||
dlclose(x11Lib);
|
||||
throw RuntimeError("libX11 is missing XOpenDisplay/XCloseDisplay");
|
||||
}
|
||||
|
||||
auto* display = xOpenDisplay(std::getenv("DISPLAY"));
|
||||
MOBILEGL_ASSERT(display != nullptr, "XOpenDisplay failed while creating Vulkan Xlib surface");
|
||||
const char* displayName = std::getenv("DISPLAY");
|
||||
auto* display = xOpenDisplay(displayName);
|
||||
if (display == nullptr) {
|
||||
MGLOG_F("XOpenDisplay(%s) failed while creating a Vulkan Xlib window surface; there is no usable X "
|
||||
"display for the requested window surface",
|
||||
displayName != nullptr ? displayName : "<DISPLAY unset>");
|
||||
dlclose(x11Lib);
|
||||
throw RuntimeError("XOpenDisplay failed for the Vulkan Xlib window surface");
|
||||
}
|
||||
m_platformDisplay = display;
|
||||
m_platformLibrary = x11Lib;
|
||||
m_platformCloseDisplay = reinterpret_cast<void*>(xCloseDisplay);
|
||||
|
||||
if (!m_window) {
|
||||
// Pbuffer/headless fallback: the ICD didn't implement
|
||||
// VK_EXT_headless_surface (e.g. NVIDIA's proprietary Linux driver), so
|
||||
// CreateInstance() requested VK_KHR_xlib_surface instead and left
|
||||
// m_window null for us to fill in here. This window is never mapped -
|
||||
// it exists only to give the WSI a valid drawable - so nothing is ever
|
||||
// shown on screen; the swapchain image backs the GL default framebuffer
|
||||
// exactly like the headless-surface path does.
|
||||
using XDefaultRootWindowFn = Window (*)(Display*);
|
||||
using XDefaultScreenFn = int (*)(Display*);
|
||||
using XBlackPixelFn = unsigned long (*)(Display*, int);
|
||||
using XCreateSimpleWindowFn = Window (*)(Display*, Window, int, int, unsigned int, unsigned int,
|
||||
unsigned int, unsigned long, unsigned long);
|
||||
auto* xDefaultRootWindow = reinterpret_cast<XDefaultRootWindowFn>(dlsym(x11Lib, "XDefaultRootWindow"));
|
||||
auto* xDefaultScreen = reinterpret_cast<XDefaultScreenFn>(dlsym(x11Lib, "XDefaultScreen"));
|
||||
auto* xBlackPixel = reinterpret_cast<XBlackPixelFn>(dlsym(x11Lib, "XBlackPixel"));
|
||||
auto* xCreateSimpleWindow =
|
||||
reinterpret_cast<XCreateSimpleWindowFn>(dlsym(x11Lib, "XCreateSimpleWindow"));
|
||||
MOBILEGL_ASSERT(xDefaultRootWindow && xDefaultScreen && xBlackPixel && xCreateSimpleWindow,
|
||||
"Failed to resolve XCreateSimpleWindow dependencies for the Xlib pbuffer fallback");
|
||||
|
||||
const int screen = xDefaultScreen(display);
|
||||
const Uint32 width = std::max<Uint32>(m_config.SurfaceWidth, 1);
|
||||
const Uint32 height = std::max<Uint32>(m_config.SurfaceHeight, 1);
|
||||
const Window fallbackWindow = xCreateSimpleWindow(display, xDefaultRootWindow(display), 0, 0, width,
|
||||
height, 0, xBlackPixel(display, screen),
|
||||
xBlackPixel(display, screen));
|
||||
MOBILEGL_ASSERT(fallbackWindow != 0, "XCreateSimpleWindow failed for the Xlib pbuffer fallback");
|
||||
m_window = static_cast<NativeWindowType>(fallbackWindow);
|
||||
m_ownsFallbackXlibWindow = true;
|
||||
}
|
||||
|
||||
VkXlibSurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR};
|
||||
sci.dpy = display;
|
||||
sci.window = static_cast<Window>(m_window);
|
||||
@@ -12074,10 +12100,40 @@ void main() {
|
||||
}
|
||||
|
||||
Vector<VkExtensionProperties> VulkanRenderer::EnumerateInstanceExtensions() {
|
||||
Uint32 extensionCount = 0;
|
||||
VK_VERIFY(vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr));
|
||||
Vector<VkExtensionProperties> extensions(extensionCount);
|
||||
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
|
||||
// The two-call idiom has a race the spec explicitly allows for: the loader
|
||||
// re-scans ICDs, so the property count can GROW between the sizing call and
|
||||
// the fill call, and the fill then returns VK_INCOMPLETE having written only
|
||||
// as many entries as the caller asked for. The result is a silently TRUNCATED
|
||||
// extension list - and which extensions fall off the end is exactly as stable
|
||||
// as the loader's scan order, i.e. not at all. That is how a headless CI
|
||||
// runner could decide VK_EXT_headless_surface did not exist on one run and
|
||||
// did on the next, sending the pbuffer path into the Xlib fallback with no
|
||||
// X server to open. The sibling EnumerateDeviceExtensions below already
|
||||
// checked its second call; this one dropped the result on the floor.
|
||||
// Loop until a fill call agrees with its own sizing call.
|
||||
Vector<VkExtensionProperties> extensions;
|
||||
for (Uint32 attempt = 0; attempt < 8; ++attempt) {
|
||||
Uint32 extensionCount = 0;
|
||||
VK_VERIFY(vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr));
|
||||
extensions.resize(extensionCount);
|
||||
if (extensionCount == 0) {
|
||||
return extensions;
|
||||
}
|
||||
const VkResult result =
|
||||
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
|
||||
if (result == VK_SUCCESS) {
|
||||
extensions.resize(extensionCount);
|
||||
return extensions;
|
||||
}
|
||||
if (result != VK_INCOMPLETE) {
|
||||
VK_VERIFY(result, "vkEnumerateInstanceExtensionProperties failed");
|
||||
return extensions;
|
||||
}
|
||||
MGLOG_I("vkEnumerateInstanceExtensionProperties returned VK_INCOMPLETE (the loader's list grew "
|
||||
"mid-enumeration); re-enumerating");
|
||||
}
|
||||
MGLOG_F("vkEnumerateInstanceExtensionProperties never settled; the instance extension list may be "
|
||||
"truncated and surface-extension selection is about to be made on incomplete information");
|
||||
return extensions;
|
||||
}
|
||||
|
||||
|
||||
@@ -445,15 +445,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* m_platformDisplay = nullptr;
|
||||
void* m_platformLibrary = nullptr;
|
||||
void* m_platformCloseDisplay = nullptr;
|
||||
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
|
||||
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
|
||||
// enumerated instance extensions; when false, CreateSurface() falls back to a
|
||||
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
|
||||
// Whether the loader exposes VK_EXT_headless_surface, detected once in
|
||||
// CreateInstance() from the enumerated instance extensions. On desktop an
|
||||
// offscreen surface REQUIRES it: false is a clean, loud bring-up failure, never
|
||||
// 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;
|
||||
// Set when CreateSurface() had to create its own Xlib window for the fallback
|
||||
// above (rather than being handed one by the caller), so Shutdown() knows it
|
||||
// owns that window and must destroy it.
|
||||
Bool m_ownsFallbackXlibWindow = 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
|
||||
|
||||
Reference in New Issue
Block a user