[Fix] (EGL): accept surfaceless make-current and config-less contexts so eglinfo prints the GL renderer lines

This commit is contained in:
2026-10-02 23:09:59 -04:00
parent bfa7987ca6
commit 6125f941d1
6 changed files with 398 additions and 6 deletions
+55 -1
View File
@@ -32,6 +32,7 @@
#include <dlfcn.h>
#endif
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
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<EGLDisplay, EGLSurface>& SurfacelessStandIns() {
static auto* standIns = new std::unordered_map<EGLDisplay, EGLSurface>();
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<std::recursive_mutex> 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()) {
@@ -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"
+12 -2
View File
@@ -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<EGLContext*>(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;
}
@@ -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<EGLConfig>(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;
+6 -1
View File
@@ -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
@@ -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 <MG_Impl/EGLImpl/EGLImpl.h>
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<GetStringFn>(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<const char*>(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<ssize_t>(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);
}
}