feat(render): style, GLSL line/text pipelines, staging batches, bitmap font

This commit is contained in:
Martino Ferrari committed 2026-09-27 22:03:56 +02:00
1 parent 9a086b7d69
commit 7cbc1c9955
22 files changed
+1535

No files matched your search

+277
View File
@@ -0,0 +1,277 @@
#include <plot/render/bitmap_font.hpp>
#include <array>
namespace plot::render {
namespace {
// Glyph data and its provenance
// -----------------------------
// The 95 printable ASCII glyphs (slots 0..94, U+0020..U+007E) are the
// `font8x8_basic` table of the font8x8 project by Daniel Hepper, released into
// the public domain. It is itself based on Marcel Sondaar's public-domain 8x8
// VGA font (via the IBM public-domain VGA fonts). Fetched on 2026-09-27 from
// https://raw.githubusercontent.com/dhepper/font8x8/master/font8x8_basic.h
// The glyphs the plot layer's SI labels need come from the same project:
// U+00B0 DEGREE SIGN and U+00B5 MICRO SIGN from font8x8_ext_latin.h (which
// covers U+00A0..U+00FF), and U+03A9 GREEK CAPITAL LETTER OMEGA from
// font8x8_greek.h (which covers U+0390..U+03C9).
// The missing-glyph box in the last slot is authored here: a hollow 8x8 square.
// Nothing else in this table is hand-drawn, and none of it is a library: it is
// public-domain data, so `plot_core` still links nothing third-party.
//
// Row 0 is the top row and bit 0 is the leftmost pixel, one byte per row.
constexpr u8 kGlyphBits[BitmapFont::kGlyphCount][BitmapFont::kGlyphPx] = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // [ 0] U+0020 space
{0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // [ 1] U+0021 '!'
{0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // [ 2] U+0022 '"'
{0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // [ 3] U+0023 '#'
{0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // [ 4] U+0024 '$'
{0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // [ 5] U+0025 '%'
{0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // [ 6] U+0026 '&'
{0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // [ 7] U+0027 '''
{0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // [ 8] U+0028 '('
{0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // [ 9] U+0029 ')'
{0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // [10] U+002A '*'
{0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // [11] U+002B '+'
{0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // [12] U+002C ','
{0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // [13] U+002D '-'
{0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // [14] U+002E '.'
{0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // [15] U+002F '/'
{0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // [16] U+0030 '0'
{0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // [17] U+0031 '1'
{0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // [18] U+0032 '2'
{0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // [19] U+0033 '3'
{0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // [20] U+0034 '4'
{0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // [21] U+0035 '5'
{0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // [22] U+0036 '6'
{0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // [23] U+0037 '7'
{0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // [24] U+0038 '8'
{0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // [25] U+0039 '9'
{0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // [26] U+003A ':'
{0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // [27] U+003B ';'
{0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // [28] U+003C '<'
{0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // [29] U+003D '='
{0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // [30] U+003E '>'
{0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // [31] U+003F '?'
{0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // [32] U+0040 '@'
{0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // [33] U+0041 'A'
{0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // [34] U+0042 'B'
{0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // [35] U+0043 'C'
{0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // [36] U+0044 'D'
{0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // [37] U+0045 'E'
{0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // [38] U+0046 'F'
{0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // [39] U+0047 'G'
{0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // [40] U+0048 'H'
{0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [41] U+0049 'I'
{0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // [42] U+004A 'J'
{0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // [43] U+004B 'K'
{0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // [44] U+004C 'L'
{0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // [45] U+004D 'M'
{0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // [46] U+004E 'N'
{0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // [47] U+004F 'O'
{0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // [48] U+0050 'P'
{0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // [49] U+0051 'Q'
{0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // [50] U+0052 'R'
{0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // [51] U+0053 'S'
{0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [52] U+0054 'T'
{0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // [53] U+0055 'U'
{0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // [54] U+0056 'V'
{0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // [55] U+0057 'W'
{0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // [56] U+0058 'X'
{0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // [57] U+0059 'Y'
{0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // [58] U+005A 'Z'
{0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // [59] U+005B '['
{0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // [60] U+005C '\'
{0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // [61] U+005D ']'
{0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // [62] U+005E '^'
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // [63] U+005F '_'
{0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // [64] U+0060 '`'
{0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // [65] U+0061 'a'
{0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // [66] U+0062 'b'
{0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // [67] U+0063 'c'
{0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00}, // [68] U+0064 'd'
{0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00}, // [69] U+0065 'e'
{0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00}, // [70] U+0066 'f'
{0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // [71] U+0067 'g'
{0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // [72] U+0068 'h'
{0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [73] U+0069 'i'
{0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // [74] U+006A 'j'
{0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // [75] U+006B 'k'
{0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [76] U+006C 'l'
{0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // [77] U+006D 'm'
{0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // [78] U+006E 'n'
{0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // [79] U+006F 'o'
{0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // [80] U+0070 'p'
{0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // [81] U+0071 'q'
{0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // [82] U+0072 'r'
{0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // [83] U+0073 's'
{0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // [84] U+0074 't'
{0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // [85] U+0075 'u'
{0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // [86] U+0076 'v'
{0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // [87] U+0077 'w'
{0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // [88] U+0078 'x'
{0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // [89] U+0079 'y'
{0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // [90] U+007A 'z'
{0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // [91] U+007B '{'
{0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // [92] U+007C '|'
{0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // [93] U+007D '}'
{0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // [94] U+007E '~'
{0x1C, 0x36, 0x36, 0x1C, 0x00, 0x00, 0x00, 0x00}, // [95] U+00B0 degree
{0x00, 0x00, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x03}, // [96] U+00B5 micro
{0x3E, 0x63, 0x63, 0x63, 0x36, 0x36, 0x77, 0x00}, // [97] U+03A9 ohm
{0x7E, 0x42, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x00}, // [98] missing-glyph box, authored here
};
/// Repacks the glyph-major table into the atlas image the text pipeline
/// uploads: one 8x8 cell per slot, 255 for ink, straight alpha, no dilation.
constexpr std::array<u8, BitmapFont::kAtlasWidth * BitmapFont::kAtlasHeight> pack_atlas() noexcept {
std::array<u8, BitmapFont::kAtlasWidth * BitmapFont::kAtlasHeight> image{};
for (u32 slot = 0; slot < BitmapFont::kGlyphCount; ++slot) {
const u32 cell_x = (slot % BitmapFont::kAtlasCols) * BitmapFont::kGlyphPx;
const u32 cell_y = (slot / BitmapFont::kAtlasCols) * BitmapFont::kGlyphPx;
for (u32 y = 0; y < BitmapFont::kGlyphPx; ++y) {
const u8 bits = kGlyphBits[slot][y];
for (u32 x = 0; x < BitmapFont::kGlyphPx; ++x) {
image[(cell_y + y) * BitmapFont::kAtlasWidth + cell_x + x] =
static_cast<u8>(((bits >> x) & 1u) != 0u ? 0xFFu : 0x00u);
}
}
}
return image;
}
constexpr std::array<u8, BitmapFont::kAtlasWidth * BitmapFont::kAtlasHeight> kAtlasImage =
pack_atlas();
/// Span of set bits in a glyph's rows: 0 for a blank glyph such as space.
constexpr u32 ink_width(const u8* rows) noexcept {
u32 lo = BitmapFont::kGlyphPx;
u32 hi = 0;
bool any = false;
for (u32 y = 0; y < BitmapFont::kGlyphPx; ++y) {
for (u32 x = 0; x < BitmapFont::kGlyphPx; ++x) {
if (((rows[y] >> x) & 1u) != 0u) {
if (x < lo) {
lo = x;
}
if (x > hi) {
hi = x;
}
any = true;
}
}
}
return any ? hi - lo + 1u : 0u;
}
} // namespace
// The decoder is public because the text batch stages glyphs from the same
// codepoints `measure` sums: one implementation keeps "what a string is worth"
// and "what a string draws" the same thing.
u32 BitmapFont::next_codepoint(const char*& p) noexcept {
const auto b0 = static_cast<u32>(static_cast<unsigned char>(p[0]));
if (b0 < 0x80u) {
++p;
return b0;
}
u32 codepoint = 0;
u32 extra = 0;
if ((b0 & 0xE0u) == 0xC0u) {
codepoint = b0 & 0x1Fu;
extra = 1u;
} else if ((b0 & 0xF0u) == 0xE0u) {
codepoint = b0 & 0x0Fu;
extra = 2u;
} else if ((b0 & 0xF8u) == 0xF0u) {
codepoint = b0 & 0x07u;
extra = 3u;
} else {
++p;
return kInvalidCodepoint;
}
for (u32 i = 1; i <= extra; ++i) {
const auto b = static_cast<u32>(static_cast<unsigned char>(p[i]));
if ((b & 0xC0u) != 0x80u) { // truncated: stop before the terminator
++p;
return kInvalidCodepoint;
}
codepoint = (codepoint << 6) | (b & 0x3Fu);
}
p += extra + 1u;
constexpr u32 kMinForLength[4] = {0u, 0x80u, 0x800u, 0x10000u};
if (codepoint < kMinForLength[extra] || codepoint > 0x10FFFFu ||
(codepoint >= 0xD800u && codepoint <= 0xDFFFu)) {
return kInvalidCodepoint; // overlong, out of range or a surrogate
}
return codepoint;
}
BitmapFont::BitmapFont() noexcept {
for (u32 slot = 0; slot < kGlyphCount; ++slot) {
const u32 ink = ink_width(kGlyphBits[slot]);
const u32 with_spacing = ink + 1u;
advances_[slot] = static_cast<f32>(with_spacing < 2u ? 2u : with_spacing);
}
}
f32 BitmapFont::measure(const char* utf8) const {
if (utf8 == nullptr) {
return 0.0f;
}
f32 width = 0.0f;
for (const char* p = utf8; *p != '\0';) {
width += advances_[glyph_index(next_codepoint(p))];
}
return width;
}
f32 BitmapFont::height() const { return static_cast<f32>(kGlyphPx); }
f32 BitmapFont::ascent() const { return static_cast<f32>(kGlyphPx - 1u); }
f32 BitmapFont::advance(char c) const noexcept {
return advances_[glyph_index(static_cast<u32>(static_cast<unsigned char>(c)))];
}
f32 BitmapFont::advance_codepoint(u32 codepoint) const noexcept {
return advances_[glyph_index(codepoint)];
}
u32 BitmapFont::glyph_index(u32 codepoint) noexcept {
if (codepoint >= kFirstChar && codepoint < kFirstChar + kAsciiGlyphs) {
return codepoint - kFirstChar;
}
switch (codepoint) {
case 0x00B0u:
return kDegreeSlot;
case 0x00B5u:
return kMicroSlot;
case 0x03A9u:
return kOhmSlot;
default:
return kFallbackSlot;
}
}
u32 BitmapFont::glyph_rows(char) const noexcept { return kGlyphPx; }
const u8* BitmapFont::glyph_bitmap(char c) const noexcept {
return glyph_bitmap_of(glyph_index(static_cast<u32>(static_cast<unsigned char>(c))));
}
const u8* BitmapFont::glyph_bitmap_of(u32 slot) noexcept {
return kGlyphBits[slot < kGlyphCount ? slot : kFallbackSlot];
}
const u8* BitmapFont::atlas_image() noexcept { return kAtlasImage.data(); }
Vec2 BitmapFont::atlas_origin(u32 slot) noexcept {
const u32 s = slot < kGlyphCount ? slot : kFallbackSlot;
return Vec2{static_cast<f32>((s % kAtlasCols) * kGlyphPx),
static_cast<f32>((s / kAtlasCols) * kGlyphPx)};
}
} // namespace plot::render