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
@@ -15,7 +15,11 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -129,6 +133,7 @@ class OnePxPerStringBackend final : public plot::render::TextBackend {
|
||||
plot::f32 height() const override { return 1.0f; }
|
||||
plot::f32 ascent() const override { return 1.0f; }
|
||||
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; }
|
||||
plot::u32 atlas_width() const override { return 0u; }
|
||||
plot::u32 atlas_height() const override { return 0u; }
|
||||
@@ -161,6 +166,14 @@ class BakeableBackend final : public plot::render::TextBackend {
|
||||
++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
|
||||
/// bumps the revision -- which is how a backend that *starts* publishing one
|
||||
/// 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 ascent() const override { return static_cast<plot::f32>(size_); }
|
||||
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(); }
|
||||
plot::u32 atlas_width() 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);
|
||||
program.bind();
|
||||
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
|
||||
// cell's size on screen at this scale.
|
||||
const plot::Vec2 cell = batch.glyph_cell_uv();
|
||||
// The same two numbers the renderer's flush computes: the backend's cell, and
|
||||
// that cell's size on screen at this scale.
|
||||
const plot::Vec2 cell = backend.cell_uv();
|
||||
const plot::Vec2 texels = atlas.texel_size();
|
||||
program.set_glyph_uv(cell);
|
||||
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);
|
||||
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