Files
plotter/tests/egl_headless.cpp
Martino Ferrari ed3a8ce645 licence: MIT LICENSE and SPDX identifiers in every first-party file
The repository had no licence at all, so every source was all-rights-reserved
by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino
Ferrari) and a two-line SPDX header to each of the 113 first-party sources --
48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the
grant travels with each file rather than relying on the root document alone.

Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely
vendored code is untouched: third_party/stb_truetype.h carries no header of
ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py
emit() now writes the two lines into its header comment and the file was
regenerated -- gen_atlas.py --check passes and the regeneration adds only those
comment lines to the previously committed file.

README's provenance section now states MIT and links LICENSE.

No code, test or public API change.
2026-09-28 22:13:40 +02:00

210 lines
7.3 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "egl_headless.hpp"
#include <plot/gl/api.hpp>
#include <plot/gl/loader.hpp>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <cstdio>
#include <cstdlib>
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<unsigned>(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<GetPlatformDisplayEXT>(
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<plot::gl::ProcLoader>(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<EGLDisplay>(display_);
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (context_ != nullptr) {
eglDestroyContext(display, static_cast<EGLContext>(context_));
}
if (surface_ != nullptr) {
eglDestroySurface(display, static_cast<EGLSurface>(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<plot::gl::GLint>(x), static_cast<plot::gl::GLint>(y), 1, 1,
plot::gl::GL_RGBA, plot::gl::GL_UNSIGNED_BYTE, rgba);
return plot::gl::glGetError() == plot::gl::GL_NO_ERROR;
}
} // namespace plottest