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
+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
|
||||
|
||||
Reference in new issue
Block a user