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
@@ -166,23 +166,20 @@ constexpr u32 ink_width(const u8* rows) noexcept {
|
||||
return any ? hi - lo + 1u : 0u;
|
||||
}
|
||||
|
||||
/// The glyph table the pipeline stages from: one entry per slot, its rect
|
||||
/// normalised by the atlas size with `v` growing downward (see `Glyph`), a zero
|
||||
/// bearing -- the ink sits at the cell's top-left -- and the ink-derived advance.
|
||||
/// It is built from the same two tables `pack_atlas()` packs, so the rect a glyph
|
||||
/// reports and the cell its ink was written to cannot disagree.
|
||||
/// The glyph table the pipeline stages from: one entry per slot, its rect at the
|
||||
/// slot's place in the grid and one cell big -- the cell `cell_uv()` declares,
|
||||
/// so a glyph's rect and the size the draw is told cannot disagree -- a zero
|
||||
/// bearing (the ink sits at the cell's top-left) and the ink-derived advance.
|
||||
/// Built from the same tables `pack_atlas()` packs.
|
||||
constexpr std::array<Glyph, BitmapFont::kGlyphCount> build_glyphs() noexcept {
|
||||
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) {
|
||||
const f32 u0 = static_cast<f32>(slot % BitmapFont::kAtlasCols) * kU;
|
||||
const f32 v0 = static_cast<f32>(slot / BitmapFont::kAtlasCols) * kV;
|
||||
const f32 u0 = static_cast<f32>(slot % BitmapFont::kAtlasCols) * BitmapFont::kCellUV.x;
|
||||
const f32 v0 = static_cast<f32>(slot / BitmapFont::kAtlasCols) * BitmapFont::kCellUV.y;
|
||||
const u32 ink = ink_width(kGlyphBits[slot]);
|
||||
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,
|
||||
static_cast<f32>(spaced < 2u ? 2u : spaced)};
|
||||
glyphs[slot] = Glyph{u0, v0, u0 + BitmapFont::kCellUV.x, v0 + BitmapFont::kCellUV.y, 0.0f,
|
||||
0.0f, static_cast<f32>(spaced < 2u ? 2u : spaced)};
|
||||
}
|
||||
return glyphs;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
#include <plot/render/primitives.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
|
||||
namespace plot::render {
|
||||
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
|
||||
/// 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
|
||||
@@ -62,14 +70,41 @@ void LineBatch::add_polyline_xy(const f32* xy, u32 n) {
|
||||
void TextBatch::begin() noexcept {
|
||||
v_.clear();
|
||||
cell_uv_ = Vec2{};
|
||||
cell_declared_ = false;
|
||||
}
|
||||
|
||||
void TextBatch::clear() noexcept {
|
||||
v_.clear();
|
||||
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) {
|
||||
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
|
||||
// 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
|
||||
@@ -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(unit_ux.x);
|
||||
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) {
|
||||
@@ -97,6 +128,10 @@ Vec2 TextBatch::add_text(const char* utf8, Vec2 pen_px, f32 px, const TextBacken
|
||||
if (utf8 == nullptr) {
|
||||
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);
|
||||
for (const char* p = utf8; *p != '\0';) {
|
||||
// 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_;
|
||||
}
|
||||
|
||||
Vec2 Renderer::AxisFont::cell_uv() const { return font_ != nullptr ? font_->cell_uv() : Vec2{}; }
|
||||
|
||||
const u8* Renderer::AxisFont::atlas_pixels() const {
|
||||
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
|
||||
// each frame so a changed axis_font_px takes effect on the next one.
|
||||
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
|
||||
// -- 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
|
||||
@@ -385,11 +391,11 @@ void Renderer::flush_text() noexcept {
|
||||
upload_text_vertices(glyphs_.data());
|
||||
text_program_.bind();
|
||||
text_program_.set_viewport({viewport_.w, viewport_.h});
|
||||
// The cell comes from the glyphs staged and the atlas they came from -- never
|
||||
// from a constant -- so a font swap moves the quads with the artwork:
|
||||
// `glyph_uv` is the rect every instance is read through and `px_range` is that
|
||||
// same cell on screen.
|
||||
const Vec2 cell_uv = glyphs_.glyph_cell_uv();
|
||||
// The cell comes from the backend and the atlas it uploaded -- never from a
|
||||
// constant, and never from whichever glyph happened to be staged first -- so a
|
||||
// font swap moves the quads with the artwork: `glyph_uv` is the rect every
|
||||
// instance is read through and `px_range` is that same cell on screen.
|
||||
const Vec2 cell_uv = font_backend_.cell_uv();
|
||||
const Vec2 atlas_texels = atlas_.texel_size();
|
||||
text_program_.set_glyph_uv(cell_uv);
|
||||
text_program_.set_px_range({cell_uv.x * atlas_texels.x * font_backend_.scale(),
|
||||
|
||||
Reference in new issue
Block a user