The text pipeline stages a glyph's rect *origin* per instance and carries the rect's *size* once per draw, so a backend whose glyphs have differing rect sizes -- a TrueType packing trimmed to ink, which is Task 8's shape -- would have drawn every glyph at whichever size it happened to be told, with nothing downstream able to tell. Make that constraint structural rather than documented: - TextBackend::cell_uv() declares the one cell size, and the contract (an atlas is a fixed-cell grid; a proportional face draws its glyphs *into* a uniform cell instead of trimming each rect to its ink, and why the pipeline needs it) is stated on the atlas accessors. - glyph_is_cell() is the check; add_text()/set_cell() declare the cell from the backend, and the staging path asserts it for every glyph in a debug build. A release build draws on with the cell it was told, as before. - Renderer::flush_text() takes u_glyph_uv from the backend, not from the batch, so the draw's one rect size can never come from whichever glyph was staged first.
162 lines
8.1 KiB
C++
162 lines
8.1 KiB
C++
#pragma once
|
|
#include <cmath>
|
|
|
|
#include <plot/core/types.hpp>
|
|
#include <plot/math/vec2.hpp>
|
|
|
|
namespace plot::render {
|
|
|
|
/// One glyph's atlas rectangle and metrics, in font pixels.
|
|
///
|
|
/// **The rectangle is normalised, v grows downward.** `(u0, v0)` is the glyph's
|
|
/// top-left atlas texel and `(u1, v1)` its bottom-right, each divided by the
|
|
/// atlas' size -- so the atlas is stored with its top row first, exactly the row
|
|
/// order `atlas_pixels()` hands over, and a larger `v` is a row further down.
|
|
/// Text is laid out with the same handedness: a glyph's own +y runs from `v0`
|
|
/// toward `v1`, which on screen is downward when the glyph is unrotated. A
|
|
/// backend that rasterised its atlas bottom row first would draw every glyph
|
|
/// mirrored unless it divided `v` the other way, so the convention is stated
|
|
/// once, here, and every consumer -- the text shader's UVs, the vertex shader's
|
|
/// second cell axis, `TextBatch` -- uses it and nothing else.
|
|
///
|
|
/// `(xoff, yoff)` is the bearing from the pen to the glyph's top-left corner
|
|
/// (top-left origin, font pixels) and `advance` is how far the pen moves along
|
|
/// the baseline. Both are in font pixels, so a caller scaling a run multiplies
|
|
/// them by the same factor it draws the atlas with.
|
|
///
|
|
/// **The rect is the atlas cell**, not the ink box: every glyph of a backend
|
|
/// occupies one cell of one size, so `u1 - u0` and `v1 - v0` are the same pair for
|
|
/// every codepoint -- the pair `TextBackend::cell_uv()` reports. The ink sits
|
|
/// inside the cell at the bearing. See `cell_uv()` for why the pipeline needs
|
|
/// that, and `glyph_is_cell()` for the check that enforces it.
|
|
struct Glyph {
|
|
f32 u0 = 0.0f;
|
|
f32 v0 = 0.0f;
|
|
f32 u1 = 0.0f;
|
|
f32 v1 = 0.0f;
|
|
f32 xoff = 0.0f;
|
|
f32 yoff = 0.0f;
|
|
f32 advance = 0.0f;
|
|
};
|
|
|
|
/// A byte sequence that is not valid UTF-8, as reported by `next_codepoint`. It
|
|
/// is deliberately not a Unicode scalar value, so it can only map to whatever a
|
|
/// backend draws for "no glyph" rather than colliding with a real codepoint.
|
|
inline constexpr u32 kInvalidCodepoint = 0xFFFFFFFFu;
|
|
|
|
/// Tolerance for comparing two normalised atlas sizes. A backend derives a rect
|
|
/// size as `u1 - u0`, a subtraction of two rounded fractions, so a *correct* cell
|
|
/// comes back a few 1e-8 out; the smallest rect that is genuinely a different
|
|
/// size differs by one atlas texel, which is 1e-4 or coarser on any atlas a GPU
|
|
/// will hold. 1e-6 separates "the same cell, rounded twice" from "a different
|
|
/// rect" with orders of magnitude to spare on both sides.
|
|
inline constexpr f32 kAtlasCellTolerance = 1e-6f;
|
|
|
|
/// True when `glyph`'s rect is the atlas cell `cell_uv` -- the invariant an atlas
|
|
/// is required to keep (see `TextBackend::cell_uv`).
|
|
///
|
|
/// It is a function rather than an assertion inside one caller because it is the
|
|
/// check the staging path asserts for every glyph it stages, and a test has to be
|
|
/// able to drive it over a backend that breaks it without aborting the suite.
|
|
inline bool glyph_is_cell(const Glyph& glyph, Vec2 cell_uv) noexcept {
|
|
const auto near = [](f32 a, f32 b) { return std::fabs(a - b) <= kAtlasCellTolerance; };
|
|
return near(glyph.u1 - glyph.u0, cell_uv.x) && near(glyph.v1 - glyph.v0, cell_uv.y);
|
|
}
|
|
|
|
/// Decodes one UTF-8 sequence from `p` and advances `p` past it -- one byte when
|
|
/// the sequence is malformed, so a damaged label still terminates.
|
|
///
|
|
/// It belongs to this interface rather than to a font because it is what keeps a
|
|
/// backend's `measure()` and the glyphs the pipeline stages for the same string
|
|
/// in step: both walk the string with this decoder, so neither can disagree with
|
|
/// the other about where a codepoint starts.
|
|
u32 next_codepoint(const char*& p) noexcept;
|
|
|
|
/// The font a `Plot` measures with and the renderer draws with: metrics, plus the
|
|
/// coverage atlas the text pipeline samples.
|
|
///
|
|
/// Text is passed as UTF-8, not as a byte-per-character string: `plot::format_si`
|
|
/// emits U+00B5, U+00B0 and U+03A9 for micro, degree and ohm, and an
|
|
/// implementation that ignores that prints mojibake.
|
|
///
|
|
/// The atlas is the backend's own: it owns the pixels, the glyph table that
|
|
/// describes them and the `revision()` that says when they changed. The renderer
|
|
/// uploads whatever the backend exposes and re-uploads when the revision moves,
|
|
/// so a backend can be swapped -- or re-baked at another size -- without the
|
|
/// renderer knowing anything about fonts. `measure()`/`height()`/`ascent()` are
|
|
/// in the atlas' font pixels: the pipeline scales them, never re-rasterises.
|
|
///
|
|
/// **One atlas, one cell size**: the atlas is a *fixed-cell grid* and
|
|
/// `cell_uv()` is where that size is declared -- see it for the full contract
|
|
/// and for what a proportional face has to do to satisfy it. The staging path
|
|
/// asserts `glyph_is_cell()` for every glyph it stages in a debug build.
|
|
class TextBackend {
|
|
public:
|
|
TextBackend() noexcept = default;
|
|
TextBackend(const TextBackend&) = default;
|
|
TextBackend& operator=(const TextBackend&) = default;
|
|
virtual ~TextBackend() = default;
|
|
|
|
/// Width of `utf8` in the same units as `height()` (font pixels for a font
|
|
/// atlas). Zero for the empty string.
|
|
virtual f32 measure(const char* utf8) const = 0;
|
|
|
|
/// Line height: the cell a single line of text occupies.
|
|
virtual f32 height() const = 0;
|
|
|
|
/// Distance from the top of the cell to the baseline, so a caller can align
|
|
/// text bottoms instead of cell bottoms.
|
|
virtual f32 ascent() const = 0;
|
|
|
|
/// The glyph for `codepoint`, or null when this backend has none -- the caller
|
|
/// then draws nothing for it (a backend that wants a visible missing-glyph box
|
|
/// publishes one as the glyph for the codepoints it cannot render). The
|
|
/// returned pointer stays valid for the backend's lifetime, so callers may hold
|
|
/// it across a frame.
|
|
///
|
|
/// Its rect is the atlas **cell**, not the ink box: see `cell_uv()`.
|
|
virtual const Glyph* glyph(u32 codepoint) const = 0;
|
|
|
|
/// The atlas size of one cell, as a normalised rect size: `(u1 - u0, v1 - v0)`
|
|
/// of every glyph the atlas holds -- the size `glyph()` reports each of them
|
|
/// with. `(0, 0)` is legal and means "no text".
|
|
///
|
|
/// **An atlas is a fixed-cell grid, and this is where that is declared.** Each
|
|
/// glyph is drawn in a cell of this one size, because the pipeline stages a
|
|
/// glyph's rect *origin* per instance and carries the rect's *size* once for
|
|
/// the whole draw: six floats per glyph instead of eight, no per-glyph size in
|
|
/// the vertex shader, and rotation for two of those floats. The staging path
|
|
/// asserts `glyph_is_cell()` -- every staged glyph's rect against this value --
|
|
/// in a debug build, so a backend that breaks the contract fails loudly rather
|
|
/// than drawing every glyph at whichever size it happened to be told.
|
|
///
|
|
/// **For implementers:** a proportional face therefore draws its glyphs *into*
|
|
/// a uniform cell -- the ink placed inside it at `xoff`/`yoff`, the pen
|
|
/// advanced by `advance` -- rather than trimming each atlas rect to its ink,
|
|
/// and spends the atlas area instead of the correctness. The cell has to be
|
|
/// wide enough for the widest advance and tall enough for the tallest ink, and
|
|
/// this must agree with the rects `glyph()` reports; a backend whose rects are
|
|
/// ink boxes (a naive TrueType packing, which trims each bitmap to its ink)
|
|
/// violates it.
|
|
virtual Vec2 cell_uv() const = 0;
|
|
|
|
/// 8-bit single-channel coverage, row-major, `atlas_width() * atlas_height()`
|
|
/// bytes, top row first. Uploaded as GL_R8. Null is legal and means "no text".
|
|
/// The bytes are a grid of `cell_uv()`-sized cells.
|
|
virtual const u8* atlas_pixels() const = 0;
|
|
|
|
/// Atlas size in texels. Zero width or height means "no text", like a null
|
|
/// `atlas_pixels()`.
|
|
virtual u32 atlas_width() const = 0;
|
|
virtual u32 atlas_height() const = 0;
|
|
|
|
/// Bumped whenever `atlas_pixels()` content changes, so the renderer knows to
|
|
/// re-upload instead of trusting its cache. Must not change otherwise -- a
|
|
/// revision that moves on its own would re-upload the same bytes every frame --
|
|
/// and any change counts, so a backend may re-bake at a new size and keep
|
|
/// counting up.
|
|
virtual u32 revision() const = 0;
|
|
};
|
|
|
|
} // namespace plot::render
|