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
@@ -40,6 +40,13 @@ class BitmapFont final : public TextBackend {
|
|||||||
static constexpr u32 kAtlasRows = 7;
|
static constexpr u32 kAtlasRows = 7;
|
||||||
static constexpr u32 kAtlasWidth = kAtlasCols * kGlyphPx; ///< 128 texels
|
static constexpr u32 kAtlasWidth = kAtlasCols * kGlyphPx; ///< 128 texels
|
||||||
static constexpr u32 kAtlasHeight = kAtlasRows * kGlyphPx; ///< 56 texels
|
static constexpr u32 kAtlasHeight = kAtlasRows * kGlyphPx; ///< 56 texels
|
||||||
|
|
||||||
|
/// The atlas cell, normalised: one `kGlyphPx` square of the grid. Every glyph's
|
||||||
|
/// rect is this, and `cell_uv()` reports it -- one number, derived from the same
|
||||||
|
/// two constants `pack_atlas()` packs, so the grid the pipeline draws and the
|
||||||
|
/// grid the ink was written into cannot disagree.
|
||||||
|
static constexpr Vec2 kCellUV{static_cast<f32>(kGlyphPx) / static_cast<f32>(kAtlasWidth),
|
||||||
|
static_cast<f32>(kGlyphPx) / static_cast<f32>(kAtlasHeight)};
|
||||||
static_assert(kAtlasCols * kAtlasRows >= kGlyphCount, "atlas too small for the glyph set");
|
static_assert(kAtlasCols * kAtlasRows >= kGlyphCount, "atlas too small for the glyph set");
|
||||||
|
|
||||||
BitmapFont() noexcept = default;
|
BitmapFont() noexcept = default;
|
||||||
@@ -71,6 +78,9 @@ class BitmapFont final : public TextBackend {
|
|||||||
/// what a label outside the coverage set should show.
|
/// what a label outside the coverage set should show.
|
||||||
const Glyph* glyph(u32 codepoint) const override;
|
const Glyph* glyph(u32 codepoint) const override;
|
||||||
|
|
||||||
|
/// `kCellUV`: the 8x8 cell every glyph occupies.
|
||||||
|
Vec2 cell_uv() const override { return kCellUV; }
|
||||||
|
|
||||||
/// The packed atlas, as `TextBackend` sees it: the bytes `atlas_image()`
|
/// The packed atlas, as `TextBackend` sees it: the bytes `atlas_image()`
|
||||||
/// returns, `kAtlasWidth` by `kAtlasHeight`, GL_R8-ready. Never null.
|
/// returns, `kAtlasWidth` by `kAtlasHeight`, GL_R8-ready. Never null.
|
||||||
const u8* atlas_pixels() const override;
|
const u8* atlas_pixels() const override;
|
||||||
|
|||||||
@@ -61,15 +61,25 @@ class LineBatch {
|
|||||||
/// buffer.hpp).
|
/// buffer.hpp).
|
||||||
///
|
///
|
||||||
/// Every glyph a batch stages is read through one atlas rect of one size — its
|
/// Every glyph a batch stages is read through one atlas rect of one size — its
|
||||||
/// cell — so that size is recorded once (`glyph_cell_uv()`) and handed to the
|
/// cell (see `TextBackend::cell_uv`) — so that size is declared once
|
||||||
/// draw, rather than riding along with every instance.
|
/// (`set_cell()`) and handed to the draw, rather than riding along with every
|
||||||
|
/// instance. In a debug build every staged glyph is checked against it.
|
||||||
class TextBatch {
|
class TextBatch {
|
||||||
public:
|
public:
|
||||||
/// Starts a frame: empties the batch and the staged cell, keeps the capacity.
|
/// Starts a frame: empties the batch and forgets the declared cell, keeps the
|
||||||
|
/// capacity.
|
||||||
void begin() noexcept;
|
void begin() noexcept;
|
||||||
/// Same as `begin()`.
|
/// Same as `begin()`.
|
||||||
void clear() noexcept;
|
void clear() noexcept;
|
||||||
|
|
||||||
|
/// Declares the atlas cell the glyphs staged from here on are drawn in -- the
|
||||||
|
/// backend's `cell_uv()`. `add_text()` does this itself from the backend it is
|
||||||
|
/// handed; a caller staging glyphs one at a time says it here, so the draw's
|
||||||
|
/// single rect size comes from the backend rather than from whichever glyph was
|
||||||
|
/// staged first. A debug build asserts that no second, different cell arrives
|
||||||
|
/// in one batch.
|
||||||
|
void set_cell(Vec2 cell_uv) noexcept;
|
||||||
|
|
||||||
/// Stages one glyph cell whose *pen* is `pen_px`: the cell's top-left corner is
|
/// Stages one glyph cell whose *pen* is `pen_px`: the cell's top-left corner is
|
||||||
/// the pen moved by the glyph's bearing (see `Glyph`), scaled and measured
|
/// the pen moved by the glyph's bearing (see `Glyph`), scaled and measured
|
||||||
/// along the run's own axes, so a glyph keeps its place inside a rotated run.
|
/// along the run's own axes, so a glyph keeps its place inside a rotated run.
|
||||||
@@ -101,9 +111,11 @@ class TextBatch {
|
|||||||
/// Interleaved `x,y,u,v,ax,ay`, six floats per staged glyph.
|
/// Interleaved `x,y,u,v,ax,ay`, six floats per staged glyph.
|
||||||
const std::vector<f32>& data() const noexcept { return v_; }
|
const std::vector<f32>& data() const noexcept { return v_; }
|
||||||
|
|
||||||
/// The staged glyphs' cell in the atlas, normalised: `(u1 - u0, v1 - v0)` of
|
/// The cell the staged glyphs are drawn in: what `set_cell()` declared, or --
|
||||||
/// the glyphs staged, which the draw passes as `u_glyph_uv` (see
|
/// for a caller that stages raw glyphs without ever declaring one -- the rect
|
||||||
/// `TextProgram::set_glyph_uv`). `(0, 0)` while nothing is staged.
|
/// size of the first glyph staged, since nothing else can be the authority
|
||||||
|
/// there. The renderer declares the cell from the backend it draws with, so it
|
||||||
|
/// never depends on that fallback. `(0, 0)` while nothing is staged.
|
||||||
Vec2 glyph_cell_uv() const noexcept { return cell_uv_; }
|
Vec2 glyph_cell_uv() const noexcept { return cell_uv_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -113,6 +125,10 @@ class TextBatch {
|
|||||||
|
|
||||||
std::vector<f32> v_;
|
std::vector<f32> v_;
|
||||||
Vec2 cell_uv_{};
|
Vec2 cell_uv_{};
|
||||||
|
/// False until a cell arrives -- from `set_cell()`, or from the first glyph
|
||||||
|
/// staged by a caller that declared none. It keeps a zero-sized cell
|
||||||
|
/// distinguishable from "nothing said yet".
|
||||||
|
bool cell_declared_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace plot::render
|
} // namespace plot::render
|
||||||
@@ -52,9 +52,9 @@ class TextProgram {
|
|||||||
/// height -- a cell need not be square).
|
/// height -- a cell need not be square).
|
||||||
void set_px_range(Vec2 px) const noexcept;
|
void set_px_range(Vec2 px) const noexcept;
|
||||||
/// `u_glyph_uv`: the glyph cell's size in the atlas, normalised. It is the
|
/// `u_glyph_uv`: the glyph cell's size in the atlas, normalised. It is the
|
||||||
/// rect every staged glyph is read through, so it is one size per draw: a
|
/// rect every staged glyph is read through, so it is one size per draw -- and
|
||||||
/// batch stages glyphs from one atlas, and `TextBatch::glyph_cell_uv()` is
|
/// it is the *backend's* (`TextBackend::cell_uv`), never a glyph's, because a
|
||||||
/// where it comes from.
|
/// draw may stage glyphs whose origins differ even when their size cannot.
|
||||||
void set_glyph_uv(Vec2 uv) const noexcept;
|
void set_glyph_uv(Vec2 uv) const noexcept;
|
||||||
/// `u_tex_size`: atlas size in texels.
|
/// `u_tex_size`: atlas size in texels.
|
||||||
void set_texture_size(Vec2 texels) const noexcept;
|
void set_texture_size(Vec2 texels) const noexcept;
|
||||||
|
|||||||
@@ -197,6 +197,11 @@ class Renderer {
|
|||||||
/// wrapper until the next call, which is enough for a caller measuring or
|
/// wrapper until the next call, which is enough for a caller measuring or
|
||||||
/// laying out one glyph at a time.
|
/// laying out one glyph at a time.
|
||||||
const render::Glyph* glyph(u32 codepoint) const override;
|
const render::Glyph* glyph(u32 codepoint) const override;
|
||||||
|
/// The font's cell: the atlas is the font's own, and a cell's size is a
|
||||||
|
/// property of the atlas, not of the size text is drawn at, so this forwards
|
||||||
|
/// unchanged -- which is what makes it the authoritative value the staging
|
||||||
|
/// path declares and the draw is told.
|
||||||
|
Vec2 cell_uv() const override;
|
||||||
const u8* atlas_pixels() const override;
|
const u8* atlas_pixels() const override;
|
||||||
u32 atlas_width() const override;
|
u32 atlas_width() const override;
|
||||||
u32 atlas_height() const override;
|
u32 atlas_height() const override;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
#include <plot/core/types.hpp>
|
#include <plot/core/types.hpp>
|
||||||
|
#include <plot/math/vec2.hpp>
|
||||||
|
|
||||||
namespace plot::render {
|
namespace plot::render {
|
||||||
|
|
||||||
@@ -20,6 +23,12 @@ namespace plot::render {
|
|||||||
/// (top-left origin, font pixels) and `advance` is how far the pen moves along
|
/// (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
|
/// the baseline. Both are in font pixels, so a caller scaling a run multiplies
|
||||||
/// them by the same factor it draws the atlas with.
|
/// 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 {
|
struct Glyph {
|
||||||
f32 u0 = 0.0f;
|
f32 u0 = 0.0f;
|
||||||
f32 v0 = 0.0f;
|
f32 v0 = 0.0f;
|
||||||
@@ -35,6 +44,25 @@ struct Glyph {
|
|||||||
/// backend draws for "no glyph" rather than colliding with a real codepoint.
|
/// backend draws for "no glyph" rather than colliding with a real codepoint.
|
||||||
inline constexpr u32 kInvalidCodepoint = 0xFFFFFFFFu;
|
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
|
/// 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 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
|
/// renderer knowing anything about fonts. `measure()`/`height()`/`ascent()` are
|
||||||
/// in the atlas' font pixels: the pipeline scales them, never re-rasterises.
|
/// 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
|
/// **One atlas, one cell size**: the atlas is a *fixed-cell grid* and
|
||||||
/// and the draw carries the rect's size, once, for every instance in it (see
|
/// `cell_uv()` is where that size is declared -- see it for the full contract
|
||||||
/// `TextProgram::set_glyph_uv`) -- which is what keeps a glyph at six floats while
|
/// and for what a proportional face has to do to satisfy it. The staging path
|
||||||
/// still letting it be rotated. So every glyph a backend publishes must occupy a
|
/// asserts `glyph_is_cell()` for every glyph it stages in a debug build.
|
||||||
/// 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 {
|
class TextBackend {
|
||||||
public:
|
public:
|
||||||
TextBackend() noexcept = default;
|
TextBackend() noexcept = default;
|
||||||
@@ -90,10 +113,36 @@ class TextBackend {
|
|||||||
/// publishes one as the glyph for the codepoints it cannot render). The
|
/// 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
|
/// returned pointer stays valid for the backend's lifetime, so callers may hold
|
||||||
/// it across a frame.
|
/// 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;
|
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()`
|
/// 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".
|
/// 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;
|
virtual const u8* atlas_pixels() const = 0;
|
||||||
|
|
||||||
/// Atlas size in texels. Zero width or height means "no text", like a null
|
/// Atlas size in texels. Zero width or height means "no text", like a null
|
||||||
|
|||||||
@@ -166,23 +166,20 @@ constexpr u32 ink_width(const u8* rows) noexcept {
|
|||||||
return any ? hi - lo + 1u : 0u;
|
return any ? hi - lo + 1u : 0u;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The glyph table the pipeline stages from: one entry per slot, its rect
|
/// The glyph table the pipeline stages from: one entry per slot, its rect at the
|
||||||
/// normalised by the atlas size with `v` growing downward (see `Glyph`), a zero
|
/// slot's place in the grid and one cell big -- the cell `cell_uv()` declares,
|
||||||
/// bearing -- the ink sits at the cell's top-left -- and the ink-derived advance.
|
/// so a glyph's rect and the size the draw is told cannot disagree -- a zero
|
||||||
/// It is built from the same two tables `pack_atlas()` packs, so the rect a glyph
|
/// bearing (the ink sits at the cell's top-left) and the ink-derived advance.
|
||||||
/// reports and the cell its ink was written to cannot disagree.
|
/// Built from the same tables `pack_atlas()` packs.
|
||||||
constexpr std::array<Glyph, BitmapFont::kGlyphCount> build_glyphs() noexcept {
|
constexpr std::array<Glyph, BitmapFont::kGlyphCount> build_glyphs() noexcept {
|
||||||
std::array<Glyph, BitmapFont::kGlyphCount> glyphs{};
|
std::array<Glyph, BitmapFont::kGlyphCount> glyphs{};
|
||||||
constexpr f32 kCell = static_cast<f32>(BitmapFont::kGlyphPx);
|
|
||||||
constexpr f32 kU = kCell / static_cast<f32>(BitmapFont::kAtlasWidth);
|
|
||||||
constexpr f32 kV = kCell / static_cast<f32>(BitmapFont::kAtlasHeight);
|
|
||||||
for (u32 slot = 0; slot < BitmapFont::kGlyphCount; ++slot) {
|
for (u32 slot = 0; slot < BitmapFont::kGlyphCount; ++slot) {
|
||||||
const f32 u0 = static_cast<f32>(slot % BitmapFont::kAtlasCols) * kU;
|
const f32 u0 = static_cast<f32>(slot % BitmapFont::kAtlasCols) * BitmapFont::kCellUV.x;
|
||||||
const f32 v0 = static_cast<f32>(slot / BitmapFont::kAtlasCols) * kV;
|
const f32 v0 = static_cast<f32>(slot / BitmapFont::kAtlasCols) * BitmapFont::kCellUV.y;
|
||||||
const u32 ink = ink_width(kGlyphBits[slot]);
|
const u32 ink = ink_width(kGlyphBits[slot]);
|
||||||
const u32 spaced = ink + 1u; // one pixel of side bearing, floored at two
|
const u32 spaced = ink + 1u; // one pixel of side bearing, floored at two
|
||||||
glyphs[slot] = Glyph{u0, v0, u0 + kU, v0 + kV, 0.0f, 0.0f,
|
glyphs[slot] = Glyph{u0, v0, u0 + BitmapFont::kCellUV.x, v0 + BitmapFont::kCellUV.y, 0.0f,
|
||||||
static_cast<f32>(spaced < 2u ? 2u : spaced)};
|
0.0f, static_cast<f32>(spaced < 2u ? 2u : spaced)};
|
||||||
}
|
}
|
||||||
return glyphs;
|
return glyphs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
#include <plot/render/primitives.hpp>
|
#include <plot/render/primitives.hpp>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
namespace plot::render {
|
namespace plot::render {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
/// Two declared cells agree when neither size differs by more than the atlas'
|
||||||
|
/// tolerance: the test `glyph_is_cell()` makes, for a cell against a cell.
|
||||||
|
bool same_cell(Vec2 a, Vec2 b) noexcept {
|
||||||
|
const auto near = [](f32 p, f32 q) { return std::fabs(p - q) <= kAtlasCellTolerance; };
|
||||||
|
return near(a.x, b.x) && near(a.y, b.y);
|
||||||
|
}
|
||||||
|
|
||||||
/// `axis` as a unit vector: the cell's local +x, which is what the instance and
|
/// `axis` as a unit vector: the cell's local +x, which is what the instance and
|
||||||
/// the shader both take (see `TextInstance`). A zero, infinite or NaN direction
|
/// the shader both take (see `TextInstance`). A zero, infinite or NaN direction
|
||||||
/// means "unrotated" -- a caller's arithmetic slip must not turn a label into NaN
|
/// means "unrotated" -- a caller's arithmetic slip must not turn a label into NaN
|
||||||
@@ -62,14 +70,41 @@ void LineBatch::add_polyline_xy(const f32* xy, u32 n) {
|
|||||||
void TextBatch::begin() noexcept {
|
void TextBatch::begin() noexcept {
|
||||||
v_.clear();
|
v_.clear();
|
||||||
cell_uv_ = Vec2{};
|
cell_uv_ = Vec2{};
|
||||||
|
cell_declared_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextBatch::clear() noexcept {
|
void TextBatch::clear() noexcept {
|
||||||
v_.clear();
|
v_.clear();
|
||||||
cell_uv_ = Vec2{};
|
cell_uv_ = Vec2{};
|
||||||
|
cell_declared_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBatch::set_cell(Vec2 cell_uv) noexcept {
|
||||||
|
// One cell per draw (see TextBackend::cell_uv): the draw carries a single
|
||||||
|
// `u_glyph_uv`, so a second, different cell inside one batch would hand it to
|
||||||
|
// glyphs staged for another size. A debug build says so; a release build takes
|
||||||
|
// the last one, as it takes the rest of its state.
|
||||||
|
assert((!cell_declared_ || same_cell(cell_uv_, cell_uv)) &&
|
||||||
|
"plot::render::TextBatch: a second, different atlas cell in one batch");
|
||||||
|
cell_uv_ = cell_uv;
|
||||||
|
cell_declared_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextBatch::add_cell(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 unit_ux) {
|
void TextBatch::add_cell(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 unit_ux) {
|
||||||
|
if (!cell_declared_) {
|
||||||
|
// Nobody declared a cell: a caller staging raw glyphs, where the glyph's own
|
||||||
|
// rect is the only authority there is. `add_text` and the renderer both
|
||||||
|
// declare one from the backend, so the draw never depends on this.
|
||||||
|
cell_uv_ = Vec2{glyph.u1 - glyph.u0, glyph.v1 - glyph.v0};
|
||||||
|
cell_declared_ = true;
|
||||||
|
}
|
||||||
|
// A fixed-cell grid (see TextBackend::cell_uv): an instance carries the rect's
|
||||||
|
// *origin*, so a glyph whose rect is not this cell would be drawn at the wrong
|
||||||
|
// size -- and read past its own row of the atlas doing it -- with nothing
|
||||||
|
// downstream able to tell. Loud in a debug build; a release build draws on with
|
||||||
|
// the cell it was told, which is what it did before this check existed.
|
||||||
|
assert(glyph_is_cell(glyph, cell_uv_) &&
|
||||||
|
"plot::render::TextBatch: staged glyph's rect is not the atlas cell");
|
||||||
// The cell's own axes: the run direction and its perpendicular, which is the
|
// The cell's own axes: the run direction and its perpendicular, which is the
|
||||||
// cell's +v axis -- the one kTextVS builds the quad from. The bearing is in
|
// cell's +v axis -- the one kTextVS builds the quad from. The bearing is in
|
||||||
// font pixels, so it scales with the run and follows the same axes: a glyph
|
// font pixels, so it scales with the run and follows the same axes: a glyph
|
||||||
@@ -82,10 +117,6 @@ void TextBatch::add_cell(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 unit_ux)
|
|||||||
v_.push_back(glyph.v0);
|
v_.push_back(glyph.v0);
|
||||||
v_.push_back(unit_ux.x);
|
v_.push_back(unit_ux.x);
|
||||||
v_.push_back(unit_ux.y);
|
v_.push_back(unit_ux.y);
|
||||||
// One cell for the whole batch: every glyph a batch stages comes from the same
|
|
||||||
// atlas, so the rect the shader reads is the one staged here (see
|
|
||||||
// glyph_cell_uv()).
|
|
||||||
cell_uv_ = Vec2{glyph.u1 - glyph.u0, glyph.v1 - glyph.v0};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextBatch::add_glyph(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 axis) {
|
void TextBatch::add_glyph(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 axis) {
|
||||||
@@ -97,6 +128,10 @@ Vec2 TextBatch::add_text(const char* utf8, Vec2 pen_px, f32 px, const TextBacken
|
|||||||
if (utf8 == nullptr) {
|
if (utf8 == nullptr) {
|
||||||
return pen_px;
|
return pen_px;
|
||||||
}
|
}
|
||||||
|
// The cell is the backend's, declared once for the whole run: the staged
|
||||||
|
// instances carry origins only, and the draw carries this size (see
|
||||||
|
// TextBackend::cell_uv).
|
||||||
|
set_cell(font.cell_uv());
|
||||||
const Vec2 ux = unit_axis(axis);
|
const Vec2 ux = unit_axis(axis);
|
||||||
for (const char* p = utf8; *p != '\0';) {
|
for (const char* p = utf8; *p != '\0';) {
|
||||||
// The backend's own codepoints and the backend's own advances -- the same
|
// The backend's own codepoints and the backend's own advances -- the same
|
||||||
|
|||||||
+11
-5
@@ -136,6 +136,8 @@ const render::Glyph* Renderer::AxisFont::glyph(u32 codepoint) const {
|
|||||||
return &scaled_;
|
return &scaled_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vec2 Renderer::AxisFont::cell_uv() const { return font_ != nullptr ? font_->cell_uv() : Vec2{}; }
|
||||||
|
|
||||||
const u8* Renderer::AxisFont::atlas_pixels() const {
|
const u8* Renderer::AxisFont::atlas_pixels() const {
|
||||||
return font_ != nullptr ? font_->atlas_pixels() : nullptr;
|
return font_ != nullptr ? font_->atlas_pixels() : nullptr;
|
||||||
}
|
}
|
||||||
@@ -262,6 +264,10 @@ void Renderer::draw(Plot& plot) {
|
|||||||
// measure with the size the text pipeline draws with, and re-read the style
|
// measure with the size the text pipeline draws with, and re-read the style
|
||||||
// each frame so a changed axis_font_px takes effect on the next one.
|
// each frame so a changed axis_font_px takes effect on the next one.
|
||||||
font_backend_.reset(font_, style_.axis_font_px);
|
font_backend_.reset(font_, style_.axis_font_px);
|
||||||
|
// The cell every glyph staged this frame is drawn in, declared from the backend
|
||||||
|
// rather than left to whichever glyph is staged first: the instances carry rect
|
||||||
|
// origins, and the draw carries this size (see TextBackend::cell_uv).
|
||||||
|
glyphs_.set_cell(font_backend_.cell_uv());
|
||||||
// A backend whose revision moved -- a font swapped, or re-baked at another size
|
// A backend whose revision moved -- a font swapped, or re-baked at another size
|
||||||
// -- is re-uploaded here, before anything is staged from it, so the frame draws
|
// -- is re-uploaded here, before anything is staged from it, so the frame draws
|
||||||
// its glyphs from the atlas they were baked into. The first sync happened in
|
// its glyphs from the atlas they were baked into. The first sync happened in
|
||||||
@@ -385,11 +391,11 @@ void Renderer::flush_text() noexcept {
|
|||||||
upload_text_vertices(glyphs_.data());
|
upload_text_vertices(glyphs_.data());
|
||||||
text_program_.bind();
|
text_program_.bind();
|
||||||
text_program_.set_viewport({viewport_.w, viewport_.h});
|
text_program_.set_viewport({viewport_.w, viewport_.h});
|
||||||
// The cell comes from the glyphs staged and the atlas they came from -- never
|
// The cell comes from the backend and the atlas it uploaded -- never from a
|
||||||
// from a constant -- so a font swap moves the quads with the artwork:
|
// constant, and never from whichever glyph happened to be staged first -- so a
|
||||||
// `glyph_uv` is the rect every instance is read through and `px_range` is that
|
// font swap moves the quads with the artwork: `glyph_uv` is the rect every
|
||||||
// same cell on screen.
|
// instance is read through and `px_range` is that same cell on screen.
|
||||||
const Vec2 cell_uv = glyphs_.glyph_cell_uv();
|
const Vec2 cell_uv = font_backend_.cell_uv();
|
||||||
const Vec2 atlas_texels = atlas_.texel_size();
|
const Vec2 atlas_texels = atlas_.texel_size();
|
||||||
text_program_.set_glyph_uv(cell_uv);
|
text_program_.set_glyph_uv(cell_uv);
|
||||||
text_program_.set_px_range({cell_uv.x * atlas_texels.x * font_backend_.scale(),
|
text_program_.set_px_range({cell_uv.x * atlas_texels.x * font_backend_.scale(),
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ PLOT_TEST(text_pipeline_draws_a_glyph) {
|
|||||||
program.bind();
|
program.bind();
|
||||||
program.set_viewport({static_cast<f32>(ctx.width), static_cast<f32>(ctx.height)});
|
program.set_viewport({static_cast<f32>(ctx.width), static_cast<f32>(ctx.height)});
|
||||||
program.set_px_range({static_cast<f32>(BitmapFont::kGlyphPx), static_cast<f32>(BitmapFont::kGlyphPx)});
|
program.set_px_range({static_cast<f32>(BitmapFont::kGlyphPx), static_cast<f32>(BitmapFont::kGlyphPx)});
|
||||||
program.set_glyph_uv(batch.glyph_cell_uv());
|
program.set_glyph_uv(font.cell_uv());
|
||||||
program.set_texture_size(atlas.texel_size());
|
program.set_texture_size(atlas.texel_size());
|
||||||
program.set_color(plot::Color::hex(0xFF00FF));
|
program.set_color(plot::Color::hex(0xFF00FF));
|
||||||
atlas.bind();
|
atlas.bind();
|
||||||
|
|||||||
@@ -328,3 +328,159 @@ PLOT_TEST(text_batch_advances_along_its_basis) {
|
|||||||
PLOT_CHECK_NEAR(b.data()[4], 0.6, 1e-5);
|
PLOT_CHECK_NEAR(b.data()[4], 0.6, 1e-5);
|
||||||
PLOT_CHECK_NEAR(b.data()[5], 0.8, 1e-5);
|
PLOT_CHECK_NEAR(b.data()[5], 0.8, 1e-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A backend whose atlas is *not* a fixed-cell grid: it declares the cell its
|
||||||
|
/// pipeline needs and then publishes one glyph trimmed to its ink, which is what
|
||||||
|
/// a naive TrueType packing produces (`stbtt_GetCodepointBitmap` hands back an ink
|
||||||
|
/// box, not a cell). The pipeline cannot draw that glyph correctly -- a staged
|
||||||
|
/// instance carries the rect's origin, and the draw carries one size for the whole
|
||||||
|
/// run -- so the staging path asserts `glyph_is_cell()` for every glyph it stages.
|
||||||
|
class TrimmedCellBackend final : public plot::render::TextBackend {
|
||||||
|
public:
|
||||||
|
plot::f32 measure(const char*) const override { return 8.0f; }
|
||||||
|
plot::f32 height() const override { return 8.0f; }
|
||||||
|
plot::f32 ascent() const override { return 8.0f; }
|
||||||
|
plot::Vec2 cell_uv() const override { return plot::Vec2{0.5f, 0.25f}; }
|
||||||
|
plot::u32 revision() const override { return 1u; } // baked once, never re-baked
|
||||||
|
const plot::render::Glyph* glyph(plot::u32 codepoint) const override {
|
||||||
|
// 'i' is trimmed to half a cell wide, as an ink box would be.
|
||||||
|
return codepoint == static_cast<plot::u32>('i') ? &trimmed_ : &cell_;
|
||||||
|
}
|
||||||
|
const plot::u8* atlas_pixels() const override { return atlas_; }
|
||||||
|
plot::u32 atlas_width() const override { return 8u; }
|
||||||
|
plot::u32 atlas_height() const override { return 4u; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Two cells of a 2x2 grid of 0.5 x 0.25, one of them trimmed to the ink.
|
||||||
|
const plot::render::Glyph cell_{0.0f, 0.0f, 0.5f, 0.25f, 0.0f, 0.0f, 4.0f};
|
||||||
|
const plot::render::Glyph trimmed_{0.5f, 0.0f, 0.6875f, 0.25f, 0.0f, 0.0f, 4.0f};
|
||||||
|
const plot::u8 atlas_[8 * 4] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// The fixed-cell contract, and the check that enforces it. A backend that trims
|
||||||
|
// each atlas rect to its ink -- the shape a TrueType packing produces, and the
|
||||||
|
// reason the contract is stated on `TextBackend::cell_uv()` -- publishes glyphs
|
||||||
|
// the pipeline would draw at the wrong size: the instance carries the rect's
|
||||||
|
// *origin* only, so a 3/8-cell rect would be read as a half cell, past its own
|
||||||
|
// rows.
|
||||||
|
//
|
||||||
|
// This drives the predicate the staging path asserts, rather than staging the
|
||||||
|
// offending glyph, because `add_text()` with this backend aborts the test binary
|
||||||
|
// in a debug build -- which is the point of the assertion.
|
||||||
|
PLOT_TEST(a_trimmed_glyph_is_not_the_atlas_cell) {
|
||||||
|
TrimmedCellBackend backend;
|
||||||
|
const plot::Vec2 cell = backend.cell_uv();
|
||||||
|
PLOT_CHECK_NEAR(cell.x, 0.5, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(cell.y, 0.25, 1e-6);
|
||||||
|
|
||||||
|
// The glyph that occupies a whole cell passes ...
|
||||||
|
PLOT_CHECK(plot::render::glyph_is_cell(*backend.glyph(static_cast<plot::u32>('A')), cell));
|
||||||
|
// ... and the one trimmed to its ink does not: this is what the staging path
|
||||||
|
// asserts in a debug build.
|
||||||
|
PLOT_CHECK(!plot::render::glyph_is_cell(*backend.glyph(static_cast<plot::u32>('i')), cell));
|
||||||
|
|
||||||
|
// The check is per axis and per glyph, not a single "is it square" test: a rect
|
||||||
|
// that matches on one axis and not the other is still not the cell.
|
||||||
|
plot::render::Glyph wide = *backend.glyph(static_cast<plot::u32>('A'));
|
||||||
|
wide.u1 = wide.u0 + 0.75f;
|
||||||
|
PLOT_CHECK(!plot::render::glyph_is_cell(wide, cell));
|
||||||
|
plot::render::Glyph tall = *backend.glyph(static_cast<plot::u32>('A'));
|
||||||
|
tall.v1 = tall.v0 + 0.5f;
|
||||||
|
PLOT_CHECK(!plot::render::glyph_is_cell(tall, cell));
|
||||||
|
|
||||||
|
// Rounding a correct cell through the atlas arithmetic twice is not a failure:
|
||||||
|
// a rect is a subtraction of two rounded fractions, and the tolerance is two
|
||||||
|
// orders of magnitude above that slop and below one texel.
|
||||||
|
plot::render::Glyph rounded = *backend.glyph(static_cast<plot::u32>('A'));
|
||||||
|
rounded.u1 = rounded.u0 + cell.x + 1e-7f;
|
||||||
|
PLOT_CHECK(plot::render::glyph_is_cell(rounded, cell));
|
||||||
|
|
||||||
|
// A real font's grid holds for every glyph it publishes, not just the first.
|
||||||
|
plot::render::BitmapFont font;
|
||||||
|
for (plot::u32 codepoint = 32u; codepoint < 127u; ++codepoint) {
|
||||||
|
const plot::render::Glyph* g = font.glyph(codepoint);
|
||||||
|
PLOT_CHECK(g != nullptr);
|
||||||
|
PLOT_CHECK(plot::render::glyph_is_cell(*g, font.cell_uv()));
|
||||||
|
}
|
||||||
|
PLOT_CHECK_NEAR(font.cell_uv().x, static_cast<plot::f32>(plot::render::BitmapFont::kGlyphPx) /
|
||||||
|
static_cast<plot::f32>(plot::render::BitmapFont::kAtlasWidth),
|
||||||
|
1e-9);
|
||||||
|
PLOT_CHECK_NEAR(font.cell_uv().y, static_cast<plot::f32>(plot::render::BitmapFont::kGlyphPx) /
|
||||||
|
static_cast<plot::f32>(plot::render::BitmapFont::kAtlasHeight),
|
||||||
|
1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A backend whose cell is nothing like the bitmap font's -- half by a quarter of
|
||||||
|
/// a small atlas -- so anything a batch reports that equals it came from the
|
||||||
|
/// backend and not from a glyph of some other font.
|
||||||
|
class HalfCellBackend final : public plot::render::TextBackend {
|
||||||
|
public:
|
||||||
|
plot::f32 measure(const char*) const override { return 4.0f; }
|
||||||
|
plot::f32 height() const override { return 8.0f; }
|
||||||
|
plot::f32 ascent() const override { return 6.0f; }
|
||||||
|
plot::Vec2 cell_uv() const override { return plot::Vec2{0.5f, 0.25f}; }
|
||||||
|
plot::u32 revision() const override { return 1u; }
|
||||||
|
const plot::render::Glyph* glyph(plot::u32) const override { return &cell_; }
|
||||||
|
const plot::u8* atlas_pixels() const override { return atlas_; }
|
||||||
|
plot::u32 atlas_width() const override { return 8u; }
|
||||||
|
plot::u32 atlas_height() const override { return 4u; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// One cell, at the second slot of a 2x2 grid. Its rect is the declared cell to
|
||||||
|
// within the atlas tolerance (5e-7) but not bit for bit -- which is what any
|
||||||
|
// backend's rounded arithmetic looks like, and what makes "the backend's
|
||||||
|
// number" and "this glyph's rect size" two distinguishable values.
|
||||||
|
const plot::render::Glyph cell_{0.5f, 0.0f, 1.0000005f, 0.2500005f, 0.0f, 0.0f, 4.0f};
|
||||||
|
const plot::u8 atlas_[8 * 4] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// The draw's one rect size is the *backend's* `cell_uv()`, and it is known before
|
||||||
|
// a glyph is staged: a batch that took it from whatever glyph it happened to
|
||||||
|
// stage first could not answer the first of these, and would take another font's
|
||||||
|
// grid the moment a caller staged two backends into one batch. `add_text()`
|
||||||
|
// declares it from the backend it is handed, and `set_cell()` is how a caller
|
||||||
|
// staging glyphs one at a time -- the renderer's clipped walk -- says it.
|
||||||
|
PLOT_TEST(text_batch_takes_its_cell_from_the_backend) {
|
||||||
|
HalfCellBackend backend;
|
||||||
|
plot::render::TextBatch b;
|
||||||
|
|
||||||
|
b.begin();
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().x, 0.0, 1e-6); // nothing declared, nothing staged
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().y, 0.0, 1e-6);
|
||||||
|
b.set_cell(backend.cell_uv());
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().x, 0.5, 1e-6); // declared, still nothing staged
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().y, 0.25, 1e-6);
|
||||||
|
|
||||||
|
// A staged run keeps it -- and keeps the *backend's* number, not the staged
|
||||||
|
// glyph's: this backend's rects are 5e-7 off its declared cell (inside the
|
||||||
|
// tolerance, so it is a legal grid), and an implementation that recorded
|
||||||
|
// whatever glyph it staged first would land off these by exactly that.
|
||||||
|
b.begin();
|
||||||
|
const plot::Vec2 end = b.add_text("AB", {0.0f, 0.0f}, 1.0f, backend);
|
||||||
|
PLOT_CHECK(b.glyph_count() == 2);
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().x, backend.cell_uv().x, 1e-12);
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().y, backend.cell_uv().y, 1e-12);
|
||||||
|
PLOT_CHECK(backend.cell_uv().x != backend.glyph(0u)->u1 - backend.glyph(0u)->u0);
|
||||||
|
PLOT_CHECK(plot::render::glyph_is_cell(*backend.glyph(0u), b.glyph_cell_uv()));
|
||||||
|
PLOT_CHECK_NEAR(end.x, 2.0f * backend.glyph(static_cast<plot::u32>('A'))->advance, 1e-6);
|
||||||
|
// The instances carry the cell's *origin* in the atlas; the size rides along
|
||||||
|
// once, above, which is the whole reason a glyph fits in six floats.
|
||||||
|
PLOT_CHECK_NEAR(b.data()[2], backend.glyph(0u)->u0, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(b.data()[3], backend.glyph(0u)->v0, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(b.data()[4], 1.0, 1e-6); // unrotated basis
|
||||||
|
|
||||||
|
// And the real font's grid, staged the same way, reports the font's cell -- the
|
||||||
|
// two never mix silently, and the batch's value tracks the backend it was given.
|
||||||
|
plot::render::BitmapFont font;
|
||||||
|
b.begin();
|
||||||
|
b.add_text("A", {0.0f, 0.0f}, 1.0f, font);
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().x, font.cell_uv().x, 1e-9);
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().y, font.cell_uv().y, 1e-9);
|
||||||
|
PLOT_CHECK(b.glyph_cell_uv().x != backend.cell_uv().x);
|
||||||
|
|
||||||
|
// begin() forgets the declaration along with the instances, so a frame cannot
|
||||||
|
// inherit the previous one's cell.
|
||||||
|
b.begin();
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().x, 0.0, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(b.glyph_cell_uv().y, 0.0, 1e-6);
|
||||||
|
}
|
||||||
@@ -15,7 +15,11 @@
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <csignal>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -129,6 +133,7 @@ class OnePxPerStringBackend final : public plot::render::TextBackend {
|
|||||||
plot::f32 height() const override { return 1.0f; }
|
plot::f32 height() const override { return 1.0f; }
|
||||||
plot::f32 ascent() const override { return 1.0f; }
|
plot::f32 ascent() const override { return 1.0f; }
|
||||||
const plot::render::Glyph* glyph(plot::u32) const override { return nullptr; }
|
const plot::render::Glyph* glyph(plot::u32) const override { return nullptr; }
|
||||||
|
plot::Vec2 cell_uv() const override { return plot::Vec2{}; } // no atlas, no cell
|
||||||
const plot::u8* atlas_pixels() const override { return nullptr; }
|
const plot::u8* atlas_pixels() const override { return nullptr; }
|
||||||
plot::u32 atlas_width() const override { return 0u; }
|
plot::u32 atlas_width() const override { return 0u; }
|
||||||
plot::u32 atlas_height() const override { return 0u; }
|
plot::u32 atlas_height() const override { return 0u; }
|
||||||
@@ -161,6 +166,14 @@ class BakeableBackend final : public plot::render::TextBackend {
|
|||||||
++revision_;
|
++revision_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trims the published glyph's rect to an ink box, which is what breaks the
|
||||||
|
/// fixed-cell grid (see `TextBackend::cell_uv`): the rect is no longer the cell
|
||||||
|
/// `cell_uv()` declares, so the staging path's check fails. Used by the case
|
||||||
|
/// that pins that check.
|
||||||
|
void trim_glyph_rect(plot::f32 width) {
|
||||||
|
glyph_.u1 = glyph_.u0 + width;
|
||||||
|
}
|
||||||
|
|
||||||
/// Publishes no atlas at all (the "no text" a backend is allowed to be), and
|
/// Publishes no atlas at all (the "no text" a backend is allowed to be), and
|
||||||
/// bumps the revision -- which is how a backend that *starts* publishing one
|
/// bumps the revision -- which is how a backend that *starts* publishing one
|
||||||
/// announces it.
|
/// announces it.
|
||||||
@@ -178,6 +191,7 @@ class BakeableBackend final : public plot::render::TextBackend {
|
|||||||
plot::f32 height() const override { return static_cast<plot::f32>(size_); }
|
plot::f32 height() const override { return static_cast<plot::f32>(size_); }
|
||||||
plot::f32 ascent() const override { return static_cast<plot::f32>(size_); }
|
plot::f32 ascent() const override { return static_cast<plot::f32>(size_); }
|
||||||
const plot::render::Glyph* glyph(plot::u32) const override { return &glyph_; }
|
const plot::render::Glyph* glyph(plot::u32) const override { return &glyph_; }
|
||||||
|
plot::Vec2 cell_uv() const override { return pixels_.empty() ? plot::Vec2{} : plot::Vec2{1.0f, 1.0f}; }
|
||||||
const plot::u8* atlas_pixels() const override { return pixels_.empty() ? nullptr : pixels_.data(); }
|
const plot::u8* atlas_pixels() const override { return pixels_.empty() ? nullptr : pixels_.data(); }
|
||||||
plot::u32 atlas_width() const override { return size_; }
|
plot::u32 atlas_width() const override { return size_; }
|
||||||
plot::u32 atlas_height() const override { return size_; }
|
plot::u32 atlas_height() const override { return size_; }
|
||||||
@@ -252,9 +266,9 @@ InkBox draw_text_run(plottest::HeadlessContext& ctx, plot::render::TextAtlas& at
|
|||||||
plot::gl::glBlendFunc(plot::gl::GL_SRC_ALPHA, plot::gl::GL_ONE_MINUS_SRC_ALPHA);
|
plot::gl::glBlendFunc(plot::gl::GL_SRC_ALPHA, plot::gl::GL_ONE_MINUS_SRC_ALPHA);
|
||||||
program.bind();
|
program.bind();
|
||||||
program.set_viewport({static_cast<plot::f32>(ctx.width), static_cast<plot::f32>(ctx.height)});
|
program.set_viewport({static_cast<plot::f32>(ctx.width), static_cast<plot::f32>(ctx.height)});
|
||||||
// The same two numbers the renderer's flush computes: the staged cell, and that
|
// The same two numbers the renderer's flush computes: the backend's cell, and
|
||||||
// cell's size on screen at this scale.
|
// that cell's size on screen at this scale.
|
||||||
const plot::Vec2 cell = batch.glyph_cell_uv();
|
const plot::Vec2 cell = backend.cell_uv();
|
||||||
const plot::Vec2 texels = atlas.texel_size();
|
const plot::Vec2 texels = atlas.texel_size();
|
||||||
program.set_glyph_uv(cell);
|
program.set_glyph_uv(cell);
|
||||||
program.set_px_range({cell.x * texels.x * px, cell.y * texels.y * px});
|
program.set_px_range({cell.x * texels.x * px, cell.y * texels.y * px});
|
||||||
@@ -1409,3 +1423,36 @@ PLOT_TEST(a_backend_without_an_atlas_uploads_nothing) {
|
|||||||
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||||
ctx.destroy();
|
ctx.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The fixed-cell check itself, in the build where it is fatal. A backend whose
|
||||||
|
// glyph rects are ink boxes (see `TextBackend::cell_uv`) would draw every glyph at
|
||||||
|
// the size the draw was told, reading past its own rows of the atlas -- so the
|
||||||
|
// staging path asserts `glyph_is_cell()` and this case pins that it does, not
|
||||||
|
// merely that the predicate can fail.
|
||||||
|
//
|
||||||
|
// The assertion's death is the observable, and the only protocol for observing a
|
||||||
|
// death is a child process: staging this glyph in-process would take the suite
|
||||||
|
// with it. The child allocates nothing before it dies -- the check comes before
|
||||||
|
// the first `push_back` -- and its own `assert` line lands on stderr above this
|
||||||
|
// case's line, which is the evidence for a human reading the log.
|
||||||
|
PLOT_TEST(staging_a_glyph_that_is_not_the_cell_aborts_in_a_debug_build) {
|
||||||
|
#ifndef NDEBUG
|
||||||
|
BakeableBackend backend;
|
||||||
|
backend.trim_glyph_rect(0.5f); // half a cell: no longer the declared cell
|
||||||
|
PLOT_CHECK(!plot::render::glyph_is_cell(*backend.glyph(0u), backend.cell_uv()));
|
||||||
|
|
||||||
|
std::fflush(nullptr);
|
||||||
|
const pid_t child = ::fork();
|
||||||
|
PLOT_CHECK(child >= 0);
|
||||||
|
if (child == 0) {
|
||||||
|
plot::render::TextBatch batch;
|
||||||
|
batch.begin();
|
||||||
|
batch.add_text("X", {0.0f, 0.0f}, 1.0f, backend); // dies here, in debug
|
||||||
|
::_exit(0); // reached only when the check did not fire
|
||||||
|
}
|
||||||
|
int status = 0;
|
||||||
|
PLOT_CHECK(::waitpid(child, &status, 0) == child);
|
||||||
|
PLOT_CHECK(WIFSIGNALED(status)); // it did not return
|
||||||
|
PLOT_CHECK(WTERMSIG(status) == SIGABRT); // and it was the assertion
|
||||||
|
#endif
|
||||||
|
}
|
||||||
Reference in new issue
Block a user