Files
plotter/tests/test_gl_context.cpp
T
Martino Ferrari 6866457155 fix(gl): gate fallible constructors on a complete loader table
- loader: add available(), true only after load() has resolved every
  required entry point (false at the start of load()). Program::from_source
  and the other fallible constructors return ErrorCode::Unsupported while it
  is false, so a failed load() with a partially populated table can no longer
  fault on an unresolved entry point.
- caps: GL_CONTEXT_PROFILE_MASK only exists from 3.2, so query it only on
  3.2+ contexts. A 3.0/3.1 query latches GL_INVALID_ENUM in the context, which
  the next unrelated glGetError() (e.g. read_pixel) then reports as failure.
- tests: gate the dlsym/libGL.so.1 fallback assertions on libGL being
  resolvable, so the GL suite also passes on a GLES-only host.
2026-09-27 21:15:57 +02:00

204 lines
7.2 KiB
C++

#include "harness.hpp"
#include "egl_headless.hpp"
#include <EGL/egl.h> // eglGetProcAddress: the context-aware entry point resolver
#include <plot/gl/caps.hpp>
#include <plot/gl/handle.hpp>
#include <plot/gl/loader.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <dlfcn.h>
#include <utility>
namespace {
/// `eglGetProcAddress` is the context-aware resolver of the EGL context: in a
/// surfaceless context there is no GLX/WGL dispatch to fall back on, so this is
/// the path that must work.
plot::gl::ProcLoader egl_loader() noexcept {
return reinterpret_cast<plot::gl::ProcLoader>(eglGetProcAddress);
}
constexpr const char* kVertexSrc = R"(#version 330 core
layout(location = 0) in vec2 a_pos;
uniform vec2 u_offset;
void main() { gl_Position = vec4(a_pos + u_offset, 0.0, 1.0); }
)";
constexpr const char* kFragmentSrc = R"(#version 330 core
uniform float u_alpha;
out vec4 o_color;
void main() { o_color = vec4(1.0, 0.0, 0.0, u_alpha); }
)";
/// Deliberately unparseable GLSL, to exercise the failure path of
/// `Program::from_source`.
constexpr const char* kBrokenFragmentSrc = R"(#version 330 core
out vec4 o_color;
void main() { o_color = this is not glsl; }
)";
} // namespace
// Must stay the first GL case in this translation unit: `caps()` caches after
// the first successful query, so "before any load()" is observable only once.
PLOT_TEST(caps_before_load_is_zeroed) {
const plot::gl::Caps& caps = plot::gl::caps();
PLOT_CHECK(caps.major == 0 && caps.minor == 0);
PLOT_CHECK(!caps.compute && !caps.buffer_storage && !caps.persistent_map);
PLOT_CHECK(!caps.core_profile);
PLOT_CHECK(caps.renderer != nullptr && caps.renderer[0] == '\0');
PLOT_CHECK(caps.vendor != nullptr && caps.vendor[0] == '\0');
}
PLOT_TEST(headless_context_provides_gl33) {
plottest::HeadlessContext ctx;
plot::Status st;
PLOT_CHECK(ctx.create(&st)); // fails loudly if surfaceless EGL is unavailable
// Primary path: resolve through the current context.
PLOT_CHECK(plot::gl::load(egl_loader()));
const plot::gl::Caps& caps = plot::gl::caps();
PLOT_CHECK(caps.major > 3 || (caps.major == 3 && caps.minor >= 3));
PLOT_CHECK(caps.renderer != nullptr && caps.renderer[0] != '\0');
PLOT_CHECK(caps.vendor != nullptr && caps.vendor[0] != '\0');
// EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR is requested, so this checks our profile-mask parsing.
PLOT_CHECK(caps.core_profile);
const char* version = reinterpret_cast<const char*>(plot::gl::glGetString(plot::gl::GL_VERSION));
PLOT_CHECK(version != nullptr);
const int major = caps.major;
const int minor = caps.minor;
std::printf("[info] GL %d.%d, renderer \"%s\", vendor \"%s\", %s profile, EGL path \"%s\"\n",
major, minor, caps.renderer, caps.vendor, caps.core_profile ? "core" : "compat",
ctx.egl_path);
std::printf("[info] caps: compute=%d buffer_storage=%d persistent_map=%d core_profile=%d\n",
caps.compute ? 1 : 0, caps.buffer_storage ? 1 : 0, caps.persistent_map ? 1 : 0,
caps.core_profile ? 1 : 0);
// The dlsym(RTLD_DEFAULT) / libGL.so.1 fallback must resolve the whole table
// too, and its pointers must still dispatch to the current EGL context. Only
// a host that ships desktop libGL can exercise it; on a GLES-only host there
// is nothing to assert.
if (dlopen("libGL.so.1", RTLD_LAZY) != nullptr) {
PLOT_CHECK(plot::gl::load(nullptr));
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
const char* fallback_version =
reinterpret_cast<const char*>(plot::gl::glGetString(plot::gl::GL_VERSION));
PLOT_CHECK(fallback_version != nullptr);
PLOT_CHECK(std::strcmp(fallback_version, version) == 0); // same context, same driver
const plot::gl::Caps& reprobed = plot::gl::caps(); // load() invalidated the cache
PLOT_CHECK(reprobed.major == major && reprobed.minor == minor);
PLOT_CHECK(reprobed.renderer != nullptr && reprobed.renderer[0] != '\0');
}
// Hand the table back to the context-aware resolver so teardown (deleting the
// framebuffer) runs through the entry points of the context it belongs to.
PLOT_CHECK(plot::gl::load(egl_loader()));
ctx.destroy();
}
PLOT_TEST(headless_framebuffer_reads_back_clear_color) {
plottest::HeadlessContext ctx;
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(egl_loader()));
plot::gl::glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
std::uint8_t px[4] = {0, 0, 0, 0};
PLOT_CHECK(ctx.read_pixel(0, 0, px));
PLOT_CHECK_NEAR(px[0], 64, 2); // round(0.25 * 255)
PLOT_CHECK_NEAR(px[1], 128, 2);
PLOT_CHECK_NEAR(px[2], 191, 2);
PLOT_CHECK_NEAR(px[3], 255, 2);
// A second corner proves the whole attachment is readable.
PLOT_CHECK(ctx.read_pixel(ctx.width - 1, ctx.height - 1, px));
PLOT_CHECK_NEAR(px[2], 191, 2);
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
ctx.destroy();
}
PLOT_TEST(raii_handles_round_trip) {
plottest::HeadlessContext ctx;
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(egl_loader()));
const float verts[6] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f};
plot::gl::Buffer vbo;
PLOT_CHECK(vbo.create(&st));
PLOT_CHECK(vbo.id() != 0);
vbo.upload(verts, sizeof verts, plot::gl::GL_STATIC_DRAW);
vbo.upload_sub(verts, sizeof verts, 0);
plot::gl::VertexArray vao;
PLOT_CHECK(vao.create(&st));
vao.bind();
vbo.bind();
vao.attrib(0, 2, 2 * sizeof(float), 0);
const std::uint8_t texels[8 * 8 * 4] = {0};
plot::gl::Texture color;
PLOT_CHECK(color.create_rgba(8, 8, &st));
color.upload_rgba(8, 8, texels);
const std::uint8_t coverage[8 * 8] = {0};
plot::gl::Texture mask;
PLOT_CHECK(mask.create_r8(8, 8, &st));
mask.upload_r8(8, 8, coverage);
mask.set_filter(plot::gl::GL_NEAREST, plot::gl::GL_NEAREST);
mask.bind();
plot::gl::Framebuffer fbo;
PLOT_CHECK(fbo.create(&st));
fbo.attach_color(color);
PLOT_CHECK(fbo.complete());
plot::gl::Fence fence;
PLOT_CHECK(fence.create(&st));
plot::gl::glFinish();
PLOT_CHECK(fence.wait(0));
// Move-only: the source is emptied and the GL name survives.
plot::gl::Buffer moved = std::move(vbo);
PLOT_CHECK(moved.id() != 0);
PLOT_CHECK(vbo.id() == 0);
moved.reset();
PLOT_CHECK(moved.id() == 0);
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
ctx.destroy();
}
PLOT_TEST(program_compiles_trivial_shaders) {
plottest::HeadlessContext ctx;
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(egl_loader()));
plot::Status ok;
plot::gl::Program program = plot::gl::Program::from_source(kVertexSrc, kFragmentSrc, &ok);
PLOT_CHECK(bool(ok));
PLOT_CHECK(program.id() != 0);
program.use();
program.set_uniform("u_offset", plot::Vec2{1.0f, 2.0f});
program.set_uniform("u_alpha", 0.5f);
program.set_uniform("u_missing", 0.25f); // unknown name: silent no-op
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
plot::Status bad;
plot::gl::Program broken = plot::gl::Program::from_source(kVertexSrc, kBrokenFragmentSrc, &bad);
PLOT_CHECK(!bad);
PLOT_CHECK(bad.code == plot::ErrorCode::GLFailure);
PLOT_CHECK(bad.message != nullptr && bad.message[0] != '\0');
PLOT_CHECK(broken.id() == 0);
ctx.destroy();
}