fix(render): gate TextAtlas::sync on the loader, and cover three review findings
Review of the atlas refactor found one Important and three Minor issues: - Important: TextAtlas::sync called glGetIntegerv before any available() check, so a host that synced before plot::gl::load() -- which is legal for a public class Tasks 2 and 8 build backends on -- segfaulted on a null entry point instead of receiving the Status the interface promises. It now follows the handle-constructor convention (available(), then the entry point itself) and reports Unsupported. - empty() no longer means "a GL name exists": releasing one is a glDeleteTextures call, so the no-atlas path forgets the atlas and only resets the texture when there is a table to release it with. - end_frame's comment still claimed the frame hands back the unpack alignment init() set, contradicting the code below it and renderer.hpp. - The orientation case's texel probe mapped the atlas' u along the cell's +v axis and vice versa, which is a no-op for its two diagonal probes -- so it could not fail on a transposed UV mapping, and its own comment misstated the rule. Probes now follow the shader's rule and include an off-diagonal pair. - The null-glyph path of add_text (and its twin in walk_text_clipped) was unreached by any case. Cases: text_atlas_reports_a_status_when_the_entry_points_are_missing, add_text_skips_a_codepoint_the_backend_cannot_draw.
This commit is contained in:
1 parent
e1f6b61b7d
commit
3ff7b9c978
5 files changed
+230
-16
No files matched your search
@@ -48,10 +48,17 @@ class TextAtlas {
|
|||||||
///
|
///
|
||||||
/// False, with `st` filled in, when the driver refuses the texture; the last
|
/// False, with `st` filled in, when the driver refuses the texture; the last
|
||||||
/// successful upload is left in place and the next call retries.
|
/// successful upload is left in place and the next call retries.
|
||||||
|
///
|
||||||
|
/// It is `ErrorCode::Unsupported` when the GL entry points are not resolved --
|
||||||
|
/// a host may hold this class and sync it before `plot::gl::load()`, and no
|
||||||
|
/// `gl*` call may be made while the table is unresolved (see
|
||||||
|
/// `plot::gl::loader.hpp`) -- reported exactly as every fallible `plot::gl`
|
||||||
|
/// handle constructor reports it, rather than called through a null pointer.
|
||||||
bool sync(const TextBackend& backend, Status* st = nullptr) noexcept;
|
bool sync(const TextBackend& backend, Status* st = nullptr) noexcept;
|
||||||
|
|
||||||
/// Binds to `unit` (texture unit 0 by default, which is where the text
|
/// Binds to `unit` (texture unit 0 by default, which is where the text
|
||||||
/// program's sampler points).
|
/// program's sampler points). An empty atlas binds nothing: there is nothing to
|
||||||
|
/// sample, and a caller must not draw text from it.
|
||||||
void bind(gl::GLenum unit = gl::GL_TEXTURE0) const noexcept;
|
void bind(gl::GLenum unit = gl::GL_TEXTURE0) const noexcept;
|
||||||
|
|
||||||
/// Atlas size in texels; `(0, 0)` while nothing has been uploaded.
|
/// Atlas size in texels; `(0, 0)` while nothing has been uploaded.
|
||||||
@@ -60,8 +67,8 @@ class TextAtlas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// True while no atlas is on the GPU: there is no text to draw, and `bind()`
|
/// True while no atlas is on the GPU: there is no text to draw, and `bind()`
|
||||||
/// would bind nothing.
|
/// binds nothing.
|
||||||
bool empty() const noexcept { return texture_.id() == 0; }
|
bool empty() const noexcept { return !has_atlas_; }
|
||||||
|
|
||||||
/// The revision of the atlas currently on the GPU. Meaningless before the
|
/// The revision of the atlas currently on the GPU. Meaningless before the
|
||||||
/// first `sync()`, which is the one call that always uploads.
|
/// first `sync()`, which is the one call that always uploads.
|
||||||
@@ -75,6 +82,11 @@ class TextAtlas {
|
|||||||
/// False until a revision has been recorded, so a backend whose `revision()`
|
/// False until a revision has been recorded, so a backend whose `revision()`
|
||||||
/// is 0 is still uploaded once instead of being mistaken for "already there".
|
/// is 0 is still uploaded once instead of being mistaken for "already there".
|
||||||
bool synced_ = false;
|
bool synced_ = false;
|
||||||
|
/// False once the backend stops publishing an atlas. It is deliberately not
|
||||||
|
/// `texture_.id() != 0`: releasing a GL name needs a resolved entry-point
|
||||||
|
/// table, and `empty()` has to keep meaning "there is nothing to draw" even
|
||||||
|
/// when a name is still lying around.
|
||||||
|
bool has_atlas_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace plot::render
|
} // namespace plot::render
|
||||||
@@ -326,11 +326,14 @@ void Renderer::end_frame() noexcept {
|
|||||||
gl_error_ = true;
|
gl_error_ = true;
|
||||||
}
|
}
|
||||||
// Leave the host's context as close to neutral as a frame can: nothing bound to
|
// Leave the host's context as close to neutral as a frame can: nothing bound to
|
||||||
// a pipeline stage, off every state bit the frame turned on, and the two values
|
// a pipeline stage, off every state bit the frame turned on, and the one value
|
||||||
// the frame *changed* rather than merely used handed back -- the unpack
|
// the frame *changed* rather than merely used handed back -- the host's clear
|
||||||
// alignment `init()` set for the atlas, and the host's clear colour. What
|
// colour. What remains is the viewport (the caller's to set for its next pass),
|
||||||
// remains is the viewport (the caller's to set for its next pass), the pixel-pack
|
// the pixel-pack alignment a PPM dump may have set, and the vertex buffers' own
|
||||||
// alignment a PPM dump may have set, and the vertex buffers' own contents.
|
// contents. `GL_UNPACK_ALIGNMENT` is not in this list: `render::TextAtlas`
|
||||||
|
// saves and restores it around every atlas upload, so the frame never leaves it
|
||||||
|
// moved for the host's own uploads to inherit (see end_frame's contract in
|
||||||
|
// renderer.hpp).
|
||||||
gl::glBindVertexArray(0);
|
gl::glBindVertexArray(0);
|
||||||
gl::glUseProgram(0);
|
gl::glUseProgram(0);
|
||||||
gl::glBindBuffer(gl::GL_ARRAY_BUFFER, 0);
|
gl::glBindBuffer(gl::GL_ARRAY_BUFFER, 0);
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
#include <plot/render/text_atlas.hpp>
|
#include <plot/render/text_atlas.hpp>
|
||||||
|
|
||||||
#include <plot/gl/api.hpp>
|
#include <plot/gl/api.hpp>
|
||||||
|
#include <plot/gl/loader.hpp>
|
||||||
|
|
||||||
namespace plot::render {
|
namespace plot::render {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// The loader's own wording, because `Status::message` is a non-owning pointer to
|
||||||
|
/// a string this file has to keep alive (`plot::gl::load()`'s private constant is
|
||||||
|
/// not reachable from here).
|
||||||
|
constexpr const char* kNoLoader = "plot::gl::load() has not resolved the GL entry points";
|
||||||
|
|
||||||
|
bool fail(Status* st, ErrorCode code, const char* message) noexcept {
|
||||||
|
if (st != nullptr) {
|
||||||
|
st->code = code;
|
||||||
|
st->message = message;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool TextAtlas::sync(const TextBackend& backend, Status* st) noexcept {
|
bool TextAtlas::sync(const TextBackend& backend, Status* st) noexcept {
|
||||||
if (synced_ && revision_ == backend.revision()) {
|
if (synced_ && revision_ == backend.revision()) {
|
||||||
@@ -13,17 +30,34 @@ bool TextAtlas::sync(const TextBackend& backend, Status* st) noexcept {
|
|||||||
const u32 height = backend.atlas_height();
|
const u32 height = backend.atlas_height();
|
||||||
if (pixels == nullptr || width == 0u || height == 0u) {
|
if (pixels == nullptr || width == 0u || height == 0u) {
|
||||||
// "No text", not a failure: record the revision so the check above settles,
|
// "No text", not a failure: record the revision so the check above settles,
|
||||||
// and release the texture as well as the size -- an atlas a backend has
|
// and stop reporting an atlas -- an atlas a backend has stopped publishing
|
||||||
// stopped publishing must not stay bound, or a draw would sample glyphs that
|
// must not stay bound, or a draw would sample glyphs that are no longer its
|
||||||
// are no longer its and divide by a zero texture size doing it.
|
// and divide by a zero texture size doing it.
|
||||||
revision_ = backend.revision();
|
revision_ = backend.revision();
|
||||||
synced_ = true;
|
synced_ = true;
|
||||||
|
has_atlas_ = false;
|
||||||
width_ = 0;
|
width_ = 0;
|
||||||
height_ = 0;
|
height_ = 0;
|
||||||
texture_.reset();
|
// Releasing the name is a `glDeleteTextures` call, so it happens only when
|
||||||
|
// there is a table to make it with. That costs nothing when the loader never
|
||||||
|
// resolved -- no name can have been created then (`sync` refuses before the
|
||||||
|
// first `gl*` call) -- and a later sync re-uses whatever name is left.
|
||||||
|
if (gl::available()) {
|
||||||
|
texture_.reset();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This class is public and a host may sync it before `plot::gl::load()`: while
|
||||||
|
// the entry-point table is unresolved no `gl*` call may be made at all (see
|
||||||
|
// `plot::gl::loader.hpp` -- the pointers are null until then), so the guard is
|
||||||
|
// checked before the first of them and reports the same `Unsupported` every
|
||||||
|
// fallible `plot::gl` handle constructor reports. The entry point beside it is
|
||||||
|
// the convention those constructors follow.
|
||||||
|
if (!gl::available() || gl::glGetIntegerv == nullptr) {
|
||||||
|
return fail(st, ErrorCode::Unsupported, kNoLoader);
|
||||||
|
}
|
||||||
|
|
||||||
// `upload_r8` needs GL_UNPACK_ALIGNMENT 1 for single-channel rows and leaves it
|
// `upload_r8` needs GL_UNPACK_ALIGNMENT 1 for single-channel rows and leaves it
|
||||||
// there; the caller's value is this call's to put back, wherever it runs.
|
// there; the caller's value is this call's to put back, wherever it runs.
|
||||||
gl::GLint host_alignment = 4;
|
gl::GLint host_alignment = 4;
|
||||||
@@ -44,11 +78,16 @@ bool TextAtlas::sync(const TextBackend& backend, Status* st) noexcept {
|
|||||||
}
|
}
|
||||||
revision_ = backend.revision();
|
revision_ = backend.revision();
|
||||||
synced_ = true;
|
synced_ = true;
|
||||||
|
has_atlas_ = true;
|
||||||
width_ = width;
|
width_ = width;
|
||||||
height_ = height;
|
height_ = height;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextAtlas::bind(gl::GLenum unit) const noexcept { texture_.bind(unit); }
|
void TextAtlas::bind(gl::GLenum unit) const noexcept {
|
||||||
|
if (has_atlas_) {
|
||||||
|
texture_.bind(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace plot::render
|
} // namespace plot::render
|
||||||
@@ -484,3 +484,92 @@ PLOT_TEST(text_batch_takes_its_cell_from_the_backend) {
|
|||||||
PLOT_CHECK_NEAR(b.glyph_cell_uv().x, 0.0, 1e-6);
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().x, 0.0, 1e-6);
|
||||||
PLOT_CHECK_NEAR(b.glyph_cell_uv().y, 0.0, 1e-6);
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().y, 0.0, 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A backend with a hole in its artwork: it can draw 'A' and 'B' and publishes no
|
||||||
|
/// glyph at all for anything else, which is what a font whose coverage falls short
|
||||||
|
/// of a label looks like. `measure()` counts only what it can draw, so "what a
|
||||||
|
/// string is worth" and "what it stages" stay the same thing -- the pipeline draws
|
||||||
|
/// nothing for the missing codepoint and moves the pen nowhere, rather than
|
||||||
|
/// inventing a box or a width.
|
||||||
|
class HoleInCoverageBackend final : public plot::render::TextBackend {
|
||||||
|
public:
|
||||||
|
plot::f32 measure(const char* utf8) const override {
|
||||||
|
plot::f32 width = 0.0f;
|
||||||
|
for (const char* p = utf8; p != nullptr && *p != '\0';) {
|
||||||
|
const plot::render::Glyph* g = glyph(plot::render::next_codepoint(p));
|
||||||
|
if (g != nullptr) width += g->advance;
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
plot::f32 height() const override { return 8.0f; }
|
||||||
|
plot::f32 ascent() const override { return 6.0f; }
|
||||||
|
plot::Vec2 cell_uv() const override { return plot::Vec2{0.5f, 1.0f}; }
|
||||||
|
plot::u32 revision() const override { return 1u; }
|
||||||
|
const plot::render::Glyph* glyph(plot::u32 codepoint) const override {
|
||||||
|
if (codepoint == static_cast<plot::u32>('A')) return &a_;
|
||||||
|
if (codepoint == static_cast<plot::u32>('B')) return &b_;
|
||||||
|
return nullptr; // no artwork at all: not even a fallback box
|
||||||
|
}
|
||||||
|
const plot::u8* atlas_pixels() const override { return nullptr; }
|
||||||
|
plot::u32 atlas_width() const override { return 0u; }
|
||||||
|
plot::u32 atlas_height() const override { return 0u; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const plot::render::Glyph a_{0.0f, 0.0f, 0.5f, 1.0f, 0.0f, 0.0f, 6.0f};
|
||||||
|
const plot::render::Glyph b_{0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 4.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
// A backend that publishes no glyph for a codepoint: `add_text` stages nothing for
|
||||||
|
// it and does not advance the pen, so a run stays exactly as long as the same
|
||||||
|
// backend measured it and the glyphs it *can* draw keep their places. A backend
|
||||||
|
// that would rather show something publishes a fallback box as that codepoint's
|
||||||
|
// glyph (which is what `BitmapFont` does); this is the other legal answer, and the
|
||||||
|
// staging path has to honour it rather than draw whatever was staged last or
|
||||||
|
// advance by a width nobody published.
|
||||||
|
PLOT_TEST(add_text_skips_a_codepoint_the_backend_cannot_draw) {
|
||||||
|
HoleInCoverageBackend backend;
|
||||||
|
const plot::render::Glyph* a = backend.glyph(static_cast<plot::u32>('A'));
|
||||||
|
const plot::render::Glyph* b = backend.glyph(static_cast<plot::u32>('B'));
|
||||||
|
PLOT_CHECK(a != nullptr);
|
||||||
|
PLOT_CHECK(b != nullptr);
|
||||||
|
PLOT_CHECK(backend.glyph(static_cast<plot::u32>('?')) == nullptr);
|
||||||
|
|
||||||
|
const plot::Vec2 pen{10.0f, 4.0f};
|
||||||
|
plot::render::TextBatch batch;
|
||||||
|
batch.begin();
|
||||||
|
const plot::Vec2 end = batch.add_text("A?B", pen, 1.0f, backend);
|
||||||
|
|
||||||
|
// Two glyphs staged for three codepoints, and the pen moved by exactly the two
|
||||||
|
// advances the backend published.
|
||||||
|
PLOT_CHECK(batch.glyph_count() == 2);
|
||||||
|
PLOT_CHECK_NEAR(end.x, pen.x + a->advance + b->advance, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(end.y, pen.y, 1e-6);
|
||||||
|
// Which is the length the backend measured for the same string, so a label's
|
||||||
|
// reserved space still covers the run drawn into it.
|
||||||
|
PLOT_CHECK_NEAR((end - pen).length(), backend.measure("A?B"), 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(backend.measure("A?B"), a->advance + b->advance, 1e-6);
|
||||||
|
// 'B' starts where 'A' ended -- the skipped codepoint left no gap and drew no
|
||||||
|
// glyph of its own, which the second instance's atlas rect says outright.
|
||||||
|
PLOT_CHECK_NEAR(batch.data()[0], pen.x, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(batch.data()[2], a->u0, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(batch.data()[6], pen.x + a->advance, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(batch.data()[8], b->u0, 1e-6);
|
||||||
|
|
||||||
|
// A run of nothing but undrawable codepoints stages nothing at all and leaves
|
||||||
|
// the pen where it was, rather than drawing at the pen.
|
||||||
|
batch.begin();
|
||||||
|
const plot::Vec2 same = batch.add_text("??", pen, 1.0f, backend);
|
||||||
|
PLOT_CHECK(batch.glyph_count() == 0);
|
||||||
|
PLOT_CHECK(batch.data().empty());
|
||||||
|
PLOT_CHECK_NEAR(same.x, pen.x, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(same.y, pen.y, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(backend.measure("??"), 0.0, 1e-6);
|
||||||
|
|
||||||
|
// A real font never hits this path (every codepoint maps to its box), so the
|
||||||
|
// same string through it stages one glyph per codepoint: the two answers to
|
||||||
|
// "no glyph" are the backend's choice, not the pipeline's.
|
||||||
|
plot::render::BitmapFont font;
|
||||||
|
batch.begin();
|
||||||
|
batch.add_text("A?B", pen, 1.0f, font);
|
||||||
|
PLOT_CHECK(batch.glyph_count() == 3);
|
||||||
|
}
|
||||||
@@ -336,6 +336,24 @@ class RefusePersistentMap {
|
|||||||
decltype(plot::gl::glMapBufferRange) saved_;
|
decltype(plot::gl::glMapBufferRange) saved_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Holds `plot::gl::glGetIntegerv` at null for as long as it lives: the entry
|
||||||
|
/// point `TextAtlas::sync` reaches first, so a sync with it missing has to report
|
||||||
|
/// a `Status` rather than call through a null pointer. Saved on construction,
|
||||||
|
/// restored on destruction, exactly like the two stubs above -- so a failed check
|
||||||
|
/// cannot leak it into the next case.
|
||||||
|
class RefuseGetIntegerv {
|
||||||
|
public:
|
||||||
|
RefuseGetIntegerv() noexcept : saved_(plot::gl::glGetIntegerv) {
|
||||||
|
plot::gl::glGetIntegerv = nullptr;
|
||||||
|
}
|
||||||
|
~RefuseGetIntegerv() { plot::gl::glGetIntegerv = saved_; }
|
||||||
|
RefuseGetIntegerv(const RefuseGetIntegerv&) = delete;
|
||||||
|
RefuseGetIntegerv& operator=(const RefuseGetIntegerv&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
decltype(plot::gl::glGetIntegerv) saved_;
|
||||||
|
};
|
||||||
|
|
||||||
/// Same, for `glFenceSync`: the sync object the ring's slot guard needs is
|
/// Same, for `glFenceSync`: the sync object the ring's slot guard needs is
|
||||||
/// refused while the caps and the entry point still say the mapped path is on.
|
/// refused while the caps and the entry point still say the mapped path is on.
|
||||||
class RefuseFenceSync {
|
class RefuseFenceSync {
|
||||||
@@ -1294,12 +1312,15 @@ PLOT_TEST(rotated_glyph_orientation_is_not_mirrored) {
|
|||||||
const plot::Vec2 uy{-ux.y, ux.x}; // (1, 0): the cell's own +v axis
|
const plot::Vec2 uy{-ux.y, ux.x}; // (1, 0): the cell's own +v axis
|
||||||
|
|
||||||
// Where the centre of the glyph's texel (col, row) is drawn. Every cell is
|
// Where the centre of the glyph's texel (col, row) is drawn. Every cell is
|
||||||
// `kGlyphPx` texels across, and the cell's top-left is the pen because the
|
// `kGlyphPx` texels across, the cell's top-left is the pen because the bitmap
|
||||||
// bitmap font's bearings are zero.
|
// font's bearings are zero, and the rule is the shader's own: the atlas' u runs
|
||||||
|
// along the cell's +x axis (`ux` here, `a_axis` there) and its v along the
|
||||||
|
// perpendicular (`v_axis`), which is what `v_uv = a_uv + corner * u_glyph_uv`
|
||||||
|
// maps corner by corner.
|
||||||
auto texel_centre = [&](plot::f32 col, plot::f32 row, plot::Vec2 v_axis) {
|
auto texel_centre = [&](plot::f32 col, plot::f32 row, plot::Vec2 v_axis) {
|
||||||
const plot::f32 du = (col + 0.5f) * px;
|
const plot::f32 du = (col + 0.5f) * px;
|
||||||
const plot::f32 dv = (row + 0.5f) * px;
|
const plot::f32 dv = (row + 0.5f) * px;
|
||||||
return plot::Vec2{pen.x + ux.x * dv + v_axis.x * du, pen.y + ux.y * dv + v_axis.y * du};
|
return plot::Vec2{pen.x + ux.x * du + v_axis.x * dv, pen.y + ux.y * du + v_axis.y * dv};
|
||||||
};
|
};
|
||||||
auto inked = [&](plot::Vec2 at) {
|
auto inked = [&](plot::Vec2 at) {
|
||||||
plot::u8 pxv[4] = {};
|
plot::u8 pxv[4] = {};
|
||||||
@@ -1314,6 +1335,13 @@ PLOT_TEST(rotated_glyph_orientation_is_not_mirrored) {
|
|||||||
// its foot. Both corners are where the basis puts them ...
|
// its foot. Both corners are where the basis puts them ...
|
||||||
PLOT_CHECK(inked(texel_centre(0.0f, 0.0f, uy)));
|
PLOT_CHECK(inked(texel_centre(0.0f, 0.0f, uy)));
|
||||||
PLOT_CHECK(inked(texel_centre(6.0f, 6.0f, uy)));
|
PLOT_CHECK(inked(texel_centre(6.0f, 6.0f, uy)));
|
||||||
|
// ... and the two *off-diagonal* texels of its ink state the rule the diagonal
|
||||||
|
// pair cannot: the stem meets the foot at (0, 6), which is where the cell's
|
||||||
|
// first texel row and last texel column meet, while (6, 0) is blank. A shader
|
||||||
|
// that swapped the cell's two axes would trade those two, and both of these
|
||||||
|
// would fail -- the diagonal probes would not.
|
||||||
|
PLOT_CHECK(inked(texel_centre(0.0f, 6.0f, uy)));
|
||||||
|
PLOT_CHECK(!inked(texel_centre(6.0f, 0.0f, uy)));
|
||||||
// ... and the mirrored basis -- the other perpendicular a shader could build --
|
// ... and the mirrored basis -- the other perpendicular a shader could build --
|
||||||
// would put the stem's top on the opposite side of the pen, where this frame has
|
// would put the stem's top on the opposite side of the pen, where this frame has
|
||||||
// no ink at all.
|
// no ink at all.
|
||||||
@@ -1456,3 +1484,46 @@ PLOT_TEST(staging_a_glyph_that_is_not_the_cell_aborts_in_a_debug_build) {
|
|||||||
PLOT_CHECK(WTERMSIG(status) == SIGABRT); // and it was the assertion
|
PLOT_CHECK(WTERMSIG(status) == SIGABRT); // and it was the assertion
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The loader invariant (see `plot::gl::loader.hpp`): while the entry-point table
|
||||||
|
// is unresolved no `gl*` call may be made at all, and `TextAtlas` is public, so a
|
||||||
|
// host may hold one and sync it before `plot::gl::load()` -- Tasks 2 and 8 build
|
||||||
|
// backends on this class. Every `gl*` call it makes is therefore gated, and a sync
|
||||||
|
// in that state reports the same `ErrorCode::Unsupported` every fallible
|
||||||
|
// `plot::gl` handle constructor reports, instead of calling a null pointer.
|
||||||
|
//
|
||||||
|
// `available()` itself cannot be driven false in this suite: `load()` is
|
||||||
|
// process-global and one-way (no API makes the table unresolved again) and it
|
||||||
|
// falls back to `dlsym` on the system GL library, so there is no state a test can
|
||||||
|
// restore the process to -- faking it would poison every later case's GL. What is
|
||||||
|
// reachable is the same guard from the entry point it protects: with the first
|
||||||
|
// entry point it calls missing, the sync must report and leave the atlas empty.
|
||||||
|
PLOT_TEST(text_atlas_reports_a_status_when_the_entry_points_are_missing) {
|
||||||
|
plottest::HeadlessContext ctx(kW, kH);
|
||||||
|
plot::Status st;
|
||||||
|
PLOT_CHECK(ctx.create(&st));
|
||||||
|
PLOT_CHECK(plot::gl::load(nullptr));
|
||||||
|
PLOT_CHECK(plot::gl::available());
|
||||||
|
|
||||||
|
plot::render::BitmapFont font;
|
||||||
|
plot::render::TextAtlas atlas;
|
||||||
|
{
|
||||||
|
RefuseGetIntegerv refused;
|
||||||
|
PLOT_CHECK(!atlas.sync(font, &st));
|
||||||
|
PLOT_CHECK(st.code == plot::ErrorCode::Unsupported);
|
||||||
|
PLOT_CHECK(st.message != nullptr);
|
||||||
|
PLOT_CHECK(atlas.empty());
|
||||||
|
PLOT_CHECK_NEAR(atlas.texel_size().x, 0.0, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(atlas.texel_size().y, 0.0, 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the table back the same atlas uploads, so the refusal was the missing
|
||||||
|
// entry point and not a poisoned owner, and the retry is what a caller does with
|
||||||
|
// the Status it got.
|
||||||
|
PLOT_CHECK(atlas.sync(font, &st));
|
||||||
|
PLOT_CHECK(!atlas.empty());
|
||||||
|
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(plot::render::BitmapFont::kAtlasWidth),
|
||||||
|
1e-6);
|
||||||
|
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user