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
+27
-1
@@ -1,8 +1,34 @@
|
||||
# EGL powers the surfaceless headless context the GL tests assert pixels
|
||||
# against. It is a *test-only* dependency: plot_core never links or includes it,
|
||||
# so a machine without EGL can still build the library (-DPLOT_BUILD_TESTS=OFF).
|
||||
# Prefer CMake's own module, fall back to pkg-config, and fail loudly if neither
|
||||
# can supply EGL rather than silently dropping the GL tests.
|
||||
find_package(EGL QUIET)
|
||||
if(NOT EGL_FOUND)
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(EGL_PC QUIET IMPORTED_TARGET egl)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(TARGET EGL::EGL)
|
||||
set(plot_egl_library EGL::EGL)
|
||||
elseif(TARGET PkgConfig::EGL_PC)
|
||||
set(plot_egl_library PkgConfig::EGL_PC)
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"plot: PLOT_BUILD_TESTS=ON requires EGL (EGL/egl.h plus libEGL) for the headless "
|
||||
"OpenGL context used by the GL tests. Install it (e.g. libegl-dev, libegl1-mesa-dev "
|
||||
"or mesa-libEGL-devel) or configure with -DPLOT_BUILD_TESTS=OFF.")
|
||||
endif()
|
||||
|
||||
add_executable(plot_tests
|
||||
main.cpp
|
||||
test_transform.cpp
|
||||
test_gl_context.cpp
|
||||
egl_headless.cpp
|
||||
)
|
||||
target_link_libraries(plot_tests PRIVATE plot::core)
|
||||
target_link_libraries(plot_tests PRIVATE plot::core ${plot_egl_library})
|
||||
plot_apply_warnings(plot_tests)
|
||||
|
||||
add_test(NAME plot_tests COMMAND plot_tests)
|
||||
@@ -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
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
#include <plot/core/status.hpp>
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/gl/handle.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace plottest {
|
||||
|
||||
using plot::u32;
|
||||
|
||||
/// A surfaceless EGL context plus an off-screen RGBA framebuffer, so renderer
|
||||
/// output can be asserted pixel by pixel without a window or a display server.
|
||||
///
|
||||
/// Test-only: `plot_core` neither links nor includes EGL.
|
||||
class HeadlessContext {
|
||||
public:
|
||||
explicit HeadlessContext(u32 w = 256, u32 h = 256) noexcept;
|
||||
~HeadlessContext();
|
||||
|
||||
HeadlessContext(const HeadlessContext&) = delete;
|
||||
HeadlessContext& operator=(const HeadlessContext&) = delete;
|
||||
|
||||
/// Makes an OpenGL 3.3 core context current on this thread, resolves the GL
|
||||
/// entry points against it (`plot::gl::load(eglGetProcAddress)`) and
|
||||
/// allocates the colour attachment. Idempotent. On failure `st` (when
|
||||
/// non-null) carries the reason, every resource already created is released,
|
||||
/// and the error is also printed to stderr so a failing CTest run is
|
||||
/// diagnosable.
|
||||
bool create(plot::Status* st = nullptr) noexcept;
|
||||
|
||||
/// Releases the framebuffer, the GL context and the EGL display. Safe to call
|
||||
/// more than once, and to call after a failed create().
|
||||
void destroy() noexcept;
|
||||
|
||||
/// Binds the off-screen target and reads one RGBA8 pixel, in GL's
|
||||
/// origin-at-the-bottom-left convention. False when it cannot be read.
|
||||
bool read_pixel(u32 x, u32 y, std::uint8_t rgba[4]) const noexcept;
|
||||
|
||||
u32 width = 0;
|
||||
u32 height = 0;
|
||||
/// The headless path create() selected ("egl surfaceless" or "egl pbuffer");
|
||||
/// empty before a successful create() and after destroy(). Setting
|
||||
/// PLOT_HEADLESS_FORCE_PBUFFER in the environment skips the surfaceless
|
||||
/// attempt, so the pbuffer fallback can be exercised on a driver that does
|
||||
/// support surfaceless contexts.
|
||||
const char* egl_path = "";
|
||||
|
||||
private:
|
||||
plot::gl::Texture color_;
|
||||
plot::gl::Framebuffer framebuffer_;
|
||||
void* display_ = nullptr; // EGLDisplay
|
||||
void* context_ = nullptr; // EGLContext
|
||||
void* surface_ = nullptr; // EGLSurface, null on the surfaceless path
|
||||
};
|
||||
|
||||
} // namespace plottest
|
||||
@@ -0,0 +1,197 @@
|
||||
#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 <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');
|
||||
PLOT_CHECK(caps.core_profile); // the context was created as 3.3 core
|
||||
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.
|
||||
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();
|
||||
}
|
||||
Reference in new issue
Block a user