feat(gl): dependency-free GL loader, RAII handles, caps probe, headless EGL harness
This commit is contained in:
1 parent
ed71719df7
commit
258caa38a7
11 files changed
+1458
-1
No files matched your search
@@ -0,0 +1,206 @@
|
||||
#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
|
||||
Reference in new issue
Block a user