refactor(render): TextBackend owns its atlas, and staged glyphs carry a rotation basis

This commit is contained in:
Martino Ferrari committed 2026-09-28 09:57:28 +02:00
1 parent 90e17a0a11
commit e074f57ddf
23 files changed
+1073 -227

No files matched your search

+41 -51
View File
@@ -166,56 +166,32 @@ constexpr u32 ink_width(const u8* rows) noexcept {
return any ? hi - lo + 1u : 0u;
}
/// The glyph table the pipeline stages from: one entry per slot, its rect
/// normalised by the atlas size with `v` growing downward (see `Glyph`), a zero
/// bearing -- the ink sits at the cell's top-left -- and the ink-derived advance.
/// It is built from the same two tables `pack_atlas()` packs, so the rect a glyph
/// reports and the cell its ink was written to cannot disagree.
constexpr std::array<Glyph, BitmapFont::kGlyphCount> build_glyphs() noexcept {
std::array<Glyph, BitmapFont::kGlyphCount> glyphs{};
constexpr f32 kCell = static_cast<f32>(BitmapFont::kGlyphPx);
constexpr f32 kU = kCell / static_cast<f32>(BitmapFont::kAtlasWidth);
constexpr f32 kV = kCell / static_cast<f32>(BitmapFont::kAtlasHeight);
for (u32 slot = 0; slot < BitmapFont::kGlyphCount; ++slot) {
const f32 u0 = static_cast<f32>(slot % BitmapFont::kAtlasCols) * kU;
const f32 v0 = static_cast<f32>(slot / BitmapFont::kAtlasCols) * kV;
const u32 ink = ink_width(kGlyphBits[slot]);
const u32 spaced = ink + 1u; // one pixel of side bearing, floored at two
glyphs[slot] = Glyph{u0, v0, u0 + kU, v0 + kV, 0.0f, 0.0f,
static_cast<f32>(spaced < 2u ? 2u : spaced)};
}
return glyphs;
}
constexpr std::array<Glyph, BitmapFont::kGlyphCount> kGlyphs = build_glyphs();
} // 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);
}
}
const Glyph* BitmapFont::glyph_table() noexcept { return kGlyphs.data(); }
f32 BitmapFont::measure(const char* utf8) const {
if (utf8 == nullptr) {
@@ -223,21 +199,35 @@ f32 BitmapFont::measure(const char* utf8) const {
}
f32 width = 0.0f;
for (const char* p = utf8; *p != '\0';) {
width += advances_[glyph_index(next_codepoint(p))];
width += advance_codepoint(next_codepoint(p));
}
return width;
}
const Glyph* BitmapFont::glyph(u32 codepoint) const {
// Every codepoint has a glyph: `glyph_index` maps whatever the font cannot
// draw -- including `kInvalidCodepoint` -- to the visible box.
return &glyph_table()[glyph_index(codepoint)];
}
const u8* BitmapFont::atlas_pixels() const { return atlas_image(); }
u32 BitmapFont::atlas_width() const { return kAtlasWidth; }
u32 BitmapFont::atlas_height() const { return kAtlasHeight; }
u32 BitmapFont::revision() const { return 0u; }
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)))];
return glyph_table()[glyph_index(static_cast<u32>(static_cast<unsigned char>(c)))].advance;
}
f32 BitmapFont::advance_codepoint(u32 codepoint) const noexcept {
return advances_[glyph_index(codepoint)];
return glyph_table()[glyph_index(codepoint)].advance;
}
u32 BitmapFont::glyph_index(u32 codepoint) noexcept {
+61 -19
View File
@@ -1,6 +1,23 @@
#include <plot/render/primitives.hpp>
#include <cmath>
namespace plot::render {
namespace {
/// `axis` as a unit vector: the cell's local +x, which is what the instance and
/// the shader both take (see `TextInstance`). A zero, infinite or NaN direction
/// means "unrotated" -- a caller's arithmetic slip must not turn a label into NaN
/// geometry, and a basis the shader trusts to be unit must be one.
Vec2 unit_axis(Vec2 axis) noexcept {
const f32 len = axis.length();
if (std::isfinite(len) && len > 0.0f) {
return axis / len;
}
return Vec2{1.0f, 0.0f};
}
} // namespace
// ----------------------------------------------------------------- LineBatch
@@ -42,31 +59,56 @@ void LineBatch::add_polyline_xy(const f32* xy, u32 n) {
// ----------------------------------------------------------------- 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);
void TextBatch::begin() noexcept {
v_.clear();
cell_uv_ = Vec2{};
}
Vec2 TextBatch::add_text(Vec2 pen_px, const char* utf8, const BitmapFont& font, f32 px_per_cell) {
void TextBatch::clear() noexcept {
v_.clear();
cell_uv_ = Vec2{};
}
void TextBatch::add_cell(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 unit_ux) {
// The cell's own axes: the run direction and its perpendicular, which is the
// cell's +v axis -- the one kTextVS builds the quad from. The bearing is in
// font pixels, so it scales with the run and follows the same axes: a glyph
// keeps its place inside a rotated run instead of drifting along screen +y.
const Vec2 uy{-unit_ux.y, unit_ux.x};
const Vec2 corner = pen_px + unit_ux * (glyph.xoff * px) + uy * (glyph.yoff * px);
v_.push_back(corner.x);
v_.push_back(corner.y);
v_.push_back(glyph.u0);
v_.push_back(glyph.v0);
v_.push_back(unit_ux.x);
v_.push_back(unit_ux.y);
// One cell for the whole batch: every glyph a batch stages comes from the same
// atlas, so the rect the shader reads is the one staged here (see
// glyph_cell_uv()).
cell_uv_ = Vec2{glyph.u1 - glyph.u0, glyph.v1 - glyph.v0};
}
void TextBatch::add_glyph(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 axis) {
add_cell(pen_px, glyph, px, unit_axis(axis));
}
Vec2 TextBatch::add_text(const char* utf8, Vec2 pen_px, f32 px, const TextBackend& font,
Vec2 axis) {
if (utf8 == nullptr) {
return pen_px;
}
const f32 scale = px_per_cell / static_cast<f32>(BitmapFont::kGlyphPx);
const Vec2 ux = unit_axis(axis);
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;
// The backend's own codepoints and the backend's own advances -- the same
// numbers `measure()` sums -- scaled by `px` and measured along the basis,
// so a run staged here is exactly as long as the plot layer measured it and
// ends where the space reserved for the label ends.
const Glyph* glyph = font.glyph(next_codepoint(p));
if (glyph == nullptr) {
continue; // no artwork for this codepoint: nothing to draw, nothing to advance
}
add_cell(pen_px, *glyph, px, ux);
pen_px = pen_px + ux * (glyph->advance * px);
}
return pen_px;
}
+2
View File
@@ -41,6 +41,8 @@ void TextProgram::set_viewport(Vec2 px) const noexcept { program_.set_uniform("u
void TextProgram::set_px_range(Vec2 px) const noexcept { program_.set_uniform("u_px_range", px); }
void TextProgram::set_glyph_uv(Vec2 uv) const noexcept { program_.set_uniform("u_glyph_uv", uv); }
void TextProgram::set_texture_size(Vec2 texels) const noexcept {
program_.set_uniform("u_tex_size", texels);
}
+83 -37
View File
@@ -51,25 +51,28 @@ constexpr usize kNumberChars = 24;
constexpr usize kReadoutChars = 160;
/// Walks the longest prefix of `utf8` that fits `budget_px`, calling `emit(pen,
/// slot)` for every glyph it keeps -- `pen` in viewport pixels, `slot` in the font
/// atlas -- and returning the width that prefix occupies. Both the measuring and
/// the staging of a clipped run walk this, so the cut lands on the same pixel
/// either way; the codepoints, the advances and the scale are the ones
/// `TextBatch::add_text()` uses, so a clipped run still ends where the plot layer
/// measured its title to end.
/// glyph)` for every glyph it keeps -- `pen` in viewport pixels, `glyph` the
/// backend's own entry for that codepoint -- and returning the width that prefix
/// occupies. Both the measuring and the staging of a clipped run walk this, so the
/// cut lands on the same pixel either way; the codepoints, the advances and the
/// scale are the ones `TextBatch::add_text()` uses, so a clipped run still ends
/// where the plot layer measured its title to end.
///
/// A codepoint the backend has no glyph for is skipped, exactly as `add_text`
/// skips it, so what is measured here is what is drawn.
template <class Emit>
f32 walk_text_clipped(Vec2 pen, const char* utf8, f32 budget_px, const render::BitmapFont& font,
f32 px_per_cell, Emit emit) {
f32 walk_text_clipped(Vec2 pen, const char* utf8, f32 budget_px, const render::TextBackend& font,
f32 scale, Emit emit) {
if (utf8 == nullptr || !(budget_px > 0.0f)) {
return 0.0f; // no room at all: a strip clipped away stages nothing
}
const f32 scale = px_per_cell / static_cast<f32>(render::BitmapFont::kGlyphPx);
f32 used = 0.0f;
for (const char* p = utf8; *p != '\0';) {
const u32 codepoint = render::BitmapFont::next_codepoint(p);
const f32 advance = font.advance_codepoint(codepoint) * scale;
const render::Glyph* glyph = font.glyph(render::next_codepoint(p));
if (glyph == nullptr) continue; // nothing to draw, nothing to reserve
const f32 advance = glyph->advance * scale;
if (used + advance > budget_px) break; // the next glyph does not fit: cut here
emit(pen, render::BitmapFont::glyph_index(codepoint));
emit(pen, *glyph);
pen.x += advance;
used += advance;
}
@@ -116,6 +119,35 @@ f32 Renderer::AxisFont::ascent() const {
return font_ != nullptr ? font_->ascent() * scale_ : 0.0f;
}
const render::Glyph* Renderer::AxisFont::glyph(u32 codepoint) const {
if (font_ == nullptr) {
return nullptr; // nothing to draw with, so nothing to describe
}
const render::Glyph* glyph = font_->glyph(codepoint);
if (glyph == nullptr) {
return nullptr;
}
// The rect is the atlas', and the atlas is not scaled; the bearing and the
// advance are font pixels, and this backend's caller works in screen pixels.
scaled_ = *glyph;
scaled_.xoff *= scale_;
scaled_.yoff *= scale_;
scaled_.advance *= scale_;
return &scaled_;
}
const u8* Renderer::AxisFont::atlas_pixels() const {
return font_ != nullptr ? font_->atlas_pixels() : nullptr;
}
u32 Renderer::AxisFont::atlas_width() const { return font_ != nullptr ? font_->atlas_width() : 0u; }
u32 Renderer::AxisFont::atlas_height() const {
return font_ != nullptr ? font_->atlas_height() : 0u;
}
u32 Renderer::AxisFont::revision() const { return font_ != nullptr ? font_->revision() : 0u; }
// ------------------------------------------------------------------ init
bool Renderer::init(Status* st) noexcept {
@@ -137,20 +169,13 @@ bool Renderer::init(Status* st) noexcept {
font_backend_.reset(font_, style_.axis_font_px);
// The atlas upload needs GL_UNPACK_ALIGNMENT 1 (single-channel rows have no
// padding), which is the only global state `init()` changes; the host's value is
// read first so `end_frame()` can hand it back rather than leave the host's own
// uploads to the alignment this renderer wanted.
gl::glGetIntegerv(gl::GL_UNPACK_ALIGNMENT, &host_unpack_alignment_);
if (!atlas_.create_r8(render::BitmapFont::kAtlasWidth, render::BitmapFont::kAtlasHeight, st)) {
// The atlas is the backend's: `sync()` uploads whatever it exposes, at whatever
// size it says, and restores the host's unpack alignment around the upload
// itself -- so this is the only place in the renderer that has to know a font
// atlas exists at all.
if (!atlas_.sync(font_backend_, st)) {
return false;
}
atlas_.upload_r8(render::BitmapFont::kAtlasWidth, render::BitmapFont::kAtlasHeight,
render::BitmapFont::atlas_image());
// Texel-for-texel: the text shader samples the nearest texel centre of the
// glyph cell, and linear filtering would smear one cell into its neighbour.
atlas_.set_filter(gl::GL_NEAREST, gl::GL_NEAREST);
if (!line_program_.create(st) || !text_program_.create(st)) {
return false;
@@ -237,6 +262,16 @@ void Renderer::draw(Plot& plot) {
// measure with the size the text pipeline draws with, and re-read the style
// each frame so a changed axis_font_px takes effect on the next one.
font_backend_.reset(font_, style_.axis_font_px);
// A backend whose revision moved -- a font swapped, or re-baked at another size
// -- is re-uploaded here, before anything is staged from it, so the frame draws
// its glyphs from the atlas they were baked into. The first sync happened in
// `init()` and a steady-state frame only compares one counter, so a font that
// never changes costs a comparison a frame. A driver that refuses the re-upload
// is reported the way the stores report a refused fence: the frame draws on (it
// has the previous atlas), and `last_frame_had_gl_error()` says so.
if (!atlas_.sync(font_backend_)) {
gl_error_ = true;
}
#ifndef NDEBUG
// The renderer owns the text of the frames it draws: every label goes through
// the text pipeline in `font_`, so the plot is handed this font's metrics
@@ -296,7 +331,6 @@ void Renderer::end_frame() noexcept {
gl::glActiveTexture(gl::GL_TEXTURE0);
gl::glBindTexture(gl::GL_TEXTURE_2D, 0);
gl::glDisable(gl::GL_BLEND);
gl::glPixelStorei(gl::GL_UNPACK_ALIGNMENT, host_unpack_alignment_);
if (host_clear_saved_) {
gl::glClearColor(host_clear_.r, host_clear_.g, host_clear_.b, host_clear_.a);
}
@@ -318,7 +352,9 @@ void Renderer::upload_vertices(std::span<const f32> interleaved) noexcept {
}
void Renderer::upload_text_vertices(std::span<const f32> interleaved) noexcept {
text_vao_.upload(interleaved.data(), static_cast<u32>(interleaved.size() / 4u));
// Six floats per instance is the text pipeline's layout (see TextInstance):
// the cell's corner, its atlas rect and the basis that rotates it.
text_vao_.upload(interleaved.data(), static_cast<u32>(interleaved.size() / 6u));
}
void Renderer::flush_lines(const Mapping& map, Color color, f32 width) noexcept {
@@ -343,15 +379,22 @@ void Renderer::flush_lines(const Mapping& map, Color color, f32 width) noexcept
void Renderer::flush_text() noexcept {
const u32 glyphs = glyphs_.glyph_count();
if (glyphs != 0 && ready_) {
// No atlas is no text: a backend that publishes none has nothing to sample, and
// drawing its instances would read whatever texture happened to be bound.
if (glyphs != 0 && ready_ && !atlas_.empty()) {
upload_text_vertices(glyphs_.data());
text_program_.bind();
text_program_.set_viewport({viewport_.w, viewport_.h});
const f32 cell = font_backend_.cell_px();
text_program_.set_px_range({cell, cell});
text_program_.set_texture_size(
{static_cast<f32>(render::BitmapFont::kAtlasWidth),
static_cast<f32>(render::BitmapFont::kAtlasHeight)});
// The cell comes from the glyphs staged and the atlas they came from -- never
// from a constant -- so a font swap moves the quads with the artwork:
// `glyph_uv` is the rect every instance is read through and `px_range` is that
// same cell on screen.
const Vec2 cell_uv = glyphs_.glyph_cell_uv();
const Vec2 atlas_texels = atlas_.texel_size();
text_program_.set_glyph_uv(cell_uv);
text_program_.set_px_range({cell_uv.x * atlas_texels.x * font_backend_.scale(),
cell_uv.y * atlas_texels.y * font_backend_.scale()});
text_program_.set_texture_size(atlas_texels);
text_program_.set_color(style_.axis_text);
atlas_.bind();
text_vao_.bind();
@@ -404,17 +447,20 @@ void Renderer::stage_xy(const f32* xy, u32 n, bool log_x, bool log_y) {
Vec2 Renderer::stage_text(Vec2 pos_px, const char* utf8) {
// Text is staged in viewport pixels: the text program has no offset uniform.
return glyphs_.add_text(to_viewport(pos_px), utf8, font_, font_backend_.cell_px());
return glyphs_.add_text(utf8, to_viewport(pos_px), font_backend_.scale(), font_);
}
f32 Renderer::clipped_text_width(const char* utf8, f32 budget_px) const {
return walk_text_clipped(Vec2{}, utf8, budget_px, font_, font_backend_.cell_px(),
[](Vec2, u32) {});
return walk_text_clipped(Vec2{}, utf8, budget_px, font_, font_backend_.scale(),
[](Vec2, const render::Glyph&) {});
}
f32 Renderer::stage_text_clipped(Vec2 pos_px, const char* utf8, f32 budget_px) {
return walk_text_clipped(to_viewport(pos_px), utf8, budget_px, font_, font_backend_.cell_px(),
[this](Vec2 pen, u32 slot) { glyphs_.add_glyph(pen, slot); });
const f32 scale = font_backend_.scale();
return walk_text_clipped(to_viewport(pos_px), utf8, budget_px, font_, scale,
[this, scale](Vec2 pen, const render::Glyph& glyph) {
glyphs_.add_glyph(pen, glyph, scale);
});
}
// -------------------------------------------------------------------- grid
+20 -10
View File
@@ -73,28 +73,38 @@ void main() {
)";
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
layout(location = 0) in vec2 a_pos; // per-instance glyph cell top-left, pixels
layout(location = 1) in vec2 a_uv; // per-instance glyph rect top-left, normalised
layout(location = 2) in vec2 a_axis; // per-instance cell +x axis, unit, pixels
uniform vec2 u_viewport; // render target size, pixels
uniform vec2 u_px_range; // pixels per glyph cell on screen
uniform vec2 u_px_range; // glyph cell on screen, pixels (width, height)
uniform vec2 u_glyph_uv; // glyph cell in the atlas, normalised (width, height)
out vec2 v_texel; // atlas texel coordinate of this fragment
const vec2 kGlyphTexels = vec2(8.0); // BitmapFont::kGlyphPx
out vec2 v_uv; // normalised atlas coordinate of this fragment
void main() {
// The cell's two axes: the run direction the instance carries and its
// perpendicular, so the cell follows the basis instead of the screen axes. The
// perpendicular is the cell's own +v axis -- with an unrotated basis
// (1.0, 0.0) it is (0.0, 1.0), i.e. down the screen, which is the direction v
// grows in the atlas -- and building it this way keeps the quad's orientation:
// a mirrored perpendicular would draw every glyph flipped about its diagonal.
vec2 ux = a_axis;
vec2 uy = vec2(-ux.y, ux.x);
vec2 corner = vec2(float(gl_VertexID & 1), float((gl_VertexID >> 1) & 1));
vec2 px = a_pos + corner * u_px_range;
vec2 px = a_pos + corner.x * u_px_range.x * ux + corner.y * u_px_range.y * uy;
// 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;
// The same handedness as the cell, so the glyph's rect lands on it the right
// way round: u runs along the basis, v down the glyph.
v_uv = a_uv + corner * u_glyph_uv;
}
)";
const char* const kTextFS = R"(#version 330 core
in vec2 v_texel; // atlas texel coordinate of this fragment
in vec2 v_uv; // normalised atlas coordinate of this fragment
uniform sampler2D u_atlas; // GL_R8 glyph atlas, GL_NEAREST
uniform vec2 u_tex_size; // atlas size in texels
@@ -106,7 +116,7 @@ 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));
vec2 texel = clamp(floor(v_uv * u_tex_size), 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);
}
+48
View File
@@ -0,0 +1,48 @@
#include <plot/render/text.hpp>
namespace plot::render {
// The decoder lives with the interface, not with a font, because it is what keeps
// "what a string is worth" (`TextBackend::measure`) and "what a string draws"
// (`TextBatch::add_text`) the same thing: both walk a label with this function, so
// neither can disagree with the other about where a codepoint starts or how many
// bytes a malformed sequence costs.
u32 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;
}
} // namespace plot::render
+54
View File
@@ -0,0 +1,54 @@
#include <plot/render/text_atlas.hpp>
#include <plot/gl/api.hpp>
namespace plot::render {
bool TextAtlas::sync(const TextBackend& backend, Status* st) noexcept {
if (synced_ && revision_ == backend.revision()) {
return true; // the GPU holds exactly the bytes this revision describes
}
const u8* pixels = backend.atlas_pixels();
const u32 width = backend.atlas_width();
const u32 height = backend.atlas_height();
if (pixels == nullptr || width == 0u || height == 0u) {
// "No text", not a failure: record the revision so the check above settles,
// and release the texture as well as the size -- an atlas a backend has
// stopped publishing must not stay bound, or a draw would sample glyphs that
// are no longer its and divide by a zero texture size doing it.
revision_ = backend.revision();
synced_ = true;
width_ = 0;
height_ = 0;
texture_.reset();
return true;
}
// `upload_r8` needs GL_UNPACK_ALIGNMENT 1 for single-channel rows and leaves it
// there; the caller's value is this call's to put back, wherever it runs.
gl::GLint host_alignment = 4;
gl::glGetIntegerv(gl::GL_UNPACK_ALIGNMENT, &host_alignment);
const bool ready = texture_.id() != 0 || texture_.create_r8(width, height, st);
if (ready) {
// `glTexImage2D` re-specifies the image, so a re-upload at another size needs
// no second create -- and it keeps the filter, which is why the filter is set
// on every upload rather than once at creation.
texture_.upload_r8(width, height, pixels);
texture_.set_filter(gl::GL_NEAREST, gl::GL_NEAREST);
}
gl::glPixelStorei(gl::GL_UNPACK_ALIGNMENT, host_alignment);
if (!ready) {
// Leave the last good upload and its revision in place: the next sync tries
// again, and a frame in between draws the text it already had.
return false;
}
revision_ = backend.revision();
synced_ = true;
width_ = width;
height_ = height;
return true;
}
void TextAtlas::bind(gl::GLenum unit) const noexcept { texture_.bind(unit); }
} // namespace plot::render
+6
View File
@@ -69,10 +69,15 @@ bool TextVertexArray::create(Status* st, UploadMode mode) noexcept {
}
vao_.bind();
buffer_.bind();
// Three attributes now: the cell's corner, its atlas rect and the basis that
// rotates it (see TextInstance). All of them advance once per instance, like
// the line pipeline's, because the corners come from gl_VertexID.
vao_.attrib(0u, 2, sizeof(TextInstance), offsetof(TextInstance, x));
vao_.attrib(1u, 2, sizeof(TextInstance), offsetof(TextInstance, u));
vao_.attrib(2u, 2, sizeof(TextInstance), offsetof(TextInstance, ax));
gl::glVertexAttribDivisor(0u, kPerInstance);
gl::glVertexAttribDivisor(1u, kPerInstance);
gl::glVertexAttribDivisor(2u, kPerInstance);
gl::glBindVertexArray(0);
epoch_ = buffer_.epoch();
return true;
@@ -90,6 +95,7 @@ void TextVertexArray::upload(const f32* interleaved, u32 glyphs) noexcept {
buffer_.bind();
vao_.attrib(0u, 2, sizeof(TextInstance), base + offsetof(TextInstance, x));
vao_.attrib(1u, 2, sizeof(TextInstance), base + offsetof(TextInstance, u));
vao_.attrib(2u, 2, sizeof(TextInstance), base + offsetof(TextInstance, ax));
epoch_ = buffer_.epoch();
}
instances_ = glyphs;