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
+32
View File
@@ -0,0 +1,32 @@
#include <plot/render/buffer.hpp>
namespace plot::render {
namespace detail {
bool InstanceStore::create(Status* st) noexcept {
// plot::gl::Buffer::create() already reports Unsupported while the entry
// points are unresolved and GLFailure when the driver hands out no name, so
// the wrapper only has to propagate its answer.
if (!buffer_.create(st)) {
return false;
}
capacity_ = 0;
return true;
}
void InstanceStore::upload(const void* data, u32 count, usize instance_bytes) noexcept {
if (buffer_.id() == 0 || count == 0 || instance_bytes == 0) {
return;
}
if (count > capacity_) {
// Only a workload larger than anything staged so far pays for an
// allocation; a frame that repeats the previous one writes into the store
// that is already there.
buffer_.upload(nullptr, static_cast<usize>(count) * instance_bytes, gl::GL_STREAM_DRAW);
capacity_ = count;
}
buffer_.upload_sub(data, static_cast<usize>(count) * instance_bytes, 0);
}
} // namespace detail
} // namespace plot::render
+61
View File
@@ -0,0 +1,61 @@
#include <plot/render/primitives.hpp>
namespace plot::render {
// ----------------------------------------------------------------- LineBatch
void LineBatch::begin() noexcept { v_.clear(); }
void LineBatch::clear() noexcept { v_.clear(); }
void LineBatch::add_segment(Vec2 a, Vec2 b) {
v_.push_back(a.x);
v_.push_back(a.y);
v_.push_back(b.x);
v_.push_back(b.y);
}
void LineBatch::add_polyline(const Vec2* pts, u32 n) {
if (pts == nullptr || n < 2u) {
return; // no pair of points, no segment
}
// Grow once for the whole polyline rather than per segment, so a frame that
// stages the same workload as the last one never reallocates.
v_.reserve(v_.size() + 4u * (n - 1u));
for (u32 i = 1; i < n; ++i) {
add_segment(pts[i - 1u], pts[i]);
}
}
// ----------------------------------------------------------------- TextBatch
void TextBatch::begin() noexcept { v_.clear(); }
void TextBatch::clear() noexcept { v_.clear(); }
void TextBatch::add_glyph(Vec2 pos_px, u32 slot) {
const Vec2 origin = BitmapFont::atlas_origin(slot);
v_.push_back(pos_px.x);
v_.push_back(pos_px.y);
v_.push_back(origin.x);
v_.push_back(origin.y);
}
Vec2 TextBatch::add_text(Vec2 pen_px, const char* utf8, const BitmapFont& font, f32 px_per_cell) {
if (utf8 == nullptr) {
return pen_px;
}
const f32 scale = px_per_cell / static_cast<f32>(BitmapFont::kGlyphPx);
for (const char* p = utf8; *p != '\0';) {
// Same codepoints `measure` sums and the same advances, scaled by the
// on-screen cell size: a run staged here is exactly what the plot layer
// measured, times the scale it draws at, so a label never overruns the
// space that was reserved for it.
const u32 codepoint = BitmapFont::next_codepoint(p);
add_glyph(pen_px, BitmapFont::glyph_index(codepoint));
pen_px.x += font.advance_codepoint(codepoint) * scale;
}
return pen_px;
}
} // namespace plot::render
+50
View File
@@ -0,0 +1,50 @@
#include <plot/render/program.hpp>
#include <plot/render/shaders.hpp>
namespace plot::render {
bool LineProgram::create(Status* st) noexcept {
// plot::gl::Program::from_source() reports Unsupported while
// plot::gl::available() is false and GLFailure when the driver rejects the
// GLSL. Propagating it is the whole point of returning a bool here: the
// caller must not end up drawing with a program that never linked.
program_ = gl::Program::from_source(kLineVS, kLineFS, st);
return program_.id() != 0;
}
void LineProgram::set_scale(Vec2 data_to_px) const noexcept {
program_.set_uniform("u_scale", data_to_px);
}
void LineProgram::set_offset(Vec2 px) const noexcept { program_.set_uniform("u_offset", px); }
void LineProgram::set_viewport(Vec2 px) const noexcept { program_.set_uniform("u_viewport", px); }
void LineProgram::set_width(f32 px) const noexcept { program_.set_uniform("u_width", px); }
void LineProgram::set_color(Color color) const noexcept { program_.set_uniform("u_color", color); }
bool TextProgram::create(Status* st) noexcept {
program_ = gl::Program::from_source(kTextVS, kTextFS, st);
if (program_.id() == 0) {
return false;
}
// The atlas is bound to texture unit 0 by plot::gl::Texture::bind(), so the
// sampler has to point there. It is set once, here, not per frame.
program_.use();
program_.set_uniform_int("u_atlas", 0);
return true;
}
void TextProgram::set_viewport(Vec2 px) const noexcept { program_.set_uniform("u_viewport", px); }
void TextProgram::set_px_range(Vec2 px) const noexcept { program_.set_uniform("u_px_range", px); }
void TextProgram::set_texture_size(Vec2 texels) const noexcept {
program_.set_uniform("u_tex_size", texels);
}
void TextProgram::set_color(Color color) const noexcept { program_.set_uniform("u_color", color); }
} // namespace plot::render
+110
View File
@@ -0,0 +1,110 @@
#include <plot/render/shaders.hpp>
// Both pipelines are `#version 330 core` and stay untouched by the upload
// strategy: the 4.3+ path changes how vertex data reaches the GPU, never the
// GLSL, so one program pair serves every context the library supports. The
// declarations here are the contract the render layer's attribute layouts and
// uniform setters mirror (see program.hpp, vertex_array.hpp, shaders.hpp).
namespace plot::render {
const char* const kLineVS = R"(#version 330 core
layout(location = 0) in vec2 a_p0; // per-instance segment start, data space
layout(location = 1) in vec2 a_p1; // per-instance segment end
uniform vec2 u_scale; // data -> pixels
uniform vec2 u_offset; // pixels
uniform vec2 u_viewport; // render target size, pixels
uniform float u_width; // stroke width, pixels
out float v_dist; // signed distance to the segment axis, pixels
out float v_along; // distance travelled from a_p0, pixels
void main() {
vec2 p0 = a_p0 * u_scale + u_offset;
vec2 p1 = a_p1 * u_scale + u_offset;
vec2 d = p1 - p0;
float len = length(d);
// A degenerate segment has no direction: pin the normal so the quad stays
// finite and the instance draws as a dot instead of NaN geometry.
vec2 n = len > 1e-6 ? vec2(-d.y, d.x) / len : vec2(0.0, 1.0);
float half_w = u_width * 0.5 + 1.0; // + one pixel of AA fringe
// gl_VertexID picks the corner: 0,1 are the sides of a_p0 and 2,3 of a_p1,
// which is the order GL_TRIANGLE_STRIP expects.
float side = (gl_VertexID & 1) == 0 ? -1.0 : 1.0;
vec2 base = (gl_VertexID & 2) == 0 ? p0 : p1;
vec2 px = base + n * (side * half_w);
// Rect is top-left origin, GL is bottom-left: flip y once, here, and never
// again in the fragment shader or in a caller.
vec2 ndc = vec2(px.x / u_viewport.x * 2.0 - 1.0, 1.0 - px.y / u_viewport.y * 2.0);
gl_Position = vec4(ndc, 0.0, 1.0);
v_dist = side * half_w;
v_along = (gl_VertexID & 2) == 0 ? 0.0 : len;
}
)";
const char* const kLineFS = R"(#version 330 core
in float v_dist; // signed distance to the segment axis, pixels
uniform float u_width; // stroke width, pixels
uniform vec4 u_color;
out vec4 o_color;
void main() {
float half_w = u_width * 0.5;
float d = abs(v_dist);
if (d > half_w + 1.0) {
discard; // outside the fringe; nothing to blend
}
// Ramp over the last pixel of the half width so a thin stroke keeps its
// weight instead of aliasing into a dashed line.
float alpha = 1.0 - smoothstep(half_w - 0.5, half_w + 0.5, d);
o_color = vec4(u_color.rgb, u_color.a * alpha);
}
)";
const char* const kTextVS = R"(#version 330 core
layout(location = 0) in vec2 a_pos; // per-instance glyph quad top-left, pixels
layout(location = 1) in vec2 a_uv; // per-instance glyph atlas texel origin
uniform vec2 u_viewport; // render target size, pixels
uniform vec2 u_px_range; // pixels per glyph cell on screen
out vec2 v_texel; // atlas texel coordinate of this fragment
const vec2 kGlyphTexels = vec2(8.0); // BitmapFont::kGlyphPx
void main() {
vec2 corner = vec2(float(gl_VertexID & 1), float((gl_VertexID >> 1) & 1));
vec2 px = a_pos + corner * u_px_range;
// Rect is top-left origin, GL is bottom-left: the one and only y flip.
vec2 ndc = vec2(px.x / u_viewport.x * 2.0 - 1.0, 1.0 - px.y / u_viewport.y * 2.0);
gl_Position = vec4(ndc, 0.0, 1.0);
v_texel = a_uv + corner * kGlyphTexels;
}
)";
const char* const kTextFS = R"(#version 330 core
in vec2 v_texel; // atlas texel coordinate of this fragment
uniform sampler2D u_atlas; // GL_R8 glyph atlas, GL_NEAREST
uniform vec2 u_tex_size; // atlas size in texels
uniform vec4 u_color;
out vec4 o_color;
void main() {
// Sample the centre of the texel the fragment lands in. GL_NEAREST alone
// would tie at a texel boundary, where the driver may pick the neighbouring
// glyph cell; rounding to a centre removes the tie by construction.
vec2 texel = clamp(floor(v_texel), vec2(0.0), u_tex_size - vec2(1.0));
float coverage = texture(u_atlas, (texel + vec2(0.5)) / u_tex_size).r;
o_color = vec4(u_color.rgb, u_color.a * coverage);
}
)";
} // namespace plot::render
+76
View File
@@ -0,0 +1,76 @@
#include <plot/render/vertex_array.hpp>
#include <plot/gl/api.hpp>
#include <cstddef>
namespace plot::render {
namespace {
/// Instance data advances one step per instance, never per vertex: both
/// pipelines draw one quad per instance with corners synthesised from
/// `gl_VertexID`, so every attribute has a divisor of 1 (core since GL 3.3).
constexpr gl::GLuint kPerInstance = 1;
} // namespace
bool LineVertexArray::create(Status* st) noexcept {
if (!vao_.create(st)) {
return false;
}
if (!buffer_.create(st)) {
vao_.reset(); // do not leave a vertex array behind on the failure path
return false;
}
// Attribute pointers capture the bound GL_ARRAY_BUFFER, so bind both first.
vao_.bind();
buffer_.bind();
// Offsets come from the struct, not from counting floats: the layout is the
// one shaders.hpp declares, and it stays in step with LineInstance.
vao_.attrib(0u, 2, sizeof(LineInstance), offsetof(LineInstance, x0));
vao_.attrib(1u, 2, sizeof(LineInstance), offsetof(LineInstance, x1));
gl::glVertexAttribDivisor(0u, kPerInstance);
gl::glVertexAttribDivisor(1u, kPerInstance);
gl::glBindVertexArray(0); // leave the binding as we found it
return true;
}
void LineVertexArray::upload(const f32* interleaved, u32 segments) noexcept {
// An array that was never created reports nothing to draw, so a caller that
// ignored create()'s status rasterises nothing instead of stale memory.
if (interleaved == nullptr || vao_.id() == 0) {
instances_ = 0;
return;
}
buffer_.upload(interleaved, segments);
instances_ = segments;
}
bool TextVertexArray::create(Status* st) noexcept {
if (!vao_.create(st)) {
return false;
}
if (!buffer_.create(st)) {
vao_.reset();
return false;
}
vao_.bind();
buffer_.bind();
vao_.attrib(0u, 2, sizeof(TextInstance), offsetof(TextInstance, x));
vao_.attrib(1u, 2, sizeof(TextInstance), offsetof(TextInstance, u));
gl::glVertexAttribDivisor(0u, kPerInstance);
gl::glVertexAttribDivisor(1u, kPerInstance);
gl::glBindVertexArray(0);
return true;
}
void TextVertexArray::upload(const f32* interleaved, u32 glyphs) noexcept {
if (interleaved == nullptr || vao_.id() == 0) {
instances_ = 0;
return;
}
buffer_.upload(interleaved, glyphs);
instances_ = glyphs;
}
} // namespace plot::render