refactor(render): TextBackend owns its atlas, and staged glyphs carry a rotation basis

This commit is contained in:
Martino Ferrari committed 2026-09-28 09:57:28 +02:00
1 parent 90e17a0a11
commit e074f57ddf
23 files changed
+1073 -227

No files matched your search

+85 -5
View File
@@ -3,13 +3,70 @@
namespace plot::render {
/// Text metrics the plot layer lays axes and labels out with. It is a pure
/// interface so the layout code never depends on the bitmap font, and so a
/// caller can measure with a host toolkit's font instead.
/// 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.
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;
/// 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.** A staged glyph carries the *origin* of its rect
/// and the draw carries the rect's size, once, for every instance in it (see
/// `TextProgram::set_glyph_uv`) -- which is what keeps a glyph at six floats while
/// still letting it be rotated. So every glyph a backend publishes must occupy a
/// rect of the same size: its cell, with the ink inside it at `(xoff, yoff)`. A
/// proportional face therefore bakes each glyph into a cell wide enough for the
/// widest advance and spends the atlas area rather than the correctness; a
/// backend whose rects differ in size would have its glyphs read through
/// whichever rect was staged first.
class TextBackend {
public:
TextBackend() noexcept = default;
@@ -17,8 +74,8 @@ class TextBackend {
TextBackend& operator=(const TextBackend&) = default;
virtual ~TextBackend() = default;
/// Width of `utf8` in the same units as `height()` (pixels for the bitmap
/// font). Zero for the empty string.
/// 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.
@@ -27,6 +84,29 @@ class TextBackend {
/// 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.
virtual const Glyph* glyph(u32 codepoint) 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".
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