#pragma once #include #include #include 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