From 6125f941d15ddca2c1886665097ea27449696a30 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 2 Oct 2026 23:09:59 -0400 Subject: [PATCH] [Fix] (EGL): accept surfaceless make-current and config-less contexts so eglinfo prints the GL renderer lines --- MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp | 56 +++- .../MG_Impl/EGLImpl/EGLPlatformExtensions.h | 6 +- MobileGL/MG_State/EGLState/Core.cpp | 14 +- MobileGL/MG_Test/EGLState/EGLStateTest.cpp | 45 +++ MobileGL/MG_Test/Wire/CMakeLists.txt | 7 +- MobileGL/MG_Test/Wire/EglInfoSequenceTest.cpp | 276 ++++++++++++++++++ 6 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 MobileGL/MG_Test/Wire/EglInfoSequenceTest.cpp diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index c7dee0a1..f52ad2c1 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -32,6 +32,7 @@ #include #endif #include +#include #include namespace MobileGL::MG_Impl::EGLImpl { @@ -216,6 +217,38 @@ namespace MobileGL::MG_Impl::EGLImpl { return *surfaces; } + // EGL_KHR_surfaceless_context. The EGL state records a surfaceless binding as exactly that + // (eglGetCurrentSurface answers EGL_NO_SURFACE), but every backend renders into the surface + // it was bound to, and a session's capabilities are published on its first bind to one. So + // the backend is bound to a 1x1 pbuffer of the display's own instead: one per display, made + // on the first surfaceless bind, never handed to the application, and gone with the display + // (eglTerminate). Guarded by EGLOperationMutex. + std::unordered_map& SurfacelessStandIns() { + static auto* standIns = new std::unordered_map(); + return *standIns; + } + + // The display's stand-in, made now if it has none (or the one it had did not survive). + // EGL_NO_SURFACE when the backend could not make a pbuffer; the EGL error is then set. + EGLSurface SurfacelessStandIn(EGLStateContext* state, EGLDisplay dpy) { + auto& standIns = SurfacelessStandIns(); + if (const auto it = standIns.find(dpy); it != standIns.end()) { + if (state->ValidateSurfaceOnDisplay(dpy, it->second)) return it->second; + standIns.erase(it); + } + // Any of the display's configs will do: a stand-in is never read back or presented. + EGLConfig config = nullptr; + EGLint count = 0; + if (!state->GetConfigs(dpy, &config, 1, &count) || count < 1 || config == nullptr) { + state->SetError(EGL_BAD_ALLOC); + return EGL_NO_SURFACE; + } + const EGLint attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + const EGLSurface standIn = CreatePbufferSurface(dpy, config, attribs); + if (standIn != EGL_NO_SURFACE) standIns[dpy] = standIn; + return standIn; + } + #if MOBILEGL_WAYLAND_WINDOWS // Every Wayland window surface's presentation, by surface. Guarded by EGLOperationMutex, // which every entry point that reaches it holds. @@ -619,7 +652,22 @@ namespace MobileGL::MG_Impl::EGLImpl { state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext); return EGL_FALSE; } - if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) { + // A surfaceless binding (EGL_KHR_surfaceless_context; EGLState took it above and records + // EGL_NO_SURFACE): the backend gets the display's stand-in pbuffer to render into. + EGLSurface backendDraw = draw; + EGLSurface backendRead = read; + if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE) { + backendDraw = backendRead = SurfacelessStandIn(state, dpy); + if (backendDraw == EGL_NO_SURFACE) { + const EGLint error = state->ConsumeError(); + MGLOG_E_ONCE("eglMakeCurrent: no stand-in pbuffer for a surfaceless binding thread=%s (error=0x%04x)", + threadId.c_str(), error); + state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext); + state->SetError(error != EGL_SUCCESS ? error : EGL_BAD_ALLOC); + return EGL_FALSE; + } + } + if (!backendObject->MakeEGLCurrent(dpy, backendDraw, backendRead, ctx)) { MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(), dpy, draw, read, ctx); state->SetError(EGL_BAD_ACCESS); @@ -699,6 +747,12 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state->TerminateDisplay(dpy)) { return EGL_FALSE; } + { + // The display's surfaces went with it, its surfaceless stand-in among them; the backend + // side goes with ReleaseEGLResources below. + const std::lock_guard operationLock(EGLOperationMutex()); + SurfacelessStandIns().erase(dpy); + } if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) { // The display's dma-buf EGLImages went with it; so do their shared-image references. for (const Uint64 sharedImageId : state->TakeOrphanedSharedImages()) { diff --git a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h index b60fb8f7..87a2501a 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h @@ -19,14 +19,16 @@ namespace MobileGL::MG_Impl::EGLImpl { "EGL_EXT_client_extensions EGL_EXT_platform_base EGL_KHR_platform_base " "EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS; inline constexpr const char* kDisplayExtensionString = - "EGL_KHR_create_context EGL_EXT_platform_base EGL_KHR_platform_base " + "EGL_KHR_create_context EGL_KHR_surfaceless_context EGL_KHR_no_config_context EGL_EXT_platform_base " + "EGL_KHR_platform_base " "EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS; // The display list again, with the dma-buf import a backend with shared images has (a split // client: the server allocates the images and names the descriptors that come back). A // monolith has no allocator to recognise a dma-buf with, so it never advertises these. The // same server answers the buffer age of its surfaces and takes swap damage to its presents. inline constexpr const char* kDisplayExtensionStringWithSharedImages = - "EGL_KHR_create_context EGL_EXT_platform_base EGL_KHR_platform_base " + "EGL_KHR_create_context EGL_KHR_surfaceless_context EGL_KHR_no_config_context EGL_EXT_platform_base " + "EGL_KHR_platform_base " "EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS " EGL_KHR_image_base EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers" " EGL_EXT_buffer_age EGL_KHR_partial_update EGL_KHR_swap_buffers_with_damage" diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index c4400f81..ed67f931 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -659,7 +659,10 @@ namespace MobileGL { SetError(EGL_NOT_INITIALIZED); return nullptr; } - if (!ValidateConfigOnDisplay(display, config)) { + // EGL_KHR_no_config_context: EGL_NO_CONFIG_KHR is a context with no config, which + // every surface is compatible with. eglinfo's ES probe relies on it: it chooses with + // EGL_OPENGL_ES_BIT (ES 1.x), which no config here claims, and passes the NULL on. + if (config != EGL_NO_CONFIG_KHR && !ValidateConfigOnDisplay(display, config)) { SetError(EGL_BAD_CONFIG); return nullptr; } @@ -802,6 +805,11 @@ namespace MobileGL { *value = contextObject->EGLContextFlags; return true; case EGL_CONFIG_ID: { + // EGL_KHR_no_config_context: a context created without a config reports 0. + if (contextObject->Config == EGL_NO_CONFIG_KHR) { + *value = 0; + return true; + } const auto* cfg = TryGetConfig(contextObject->Config); if (!cfg) { const_cast(this)->SetError(EGL_BAD_CONFIG); @@ -1327,7 +1335,9 @@ namespace MobileGL { SetError(EGL_BAD_CONTEXT); return false; } - if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE) { + // EGL_KHR_surfaceless_context: with BOTH surfaces absent the context is bound + // surfacelessly (no default framebuffer). One without the other is still a mismatch. + if ((draw == EGL_NO_SURFACE) != (read == EGL_NO_SURFACE)) { SetError(EGL_BAD_MATCH); return false; } diff --git a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp index 7961c782..9d236ac9 100644 --- a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp +++ b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp @@ -135,6 +135,51 @@ TEST(EGLStateProfile, CompatibilityProfileRequiresExplicitCompatBit) { EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS); } +// What `eglinfo -B` does for every API: eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx) +// (EGL_KHR_surfaceless_context). It used to be EGL_BAD_MATCH, so eglinfo printed the EGL lines and +// no GL renderer/version lines at all. The binding records no surface: a client asking for the +// current surface must see what it bound. +TEST(EGLStateMakeCurrent, SurfacelessBindIsAcceptedAndRecordsNoSurface) { + auto fixture = CreateFixture(); + EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, EGL_NO_SURFACE, EGL_NO_SURFACE, fixture->Context)); + EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS); + EXPECT_EQ(fixture->State.GetCurrentContext(), fixture->Context); + EXPECT_EQ(fixture->State.GetCurrentSurface(EGL_DRAW), EGL_NO_SURFACE); + EXPECT_EQ(fixture->State.GetCurrentSurface(EGL_READ), EGL_NO_SURFACE); + EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); +} + +// The extension relaxes only the both-absent case: one surface without the other stays a mismatch. +TEST(EGLStateMakeCurrent, OneSidedSurfaceIsStillAMismatch) { + auto fixture = CreateFixture(); + EXPECT_FALSE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, EGL_NO_SURFACE, fixture->Context)); + EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_MATCH); + EXPECT_FALSE(fixture->State.MakeCurrent(fixture->Display, EGL_NO_SURFACE, fixture->Surface, fixture->Context)); + EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_MATCH); +} + +// eglinfo's ES probe chooses with EGL_RENDERABLE_TYPE/EGL_CONFORMANT = EGL_OPENGL_ES_BIT (ES 1.x), +// which no MobileGL config claims, so it passes the NULL config on to eglCreateContext - legal +// with EGL_KHR_no_config_context, where it means "no config". Without it that was +// EGL_BAD_CONFIG and the "OpenGL ES profile" lines never printed. +TEST(EGLStateCreateContext, NoConfigContextIsAcceptedAndReportsConfigIdZero) { + auto fixture = CreateFixture(); + const EGLint esAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE}; + const auto context = fixture->State.CreateContext(fixture->Display, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, esAttribs); + ASSERT_NE(context, EGL_NO_CONTEXT); + EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS); + EGLint configId = -1; + EXPECT_TRUE(fixture->State.QueryContext(fixture->Display, context, EGL_CONFIG_ID, &configId)); + EXPECT_EQ(configId, 0); + EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, EGL_NO_SURFACE, EGL_NO_SURFACE, context)); + EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); + // A non-null handle that names no config is still EGL_BAD_CONFIG. + EXPECT_EQ(fixture->State.CreateContext(fixture->Display, reinterpret_cast(0xdead), EGL_NO_CONTEXT, + esAttribs), + EGL_NO_CONTEXT); + EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_CONFIG); +} + namespace { constexpr EGLAttrib kAbgr8888 = StateContext::kDrmFourccAbgr8888; constexpr EGLAttrib kXbgr8888 = StateContext::kDrmFourccXbgr8888; diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index ce8d6c6f..b2bb5bcb 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -67,6 +67,9 @@ if (NOT WIN32) # MOBILEGL_IPC_SURFACE=server against a display-less `--serve` supervisor. Both drive real peers # and (the second, and D8's case) the real server image, as PeerLatchTest does. list(APPEND MOBILEGL_WIRE_TESTS InProcessServerTest ServerOwnedSurfaceTest) + # `eglinfo -B`'s per-API sequence (surfaceless make-current, the ES probe's NULL config) through + # a real MobileGL client against a real `--serve` supervisor, as ServerOwnedSurfaceTest does. + list(APPEND MOBILEGL_WIRE_TESTS EglInfoSequenceTest) # P14 S2 (docs/Disaggregated/design/11-state-ownership.md): several sessions in ONE process. # The in-process unix supervisor serves each session on its own control thread with its own # ServerSession/ServerLoop/PipeInputs block/latch domain/segment resolver; two SHM clients are @@ -99,6 +102,7 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) ${MGL_ROOT}/3rdparty/flatbuffers/include ) if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR + test STREQUAL "EglInfoSequenceTest" OR test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest" OR test STREQUAL "ServerProbeTest") # It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State # the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy @@ -128,7 +132,8 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) endif () if (test STREQUAL "ServerSpawnTest" OR test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR - test STREQUAL "ServerOwnedSurfaceTest" OR test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "ServerProbeTest") + test STREQUAL "ServerOwnedSurfaceTest" OR test STREQUAL "EglInfoSequenceTest" OR test STREQUAL "AdoptTierStreamTest" OR + test STREQUAL "ServerProbeTest") # It execs the real image, so it must be built first and must be told # where it is. MOBILEGL_TEST_SERVER_PATH is read by the test's # ServerImage() helper; without it the test would depend on the working diff --git a/MobileGL/MG_Test/Wire/EglInfoSequenceTest.cpp b/MobileGL/MG_Test/Wire/EglInfoSequenceTest.cpp new file mode 100644 index 00000000..b9c30012 --- /dev/null +++ b/MobileGL/MG_Test/Wire/EglInfoSequenceTest.cpp @@ -0,0 +1,276 @@ +// MobileGL - MobileGL/MG_Test/Wire/EglInfoSequenceTest.cpp +// 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 + +// `eglinfo -B` AGAINST MOBILEGL, END TO END ON THE HOST: a real MobileGL client (the EGL entry +// points, the spawn/tcp session) against a real `--serve` supervisor, driven through the exact call +// sequence the distribution's eglinfo runs per display: +// +// eglInitialize; EGL_CLIENT_APIS must name "OpenGL" / "OpenGL_ES"; EGL_KHR_create_context decides +// the attribute form. Then, per API: +// eglBindAPI(api) +// eglChooseConfig({EGL_CONFORMANT, bit, EGL_RED/GREEN/BLUE/ALPHA_SIZE 1, EGL_RENDERABLE_TYPE, +// bit}) and take configs[0], or NULL when nothing matched +// desktop: eglCreateContext({MAJOR, MINOR, PROFILE_MASK}) from 4.6 down (core stops at 3.1) +// ES: eglCreateContext({EGL_CONTEXT_MAJOR_VERSION, n}) for n = 3, 2, 1 +// eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx) - surfaceless +// glGetString(GL_VENDOR / GL_RENDERER / GL_VERSION) through eglGetProcAddress +// +// On the device eglinfo printed MobileGL's EGL lines for every platform and no GL lines at all. Two +// refusals, both red here before the fix: +// * the surfaceless eglMakeCurrent was EGL_BAD_MATCH, so no desktop context ever became current; +// * the ES probe's bit is EGL_OPENGL_ES_BIT (ES 1.x), which no MobileGL config claims, so the +// config is NULL and eglCreateContext answered EGL_BAD_CONFIG. + +#include "P12ServerRig.h" + +#include + +using namespace MobileGL; +using namespace P12; + +namespace { + + namespace EGL = MobileGL::MG_Impl::EGLImpl; + + constexpr unsigned kGlVendor = 0x1F00; + constexpr unsigned kGlRenderer = 0x1F01; + constexpr unsigned kGlVersion = 0x1F02; + + // One "OpenGL ... profile" block of eglinfo's output, or why it is missing. + struct ProfileReport { + Int32 configFound = 0; // eglChooseConfig matched something + Int32 created = 0; // some eglCreateContext in the version walk returned a context + Int32 madeCurrent = 0; // ...and the surfaceless eglMakeCurrent took it + Int32 lastError = 0; // eglGetError after the last failed create / make-current + Int32 drawIsNone = 0; // eglGetCurrentSurface(EGL_DRAW) == EGL_NO_SURFACE while current + Int32 major = 0; + Int32 minor = 0; + char vendor[96] = {}; + char renderer[96] = {}; + char version[96] = {}; + }; + + struct EglInfoReport { + Int32 initialized = 0; + Int32 eglMajor = 0; + Int32 eglMinor = 0; + Int32 hasOpenGL = 0; + Int32 hasOpenGLES = 0; + Int32 khrCreateContext = 0; + Int32 surfacelessExtensions = 0; // EGL_KHR_surfaceless_context and EGL_KHR_no_config_context advertised + ProfileReport core{}; + ProfileReport compat{}; + ProfileReport es{}; + char note[128] = {}; + }; + + // eglinfo's extension_supported(): a whole space-separated token. + bool HasToken(const char* list, const char* token) { + if (list == nullptr) return false; + const std::string all = std::string(" ") + list + " "; + return all.find(std::string(" ") + token + " ") != std::string::npos; + } + + EGLConfig ChooseLikeEglInfo(EGLDisplay dpy, EGLint apiBit, ProfileReport& out) { + const EGLint attribs[] = {EGL_CONFORMANT, apiBit, EGL_RED_SIZE, 1, EGL_GREEN_SIZE, 1, EGL_BLUE_SIZE, 1, + EGL_ALPHA_SIZE, 1, EGL_RENDERABLE_TYPE, apiBit, EGL_NONE}; + EGLConfig configs[64] = {}; + EGLint count = 0; + (void)EGL::ChooseConfig(dpy, attribs, configs, 64, &count); + out.configFound = count > 0 ? 1 : 0; + return count > 0 ? configs[0] : nullptr; + } + + // eglinfo's doOneContext, minus the printing. + void ReadStrings(ProfileReport& out) { + using GetStringFn = const unsigned char* (*)(unsigned); + const auto getString = reinterpret_cast(EGL::GetProcAddress("glGetString")); + if (getString == nullptr) return; + const auto copy = [&](unsigned name, char* into, size_t size) { + const unsigned char* text = getString(name); + std::snprintf(into, size, "%s", text != nullptr ? reinterpret_cast(text) : ""); + }; + copy(kGlVendor, out.vendor, sizeof(out.vendor)); + copy(kGlRenderer, out.renderer, sizeof(out.renderer)); + copy(kGlVersion, out.version, sizeof(out.version)); + } + + // Returns the context made current, or EGL_NO_CONTEXT - eglinfo's createEGLContext. + EGLContext TryCurrent(EGLDisplay dpy, EGLConfig config, const EGLint* attribs, ProfileReport& out) { + const EGLContext ctx = EGL::CreateContext(dpy, config, EGL_NO_CONTEXT, attribs); + if (ctx == EGL_NO_CONTEXT) { + out.lastError = EGL::GetError(); + return EGL_NO_CONTEXT; + } + out.created = 1; + if (EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx) != EGL_TRUE) { + out.lastError = EGL::GetError(); + (void)EGL::DestroyContext(dpy, ctx); + return EGL_NO_CONTEXT; + } + out.madeCurrent = 1; + out.drawIsNone = EGL::GetCurrentSurface(EGL_DRAW) == EGL_NO_SURFACE ? 1 : 0; + return ctx; + } + + void Release(EGLDisplay dpy, EGLContext ctx, ProfileReport& out) { + ReadStrings(out); + (void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + (void)EGL::DestroyContext(dpy, ctx); + } + + void DesktopProfile(EGLDisplay dpy, EGLConfig config, bool core, bool khrCreateContext, ProfileReport& out) { + static const int kVersions[][2] = {{4, 6}, {4, 5}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0}, {3, 3}, + {3, 2}, {3, 1}, {3, 0}, {2, 1}, {2, 0}, {1, 5}, {1, 4}, {1, 3}, + {1, 2}, {1, 1}, {1, 0}}; + for (const auto& v : kVersions) { + if (core && v[0] == 3 && v[1] == 0) return; + const EGLint withProfile[] = {EGL_CONTEXT_MAJOR_VERSION, v[0], EGL_CONTEXT_MINOR_VERSION, v[1], + EGL_CONTEXT_OPENGL_PROFILE_MASK, + core ? EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT + : EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT, + EGL_NONE}; + const EGLint old[] = {EGL_CONTEXT_CLIENT_VERSION, v[0], EGL_NONE}; + const EGLContext ctx = TryCurrent(dpy, config, khrCreateContext ? withProfile : old, out); + if (ctx != EGL_NO_CONTEXT) { + out.major = v[0]; + out.minor = v[1]; + Release(dpy, ctx, out); + return; + } + } + } + + void EsProfile(EGLDisplay dpy, EGLConfig config, ProfileReport& out) { + for (int major = 3; major > 0; --major) { + const EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, major, EGL_NONE}; + const EGLContext ctx = TryCurrent(dpy, config, attribs, out); + if (ctx != EGL_NO_CONTEXT) { + out.major = major; + Release(dpy, ctx, out); + return; + } + } + } + + EglInfoReport RunEglInfoPeer(const ServerProcess& server, const std::string& clientLogBase) { + EglInfoReport report{}; + int fds[2] = {-1, -1}; + if (::pipe(fds) != 0) return report; + std::fflush(nullptr); + const pid_t pid = ::fork(); + if (pid == 0) { + ::close(fds[0]); + ::setenv("MOBILEGL_LOG_FILE_PATH", clientLogBase.c_str(), 1); + ::setenv("MOBILEGL_TRANSPORT", "spawn", 1); + ::setenv("MOBILEGL_IPC_CONTROL", server.endpoint.c_str(), 1); + ::setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1); + ::unsetenv("MOBILEGL_IPC_SURFACE"); + EglInfoReport r{}; + // eglinfo walks every platform; surfaceless is the one a display-less container has. + const EGLDisplay dpy = EGL::GetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, nullptr); + EGLint major = 0; + EGLint minor = 0; + if (dpy == EGL_NO_DISPLAY || EGL::Initialize(dpy, &major, &minor) != EGL_TRUE) { + std::snprintf(r.note, sizeof(r.note), "eglInitialize failed (no spawn session?)"); + } else { + r.initialized = 1; + r.eglMajor = major; + r.eglMinor = minor; + const char* apis = EGL::QueryString(dpy, EGL_CLIENT_APIS); + const char* exts = EGL::QueryString(dpy, EGL_EXTENSIONS); + r.hasOpenGL = HasToken(apis, "OpenGL") ? 1 : 0; + r.hasOpenGLES = HasToken(apis, "OpenGL_ES") ? 1 : 0; + const bool khr = major == 1 && minor >= 4 && HasToken(exts, "EGL_KHR_create_context"); + r.khrCreateContext = khr ? 1 : 0; + r.surfacelessExtensions = + HasToken(exts, "EGL_KHR_surfaceless_context") && HasToken(exts, "EGL_KHR_no_config_context") ? 1 : 0; + if (r.hasOpenGL && EGL::BindAPI(EGL_OPENGL_API) == EGL_TRUE) { + const EGLConfig config = ChooseLikeEglInfo(dpy, EGL_OPENGL_BIT, r.core); + r.compat.configFound = r.core.configFound; + if (khr) DesktopProfile(dpy, config, /*core=*/true, khr, r.core); + DesktopProfile(dpy, config, /*core=*/false, khr, r.compat); + } + if (r.hasOpenGLES && EGL::BindAPI(EGL_OPENGL_ES_API) == EGL_TRUE) { + const EGLConfig config = ChooseLikeEglInfo(dpy, EGL_OPENGL_ES_BIT, r.es); + EsProfile(dpy, config, r.es); + } + (void)EGL::Terminate(dpy); + } + const ssize_t wrote = ::write(fds[1], &r, sizeof(r)); + (void)wrote; + std::fflush(nullptr); + ::_exit(0); + } + ::close(fds[1]); + if (pid < 0) { + ::close(fds[0]); + return report; + } + pollfd pfd{fds[0], POLLIN, 0}; + if (::poll(&pfd, 1, 90000) > 0) { + if (::read(fds[0], &report, sizeof(report)) != static_cast(sizeof(report))) { + report = EglInfoReport{}; + std::snprintf(report.note, sizeof(report.note), "peer died before reporting"); + } + } else { + std::snprintf(report.note, sizeof(report.note), "peer timed out"); + ::kill(pid, SIGKILL); + } + ::close(fds[0]); + int status = 0; + ::waitpid(pid, &status, 0); + return report; + } + + std::string Describe(const char* label, const ProfileReport& p) { + char text[512]; + std::snprintf(text, sizeof(text), + "%s: config=%d created=%d current=%d eglError=0x%04x version=%d.%d vendor='%s' renderer='%s' " + "GL_VERSION='%s'", + label, p.configFound, p.created, p.madeCurrent, p.lastError, p.major, p.minor, p.vendor, + p.renderer, p.version); + return text; + } + + void ExpectProfilePrinted(const char* label, const ProfileReport& p) { + EXPECT_EQ(p.created, 1) << Describe(label, p); + EXPECT_EQ(p.madeCurrent, 1) << "the surfaceless eglMakeCurrent was refused - " << Describe(label, p); + EXPECT_EQ(p.drawIsNone, 1) << "a surfaceless binding reported a current draw surface - " << Describe(label, p); + EXPECT_NE(p.renderer[0], '\0') << "no renderer line - " << Describe(label, p); + EXPECT_NE(p.version[0], '\0') << "no version line - " << Describe(label, p); + } + +} // namespace + +TEST(EglInfoSequence, EveryProfilePrintsItsRendererAndVersion) { + ServerProcess server; + ASSERT_TRUE(LaunchSupervisor("eglinfo", &server)) << server.Log(); + const std::string clientLog = "/tmp/mgl-eglinfo-client-" + std::to_string(::getpid()) + ".log"; + Debug::TruncateRoleLogs(clientLog.c_str()); + const EglInfoReport r = RunEglInfoPeer(server, clientLog); + ASSERT_EQ(r.initialized, 1) << r.note; + // eglinfo only takes the KHR_create_context attribute form on EGL >= 1.4 with the extension. + EXPECT_EQ(r.khrCreateContext, 1) << "EGL " << r.eglMajor << "." << r.eglMinor; + ASSERT_EQ(r.hasOpenGL, 1); + ASSERT_EQ(r.hasOpenGLES, 1); + EXPECT_EQ(r.surfacelessExtensions, 1) << "the display does not advertise what it now accepts"; + std::printf("[ eglinfo ] %s\n[ eglinfo ] %s\n[ eglinfo ] %s\n", Describe("core", r.core).c_str(), + Describe("compat", r.compat).c_str(), Describe("es", r.es).c_str()); + if (r.core.madeCurrent == 1 && r.core.renderer[0] == '\0') + GTEST_SKIP() << "the surfaceless binding was accepted; the strings need headless EGL on the server"; + ExpectProfilePrinted("OpenGL core profile", r.core); + ExpectProfilePrinted("OpenGL compatibility profile", r.compat); + ExpectProfilePrinted("OpenGL ES profile", r.es); + std::error_code ec; + if (!::testing::Test::HasFailure()) { + std::filesystem::remove(Debug::RoleLogPath(clientLog.c_str(), Debug::LogRole::Client), ec); + std::filesystem::remove(Debug::RoleLogPath(clientLog.c_str(), Debug::LogRole::Server), ec); + std::filesystem::remove(clientLog, ec); + } +}