refactor(render): the atlas is a declared fixed-cell grid, and staging checks it
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.
This commit is contained in:
1 parent
e074f57ddf
commit
ffc060d6ca
11 files changed
+364
-43
No files matched your search
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
|
||||
namespace plot::render {
|
||||
|
||||
@@ -20,6 +23,12 @@ namespace plot::render {
|
||||
/// (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;
|
||||
@@ -35,6 +44,25 @@ struct Glyph {
|
||||
/// 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.
|
||||
///
|
||||
@@ -58,15 +86,10 @@ u32 next_codepoint(const char*& p) noexcept;
|
||||
/// 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.
|
||||
/// **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;
|
||||
@@ -90,10 +113,36 @@ class TextBackend {
|
||||
/// 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
|
||||
|
||||
Reference in new issue
Block a user