feat(render): anti-aliased embedded Adwaita Mono atlas, generated and committed

This commit is contained in:
Martino Ferrari committed 2026-09-28 10:40:23 +02:00
1 parent 3ff7b9c978
commit 5ca3587325
17 files changed
+3422 -627

No files matched your search

+6 -1
View File
@@ -70,7 +70,12 @@ target_include_directories(plot_tests PRIVATE "${CMAKE_SOURCE_DIR}/adapters")
target_compile_definitions(plot_tests PRIVATE PLOT_PYRAMID_COUNT_COVERS
PLOT_PYRAMID_COUNT_MERGES
PLOT_RING_STATS
PLOT_COUNT_MEASURE_RECOMPUTES)
PLOT_COUNT_MEASURE_RECOMPUTES
# The committed source root, so the
# atlas-generator case can run
# tools/fonts/gen_atlas.py --check
# against the committed atlas.
PLOT_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
plot_apply_warnings(plot_tests)
add_test(NAME plot_tests COMMAND plot_tests)
+36 -20
View File
@@ -6,13 +6,14 @@
#include <plot/gl/caps.hpp>
#include <plot/gl/handle.hpp>
#include <plot/gl/loader.hpp>
#include <plot/render/bitmap_font.hpp>
#include <plot/render/embedded_mono_font.hpp>
#include <plot/render/primitives.hpp>
#include <plot/render/program.hpp>
#include <plot/render/shaders.hpp>
#include <plot/render/text_atlas.hpp>
#include <plot/render/vertex_array.hpp>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
@@ -341,10 +342,10 @@ PLOT_TEST(line_pipeline_draws_an_aa_strip) {
// The text pipeline end to end: the R8 atlas upload, the per-instance glyph UV
// rect, the NEAREST texel-centre sampling, the tint and the same single y flip.
// Every one of the cell's 64 pixels is checked against the glyph bitmap, so a
// wrong atlas cell, a wrong UV or a transposed axis cannot pass.
// Every texel of the cell is checked against the backend's own coverage byte, so
// a wrong atlas cell, a wrong UV or a transposed axis cannot pass.
PLOT_TEST(text_pipeline_draws_a_glyph) {
using plot::render::BitmapFont;
using plot::render::EmbeddedMonoFont;
plottest::HeadlessContext ctx(256, 256);
plot::Status st;
PLOT_CHECK(ctx.create(&st));
@@ -352,12 +353,12 @@ PLOT_TEST(text_pipeline_draws_a_glyph) {
// The backend's atlas, through the owner the renderer uses: single channel,
// GL_NEAREST, straight alpha, uploaded from the backend's own bytes.
BitmapFont font;
EmbeddedMonoFont font;
plot::render::TextAtlas atlas;
PLOT_CHECK(atlas.sync(font, &st));
PLOT_CHECK(!atlas.empty());
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(BitmapFont::kAtlasWidth), 1e-6);
PLOT_CHECK_NEAR(atlas.texel_size().y, static_cast<plot::f32>(BitmapFont::kAtlasHeight), 1e-6);
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(font.atlas_width()), 1e-6);
PLOT_CHECK_NEAR(atlas.texel_size().y, static_cast<plot::f32>(font.atlas_height()), 1e-6);
plot::render::TextProgram program;
PLOT_CHECK(program.create(&st));
@@ -383,29 +384,44 @@ PLOT_TEST(text_pipeline_draws_a_glyph) {
program.bind();
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)});
// The backend's own cell, on screen at 1:1: the draw's one rect size.
const plot::f32 cell_w = static_cast<plot::f32>(EmbeddedMonoFont::cell_width());
const plot::f32 cell_h = static_cast<plot::f32>(EmbeddedMonoFont::cell_height());
program.set_px_range({cell_w, cell_h});
program.set_glyph_uv(font.cell_uv());
program.set_texture_size(atlas.texel_size());
program.set_color(plot::Color::hex(0xFF00FF));
// White, so the blended pixel value *is* the coverage byte: the shader emits
// `vec4(rgb, a * coverage)`, and a white tint over the black clear lands at
// `1.0 * coverage` in every channel.
program.set_color(plot::Color::hex(0xFFFFFF));
atlas.bind();
glyphs.bind();
plot::gl::glDrawArraysInstanced(plot::gl::GL_TRIANGLE_STRIP, 0, 4, 1);
// One cell is 8x8 texels and u_px_range is 8, so texel (gx, gy) lands on one
// target pixel: Rect x = 64 + gx, and Rect y = 100 + gy is GL row 155 - gy.
const plot::u8* rows = font.glyph_bitmap('A');
// The cell is cell_w x cell_h texels and u_px_range is the same, so texel
// (gx, gy) lands on exactly one target pixel: Rect x = 64 + gx, and Rect y =
// 100 + gy is GL row 155 - gy. A transposed axis, a wrong cell or a wrong UV
// all move ink away from where the coverage byte says it is.
const plot::u32 slot = EmbeddedMonoFont::glyph_index(static_cast<plot::u32>('A'));
const plot::Vec2 origin = EmbeddedMonoFont::atlas_origin(slot);
const plot::u8* atlas_bytes = font.atlas_pixels();
plot::u32 antialiased = 0;
plot::u8 px[4] = {};
for (plot::u32 gy = 0; gy < BitmapFont::kGlyphPx; ++gy) {
for (plot::u32 gx = 0; gx < BitmapFont::kGlyphPx; ++gx) {
const bool ink = ((rows[gy] >> gx) & 1u) != 0u;
for (plot::u32 gy = 0; gy < EmbeddedMonoFont::cell_height(); ++gy) {
for (plot::u32 gx = 0; gx < EmbeddedMonoFont::cell_width(); ++gx) {
const plot::u32 ax = static_cast<plot::u32>(origin.x) + gx;
const plot::u32 ay = static_cast<plot::u32>(origin.y) + gy;
const plot::u8 want = atlas_bytes[static_cast<std::size_t>(ay) * font.atlas_width() + ax];
PLOT_CHECK(ctx.read_pixel(64u + gx, 155u - gy, px));
if (ink) {
PLOT_CHECK(px[0] > 200 && px[1] < 60 && px[2] > 200);
} else {
PLOT_CHECK(px[0] < 40 && px[2] < 40);
}
PLOT_CHECK_NEAR(px[0], static_cast<plot::f32>(want), 2.0);
PLOT_CHECK_NEAR(px[1], static_cast<plot::f32>(want), 2.0);
PLOT_CHECK_NEAR(px[2], static_cast<plot::f32>(want), 2.0);
if (want > 0u && want < 255u) ++antialiased;
}
}
// 'A' has grey edge texels and they reached the framebuffer: a 1-bit atlas
// could only ever have produced 0 or 255 here.
PLOT_CHECK(antialiased > 0u);
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
ctx.destroy();
}
+4 -4
View File
@@ -3,7 +3,7 @@
#include <plot/plot/detail/measure_count.hpp>
#include <plot/data/array_series.hpp>
#include <plot/data/stream_series.hpp>
#include <plot/render/bitmap_font.hpp>
#include <plot/render/embedded_mono_font.hpp>
#include <cmath>
#include <vector>
@@ -73,7 +73,7 @@ PLOT_TEST(layout_without_text_backend_uses_default_label_areas) {
}
PLOT_TEST(layout_derives_label_areas_from_font_metrics) {
plot::render::BitmapFont font;
plot::render::EmbeddedMonoFont font;
plot::Plot p;
p.set_text_backend(&font);
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
@@ -103,7 +103,7 @@ PLOT_TEST(layout_derives_label_areas_from_font_metrics) {
/// Every titled axis of a direction gets its own text line inside its strip;
/// without that, the second titled axis of an edge is drawn on top of the first.
PLOT_TEST(layout_stacks_one_title_line_per_titled_axis) {
plot::render::BitmapFont font;
plot::render::EmbeddedMonoFont font;
plot::Plot p;
p.set_text_backend(&font);
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
@@ -127,7 +127,7 @@ PLOT_TEST(layout_stacks_one_title_line_per_titled_axis) {
/// An axis with no title reserves nothing: the strips exist so that a titled axis
/// has somewhere outside `axes` to draw in, not as a fixed cost per plot.
PLOT_TEST(layout_reserves_no_title_strip_without_a_title) {
plot::render::BitmapFont font;
plot::render::EmbeddedMonoFont font;
plot::Plot p;
p.set_text_backend(&font);
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
+285 -134
View File
@@ -1,12 +1,17 @@
// CPU-side contract of the render layer: the staging batches that feed the
// instanced line and text pipelines, and the bitmap font's metrics and atlas.
// instanced line and text pipelines, and the embedded font's metrics and atlas.
// Nothing here touches GL, so it runs on any machine that can build the tests.
#include "harness.hpp"
#include <plot/render/bitmap_font.hpp>
#include <plot/render/embedded_mono_font.hpp>
#include <plot/render/primitives.hpp>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <sys/wait.h>
#include <vector>
PLOT_TEST(line_batch_emits_two_verts_per_segment) {
plot::render::LineBatch b;
@@ -99,152 +104,293 @@ PLOT_TEST(line_batch_reuses_capacity_across_frames) {
PLOT_CHECK(b.data().data() == store);
}
PLOT_TEST(text_batch_measures_ascii) {
plot::render::BitmapFont f;
const f32 w = f.measure("12.5 ms");
PLOT_CHECK(w > 0.0f);
PLOT_CHECK_NEAR(f.measure(""), 0.0, 1e-6);
PLOT_CHECK(f.measure("ab") < f.measure("abc")); // monotonic in length
PLOT_CHECK(f.measure("iiii") < f.measure("WWWW")); // proportional-ish, not fixed-width-blank
namespace {
using plot::render::EmbeddedMonoFont;
/// The coverage byte of one texel of `slot`'s cell: 0 background, 255 full ink.
plot::u8 cell_texel(const EmbeddedMonoFont& font, plot::u32 slot, plot::u32 col, plot::u32 row) {
const plot::Vec2 origin = EmbeddedMonoFont::atlas_origin(slot);
const plot::u32 x = static_cast<plot::u32>(origin.x) + col;
const plot::u32 y = static_cast<plot::u32>(origin.y) + row;
const std::size_t index = static_cast<std::size_t>(y) * font.atlas_width() + x;
return font.atlas_image()[index];
}
PLOT_TEST(bitmap_font_advance_follows_ink_extent) {
plot::render::BitmapFont f;
PLOT_CHECK(f.advance('i') < f.advance('W'));
// Whitespace is a blank glyph with a nonzero advance, never zero.
PLOT_CHECK(f.advance(' ') >= 2.0f);
// Advance is ink width + 1px of spacing, floored at 2px: derived here from
// the same bitmaps the atlas uploads, so a second width table cannot drift.
auto ink_width = [&f](char c) {
const plot::u8* rows = f.glyph_bitmap(c);
int lo = 8;
int hi = -1;
for (plot::u32 y = 0; y < f.glyph_rows(c); ++y) {
for (int x = 0; x < 8; ++x) {
if ((rows[y] >> x) & 1u) {
if (x < lo) lo = x;
if (x > hi) hi = x;
}
}
/// Total coverage of one cell: zero means the cell rasterised blank.
plot::u32 cell_ink_units(const EmbeddedMonoFont& font, plot::u32 slot) {
plot::u32 total = 0u;
for (plot::u32 y = 0; y < EmbeddedMonoFont::cell_height(); ++y) {
for (plot::u32 x = 0; x < EmbeddedMonoFont::cell_width(); ++x) {
total += cell_texel(font, slot, x, y);
}
return hi < 0 ? 0 : hi - lo + 1;
};
const char* samples = "iW.1_m";
for (const char* c = samples; *c != '\0'; ++c) {
const int ink = ink_width(*c);
const f32 want = ink + 1 < 2 ? 2.0f : static_cast<f32>(ink + 1);
PLOT_CHECK_NEAR(f.advance(*c), want, 1e-6);
}
// measure() is exactly the sum of the glyph advances it stages.
f32 sum = 0.0f;
for (const char* c = "12.5 ms"; *c != '\0'; ++c) sum += f.advance(*c);
PLOT_CHECK_NEAR(f.measure("12.5 ms"), sum, 1e-6);
return total;
}
PLOT_TEST(bitmap_font_decodes_si_units_and_falls_back_visibly) {
plot::render::BitmapFont f;
// plot::format_si emits U+00B5 MICRO SIGN, U+00B0 DEGREE SIGN and U+03A9 OHM.
/// The fallback slot, found by the marker codepoint rather than assumed.
plot::u32 fallback_slot() {
for (plot::u32 slot = 0; slot < EmbeddedMonoFont::glyph_count(); ++slot) {
if (EmbeddedMonoFont::codepoint_of_slot(slot) == EmbeddedMonoFont::kFallbackCodepoint) {
return slot;
}
}
return EmbeddedMonoFont::glyph_count();
}
/// The plot library's own source root, so the generator test can run the committed
/// script against the committed output. Injected by tests/CMakeLists.txt.
constexpr const char* kSourceDir = PLOT_SOURCE_DIR;
} // namespace
// The embedded atlas' metrics: a line taller than its ascent (descenders need the
// room), a non-empty advance, and -- the point of the face -- a fixed advance, so
// a label's width is its length and nothing else. A text backend with variable
// advances is the tile the plot library deliberately does not lay out (see the
// spec's non-goals), so `i` and `W` measure the same and a caller cannot be
// surprised by a label's reserved strip.
PLOT_TEST(embedded_font_metrics_are_sane) {
const EmbeddedMonoFont f;
PLOT_CHECK(f.height() > f.ascent());
PLOT_CHECK(f.ascent() > 0.0f);
PLOT_CHECK_NEAR(f.measure(""), 0.0, 1e-6);
PLOT_CHECK_NEAR(f.measure(nullptr), 0.0, 1e-6);
// One advance per codepoint, whatever the glyph: this is `i` and `W` alike.
PLOT_CHECK_NEAR(f.measure("i"), f.measure("W"), 1e-6);
PLOT_CHECK_NEAR(f.measure("i"), EmbeddedMonoFont::advance_codepoint('i'), 1e-6);
// Monotone in length, and exactly the sum of the advances it stages.
PLOT_CHECK(f.measure("ab") < f.measure("abc"));
PLOT_CHECK(f.measure("abc") < f.measure("abcd"));
PLOT_CHECK_NEAR(f.measure("12.5 ms"),
(f.glyph('1')->advance + f.glyph('2')->advance + f.glyph('.')->advance +
f.glyph('5')->advance + f.glyph(' ')->advance + f.glyph('m')->advance +
f.glyph('s')->advance),
1e-6);
// The em is the size the table was baked at, and the scale the renderer draws
// with is a ratio of it -- so it must be a positive number and not `height()`.
PLOT_CHECK_NEAR(f.em_px(), static_cast<plot::f32>(EmbeddedMonoFont::kEmPx), 1e-6);
PLOT_CHECK(f.em_px() > 0.0f);
}
// Every codepoint the atlas claims to cover has its own cell, the cell is inside
// the atlas and is the declared cell, and its ink is non-empty -- the generator
// refuses to emit a blank cell for a covered codepoint, and this pins that the
// committed atlas is the generator's output and not a hand edit. U+0020 is the
// one documented blank: a space has no ink by definition and still advances.
PLOT_TEST(embedded_atlas_covers_every_expected_codepoint) {
const EmbeddedMonoFont f;
const plot::Vec2 cell = f.cell_uv();
PLOT_CHECK_NEAR(cell.x, static_cast<plot::f32>(EmbeddedMonoFont::cell_width()) /
static_cast<plot::f32>(f.atlas_width()),
1e-9);
PLOT_CHECK_NEAR(cell.y, static_cast<plot::f32>(EmbeddedMonoFont::cell_height()) /
static_cast<plot::f32>(f.atlas_height()),
1e-9);
PLOT_CHECK(f.atlas_pixels() != nullptr);
std::vector<plot::u32> expected;
for (plot::u32 cp = 0x20u; cp <= 0x7Eu; ++cp) expected.push_back(cp);
for (plot::u32 cp : {0x00B0u, 0x00B5u, 0x03A9u, 0x2013u, 0x2014u}) expected.push_back(cp);
for (plot::u32 cp : expected) {
const plot::u32 slot = EmbeddedMonoFont::glyph_index(cp);
PLOT_CHECK(slot < EmbeddedMonoFont::glyph_count());
PLOT_CHECK(EmbeddedMonoFont::codepoint_of_slot(slot) == cp);
const plot::render::Glyph* g = f.glyph(cp);
PLOT_CHECK(g != nullptr);
// The rect is the atlas cell, and it lies inside the atlas.
PLOT_CHECK(plot::render::glyph_is_cell(*g, cell));
PLOT_CHECK(g->u0 >= 0.0f && g->u1 <= 1.0f && g->v0 >= 0.0f && g->v1 <= 1.0f);
if (cp == 0x20u) {
PLOT_CHECK(cell_ink_units(f, slot) == 0u); // a space is blank ...
PLOT_CHECK(g->advance > 0.0f); // ... and still advances
} else {
PLOT_CHECK(cell_ink_units(f, slot) > 0u); // a covered glyph has ink
}
}
// The fallback box is a real cell with real ink, at a fixed slot, and every
// codepoint outside the coverage set draws it -- so a label the atlas cannot
// spell is visible rather than silently blank.
const plot::u32 fallback = fallback_slot();
PLOT_CHECK(fallback < EmbeddedMonoFont::glyph_count());
PLOT_CHECK(cell_ink_units(f, fallback) > 0u);
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x4E00u) == fallback);
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0xFFFFu) == fallback);
PLOT_CHECK(EmbeddedMonoFont::glyph_index(plot::render::kInvalidCodepoint) == fallback);
PLOT_CHECK(f.glyph(0x4E00u)->advance > 0.0f);
}
// The atlas carries more than one coverage value: an anti-aliased raster has grey
// edges, and a 1-bit table could only ever be 0 or 255. This is the data-level
// half of the anti-aliasing evidence (the frame-level half is in the render
// smoke test): a table that passed through a threshold would fail it.
PLOT_TEST(embedded_atlas_holds_antialiased_coverage) {
const EmbeddedMonoFont f;
const plot::u8* atlas = f.atlas_pixels();
const std::size_t size = static_cast<std::size_t>(f.atlas_width()) * f.atlas_height();
plot::u32 partial = 0u;
plot::u32 full = 0u;
plot::u32 blank = 0u;
for (std::size_t i = 0; i < size; ++i) {
if (atlas[i] == 0u) ++blank;
else if (atlas[i] == 255u) ++full;
else ++partial;
}
PLOT_CHECK(full > 0u);
PLOT_CHECK(blank > 0u);
// Every glyph's stroke edge contributes: the partial population is a large
// fraction of the inked texels, not a handful of rounding artefacts.
PLOT_CHECK(partial > full / 2u);
}
// The face is monospaced, so every codepoint's advance is the same -- but it is
// the font's own `hmtx` advance carried through the bake, not a constant invented
// here: `advance_codepoint` and the per-glyph `advance` agree, and the whitespace
// glyphs that must never be zero are not.
PLOT_TEST(embedded_font_advance_matches_the_font_units) {
const EmbeddedMonoFont f;
const plot::f32 advance = EmbeddedMonoFont::advance_codepoint('A');
PLOT_CHECK(advance > 0.0f);
PLOT_CHECK(EmbeddedMonoFont::advance_codepoint(' ') > 0.0f);
const char* samples = "iW.1_m ";
for (const char* c = samples; *c != '\0'; ++c) {
PLOT_CHECK_NEAR(f.glyph(static_cast<plot::u32>(*c))->advance, advance, 1e-6);
}
// `advance_codepoint` is a lookup, not a walk: it agrees with `measure` for a
// single decoded codepoint, and disagrees with nothing in the table.
PLOT_CHECK_NEAR(f.measure("W"), EmbeddedMonoFont::advance_codepoint('W'), 1e-6);
for (plot::u32 cp = 0x20u; cp <= 0x7Eu; ++cp) {
PLOT_CHECK_NEAR(f.glyph(cp)->advance, EmbeddedMonoFont::advance_codepoint(cp), 1e-6);
}
}
// Decoding and the fallback, on the codepoints `plot::format_si` and a plotting
// label actually emit: the three SI symbols are real glyphs, everything else is
// the visible fallback box, and a malformed byte is not measured as zero.
PLOT_TEST(embedded_font_decodes_si_units_and_falls_back_visibly) {
const EmbeddedMonoFont f;
const plot::u32 fallback = fallback_slot();
const f32 micro = f.measure("\xC2\xB5s");
const f32 degree = f.measure("\xC2\xB0");
const f32 ohm = f.measure("\xCE\xA9");
PLOT_CHECK(micro > 0.0f);
PLOT_CHECK(degree > 0.0f);
PLOT_CHECK(ohm > 0.0f);
PLOT_CHECK_NEAR(micro, f.advance_codepoint(0x00B5u) + f.advance('s'), 1e-6);
PLOT_CHECK_NEAR(degree, f.advance_codepoint(0x00B0u), 1e-6);
PLOT_CHECK_NEAR(ohm, f.advance_codepoint(0x03A9u), 1e-6);
// Those three map to their own renderable glyph, not to the missing-glyph box.
const plot::u32 fallback = f.glyph_index(0x4E00u); // CJK, deliberately absent
PLOT_CHECK(f.glyph_index(0x00B5u) != fallback);
PLOT_CHECK(f.glyph_index(0x00B0u) != fallback);
PLOT_CHECK(f.glyph_index(0x03A9u) != fallback);
PLOT_CHECK(f.glyph_index(0x00B5u) != f.glyph_index(0x00B0u));
PLOT_CHECK(f.glyph_index(0x00B0u) != f.glyph_index(0x03A9u));
// Anything else non-ASCII is a visible box with a nonzero advance, and a
// malformed UTF-8 byte is treated the same way rather than measured as zero.
PLOT_CHECK_NEAR(f.measure("\xE4\xB8\x80"), f.advance_codepoint(0x4E00u), 1e-6);
PLOT_CHECK(f.advance_codepoint(0x4E00u) > 0.0f);
PLOT_CHECK(f.advance(static_cast<char>(0x80)) > 0.0f);
PLOT_CHECK_NEAR(f.advance(static_cast<char>(0x80)), f.advance_codepoint(0x4E00u), 1e-6);
// The fallback box glyph has ink; a blank bitmap would hide the problem.
bool any_ink = false;
const plot::u8* box = plot::render::BitmapFont::glyph_bitmap_of(fallback);
for (plot::u32 y = 0; y < plot::render::BitmapFont::kGlyphPx; ++y) {
any_ink = any_ink || box[y] != 0;
}
PLOT_CHECK(any_ink);
PLOT_CHECK_NEAR(micro, EmbeddedMonoFont::advance_codepoint(0x00B5u) + f.glyph('s')->advance, 1e-6);
// Each maps to its own cell, not to the box.
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x00B5u) != fallback);
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x00B0u) != fallback);
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x03A9u) != fallback);
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x00B5u) != EmbeddedMonoFont::glyph_index(0x00B0u));
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x00B0u) != EmbeddedMonoFont::glyph_index(0x03A9u));
// The two dashes the coverage adds are real cells too, not the box.
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x2013u) != fallback);
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x2014u) != fallback);
// A CJK codepoint and a bare malformed byte both draw the box, and both
// advance: a missing glyph is visible and never a zero-width hole.
PLOT_CHECK_NEAR(f.measure("\xE4\xB8\x80"), EmbeddedMonoFont::advance_codepoint(0x4E00u), 1e-6);
PLOT_CHECK_NEAR(f.measure("\x80"), EmbeddedMonoFont::advance_codepoint(0x4E00u), 1e-6);
}
// The packed atlas must be left-to-right in absolute terms: `(row >> x) & 1` is
// also how every other expectation here is derived, so a table transcribed with
// the opposite in-row bit order would keep those tests green while drawing
// mirrored text. 'A' cannot catch that (it is
// symmetric about its vertical axis); 'L', 'b' and '/' can.
PLOT_TEST(bitmap_font_bit_order_runs_left_to_right) {
using plot::render::BitmapFont;
const plot::u8* atlas = BitmapFont::atlas_image();
auto ink = [atlas](char c, plot::u32 col, plot::u32 row) {
const plot::u32 slot = static_cast<plot::u32>(c) - BitmapFont::kFirstChar;
const plot::Vec2 origin = BitmapFont::atlas_origin(slot);
const auto x = static_cast<std::size_t>(origin.x) + col;
const auto y = static_cast<std::size_t>(origin.y) + row;
return atlas[y * BitmapFont::kAtlasWidth + x] != 0u;
// The atlas must be left-to-right in absolute terms. The coverage bytes are what
// the GPU samples, so a generator that rasterised with the opposite x sense -- or
// an atlas packed with the cells reversed -- would keep every rect check green
// while drawing mirrored text. 'A' cannot catch that (it is symmetric about its
// vertical axis); 'L', 'b' and '/' can, and they are asserted against absolute
// texel coordinates rather than against the table itself.
PLOT_TEST(embedded_atlas_orientation_runs_left_to_right) {
const EmbeddedMonoFont f;
using plot::render::EmbeddedMonoFont;
auto ink = [&f](char c, plot::u32 col, plot::u32 row) {
const plot::u32 slot = EmbeddedMonoFont::glyph_index(static_cast<plot::u32>(c));
return cell_texel(f, slot, col, row) != 0u;
};
// 'L': stem and top serif on the left, the foot running right along the
// bottom row and stopping one texel short of the right edge.
PLOT_CHECK(ink('L', 0u, 0u));
PLOT_CHECK(!ink('L', 7u, 0u));
PLOT_CHECK(ink('L', 1u, 3u));
PLOT_CHECK(!ink('L', 6u, 3u));
PLOT_CHECK(ink('L', 6u, 6u));
PLOT_CHECK(!ink('L', 7u, 6u));
// 'b': the stem is the leftmost column of the upper rows.
PLOT_CHECK(ink('b', 0u, 0u));
PLOT_CHECK(!ink('b', 7u, 0u));
// '/': starts at the right of the top row and ends at the left of the last
// one — the opposite of '\'.
PLOT_CHECK(ink('/', 6u, 0u));
PLOT_CHECK(!ink('/', 0u, 0u));
PLOT_CHECK(ink('/', 0u, 6u));
PLOT_CHECK(!ink('/', 6u, 6u));
// 'L': the stem runs down the left, and the foot runs right along the bottom --
// so at cap height there is ink on the left and none on the right, and the
// bottom row has ink to the right of where the stem stopped.
PLOT_CHECK(ink('L', 1u, 6u));
PLOT_CHECK(!ink('L', 6u, 6u));
PLOT_CHECK(ink('L', 5u, 11u));
PLOT_CHECK(!ink('L', 5u, 3u));
// 'b': the stem is the left column of the upper rows, the bowl the right.
PLOT_CHECK(ink('b', 1u, 4u));
PLOT_CHECK(!ink('b', 6u, 4u));
PLOT_CHECK(ink('b', 6u, 8u));
// '/': starts at the top right and ends at the bottom left -- the opposite of
// '\'. This is the pair that pins the diagonal's direction.
PLOT_CHECK(ink('/', 5u, 2u));
PLOT_CHECK(!ink('/', 2u, 2u));
PLOT_CHECK(ink('/', 2u, 12u));
PLOT_CHECK(!ink('/', 5u, 12u));
}
PLOT_TEST(bitmap_font_atlas_is_uploadable) {
using plot::render::BitmapFont;
PLOT_CHECK(BitmapFont::kAtlasWidth == BitmapFont::kAtlasCols * BitmapFont::kGlyphPx);
PLOT_CHECK(BitmapFont::kAtlasHeight % BitmapFont::kGlyphPx == 0);
PLOT_CHECK(BitmapFont::kAtlasCols * BitmapFont::kAtlasRows >= BitmapFont::kGlyphCount);
PLOT_CHECK(BitmapFont::kAsciiGlyphs == 95);
// The atlas is a fixed-cell grid of one texel format: a whole number of cells
// across, the cell dividing the height, and every glyph's rect the declared cell
// -- so the pipeline's single per-draw rect size is the right one for every glyph.
PLOT_TEST(embedded_atlas_is_uploadable) {
using plot::render::EmbeddedMonoFont;
PLOT_CHECK(EmbeddedMonoFont::cell_width() > 0u);
PLOT_CHECK(EmbeddedMonoFont::cell_height() > 0u);
PLOT_CHECK(EmbeddedMonoFont::glyph_count() == 95u + 5u + 1u); // ASCII, SI/dashes, box
plot::render::BitmapFont f;
PLOT_CHECK(f.glyph_rows('A') == 8);
const EmbeddedMonoFont f;
PLOT_CHECK(f.atlas_width() == EmbeddedMonoFont::cell_width() * EmbeddedMonoFont::glyph_count());
PLOT_CHECK(f.atlas_height() == EmbeddedMonoFont::cell_height());
PLOT_CHECK(f.atlas_pixels() != nullptr);
PLOT_CHECK(f.height() > 0.0f);
PLOT_CHECK(f.ascent() > 0.0f);
PLOT_CHECK(f.ascent() <= f.height());
PLOT_CHECK(f.ascent() < f.height());
// Each glyph occupies its own atlas cell, and that cell is the bitmap byte
// for byte: 255 where a row bit is set, 0 elsewhere (straight alpha).
const plot::u8* atlas = BitmapFont::atlas_image();
const plot::u32 slots[] = {'A' - 32, 'g' - 32, 'i' - 32, 'W' - 32, 0, '.' - 32,
BitmapFont::kFallbackSlot, BitmapFont::kMicroSlot};
for (const plot::u32 slot : slots) {
const plot::Vec2 o = BitmapFont::atlas_origin(slot);
const plot::u8* rows = BitmapFont::glyph_bitmap_of(slot);
for (plot::u32 y = 0; y < BitmapFont::kGlyphPx; ++y) {
for (plot::u32 x = 0; x < BitmapFont::kGlyphPx; ++x) {
const plot::u8 want = ((rows[y] >> x) & 1u) != 0u ? 0xFFu : 0x00u;
const auto row = static_cast<std::size_t>(o.y) + y;
const auto col = static_cast<std::size_t>(o.x) + x;
PLOT_CHECK(atlas[row * BitmapFont::kAtlasWidth + col] == want);
}
// Every cell, the fallback included, is the declared cell and holds at least
// one non-zero byte (bar the space), and the ASCII codepoints are a direct
// index into the table.
for (plot::u32 slot = 0; slot < EmbeddedMonoFont::glyph_count(); ++slot) {
const plot::u32 cp = EmbeddedMonoFont::codepoint_of_slot(slot);
if (cp != EmbeddedMonoFont::kFallbackCodepoint) {
PLOT_CHECK(EmbeddedMonoFont::glyph_index(cp) == slot);
}
const plot::render::Glyph* g = f.glyph(cp);
PLOT_CHECK(plot::render::glyph_is_cell(*g, f.cell_uv()));
}
// The atlas is a whole number of cells and no glyph leaves its cell.
PLOT_CHECK(BitmapFont::kAtlasWidth == 16u * BitmapFont::kGlyphPx);
for (plot::u32 cp = 0x20u; cp <= 0x7Eu; ++cp) {
PLOT_CHECK(EmbeddedMonoFont::glyph_index(cp) == cp - 0x20u);
}
// A single-row atlas: the cell spans the whole height, so v is 0..1.
PLOT_CHECK_NEAR(f.cell_uv().y, 1.0, 1e-9);
PLOT_CHECK_NEAR(EmbeddedMonoFont::atlas_origin(0u).x, 0.0, 1e-9);
PLOT_CHECK_NEAR(EmbeddedMonoFont::atlas_origin(1u).x,
static_cast<plot::f32>(EmbeddedMonoFont::cell_width()), 1e-9);
}
// The committed atlas is the generator's output, not a hand edit, and the
// generator is deterministic: the same source font, em size and supersample
// factor produce it byte for byte. The check runs the committed script against
// the committed file, so a table touched by hand, a stale bake after a coverage
// change, or a non-deterministic generator all fail here.
//
// `fontTools` is an authoring-time tool and the test machine is not required to
// have it. Its absence is *reported*, not silently passed: the generator exits
// with a distinct code for it, and this case says so on stdout.
PLOT_TEST(generator_is_deterministic_and_current) {
const std::string command = std::string("python3 ") + kSourceDir +
"/tools/fonts/gen_atlas.py --check --out " + kSourceDir +
"/src/render/embedded_mono_font.cpp";
const int status = std::system(command.c_str());
PLOT_CHECK(status != -1);
PLOT_CHECK(WIFEXITED(status));
const int code = WEXITSTATUS(status);
if (code == 3) { // `gen_atlas.py`'s "fontTools is not installed" code
std::printf(
"SKIPPED: generator_is_deterministic_and_current -- fontTools is not "
"installed, so the atlas could not be re-baked\n");
return;
}
PLOT_CHECK(code == 0);
}
PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
plot::render::BitmapFont f;
plot::render::EmbeddedMonoFont f;
plot::render::TextBatch b;
b.begin();
const plot::Vec2 end = b.add_text("12.5 ms", {0.0f, 0.0f}, 1.0f, f);
@@ -254,7 +400,7 @@ PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
PLOT_CHECK_NEAR(end.y, 0.0, 1e-6);
PLOT_CHECK_NEAR(b.data()[0], 0.0, 1e-6); // first cell at the pen
PLOT_CHECK_NEAR(b.data()[1], 0.0, 1e-6);
PLOT_CHECK_NEAR(b.data()[6], f.advance('1'), 1e-6); // pen advanced per glyph
PLOT_CHECK_NEAR(b.data()[6], f.glyph('1')->advance, 1e-6); // pen advanced per glyph
// The staged cell's UV is the glyph's own atlas rect (see `Glyph`), and the
// basis of an unrotated run is the screen's +x. '.' is the string's third
// glyph, so its instance is the third: floats 12..17.
@@ -284,7 +430,7 @@ PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
PLOT_CHECK(b.glyph_count() == 2);
PLOT_CHECK_NEAR(scaled.x, 10.0f + 2.0f * f.measure("AW"), 1e-6);
PLOT_CHECK_NEAR(scaled.y, 4.0, 1e-6);
PLOT_CHECK_NEAR(b.data()[6], 10.0f + 2.0f * f.advance('A'), 1e-6); // second cell's pen
PLOT_CHECK_NEAR(b.data()[6], 10.0f + 2.0f * f.glyph('A')->advance, 1e-6); // second cell's pen
}
// The pen follows the basis, not the screen axes. A run staged with `axis` has to
@@ -292,7 +438,7 @@ PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
// what reserves the room for a rotated y label -- and its cells have to march along
// the same direction, or the ink lands beside the pen instead of up from it.
PLOT_TEST(text_batch_advances_along_its_basis) {
plot::render::BitmapFont f;
plot::render::EmbeddedMonoFont f;
plot::render::TextBatch b;
const plot::Vec2 pen{100.0f, 40.0f};
const f32 width = f.measure("12.5 ms");
@@ -318,7 +464,7 @@ PLOT_TEST(text_batch_advances_along_its_basis) {
// The second cell is one advance further up than the pen, which is the same rule
// the unrotated case follows in x.
PLOT_CHECK_NEAR(b.data()[6], pen.x, 1e-6);
PLOT_CHECK_NEAR(b.data()[7], pen.y - f.advance('1'), 1e-6);
PLOT_CHECK_NEAR(b.data()[7], pen.y - f.glyph('1')->advance, 1e-6);
// A diagonal direction is normalised, so a caller can hand over any direction
// and the basis the shader receives is still unit.
@@ -340,6 +486,7 @@ class TrimmedCellBackend final : public plot::render::TextBackend {
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::f32 em_px() 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 {
@@ -396,17 +543,19 @@ PLOT_TEST(a_trimmed_glyph_is_not_the_atlas_cell) {
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;
plot::render::EmbeddedMonoFont 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),
PLOT_CHECK_NEAR(font.cell_uv().x,
static_cast<plot::f32>(plot::render::EmbeddedMonoFont::cell_width()) /
static_cast<plot::f32>(font.atlas_width()),
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),
PLOT_CHECK_NEAR(font.cell_uv().y,
static_cast<plot::f32>(plot::render::EmbeddedMonoFont::cell_height()) /
static_cast<plot::f32>(font.atlas_height()),
1e-9);
}
@@ -418,6 +567,7 @@ class HalfCellBackend final : public plot::render::TextBackend {
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::f32 em_px() 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; }
const plot::render::Glyph* glyph(plot::u32) const override { return &cell_; }
@@ -471,7 +621,7 @@ PLOT_TEST(text_batch_takes_its_cell_from_the_backend) {
// 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;
plot::render::EmbeddedMonoFont 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);
@@ -503,6 +653,7 @@ class HoleInCoverageBackend final : public plot::render::TextBackend {
}
plot::f32 height() const override { return 8.0f; }
plot::f32 ascent() const override { return 6.0f; }
plot::f32 em_px() const override { return 8.0f; }
plot::Vec2 cell_uv() const override { return plot::Vec2{0.5f, 1.0f}; }
plot::u32 revision() const override { return 1u; }
const plot::render::Glyph* glyph(plot::u32 codepoint) const override {
@@ -523,7 +674,7 @@ class HoleInCoverageBackend final : public plot::render::TextBackend {
// it and does not advance the pen, so a run stays exactly as long as the same
// backend measured it and the glyphs it *can* draw keep their places. A backend
// that would rather show something publishes a fallback box as that codepoint's
// glyph (which is what `BitmapFont` does); this is the other legal answer, and the
// glyph (which is what `EmbeddedMonoFont` does); this is the other legal answer, and the
// staging path has to honour it rather than draw whatever was staged last or
// advance by a width nobody published.
PLOT_TEST(add_text_skips_a_codepoint_the_backend_cannot_draw) {
@@ -568,7 +719,7 @@ PLOT_TEST(add_text_skips_a_codepoint_the_backend_cannot_draw) {
// A real font never hits this path (every codepoint maps to its box), so the
// same string through it stages one glyph per codepoint: the two answers to
// "no glyph" are the backend's choice, not the pipeline's.
plot::render::BitmapFont font;
plot::render::EmbeddedMonoFont font;
batch.begin();
batch.add_text("A?B", pen, 1.0f, font);
PLOT_CHECK(batch.glyph_count() == 3);
+81 -48
View File
@@ -119,6 +119,24 @@ bool pixel_is(const plot::u8 px[4], plot::Color c) {
return near(px[0], c.r) && near(px[1], c.g) && near(px[2], c.b);
}
/// True when `px` holds `c` at some coverage -- the anti-aliased case `pixel_is`
/// cannot see. The embedded atlas is greyscale and the renderer draws it at
/// `Style::axis_font_px` from a 13 px bake, so at the default size a glyph stroke
/// is barely a pixel wide and *no* pixel is ever fully covered: demanding the
/// exact colour finds no text at all.
///
/// A partially covered pixel is `c` scaled toward the frame's dark chrome, so this
/// asks for every channel to be at least a third of `c`'s: bright enough to be
/// glyph ink, dark enough to exclude a background, and deliberately blind to which
/// of two chalk-toned chrome colours a pixel is -- which is why every caller scans
/// a region only one element draws in.
bool text_ink(const plot::u8 px[4], plot::Color c) {
const auto lifted = [](plot::u8 got, plot::f32 want) {
return static_cast<plot::f32>(got) >= want * 255.0f * 0.35f;
};
return lifted(px[0], c.r) && lifted(px[1], c.g) && lifted(px[2], c.b);
}
/// No column held ink.
constexpr plot::u32 kNoColumn = 0xFFFFFFFFu;
@@ -132,6 +150,7 @@ class OnePxPerStringBackend final : public plot::render::TextBackend {
plot::f32 measure(const char*) const override { return 1.0f; }
plot::f32 height() const override { return 1.0f; }
plot::f32 ascent() const override { return 1.0f; }
plot::f32 em_px() 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; }
@@ -190,6 +209,8 @@ class BakeableBackend final : public plot::render::TextBackend {
plot::f32 measure(const char*) const override { return 8.0f; }
plot::f32 height() const override { return static_cast<plot::f32>(size_); }
plot::f32 ascent() const override { return static_cast<plot::f32>(size_); }
/// Its cell is the whole atlas, so its em is the bake size: one unit is one pixel.
plot::f32 em_px() 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(); }
@@ -217,14 +238,16 @@ struct InkBox {
plot::u32 height() const noexcept { return any() ? y1 - y0 + 1u : 0u; }
};
/// Scans the whole framebuffer for fully covered `text`-coloured pixels.
/// Scans the whole framebuffer for `text`-coloured ink, anti-aliased fringe and
/// all (see `text_ink`). These cases draw nothing but the run, so everything lit
/// in the target's colour is the run.
InkBox scan_ink_box(plottest::HeadlessContext& ctx, plot::Color text) {
InkBox box;
for (plot::u32 y = 0; y < ctx.height; ++y) {
for (plot::u32 x = 0; x < ctx.width; ++x) {
plot::u8 px[4] = {};
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - y, px));
if (!pixel_is(px, text)) continue;
if (!text_ink(px, text)) continue;
if (!box.any()) {
box.x0 = x;
box.y0 = y;
@@ -290,8 +313,8 @@ struct TextInk {
plot::u32 widest_row_run = 0;
};
/// Scans the top-down pixels of [x0, x1) x [y, y + rows) for fully covered text
/// pixels in `text`.
/// Scans the top-down pixels of [x0, x1) x [y, y + rows) for `text`-coloured ink,
/// anti-aliased fringe and all (see `text_ink`).
TextInk scan_text_ink(plottest::HeadlessContext& ctx, plot::u32 x0, plot::u32 x1, plot::u32 y,
plot::u32 rows, plot::Color text) {
TextInk ink;
@@ -301,7 +324,7 @@ TextInk scan_text_ink(plottest::HeadlessContext& ctx, plot::u32 x0, plot::u32 x1
for (plot::u32 x = x0; x < x1; ++x) {
plot::u8 px[4] = {};
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - (y + r), px));
if (!pixel_is(px, text)) continue;
if (!text_ink(px, text)) continue;
if (first == kNoColumn) first = x;
last = x;
if (ink.leftmost == kNoColumn) ink.leftmost = x;
@@ -661,8 +684,12 @@ PLOT_TEST(renderer_measures_the_labels_it_draws) {
const plot::u32 y = static_cast<plot::u32>(labels.y);
const plot::u32 rows = static_cast<plot::u32>(labels.h);
const plot::u32 strip_x = static_cast<plot::u32>(labels.x);
// Exactly the strip, and no overhang: the axes' own border line runs down its
// right edge through these same rows, and a scanner that reached past the strip
// would read that line as label ink -- it is the same chalk-toned colour family
// as the text (see `text_ink`).
const TextInk ink =
scan_text_ink(ctx, strip_x, strip_x + static_cast<plot::u32>(labels.w) + 16u, y, rows, text);
scan_text_ink(ctx, strip_x, strip_x + static_cast<plot::u32>(labels.w), y, rows, text);
PLOT_CHECK(ink.leftmost != kNoColumn); // labels were drawn
PLOT_CHECK(ink.leftmost >= strip_x); // inside their strip
PLOT_CHECK(static_cast<plot::f32>(ink.widest_row_run) >= 0.6f * widest); // full width drawn
@@ -1017,7 +1044,7 @@ PLOT_TEST(titled_axis_pairs_get_their_own_rows) {
plot::u8 px[4] = {};
PLOT_CHECK(ctx.read_pixel(static_cast<plot::u32>(strip.x) + column,
ctx.height - 1u - (static_cast<plot::u32>(strip.y) + row), px));
has = pixel_is(px, title);
has = text_ink(px, title);
}
if (has && !previous) ++bands;
if (has) ++rows;
@@ -1261,11 +1288,13 @@ PLOT_TEST(rotated_text_advances_along_its_basis) {
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(nullptr));
plot::render::BitmapFont font;
plot::render::EmbeddedMonoFont font;
plot::render::TextAtlas atlas;
const plot::Vec2 pen{300.0f, 180.0f};
const plot::f32 px = 2.0f; // 16 px cells: six glyphs is over a hundred pixels
const plot::u32 cell = static_cast<plot::u32>(8.0f * px);
const plot::f32 px = 2.0f; // one cell is 16 x 34 screen pixels at this scale
const plot::u32 cell = plot::render::EmbeddedMonoFont::cell_height() * 2u;
const plot::u32 advance_px =
static_cast<plot::u32>(font.glyph(static_cast<plot::u32>('M'))->advance * px);
const InkBox flat = draw_text_run(ctx, atlas, font, "MWMWMW", pen, px, {1.0f, 0.0f});
PLOT_CHECK(flat.any());
@@ -1273,6 +1302,7 @@ PLOT_TEST(rotated_text_advances_along_its_basis) {
PLOT_CHECK(flat.x0 >= static_cast<plot::u32>(pen.x));
PLOT_CHECK(flat.y0 >= static_cast<plot::u32>(pen.y));
PLOT_CHECK(flat.height() <= cell);
PLOT_CHECK(flat.width() > 4u * advance_px); // six monospaced glyphs, and then some
const InkBox up = draw_text_run(ctx, atlas, font, "MWMWMW", pen, px, {0.0f, -1.0f});
PLOT_CHECK(up.any());
@@ -1281,42 +1311,45 @@ PLOT_TEST(rotated_text_advances_along_its_basis) {
PLOT_CHECK(up.width() <= cell); // one cell wide, so nothing marched right
PLOT_CHECK(up.x0 >= static_cast<plot::u32>(pen.x));
PLOT_CHECK(up.x1 < static_cast<plot::u32>(pen.x) + cell);
// Ink above the pen: the first cell starts at the pen and every later one is a
// cell further up, so the run's rows run from well above the pen down to the
// Ink above the pen: the first cell starts at the pen and every later one is an
// advance further up, so the run's rows run from well above the pen down to the
// pen's own cell -- and the pen's own cell is the lowest row of ink.
PLOT_CHECK(up.y0 + 4u * cell < static_cast<plot::u32>(pen.y));
PLOT_CHECK(up.y0 + 4u * advance_px < static_cast<plot::u32>(pen.y));
PLOT_CHECK(up.y1 < static_cast<plot::u32>(pen.y) + cell);
ctx.destroy();
}
// The orientation rule, stated rather than hardcoded: a glyph's texel `(col, row)`
// lands at the pen plus `col` steps along the cell's +v axis and `row` steps along
// its +x axis. That is the mapping kTextVS implements, so with `axis = (0, -1)` the
// 'L' -- whose ink is a stem down its left column and a foot along its bottom row --
// has to come out as a top bar with the foot running up the right-hand side.
// lands at the pen plus `col` steps along the cell's +x axis (`ux` here) and `row`
// steps along its +v axis (the perpendicular). That is the mapping kTextVS
// implements, so with `axis = (0, -1)` the 'L' -- whose ink is a stem down its left
// columns and a foot along its bottom rows -- has to come out as a bar standing up
// from the pen with the foot running right.
//
// The check is two-sided: the basis-derived corner holds ink and the corner a
// mirrored perpendicular would put it in does not, so a glyph flipped about its own
// diagonal fails even though it is the same size and shape.
// The check is two-sided: the texels the basis says hold ink do, and the texels a
// mirrored perpendicular would put that ink in hold none -- so a glyph flipped
// about its own diagonal fails even though it is the same size and shape. The
// coverage is anti-aliased, so "held ink" is a coverage threshold rather than an
// exact white: a texel of the stem is fully covered and a texel of the foot is
// half, and both are ink.
PLOT_TEST(rotated_glyph_orientation_is_not_mirrored) {
plottest::HeadlessContext ctx(kW, kH);
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(nullptr));
plot::render::BitmapFont font;
plot::render::EmbeddedMonoFont font;
plot::render::TextAtlas atlas;
const plot::Vec2 pen{300.0f, 200.0f};
const plot::f32 px = 4.0f; // an 8x8 glyph at 32 px: one texel is a 4x4 pixel block
const plot::f32 px = 4.0f; // one texel is a 4x4 pixel block
const plot::Vec2 ux{0.0f, -1.0f}; // a 90 degree counter-clockwise label
const plot::Vec2 uy{-ux.y, ux.x}; // (1, 0): the cell's own +v axis
// Where the centre of the glyph's texel (col, row) is drawn. Every cell is
// `kGlyphPx` texels across, the cell's top-left is the pen because the bitmap
// font's bearings are zero, and the rule is the shader's own: the atlas' u runs
// along the cell's +x axis (`ux` here, `a_axis` there) and its v along the
// perpendicular (`v_axis`), which is what `v_uv = a_uv + corner * u_glyph_uv`
// maps corner by corner.
// Where the centre of the glyph's texel (col, row) is drawn. The cell's top-left
// is the pen (the embedded font bakes its bearings into the cell), and the rule
// is the shader's own: the atlas' u runs along the cell's +x axis (`ux` here,
// `a_axis` there) and its v along the perpendicular (`v_axis`), which is what
// `v_uv = a_uv + corner * u_glyph_uv` maps corner by corner.
auto texel_centre = [&](plot::f32 col, plot::f32 row, plot::Vec2 v_axis) {
const plot::f32 du = (col + 0.5f) * px;
const plot::f32 dv = (row + 0.5f) * px;
@@ -1326,28 +1359,28 @@ PLOT_TEST(rotated_glyph_orientation_is_not_mirrored) {
plot::u8 pxv[4] = {};
PLOT_CHECK(ctx.read_pixel(static_cast<plot::u32>(at.x),
ctx.height - 1u - static_cast<plot::u32>(at.y), pxv));
return pixel_is(pxv, plot::Color::hex(0xFFFFFF));
return pxv[0] >= 96u; // white tint over black: the pixel *is* the coverage
};
PLOT_CHECK(draw_text_run(ctx, atlas, font, "L", pen, px, ux).any());
// 'L' is inked at texel (0, 0) -- the stem's top -- and at (6, 6), the far end of
// its foot. Both corners are where the basis puts them ...
PLOT_CHECK(inked(texel_centre(0.0f, 0.0f, uy)));
PLOT_CHECK(inked(texel_centre(6.0f, 6.0f, uy)));
// ... and the two *off-diagonal* texels of its ink state the rule the diagonal
// pair cannot: the stem meets the foot at (0, 6), which is where the cell's
// first texel row and last texel column meet, while (6, 0) is blank. A shader
// that swapped the cell's two axes would trade those two, and both of these
// would fail -- the diagonal probes would not.
PLOT_CHECK(inked(texel_centre(0.0f, 6.0f, uy)));
PLOT_CHECK(!inked(texel_centre(6.0f, 0.0f, uy)));
// ... and the mirrored basis -- the other perpendicular a shader could build --
// would put the stem's top on the opposite side of the pen, where this frame has
// no ink at all.
// The stem (cell columns 1-2, rows 3-12) and the foot (columns 2-6, rows 11-12)
// put ink at these texels ...
PLOT_CHECK(inked(texel_centre(2.0f, 4.0f, uy))); // stem, fully covered
PLOT_CHECK(inked(texel_centre(2.0f, 11.0f, uy))); // stem meets the foot
PLOT_CHECK(inked(texel_centre(5.0f, 11.0f, uy))); // the foot's far end
// ... and none at these, which are the cell's blank margin: a shader that
// swapped the cell's two axes would put the ink in the wrong half and fail both
// sides.
PLOT_CHECK(!inked(texel_centre(2.0f, 0.0f, uy))); // above the first row of ink
PLOT_CHECK(!inked(texel_centre(6.0f, 4.0f, uy))); // right of the stem at cap height
PLOT_CHECK(!inked(texel_centre(0.0f, 4.0f, uy))); // the cell's empty first column
// The mirrored perpendicular -- the other basis a shader could build -- would
// draw the whole cell on the opposite side of the pen, where this frame has no
// ink at all.
const plot::Vec2 mirrored{-uy.x, -uy.y};
PLOT_CHECK(!inked(texel_centre(0.0f, 0.0f, mirrored)));
PLOT_CHECK(!inked(texel_centre(6.0f, 6.0f, mirrored)));
PLOT_CHECK(!inked(texel_centre(2.0f, 4.0f, mirrored)));
PLOT_CHECK(!inked(texel_centre(5.0f, 11.0f, mirrored)));
ctx.destroy();
}
@@ -1505,7 +1538,7 @@ PLOT_TEST(text_atlas_reports_a_status_when_the_entry_points_are_missing) {
PLOT_CHECK(plot::gl::load(nullptr));
PLOT_CHECK(plot::gl::available());
plot::render::BitmapFont font;
plot::render::EmbeddedMonoFont font;
plot::render::TextAtlas atlas;
{
RefuseGetIntegerv refused;
@@ -1522,8 +1555,8 @@ PLOT_TEST(text_atlas_reports_a_status_when_the_entry_points_are_missing) {
// the Status it got.
PLOT_CHECK(atlas.sync(font, &st));
PLOT_CHECK(!atlas.empty());
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(plot::render::BitmapFont::kAtlasWidth),
1e-6);
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(font.atlas_width()), 1e-6);
PLOT_CHECK_NEAR(atlas.texel_size().y, static_cast<plot::f32>(font.atlas_height()), 1e-6);
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
ctx.destroy();
}