// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include "egl_headless.hpp" #include #include #include #include #include #include namespace plottest { namespace { /// `eglGetPlatformDisplayEXT`, declared locally so the harness does not depend /// on the exact spelling of the EGL_EXT_platform_base typedef. using GetPlatformDisplayEXT = EGLDisplay (*)(EGLenum platform, void* native_display, const EGLint* attrib_list); /// Keeps the text of the last EGL failure alive for the returned Status. thread_local char g_message[512]; /// Records the pending EGL error in `g_message`, echoes it to stderr so a /// failing CTest run shows the driver's own error code, and returns false. bool fail(plot::Status* st, const char* what) noexcept { const EGLint error = eglGetError(); std::snprintf(g_message, sizeof g_message, "%s (EGL error 0x%04X)", what, static_cast(error)); std::fprintf(stderr, "plot_tests: headless EGL: %s\n", g_message); if (st != nullptr) { st->code = plot::ErrorCode::GLFailure; st->message = g_message; } return false; } } // namespace HeadlessContext::HeadlessContext(u32 w, u32 h) noexcept : width(w), height(h) {} HeadlessContext::~HeadlessContext() { destroy(); } bool HeadlessContext::create(plot::Status* st) noexcept { if (framebuffer_.id() != 0) { return true; // already created } if (width == 0 || height == 0) { if (st != nullptr) { st->code = plot::ErrorCode::InvalidArgument; st->message = "headless context size must be non-zero"; } return false; } auto abort = [&](const char* what) { const bool reported = fail(st, what); destroy(); return reported; }; // 1. Prefer the surfaceless platform: no window system at all. Fall back to // the default display (still surfaceless when it can be), and only then // accept a pbuffer surface. const GetPlatformDisplayEXT get_platform_display = reinterpret_cast( eglGetProcAddress("eglGetPlatformDisplayEXT")); EGLDisplay display = EGL_NO_DISPLAY; if (get_platform_display != nullptr) { display = get_platform_display(EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, nullptr); } if (display == EGL_NO_DISPLAY) { display = eglGetDisplay(EGL_DEFAULT_DISPLAY); } if (display == EGL_NO_DISPLAY) { return abort("eglGetPlatformDisplayEXT(EGL_PLATFORM_SURFACELESS_MESA) and eglGetDisplay failed"); } display_ = display; // 2. Initialise the display for the desktop GL API. EGLint egl_major = 0; EGLint egl_minor = 0; if (eglInitialize(display, &egl_major, &egl_minor) != EGL_TRUE) { return abort("eglInitialize failed"); } if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) { return abort("eglBindAPI(EGL_OPENGL_API) failed"); } // 3. An RGBA8 config that can back an OpenGL context. const EGLint config_attribs[] = { EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 0, EGL_NONE, }; EGLConfig config = nullptr; EGLint config_count = 0; if (eglChooseConfig(display, config_attribs, &config, 1, &config_count) != EGL_TRUE || config_count == 0) { return abort("no EGL config offers 8-bit RGBA with EGL_OPENGL_BIT"); } // 4. OpenGL 3.3 core, the baseline the library targets. const EGLint context_attribs[] = { EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR, EGL_NONE, }; const EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attribs); if (context == EGL_NO_CONTEXT) { return abort("eglCreateContext(OpenGL 3.3 core) failed"); } context_ = context; // 5. Surfaceless current context first; a 1x1 pbuffer only if the driver // lacks EGL_KHR_surfaceless_context. PLOT_HEADLESS_FORCE_PBUFFER=1 skips // straight to the fallback, so that path can be verified somewhere the // surfaceless path would otherwise always win. const bool force_pbuffer = std::getenv("PLOT_HEADLESS_FORCE_PBUFFER") != nullptr; if (!force_pbuffer && eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context) == EGL_TRUE) { egl_path = "egl surfaceless"; } else { const EGLint pbuffer_attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; const EGLSurface surface = eglCreatePbufferSurface(display, config, pbuffer_attribs); if (surface == EGL_NO_SURFACE || eglMakeCurrent(display, surface, surface, context) != EGL_TRUE) { return abort("neither a surfaceless context nor a 1x1 pbuffer surface could be made current"); } surface_ = surface; egl_path = "egl pbuffer"; } // 6. Resolve the entry points against the context that is now current. if (!plot::gl::load(reinterpret_cast(eglGetProcAddress))) { if (st != nullptr) { st->code = plot::ErrorCode::GLFailure; st->message = "plot::gl::load(eglGetProcAddress) could not resolve every entry point"; } std::fprintf(stderr, "plot_tests: headless EGL: %s\n", st != nullptr ? st->message : ""); destroy(); return false; } // 7. The off-screen target: an RGBA8 texture on colour attachment 0. plot::Status gl_status; if (!color_.create_rgba(width, height, &gl_status) || !framebuffer_.create(&gl_status)) { if (st != nullptr) { *st = gl_status; } std::fprintf(stderr, "plot_tests: headless EGL: %s\n", gl_status.message != nullptr ? gl_status.message : "framebuffer setup failed"); destroy(); return false; } framebuffer_.attach_color(color_); if (!framebuffer_.complete()) { std::fprintf(stderr, "plot_tests: headless EGL: framebuffer is not complete\n"); if (st != nullptr) { st->code = plot::ErrorCode::GLFailure; st->message = "framebuffer is not complete"; } destroy(); return false; } return true; } void HeadlessContext::destroy() noexcept { // GL names belong to the context that created them, so drop them while it is // still current. framebuffer_.reset(); color_.reset(); if (display_ != nullptr) { const EGLDisplay display = static_cast(display_); eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (context_ != nullptr) { eglDestroyContext(display, static_cast(context_)); } if (surface_ != nullptr) { eglDestroySurface(display, static_cast(surface_)); } eglTerminate(display); } display_ = nullptr; context_ = nullptr; surface_ = nullptr; egl_path = ""; } bool HeadlessContext::read_pixel(u32 x, u32 y, std::uint8_t rgba[4]) const noexcept { if (framebuffer_.id() == 0 || x >= width || y >= height) { return false; } framebuffer_.bind(); plot::gl::glReadPixels(static_cast(x), static_cast(y), 1, 1, plot::gl::GL_RGBA, plot::gl::GL_UNSIGNED_BYTE, rgba); return plot::gl::glGetError() == plot::gl::GL_NO_ERROR; } } // namespace plottest