Files
plotter/tests/egl_headless.cpp
T

207 lines
7.2 KiB
C++

#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