refactor(render): TextBackend owns its atlas, and staged glyphs carry a rotation basis
This commit is contained in:
1 parent
90e17a0a11
commit
e074f57ddf
23 files changed
+1073
-227
No files matched your search
+41
-51
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user