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

+36 -27
View File
@@ -12,22 +12,22 @@ namespace plot::render {
/// font file, FreeType or stb_truetype: `plot_core` stays dependency-free.
///
/// Metrics are proportional, not fixed width: an advance is the glyph's ink
/// width plus one pixel of spacing, floored at two pixels, computed once when
/// the font is constructed from the same bitmaps the atlas uploads. Whitespace
/// is a blank glyph with a nonzero advance, and every codepoint without a glyph
/// draws a visible box, so a missing glyph is obvious rather than silently
/// blank.
/// width plus one pixel of spacing, floored at two pixels, derived from the
/// same bitmaps the atlas uploads, so a second width table cannot drift from the
/// artwork. Whitespace is a blank glyph with a nonzero advance, and every
/// codepoint without a glyph draws a visible box, so a missing glyph is obvious
/// rather than silently blank.
///
/// Metrics are in unscaled font pixels, one per glyph texel. The text pipeline
/// draws a cell `u_px_range` pixels across, so a run at another size measures
/// `measure(utf8) * u_px_range.x / kGlyphPx`; `TextBatch::add_text` takes that
/// cell size and applies the same scale to the pen it advances.
/// Metrics are in unscaled font pixels, one per glyph texel, and the atlas is a
/// grid of `kGlyphPx` cells: every glyph's rect is its own cell, so a run drawn
/// at `s` pixels per font pixel measures `measure(utf8) * s` and sits in a cell
/// of `s * kGlyphPx` pixels. `TextBatch::add_text` takes that factor and applies
/// it to the pen it advances.
class BitmapFont final : public TextBackend {
public:
// Atlas geometry. It is part of the render pipeline's contract: the text
// vertex shader expands a glyph cell by `u_px_range` pixels and reads the cell
// at the instance's texel origin, so the atlas is a grid of 8x8 cells whose
// width must stay a whole number of cells.
// Atlas geometry: a grid of `kGlyphPx` cells whose width is a whole number of
// them. A glyph's rect is its own cell, normalised by these sizes, so the grid
// is all the text pipeline has to know about the atlas.
static constexpr u32 kGlyphPx = 8; ///< glyph cell edge, texels
static constexpr u32 kFirstChar = 32; ///< U+0020
static constexpr u32 kAsciiGlyphs = 95; ///< U+0020..U+007E
@@ -42,17 +42,7 @@ class BitmapFont final : public TextBackend {
static constexpr u32 kAtlasHeight = kAtlasRows * kGlyphPx; ///< 56 texels
static_assert(kAtlasCols * kAtlasRows >= kGlyphCount, "atlas too small for the glyph set");
/// Reported by `next_codepoint` for a byte sequence that is not valid UTF-8.
/// It is not a Unicode scalar value, so it can only map to the fallback box.
static 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. The
/// text batch and `measure` share it, which is what keeps a staged run exactly
/// `measure`-wide.
static u32 next_codepoint(const char*& p) noexcept;
BitmapFont() noexcept;
BitmapFont() noexcept = default;
/// Width of `utf8`, the sum of the glyph advances: zero for the empty string
/// and for null. The ASCII path allocates nothing.
@@ -75,6 +65,21 @@ class BitmapFont final : public TextBackend {
/// codepoints `format_si` emits map to real glyphs, and anything else maps to
/// the visible missing-glyph box.
static u32 glyph_index(u32 codepoint) noexcept;
/// The glyph for `codepoint`: its atlas cell as a normalised rect (see
/// `Glyph`), a zero bearing and its ink-derived advance. Never null -- a
/// codepoint with no artwork maps to the visible missing-glyph box, which is
/// what a label outside the coverage set should show.
const Glyph* glyph(u32 codepoint) const override;
/// The packed atlas, as `TextBackend` sees it: the bytes `atlas_image()`
/// returns, `kAtlasWidth` by `kAtlasHeight`, GL_R8-ready. Never null.
const u8* atlas_pixels() const override;
u32 atlas_width() const override;
u32 atlas_height() const override;
/// Always zero: the atlas is a compile-time constant, so it never changes and
/// the renderer uploads it once.
u32 revision() const override;
/// Rows in a glyph bitmap, always `kGlyphPx`.
u32 glyph_rows(char c) const noexcept;
@@ -90,12 +95,16 @@ class BitmapFont final : public TextBackend {
/// (straight alpha, no dilation or gamma applied).
static const u8* atlas_image() noexcept;
/// Texel origin of a glyph slot in the atlas; this is the per-instance `a_uv`
/// the text pipeline reads.
/// Texel origin of a glyph slot in the atlas, in texels; `glyph()` gives the
/// same rectangle normalised, which is what the text pipeline stages.
static Vec2 atlas_origin(u32 slot) noexcept;
private:
f32 advances_[kGlyphCount];
/// Every slot's rect, bearing and advance, derived from the bitmaps above at
/// compile time. This replaced a per-instance `advances_` array holding the
/// same numbers, so the advances the pen uses and the ones `glyph()` publishes
/// cannot drift apart.
static const Glyph* glyph_table() noexcept;
};
} // namespace plot::render