280 lines
12 KiB
C++
280 lines
12 KiB
C++
// 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.
|
|
// 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/primitives.hpp>
|
|
|
|
#include <cstddef>
|
|
|
|
PLOT_TEST(line_batch_emits_two_verts_per_segment) {
|
|
plot::render::LineBatch b;
|
|
b.begin();
|
|
const plot::Vec2 pts[3] = {{0, 0}, {1, 1}, {2, 0}};
|
|
b.add_polyline(pts, 3); // 2 segments
|
|
PLOT_CHECK(b.vertex_count() == 4);
|
|
b.clear();
|
|
PLOT_CHECK(b.vertex_count() == 0);
|
|
PLOT_CHECK(b.data().capacity() >= 4); // clear() must not free capacity
|
|
b.add_segment({0, 0}, {1, 1});
|
|
PLOT_CHECK(b.vertex_count() == 2);
|
|
}
|
|
|
|
PLOT_TEST(line_batch_stages_segment_endpoints) {
|
|
plot::render::LineBatch b;
|
|
b.begin();
|
|
b.add_segment({1.0f, 2.0f}, {3.0f, 4.0f});
|
|
PLOT_CHECK(b.data().size() == 4); // x0,y0,x1,y1 for the one instance
|
|
PLOT_CHECK_NEAR(b.data()[0], 1.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[1], 2.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[2], 3.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[3], 4.0, 0.0);
|
|
|
|
// A polyline stages one instance per consecutive pair, in order.
|
|
const plot::Vec2 pts[3] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}};
|
|
b.begin();
|
|
b.add_polyline(pts, 3);
|
|
PLOT_CHECK(b.data().size() == 8);
|
|
PLOT_CHECK_NEAR(b.data()[2], 1.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[4], 1.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[5], 0.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[6], 1.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[7], 1.0, 0.0);
|
|
}
|
|
|
|
// `add_polyline_xy` takes the layout `Series::gather` appends to, so this pins
|
|
// that a gathered window becomes the segments it says it does -- an off-by-one in
|
|
// the stride here would draw a diagonal where the data has a corner.
|
|
PLOT_TEST(line_batch_stages_interleaved_points) {
|
|
plot::render::LineBatch b;
|
|
b.begin();
|
|
const plot::f32 xy[8] = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f};
|
|
b.add_polyline_xy(xy, 4); // 3 segments
|
|
PLOT_CHECK(b.segment_count() == 3);
|
|
PLOT_CHECK(b.vertex_count() == 6);
|
|
PLOT_CHECK(b.data().size() == 12);
|
|
PLOT_CHECK_NEAR(b.data()[0], 0.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[1], 1.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[2], 2.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[3], 3.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[10], 6.0, 0.0);
|
|
PLOT_CHECK_NEAR(b.data()[11], 7.0, 0.0);
|
|
// Fewer than two points, or none: nothing to draw, and nothing to crash on.
|
|
b.clear();
|
|
b.add_polyline_xy(xy, 1);
|
|
b.add_polyline_xy(nullptr, 4);
|
|
PLOT_CHECK(b.vertex_count() == 0);
|
|
}
|
|
|
|
PLOT_TEST(line_batch_ignores_degenerate_polylines) {
|
|
plot::render::LineBatch b;
|
|
b.begin();
|
|
const plot::Vec2 one[1] = {{5.0f, 5.0f}};
|
|
b.add_polyline(one, 1); // no pair -> no instance
|
|
b.add_polyline(nullptr, 0);
|
|
PLOT_CHECK(b.vertex_count() == 0);
|
|
PLOT_CHECK(b.data().empty());
|
|
b.add_polyline(nullptr, 3); // null with a count: still no deref
|
|
PLOT_CHECK(b.vertex_count() == 0);
|
|
}
|
|
|
|
PLOT_TEST(line_batch_reuses_capacity_across_frames) {
|
|
plot::render::LineBatch b;
|
|
const plot::Vec2 pts[5] = {{0, 0}, {1, 1}, {2, 0}, {3, 1}, {4, 0}};
|
|
b.begin();
|
|
b.add_polyline(pts, 5); // 4 segments -> 16 floats
|
|
const std::size_t cap = b.data().capacity();
|
|
const f32* store = b.data().data();
|
|
PLOT_CHECK(cap >= 16);
|
|
|
|
b.clear();
|
|
PLOT_CHECK(b.vertex_count() == 0);
|
|
PLOT_CHECK(b.data().capacity() == cap); // clear() keeps the store
|
|
|
|
b.begin();
|
|
b.add_polyline(pts, 5); // same workload: must not reallocate
|
|
PLOT_CHECK(b.data().size() == 16);
|
|
PLOT_CHECK(b.data().capacity() == cap);
|
|
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
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
|
|
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.
|
|
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);
|
|
}
|
|
|
|
// 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;
|
|
};
|
|
// '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));
|
|
}
|
|
|
|
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);
|
|
|
|
plot::render::BitmapFont f;
|
|
PLOT_CHECK(f.glyph_rows('A') == 8);
|
|
PLOT_CHECK(f.height() > 0.0f);
|
|
PLOT_CHECK(f.ascent() > 0.0f);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
// The atlas is a whole number of cells and no glyph leaves its cell.
|
|
PLOT_CHECK(BitmapFont::kAtlasWidth == 16u * BitmapFont::kGlyphPx);
|
|
}
|
|
|
|
PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
|
|
plot::render::BitmapFont f;
|
|
plot::render::TextBatch b;
|
|
b.begin();
|
|
const plot::Vec2 end = b.add_text({0.0f, 0.0f}, "12.5 ms", f);
|
|
PLOT_CHECK(b.glyph_count() == 7);
|
|
PLOT_CHECK(b.data().size() == 28); // x,y,u,v per glyph
|
|
PLOT_CHECK_NEAR(end.x, f.measure("12.5 ms"), 1e-6);
|
|
PLOT_CHECK_NEAR(end.y, 0.0, 1e-6);
|
|
PLOT_CHECK_NEAR(b.data()[0], 0.0, 1e-6); // first quad at the pen
|
|
PLOT_CHECK_NEAR(b.data()[1], 0.0, 1e-6);
|
|
PLOT_CHECK_NEAR(b.data()[4], f.advance('1'), 1e-6); // pen advanced per glyph
|
|
// The UV origin of the staged quad is the glyph's atlas cell.
|
|
const plot::Vec2 dot_uv =
|
|
plot::render::BitmapFont::atlas_origin(static_cast<plot::u32>('.') - 32u);
|
|
PLOT_CHECK_NEAR(b.data()[10], dot_uv.x, 1e-6);
|
|
PLOT_CHECK_NEAR(b.data()[11], dot_uv.y, 1e-6);
|
|
|
|
// Non-ASCII runs stage one quad per codepoint, and preserve capacity on reuse.
|
|
const std::size_t cap = b.data().capacity();
|
|
b.begin();
|
|
b.add_text({0.0f, 0.0f}, "1.5\xC2\xB5s", f); // 5 codepoints, 6 bytes
|
|
PLOT_CHECK(b.glyph_count() == 5);
|
|
PLOT_CHECK(b.data().capacity() == cap);
|
|
|
|
// A run drawn at twice the glyph cell size advances twice as far, so the
|
|
// staged label still ends where the scaled measurement says it does.
|
|
b.begin();
|
|
const plot::Vec2 scaled = b.add_text({10.0f, 4.0f}, "AW", f, 16.0f);
|
|
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()[4], 10.0f + 2.0f * f.advance('A'), 1e-6); // second glyph's pen
|
|
}
|