207 lines
7.2 KiB
C++
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
|