#include "harness.hpp" #include "egl_headless.hpp" #include // eglGetProcAddress: the context-aware entry point resolver #include #include #include #include #include #include #include #include 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(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(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(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(); }