test(gl): cover the loader's extension scanner and the handle and vertex-array failure paths
This commit is contained in:
1 parent
e9985eeeaf
commit
d03f9ce51a
1 file changed
+529
@@ -16,9 +16,16 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -48,6 +55,124 @@ out vec4 o_color;
|
|||||||
void main() { o_color = this is not glsl; }
|
void main() { o_color = this is not glsl; }
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
/// Valid GLSL on both sides but an interface that cannot link: the fragment
|
||||||
|
/// reads a `vec3` where the vertex writes a `vec2`, so the link has to fail and
|
||||||
|
/// carry the driver's log through `Status::message`.
|
||||||
|
constexpr const char* kMismatchedVertexSrc = R"(#version 330 core
|
||||||
|
layout(location = 0) in vec2 a_pos;
|
||||||
|
out vec2 v_value;
|
||||||
|
void main() { v_value = a_pos; gl_Position = vec4(a_pos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kMismatchedFragmentSrc = R"(#version 330 core
|
||||||
|
in vec3 v_value;
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(v_value, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
/// Holds one GL entry point at a stub for as long as it lives, the render-smoke
|
||||||
|
/// stubs' save/restore pattern made generic over the slot's function-pointer
|
||||||
|
/// type so this file needs one class rather than one per entry point. The
|
||||||
|
/// pointer is restored by the destructor, which also runs while a failed
|
||||||
|
/// `PLOT_CHECK` unwinds, so a stub can never leak into the next case.
|
||||||
|
template <class Fn>
|
||||||
|
class EntryGuard {
|
||||||
|
public:
|
||||||
|
EntryGuard(Fn& slot, Fn value) noexcept : slot_(slot), saved_(slot) { slot_ = value; }
|
||||||
|
~EntryGuard() { slot_ = saved_; }
|
||||||
|
EntryGuard(const EntryGuard&) = delete;
|
||||||
|
EntryGuard& operator=(const EntryGuard&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Fn& slot_;
|
||||||
|
Fn saved_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- the caps probe, driven by fake strings ----------------------------------
|
||||||
|
|
||||||
|
/// Reported by the stubbed `glGetString`; each case sets them before probing.
|
||||||
|
const char* g_fake_version = "2.1";
|
||||||
|
const char* g_fake_extensions = "";
|
||||||
|
bool g_fake_version_missing = false;
|
||||||
|
|
||||||
|
const plot::gl::GLubyte* fake_get_string(plot::gl::GLenum name) noexcept {
|
||||||
|
if (name == plot::gl::GL_VERSION) {
|
||||||
|
return g_fake_version_missing ? nullptr
|
||||||
|
: reinterpret_cast<const plot::gl::GLubyte*>(g_fake_version);
|
||||||
|
}
|
||||||
|
if (name == plot::gl::GL_EXTENSIONS) {
|
||||||
|
return g_fake_extensions == nullptr
|
||||||
|
? nullptr
|
||||||
|
: reinterpret_cast<const plot::gl::GLubyte*>(g_fake_extensions);
|
||||||
|
}
|
||||||
|
if (name == plot::gl::GL_RENDERER) {
|
||||||
|
return reinterpret_cast<const plot::gl::GLubyte*>("fake renderer");
|
||||||
|
}
|
||||||
|
if (name == plot::gl::GL_VENDOR) {
|
||||||
|
return reinterpret_cast<const plot::gl::GLubyte*>("fake vendor");
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The indexed list's length and the profile mask the fake `glGetIntegerv`
|
||||||
|
/// reports; only the 3.x indexed path reads them.
|
||||||
|
void fake_get_integer_v(plot::gl::GLenum name, plot::gl::GLint* out) noexcept {
|
||||||
|
*out = name == plot::gl::GL_NUM_EXTENSIONS ? 2 : 0; // 0 = no core-profile bit
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the stubbed `glGetStringi` reports: 0 = a null pointer, 1 = the
|
||||||
|
/// extension the probe looks for, 2 = some other one.
|
||||||
|
int g_fake_stringi_mode = 0;
|
||||||
|
|
||||||
|
const plot::gl::GLubyte* fake_get_string_i(plot::gl::GLenum, plot::gl::GLuint) noexcept {
|
||||||
|
if (g_fake_stringi_mode == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return reinterpret_cast<const plot::gl::GLubyte*>(
|
||||||
|
g_fake_stringi_mode == 1 ? "GL_ARB_buffer_storage" : "GL_ARB_something_else");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- entry points that hand out no name, for the constructors' failure paths --
|
||||||
|
|
||||||
|
void zero_gen_buffers(plot::gl::GLsizei, plot::gl::GLuint* out) noexcept { *out = 0; }
|
||||||
|
void zero_gen_vertex_arrays(plot::gl::GLsizei, plot::gl::GLuint* out) noexcept { *out = 0; }
|
||||||
|
void zero_gen_textures(plot::gl::GLsizei, plot::gl::GLuint* out) noexcept { *out = 0; }
|
||||||
|
void zero_gen_framebuffers(plot::gl::GLsizei, plot::gl::GLuint* out) noexcept { *out = 0; }
|
||||||
|
plot::gl::GLuint zero_create_shader(plot::gl::GLenum) noexcept { return 0; }
|
||||||
|
plot::gl::GLuint zero_create_program() noexcept { return 0; }
|
||||||
|
|
||||||
|
/// Probes caps once with the fake string table installed and `GL_EXTENSIONS`
|
||||||
|
/// reporting `extensions` (null meaning the query returns no string). The
|
||||||
|
/// `load()` first clears the cached probe *and* restores the real entry point
|
||||||
|
/// -- the previous probe's guard has already run -- so each call reads the stub.
|
||||||
|
/// Returning a copy keeps the probe valid after the guard restores the table.
|
||||||
|
plot::gl::Caps probe_legacy_caps(const char* extensions) noexcept {
|
||||||
|
plot::gl::load(nullptr);
|
||||||
|
EntryGuard<decltype(plot::gl::glGetString)> guard(plot::gl::glGetString, &fake_get_string);
|
||||||
|
g_fake_version = "2.1";
|
||||||
|
g_fake_version_missing = false;
|
||||||
|
g_fake_extensions = extensions;
|
||||||
|
return plot::gl::caps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Probes caps once on a 3.2+ context, where the indexed list supplies the
|
||||||
|
/// names: `GL_NUM_EXTENSIONS` and the profile mask come from the fake
|
||||||
|
/// `glGetIntegerv`, each name from the fake `glGetStringi` (`stringi_mode`:
|
||||||
|
/// 0 = null pointer, 1 = the extension sought, 2 = another).
|
||||||
|
plot::gl::Caps probe_indexed_caps(int stringi_mode) noexcept {
|
||||||
|
plot::gl::load(nullptr);
|
||||||
|
EntryGuard<decltype(plot::gl::glGetString)> string_guard(plot::gl::glGetString,
|
||||||
|
&fake_get_string);
|
||||||
|
EntryGuard<decltype(plot::gl::glGetIntegerv)> int_guard(plot::gl::glGetIntegerv,
|
||||||
|
&fake_get_integer_v);
|
||||||
|
EntryGuard<decltype(plot::gl::glGetStringi)> stringi_guard(plot::gl::glGetStringi,
|
||||||
|
&fake_get_string_i);
|
||||||
|
g_fake_version = "3.3";
|
||||||
|
g_fake_version_missing = false;
|
||||||
|
g_fake_stringi_mode = stringi_mode;
|
||||||
|
return plot::gl::caps();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// Must stay the first GL case in this translation unit: `caps()` caches after
|
// Must stay the first GL case in this translation unit: `caps()` caches after
|
||||||
@@ -61,6 +186,125 @@ PLOT_TEST(caps_before_load_is_zeroed) {
|
|||||||
PLOT_CHECK(caps.vendor != nullptr && caps.vendor[0] == '\0');
|
PLOT_CHECK(caps.vendor != nullptr && caps.vendor[0] == '\0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The `kNoLoader` family and the `load()` failure branch need `available()` to
|
||||||
|
// be false, and `load()` is one-way and process-wide: once this suite has
|
||||||
|
// resolved the table -- the very next case does -- a failure cannot be produced
|
||||||
|
// in-process. So the whole case runs in a forked child, where poisoning the
|
||||||
|
// table poisons only its own copy. On a host that ships libGL.so.1 the only way
|
||||||
|
// left to make a required entry point unresolvable is to block `dlopen` by
|
||||||
|
// exhausting the file descriptors, which is what the child does first.
|
||||||
|
//
|
||||||
|
// It must run before `headless_context_provides_gl33`: after that case the
|
||||||
|
// process has opened libGL.so.1, and the excluded descriptors would no longer
|
||||||
|
// stop `dlopen` from handing the loader the table it wants.
|
||||||
|
PLOT_TEST(a_gl_failure_without_a_loader_is_safe_in_a_forked_child) {
|
||||||
|
std::fflush(nullptr); // so the child's exit() cannot re-flush the parent's stdout
|
||||||
|
const pid_t child = ::fork();
|
||||||
|
PLOT_CHECK(child >= 0);
|
||||||
|
if (child == 0) {
|
||||||
|
int rc = 0;
|
||||||
|
try {
|
||||||
|
struct rlimit original {};
|
||||||
|
PLOT_CHECK(::getrlimit(RLIMIT_NOFILE, &original) == 0);
|
||||||
|
struct rlimit lowered = original;
|
||||||
|
lowered.rlim_cur = original.rlim_max < 64 ? original.rlim_max : 64;
|
||||||
|
PLOT_CHECK(::setrlimit(RLIMIT_NOFILE, &lowered) == 0);
|
||||||
|
std::vector<int> held;
|
||||||
|
int fd = -1;
|
||||||
|
while ((fd = ::open("/dev/null", O_RDONLY)) >= 0) {
|
||||||
|
held.push_back(fd);
|
||||||
|
}
|
||||||
|
// Every descriptor is spent, so dlopen("libGL.so.1") fails and no required
|
||||||
|
// entry point can be resolved: load() reports the first one it could not.
|
||||||
|
PLOT_CHECK(!plot::gl::load(nullptr));
|
||||||
|
PLOT_CHECK(!plot::gl::available());
|
||||||
|
|
||||||
|
plot::Status st;
|
||||||
|
plot::gl::Buffer buffer;
|
||||||
|
PLOT_CHECK(!buffer.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(st.message != nullptr);
|
||||||
|
PLOT_CHECK(buffer.id() == 0);
|
||||||
|
plot::gl::VertexArray vao;
|
||||||
|
PLOT_CHECK(!vao.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(vao.id() == 0);
|
||||||
|
plot::gl::Program program = plot::gl::Program::from_source(kVertexSrc, kFragmentSrc, &st);
|
||||||
|
PLOT_CHECK(!st);
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(program.id() == 0);
|
||||||
|
plot::gl::Texture rgba;
|
||||||
|
PLOT_CHECK(!rgba.create_rgba(4, 4, &st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(rgba.id() == 0);
|
||||||
|
plot::gl::Texture r8;
|
||||||
|
PLOT_CHECK(!r8.create_r8(4, 4, &st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(r8.id() == 0);
|
||||||
|
plot::gl::Framebuffer fbo;
|
||||||
|
PLOT_CHECK(!fbo.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(fbo.id() == 0);
|
||||||
|
plot::gl::Fence fence;
|
||||||
|
PLOT_CHECK(!fence.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(fence.id() == nullptr);
|
||||||
|
|
||||||
|
// An empty object's guarded operations must not reach the null table.
|
||||||
|
plot::gl::Buffer empty_buffer;
|
||||||
|
empty_buffer.storage(16, plot::gl::GL_MAP_WRITE_BIT); // entry point null: no-op
|
||||||
|
plot::gl::Program empty_program;
|
||||||
|
empty_program.set_uniform_int("u_value", 1); // id 0: no-op
|
||||||
|
PLOT_CHECK(fence.wait(0)); // nothing in flight
|
||||||
|
|
||||||
|
// Hand the descriptors back so the coverage dump at exit can open its file.
|
||||||
|
for (const int held_fd : held) {
|
||||||
|
::close(held_fd);
|
||||||
|
}
|
||||||
|
PLOT_CHECK(::setrlimit(RLIMIT_NOFILE, &original) == 0);
|
||||||
|
} catch (...) {
|
||||||
|
rc = 1;
|
||||||
|
}
|
||||||
|
std::exit(rc);
|
||||||
|
}
|
||||||
|
int status = 0;
|
||||||
|
PLOT_CHECK(::waitpid(child, &status, 0) == child);
|
||||||
|
PLOT_CHECK(WIFEXITED(status));
|
||||||
|
PLOT_CHECK(WEXITSTATUS(status) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `resolve()`'s second source, `dlsym(RTLD_DEFAULT, name)`, only answers when a
|
||||||
|
// GL library sits in the global symbol scope -- the usual arrangement on a host
|
||||||
|
// without glvnd's local dispatch. This host is the other kind, so the case
|
||||||
|
// promotes libGL into that scope itself. The promotion is one-way and
|
||||||
|
// process-wide, which is exactly why it happens in a forked child: the parent's
|
||||||
|
// namespace is untouched, and the child's exit() flushes the counters for the
|
||||||
|
// branch it walked.
|
||||||
|
PLOT_TEST(a_globally_visible_gl_library_satisfies_the_fallback_resolver) {
|
||||||
|
std::fflush(nullptr);
|
||||||
|
const pid_t child = ::fork();
|
||||||
|
PLOT_CHECK(child >= 0);
|
||||||
|
if (child == 0) {
|
||||||
|
int rc = 0;
|
||||||
|
try {
|
||||||
|
void* handle = ::dlopen("libGL.so.1", RTLD_NOW | RTLD_GLOBAL);
|
||||||
|
// Not every host ships a GL library to promote; where there is none the
|
||||||
|
// case simply cannot reach the branch rather than pretending it did.
|
||||||
|
if (handle != nullptr && ::dlsym(RTLD_DEFAULT, "glGetString") != nullptr) {
|
||||||
|
PLOT_CHECK(plot::gl::load(nullptr));
|
||||||
|
PLOT_CHECK(plot::gl::available());
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
rc = 1;
|
||||||
|
}
|
||||||
|
std::exit(rc);
|
||||||
|
}
|
||||||
|
int status = 0;
|
||||||
|
PLOT_CHECK(::waitpid(child, &status, 0) == child);
|
||||||
|
PLOT_CHECK(WIFEXITED(status));
|
||||||
|
PLOT_CHECK(WEXITSTATUS(status) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
PLOT_TEST(headless_context_provides_gl33) {
|
PLOT_TEST(headless_context_provides_gl33) {
|
||||||
plottest::HeadlessContext ctx;
|
plottest::HeadlessContext ctx;
|
||||||
plot::Status st;
|
plot::Status st;
|
||||||
@@ -425,3 +669,288 @@ PLOT_TEST(text_pipeline_draws_a_glyph) {
|
|||||||
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||||
ctx.destroy();
|
ctx.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The loader's legacy scanner: a 2.1 version string routes `caps()` down the
|
||||||
|
// pre-3.0 path, the only one that reads `GL_EXTENSIONS` as one space-separated
|
||||||
|
// string, so these probes go through `has_token` exactly as a real 2.1 driver
|
||||||
|
// would. The near-misses are the assertions that matter -- a `strstr`-only
|
||||||
|
// scanner would set the cap for both of them.
|
||||||
|
PLOT_TEST(legacy_extension_string_is_scanned_a_whole_token_at_a_time) {
|
||||||
|
PLOT_CHECK(probe_legacy_caps("GL_ARB_other GL_ARB_buffer_storage GL_ARB_more").buffer_storage);
|
||||||
|
// At the very start and the very end: the boundaries the scanner exists for.
|
||||||
|
PLOT_CHECK(probe_legacy_caps("GL_ARB_buffer_storage GL_ARB_more").buffer_storage);
|
||||||
|
PLOT_CHECK(probe_legacy_caps("GL_ARB_more GL_ARB_buffer_storage").buffer_storage);
|
||||||
|
// Prefix-only near-misses must not count.
|
||||||
|
PLOT_CHECK(!probe_legacy_caps("GL_ARB_buffer_storageX").buffer_storage);
|
||||||
|
PLOT_CHECK(!probe_legacy_caps("XGL_ARB_buffer_storage").buffer_storage);
|
||||||
|
// No string at all: the null guard.
|
||||||
|
PLOT_CHECK(!probe_legacy_caps(nullptr).buffer_storage);
|
||||||
|
|
||||||
|
const plot::gl::Caps caps = probe_legacy_caps("GL_ARB_buffer_storage");
|
||||||
|
PLOT_CHECK(caps.major == 2 && caps.minor == 1); // the stub really was read
|
||||||
|
PLOT_CHECK(caps.buffer_storage && caps.persistent_map);
|
||||||
|
PLOT_CHECK(std::strcmp(caps.renderer, "fake renderer") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 3.0+ indexed path: `GL_NUM_EXTENSIONS` and the profile mask come from
|
||||||
|
// `glGetIntegerv`, each name from `glGetStringi`. A null `glGetStringi` means
|
||||||
|
// the list is unavailable, and the probe must then refuse -- the legacy string
|
||||||
|
// is illegal on 3.0+ core -- rather than quietly report no support.
|
||||||
|
PLOT_TEST(indexed_extension_list_is_probed_on_a_3x_context) {
|
||||||
|
PLOT_CHECK(probe_indexed_caps(1).buffer_storage);
|
||||||
|
PLOT_CHECK(!probe_indexed_caps(2).buffer_storage); // other names only
|
||||||
|
PLOT_CHECK(!probe_indexed_caps(0).buffer_storage); // the entries are null
|
||||||
|
{
|
||||||
|
plot::gl::load(nullptr);
|
||||||
|
EntryGuard<decltype(plot::gl::glGetString)> string_guard(plot::gl::glGetString,
|
||||||
|
&fake_get_string);
|
||||||
|
EntryGuard<decltype(plot::gl::glGetIntegerv)> int_guard(plot::gl::glGetIntegerv,
|
||||||
|
&fake_get_integer_v);
|
||||||
|
EntryGuard<decltype(plot::gl::glGetStringi)> stringi_guard(plot::gl::glGetStringi, nullptr);
|
||||||
|
g_fake_version = "3.3";
|
||||||
|
g_fake_version_missing = false;
|
||||||
|
g_fake_extensions = "GL_ARB_buffer_storage"; // present, but must not be consulted
|
||||||
|
PLOT_CHECK(!plot::gl::caps().buffer_storage);
|
||||||
|
}
|
||||||
|
const plot::gl::Caps caps = probe_indexed_caps(1);
|
||||||
|
PLOT_CHECK(caps.major == 3 && caps.minor == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neither a context that answers no version string nor one whose version does
|
||||||
|
// not parse may leave stale caps behind: both must report the zeroed probe.
|
||||||
|
PLOT_TEST(caps_are_zeroed_when_the_version_is_missing_or_unparsable) {
|
||||||
|
{
|
||||||
|
plot::gl::load(nullptr);
|
||||||
|
EntryGuard<decltype(plot::gl::glGetString)> guard(plot::gl::glGetString, &fake_get_string);
|
||||||
|
g_fake_version_missing = true;
|
||||||
|
const plot::gl::Caps caps = plot::gl::caps();
|
||||||
|
PLOT_CHECK(caps.major == 0 && caps.minor == 0);
|
||||||
|
PLOT_CHECK(caps.renderer != nullptr && caps.renderer[0] == '\0');
|
||||||
|
}
|
||||||
|
{
|
||||||
|
plot::gl::load(nullptr);
|
||||||
|
EntryGuard<decltype(plot::gl::glGetString)> guard(plot::gl::glGetString, &fake_get_string);
|
||||||
|
g_fake_version_missing = false;
|
||||||
|
g_fake_version = "not a version";
|
||||||
|
const plot::gl::Caps caps = plot::gl::caps();
|
||||||
|
PLOT_CHECK(caps.major == 0 && caps.minor == 0);
|
||||||
|
}
|
||||||
|
g_fake_version = "2.1";
|
||||||
|
g_fake_version_missing = false;
|
||||||
|
g_fake_extensions = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every fallible constructor's refusal branches: a driver that hands out name 0
|
||||||
|
// (`GLFailure`), a table with the entry point absent (`Unsupported`), and the
|
||||||
|
// argument checks. Each must report the documented code and leave an object
|
||||||
|
// that is empty yet safe to destroy. The stubs save and restore the slot, so a
|
||||||
|
// failure cannot leak into the next block.
|
||||||
|
PLOT_TEST(fallible_handles_report_a_refused_driver_name_and_stay_empty) {
|
||||||
|
plottest::HeadlessContext ctx;
|
||||||
|
plot::Status st;
|
||||||
|
PLOT_CHECK(ctx.create(&st));
|
||||||
|
PLOT_CHECK(plot::gl::load(egl_loader()));
|
||||||
|
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenBuffers)> guard(plot::gl::glGenBuffers, &zero_gen_buffers);
|
||||||
|
plot::gl::Buffer buffer;
|
||||||
|
PLOT_CHECK(!buffer.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::GLFailure);
|
||||||
|
PLOT_CHECK(std::strcmp(st.message, "glGenBuffers returned 0") == 0);
|
||||||
|
PLOT_CHECK(buffer.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenBuffers)> guard(plot::gl::glGenBuffers, nullptr);
|
||||||
|
plot::gl::Buffer buffer;
|
||||||
|
PLOT_CHECK(!buffer.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(buffer.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenVertexArrays)> guard(plot::gl::glGenVertexArrays,
|
||||||
|
&zero_gen_vertex_arrays);
|
||||||
|
plot::gl::VertexArray vao;
|
||||||
|
PLOT_CHECK(!vao.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::GLFailure);
|
||||||
|
PLOT_CHECK(std::strcmp(st.message, "glGenVertexArrays returned 0") == 0);
|
||||||
|
PLOT_CHECK(vao.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenVertexArrays)> guard(plot::gl::glGenVertexArrays, nullptr);
|
||||||
|
plot::gl::VertexArray vao;
|
||||||
|
PLOT_CHECK(!vao.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(vao.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenTextures)> guard(plot::gl::glGenTextures,
|
||||||
|
&zero_gen_textures);
|
||||||
|
plot::gl::Texture tex;
|
||||||
|
PLOT_CHECK(!tex.create_rgba(4, 4, &st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::GLFailure);
|
||||||
|
PLOT_CHECK(std::strcmp(st.message, "glGenTextures returned 0") == 0);
|
||||||
|
PLOT_CHECK(tex.id() == 0);
|
||||||
|
PLOT_CHECK(!tex.create_r8(4, 4, &st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::GLFailure);
|
||||||
|
PLOT_CHECK(tex.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenTextures)> guard(plot::gl::glGenTextures, nullptr);
|
||||||
|
plot::gl::Texture tex;
|
||||||
|
PLOT_CHECK(!tex.create_rgba(4, 4, &st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(!tex.create_r8(4, 4, &st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
plot::gl::Texture tex;
|
||||||
|
PLOT_CHECK(!tex.create_rgba(0, 4, &st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::InvalidArgument);
|
||||||
|
PLOT_CHECK(!tex.create_r8(4, 0, &st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::InvalidArgument);
|
||||||
|
PLOT_CHECK(tex.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenFramebuffers)> guard(plot::gl::glGenFramebuffers,
|
||||||
|
&zero_gen_framebuffers);
|
||||||
|
plot::gl::Framebuffer fbo;
|
||||||
|
PLOT_CHECK(!fbo.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::GLFailure);
|
||||||
|
PLOT_CHECK(std::strcmp(st.message, "glGenFramebuffers returned 0") == 0);
|
||||||
|
PLOT_CHECK(fbo.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenFramebuffers)> guard(plot::gl::glGenFramebuffers, nullptr);
|
||||||
|
plot::gl::Framebuffer fbo;
|
||||||
|
PLOT_CHECK(!fbo.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(fbo.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glFenceSync)> guard(plot::gl::glFenceSync, nullptr);
|
||||||
|
plot::gl::Fence fence;
|
||||||
|
PLOT_CHECK(!fence.create(&st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(fence.id() == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Program: a table without glCreateShader, a null source, a shader the driver
|
||||||
|
// refuses to create and a program it refuses to create.
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glCreateShader)> guard(plot::gl::glCreateShader, nullptr);
|
||||||
|
plot::Status bad;
|
||||||
|
plot::gl::Program program = plot::gl::Program::from_source(kVertexSrc, kFragmentSrc, &bad);
|
||||||
|
PLOT_CHECK(!bad);
|
||||||
|
PLOT_CHECK(bad.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(program.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
plot::Status bad;
|
||||||
|
plot::gl::Program program = plot::gl::Program::from_source(nullptr, kFragmentSrc, &bad);
|
||||||
|
PLOT_CHECK(!bad);
|
||||||
|
PLOT_CHECK(bad.code == plot::ErrorCode::InvalidArgument);
|
||||||
|
PLOT_CHECK(program.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glCreateShader)> guard(plot::gl::glCreateShader,
|
||||||
|
&zero_create_shader);
|
||||||
|
plot::Status bad;
|
||||||
|
plot::gl::Program program = plot::gl::Program::from_source(kVertexSrc, kFragmentSrc, &bad);
|
||||||
|
PLOT_CHECK(!bad);
|
||||||
|
PLOT_CHECK(bad.code == plot::ErrorCode::GLFailure);
|
||||||
|
PLOT_CHECK(std::strcmp(bad.message, "glCreateShader returned 0") == 0);
|
||||||
|
PLOT_CHECK(program.id() == 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glCreateProgram)> guard(plot::gl::glCreateProgram,
|
||||||
|
&zero_create_program);
|
||||||
|
plot::Status bad;
|
||||||
|
plot::gl::Program program = plot::gl::Program::from_source(kVertexSrc, kFragmentSrc, &bad);
|
||||||
|
PLOT_CHECK(!bad);
|
||||||
|
PLOT_CHECK(bad.code == plot::ErrorCode::GLFailure);
|
||||||
|
PLOT_CHECK(std::strcmp(bad.message, "glCreateProgram returned 0") == 0);
|
||||||
|
PLOT_CHECK(program.id() == 0);
|
||||||
|
}
|
||||||
|
plot::gl::Program empty;
|
||||||
|
empty.set_uniform_int("u_value", 1); // an empty program's setter is a no-op
|
||||||
|
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A program whose stages each compile but cannot be linked together: the
|
||||||
|
// constructor has to delete the program and report the driver's own log, not a
|
||||||
|
// bare failure. This is the path the link-failure branch implements.
|
||||||
|
PLOT_TEST(a_program_that_cannot_link_reports_the_driver_log) {
|
||||||
|
plottest::HeadlessContext ctx;
|
||||||
|
plot::Status st;
|
||||||
|
PLOT_CHECK(ctx.create(&st));
|
||||||
|
PLOT_CHECK(plot::gl::load(egl_loader()));
|
||||||
|
|
||||||
|
plot::Status bad;
|
||||||
|
plot::gl::Program program =
|
||||||
|
plot::gl::Program::from_source(kMismatchedVertexSrc, kMismatchedFragmentSrc, &bad);
|
||||||
|
PLOT_CHECK(!bad);
|
||||||
|
PLOT_CHECK(bad.code == plot::ErrorCode::GLFailure);
|
||||||
|
PLOT_CHECK(bad.message != nullptr);
|
||||||
|
PLOT_CHECK(std::strncmp(bad.message, "program link failed", std::strlen("program link failed")) ==
|
||||||
|
0);
|
||||||
|
// The driver's log follows the prefix: a real reason, not an empty tail. Mesa's
|
||||||
|
// linker names the mismatched varying.
|
||||||
|
PLOT_CHECK(std::strlen(bad.message) > std::strlen("program link failed: "));
|
||||||
|
PLOT_CHECK(std::strstr(bad.message, "v_value") != nullptr);
|
||||||
|
PLOT_CHECK(program.id() == 0);
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A vertex array that could not be fully created must not be left behind: the
|
||||||
|
// pair fails when the vertex array itself is refused, and again when the
|
||||||
|
// instance buffer is refused after the vertex array exists, and in both cases
|
||||||
|
// the object reports nothing to draw.
|
||||||
|
PLOT_TEST(vertex_arrays_leave_nothing_behind_when_create_fails) {
|
||||||
|
plottest::HeadlessContext ctx;
|
||||||
|
plot::Status st;
|
||||||
|
PLOT_CHECK(ctx.create(&st));
|
||||||
|
PLOT_CHECK(plot::gl::load(egl_loader()));
|
||||||
|
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenVertexArrays)> guard(plot::gl::glGenVertexArrays,
|
||||||
|
&zero_gen_vertex_arrays);
|
||||||
|
plot::Status bad;
|
||||||
|
plot::render::LineVertexArray lines;
|
||||||
|
PLOT_CHECK(!lines.create(&bad));
|
||||||
|
PLOT_CHECK(!lines.valid());
|
||||||
|
plot::render::TextVertexArray glyphs;
|
||||||
|
PLOT_CHECK(!glyphs.create(&bad));
|
||||||
|
PLOT_CHECK(!glyphs.valid());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EntryGuard<decltype(plot::gl::glGenBuffers)> guard(plot::gl::glGenBuffers, &zero_gen_buffers);
|
||||||
|
plot::Status bad;
|
||||||
|
plot::render::LineVertexArray lines;
|
||||||
|
PLOT_CHECK(!lines.create(&bad));
|
||||||
|
PLOT_CHECK(!lines.valid());
|
||||||
|
plot::render::TextVertexArray glyphs;
|
||||||
|
PLOT_CHECK(!glyphs.create(&bad));
|
||||||
|
PLOT_CHECK(!glyphs.valid());
|
||||||
|
}
|
||||||
|
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty batch -- a null pointer or a count of zero, with no vertex array
|
||||||
|
// created -- stages nothing and draws nothing, without touching the GL table.
|
||||||
|
PLOT_TEST(vertex_arrays_stage_nothing_for_an_empty_batch) {
|
||||||
|
plot::render::LineVertexArray lines;
|
||||||
|
lines.upload(nullptr, 3);
|
||||||
|
PLOT_CHECK(lines.instance_count() == 0);
|
||||||
|
const f32 segments[4] = {0.0f, 0.0f, 1.0f, 1.0f};
|
||||||
|
lines.upload(segments, 0);
|
||||||
|
PLOT_CHECK(lines.instance_count() == 0);
|
||||||
|
|
||||||
|
plot::render::TextVertexArray glyphs;
|
||||||
|
glyphs.upload(nullptr, 4);
|
||||||
|
PLOT_CHECK(glyphs.instance_count() == 0);
|
||||||
|
const f32 glyph_data[6] = {0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f};
|
||||||
|
glyphs.upload(glyph_data, 0);
|
||||||
|
PLOT_CHECK(glyphs.instance_count() == 0);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user