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:
Martino Ferrari committed 2026-09-28 10:22:38 +02:00
1 parent e1f6b61b7d
commit 3ff7b9c978
5 files changed
+230 -16

No files matched your search

+15 -3
View File
@@ -48,10 +48,17 @@ class TextAtlas {
///
/// False, with `st` filled in, when the driver refuses the texture; the last
/// 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;
/// 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;
/// 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()`
/// would bind nothing.
bool empty() const noexcept { return texture_.id() == 0; }
/// binds nothing.
bool empty() const noexcept { return !has_atlas_; }
/// The revision of the atlas currently on the GPU. Meaningless before the
/// 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()`
/// is 0 is still uploaded once instead of being mistaken for "already there".
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