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.
93 lines
4.3 KiB
C++
93 lines
4.3 KiB
C++
#pragma once
|
|
#include <plot/core/status.hpp>
|
|
#include <plot/core/types.hpp>
|
|
#include <plot/gl/handle.hpp>
|
|
#include <plot/math/vec2.hpp>
|
|
#include <plot/render/text.hpp>
|
|
|
|
namespace plot::render {
|
|
|
|
/// The GPU side of a `TextBackend`'s atlas.
|
|
///
|
|
/// The atlas belongs to the backend, so the renderer does not decide what it
|
|
/// contains or how big it is: `sync()` uploads whatever `atlas_pixels()` points
|
|
/// at, as a single-channel `GL_R8` image of `atlas_width()` by `atlas_height()`,
|
|
/// and re-uploads exactly when the backend's `revision()` changes. Keying that
|
|
/// cache on the revision rather than on a flag set once is what makes a font
|
|
/// swap, or a re-bake at another size, visible -- a cached atlas left in place
|
|
/// draws the previous font's glyphs and nothing in the pixels says why.
|
|
///
|
|
/// It is also where the atlas upload's `GL_UNPACK_ALIGNMENT` excursion is
|
|
/// contained: `Texture::upload_r8` needs 1 for single-channel rows and leaves it
|
|
/// there, so every upload here saves the caller's value and puts it back --
|
|
/// whether it runs in `Renderer::init()` or in the middle of a frame, which is
|
|
/// why the renderer's frame no longer has to carry that value around one.
|
|
///
|
|
/// Nearest filtering, always: the shader reads the texel centre of a glyph cell,
|
|
/// and linear filtering would smear one cell into its neighbour.
|
|
class TextAtlas {
|
|
public:
|
|
TextAtlas() noexcept = default;
|
|
TextAtlas(const TextAtlas&) = delete;
|
|
TextAtlas& operator=(const TextAtlas&) = delete;
|
|
TextAtlas(TextAtlas&&) noexcept = default;
|
|
TextAtlas& operator=(TextAtlas&&) noexcept = default;
|
|
~TextAtlas() = default;
|
|
|
|
/// Brings the texture up to date with `backend`, and does nothing at all when
|
|
/// it already is: the same revision as the last successful upload means the
|
|
/// bytes on the GPU are the ones that revision describes. Any change counts,
|
|
/// including one that moves the revision backwards, so a backend that
|
|
/// re-bakes is never mistaken for one that did not.
|
|
///
|
|
/// A null atlas, or one with a zero width or height, is legal and means "no
|
|
/// text": nothing is uploaded, `empty()` stays true and the resulting draw
|
|
/// stages nothing. The revision is recorded even then, so a backend whose
|
|
/// atlas never appears is not re-checked every frame -- the contract is that a
|
|
/// backend which *starts* providing one bumps its revision.
|
|
///
|
|
/// 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). 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.
|
|
Vec2 texel_size() const noexcept {
|
|
return Vec2{static_cast<f32>(width_), static_cast<f32>(height_)};
|
|
}
|
|
|
|
/// True while no atlas is on the GPU: there is no text to draw, and `bind()`
|
|
/// 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.
|
|
u32 revision() const noexcept { return revision_; }
|
|
|
|
private:
|
|
gl::Texture texture_;
|
|
u32 revision_ = 0;
|
|
u32 width_ = 0;
|
|
u32 height_ = 0;
|
|
/// 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
|