feat(render): anti-aliased embedded Adwaita Mono atlas, generated and committed
This commit is contained in:
1 parent
3ff7b9c978
commit
5ca3587325
17 files changed
+3422
-627
No files matched your search
@@ -35,7 +35,7 @@
|
||||
#include <plot/data/series.hpp>
|
||||
#include <plot/data/stream_series.hpp>
|
||||
|
||||
#include <plot/render/bitmap_font.hpp>
|
||||
#include <plot/render/embedded_mono_font.hpp>
|
||||
#include <plot/render/buffer.hpp>
|
||||
#include <plot/render/primitives.hpp>
|
||||
#include <plot/render/program.hpp>
|
||||
|
||||
@@ -23,7 +23,7 @@ enum class TickFormat {
|
||||
/// Writes an SI-prefixed rendering of `v` to `buf`, always NUL-terminated and
|
||||
/// never writing past `n` bytes (a too-small buffer truncates, it does not
|
||||
/// overflow). ASCII plus the UTF-8 micro sign U+00B5 (0xC2 0xB5) for 1e-6, so
|
||||
/// `plot::render::BitmapFont` renders it instead of mojibake.
|
||||
/// `plot::render::EmbeddedMonoFont` renders it instead of mojibake.
|
||||
///
|
||||
/// `format_si(0.0)` is exactly `"0"`. Values use three significant digits, so
|
||||
/// `1234.0` renders as `1.23k`. The sign of a negative value is kept. A value
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
#pragma once
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
#include <plot/render/text.hpp>
|
||||
|
||||
namespace plot::render {
|
||||
|
||||
/// 8x8 bitmap font, the text backend of the renderer.
|
||||
///
|
||||
/// The glyphs are 1-bit, one byte per row, bit 0 the leftmost pixel; the table
|
||||
/// and its provenance are in `src/render/bitmap_font.cpp`. Nothing here needs a
|
||||
/// font file, FreeType or stb_truetype: `plot_core` stays dependency-free.
|
||||
///
|
||||
/// Metrics are proportional, not fixed width: an advance is the glyph's ink
|
||||
/// width plus one pixel of spacing, floored at two pixels, derived from the
|
||||
/// same bitmaps the atlas uploads, so a second width table cannot drift from the
|
||||
/// artwork. Whitespace is a blank glyph with a nonzero advance, and every
|
||||
/// codepoint without a glyph draws a visible box, so a missing glyph is obvious
|
||||
/// rather than silently blank.
|
||||
///
|
||||
/// Metrics are in unscaled font pixels, one per glyph texel, and the atlas is a
|
||||
/// grid of `kGlyphPx` cells: every glyph's rect is its own cell, so a run drawn
|
||||
/// at `s` pixels per font pixel measures `measure(utf8) * s` and sits in a cell
|
||||
/// of `s * kGlyphPx` pixels. `TextBatch::add_text` takes that factor and applies
|
||||
/// it to the pen it advances.
|
||||
class BitmapFont final : public TextBackend {
|
||||
public:
|
||||
// Atlas geometry: a grid of `kGlyphPx` cells whose width is a whole number of
|
||||
// them. A glyph's rect is its own cell, normalised by these sizes, so the grid
|
||||
// is all the text pipeline has to know about the atlas.
|
||||
static constexpr u32 kGlyphPx = 8; ///< glyph cell edge, texels
|
||||
static constexpr u32 kFirstChar = 32; ///< U+0020
|
||||
static constexpr u32 kAsciiGlyphs = 95; ///< U+0020..U+007E
|
||||
static constexpr u32 kDegreeSlot = kAsciiGlyphs; ///< U+00B0 DEGREE SIGN
|
||||
static constexpr u32 kMicroSlot = kAsciiGlyphs + 1u; ///< U+00B5 MICRO SIGN
|
||||
static constexpr u32 kOhmSlot = kAsciiGlyphs + 2u; ///< U+03A9 OHM
|
||||
static constexpr u32 kFallbackSlot = kAsciiGlyphs + 3u; ///< missing-glyph box
|
||||
static constexpr u32 kGlyphCount = kAsciiGlyphs + 4u;
|
||||
static constexpr u32 kAtlasCols = 16;
|
||||
static constexpr u32 kAtlasRows = 7;
|
||||
static constexpr u32 kAtlasWidth = kAtlasCols * kGlyphPx; ///< 128 texels
|
||||
static constexpr u32 kAtlasHeight = kAtlasRows * kGlyphPx; ///< 56 texels
|
||||
|
||||
/// The atlas cell, normalised: one `kGlyphPx` square of the grid. Every glyph's
|
||||
/// rect is this, and `cell_uv()` reports it -- one number, derived from the same
|
||||
/// two constants `pack_atlas()` packs, so the grid the pipeline draws and the
|
||||
/// grid the ink was written into cannot disagree.
|
||||
static constexpr Vec2 kCellUV{static_cast<f32>(kGlyphPx) / static_cast<f32>(kAtlasWidth),
|
||||
static_cast<f32>(kGlyphPx) / static_cast<f32>(kAtlasHeight)};
|
||||
static_assert(kAtlasCols * kAtlasRows >= kGlyphCount, "atlas too small for the glyph set");
|
||||
|
||||
BitmapFont() noexcept = default;
|
||||
|
||||
/// Width of `utf8`, the sum of the glyph advances: zero for the empty string
|
||||
/// and for null. The ASCII path allocates nothing.
|
||||
f32 measure(const char* utf8) const override;
|
||||
|
||||
/// Cell height in pixels (one glyph row of text occupies `height()` pixels).
|
||||
f32 height() const override;
|
||||
/// Top of the cell to the baseline, in pixels.
|
||||
f32 ascent() const override;
|
||||
|
||||
/// Advance of one ASCII character in pixels. A byte that is not printable
|
||||
/// ASCII reports the missing-glyph box, matching what `measure` gives that
|
||||
/// byte on its own.
|
||||
f32 advance(char c) const noexcept;
|
||||
|
||||
/// Advance of a decoded Unicode codepoint, in pixels.
|
||||
f32 advance_codepoint(u32 codepoint) const noexcept;
|
||||
|
||||
/// Glyph slot for a codepoint: printable ASCII is its own slot, the three SI
|
||||
/// codepoints `format_si` emits map to real glyphs, and anything else maps to
|
||||
/// the visible missing-glyph box.
|
||||
static u32 glyph_index(u32 codepoint) noexcept;
|
||||
/// The glyph for `codepoint`: its atlas cell as a normalised rect (see
|
||||
/// `Glyph`), a zero bearing and its ink-derived advance. Never null -- a
|
||||
/// codepoint with no artwork maps to the visible missing-glyph box, which is
|
||||
/// what a label outside the coverage set should show.
|
||||
const Glyph* glyph(u32 codepoint) const override;
|
||||
|
||||
/// `kCellUV`: the 8x8 cell every glyph occupies.
|
||||
Vec2 cell_uv() const override { return kCellUV; }
|
||||
|
||||
/// The packed atlas, as `TextBackend` sees it: the bytes `atlas_image()`
|
||||
/// returns, `kAtlasWidth` by `kAtlasHeight`, GL_R8-ready. Never null.
|
||||
const u8* atlas_pixels() const override;
|
||||
u32 atlas_width() const override;
|
||||
u32 atlas_height() const override;
|
||||
|
||||
/// Always zero: the atlas is a compile-time constant, so it never changes and
|
||||
/// the renderer uploads it once.
|
||||
u32 revision() const override;
|
||||
|
||||
/// Rows in a glyph bitmap, always `kGlyphPx`.
|
||||
u32 glyph_rows(char c) const noexcept;
|
||||
|
||||
/// The `kGlyphPx` rows of one ASCII character's bitmap, top row first.
|
||||
const u8* glyph_bitmap(char c) const noexcept;
|
||||
|
||||
/// The `kGlyphPx` rows of a glyph slot.
|
||||
static const u8* glyph_bitmap_of(u32 slot) noexcept;
|
||||
|
||||
/// The packed atlas image, ready for `plot::gl::Texture::upload_r8`: rows of
|
||||
/// `kAtlasWidth` bytes, top row first, 255 for ink and 0 for background
|
||||
/// (straight alpha, no dilation or gamma applied).
|
||||
static const u8* atlas_image() noexcept;
|
||||
|
||||
/// Texel origin of a glyph slot in the atlas, in texels; `glyph()` gives the
|
||||
/// same rectangle normalised, which is what the text pipeline stages.
|
||||
static Vec2 atlas_origin(u32 slot) noexcept;
|
||||
|
||||
private:
|
||||
/// Every slot's rect, bearing and advance, derived from the bitmaps above at
|
||||
/// compile time. This replaced a per-instance `advances_` array holding the
|
||||
/// same numbers, so the advances the pen uses and the ones `glyph()` publishes
|
||||
/// cannot drift apart.
|
||||
static const Glyph* glyph_table() noexcept;
|
||||
};
|
||||
|
||||
} // namespace plot::render
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
#include <plot/render/text.hpp>
|
||||
|
||||
namespace plot::render {
|
||||
|
||||
/// The renderer's built-in text backend: an anti-aliased greyscale atlas of
|
||||
/// Adwaita Mono, baked at authoring time and committed as data.
|
||||
///
|
||||
/// There is no font file, FreeType or stb_truetype behind this: the coverage
|
||||
/// bytes and the glyph table are generated by `tools/fonts/gen_atlas.py` into
|
||||
/// `src/render/embedded_mono_font.cpp`, so `plot_core` stays dependency-free and
|
||||
/// nothing rasterises at build or run time. That file carries the provenance --
|
||||
/// source face, its version, the em size, the supersample factor and the
|
||||
/// coverage set -- and `gen_atlas.py --check` fails if it has drifted from its
|
||||
/// input. Adwaita Mono is SIL OFL 1.1 (a copy is committed at
|
||||
/// `licences/OFL-1.1.txt`); the class is named neutrally because OFL reserves
|
||||
/// the font name for modified redistributions.
|
||||
///
|
||||
/// **One cell, every glyph.** The atlas is a fixed-cell grid -- `cell_uv()` is
|
||||
/// the contract, and the staging path asserts it per glyph. Each cell is the
|
||||
/// face's maximum advance wide and its ascent-to-descent extent tall, and every
|
||||
/// glyph is drawn into its cell with its bearing preserved, never trimmed to its
|
||||
/// ink. Because the face is monospaced the pen advances by the same amount for
|
||||
/// every glyph, which is the face's own `hmtx` advance and not an assumption.
|
||||
///
|
||||
/// A codepoint outside the coverage set draws the baked fallback box, so a label
|
||||
/// the atlas cannot spell is visible rather than silently blank.
|
||||
class EmbeddedMonoFont final : public TextBackend {
|
||||
public:
|
||||
/// The size the atlas was baked at, in font pixels, and the coverage samples
|
||||
/// per texel. Repeated here so a caller can reason about the artwork without
|
||||
/// opening the generated table; the generated translation unit is the
|
||||
/// authority and pins the same values.
|
||||
static constexpr u32 kEmPx = 13;
|
||||
static constexpr u32 kSupersample = 4;
|
||||
|
||||
/// The codepoint value `codepoint_of_slot()` reports for the fallback slot. It
|
||||
/// is deliberately not a Unicode scalar, so it can never collide with a
|
||||
/// codepoint the atlas really holds.
|
||||
static constexpr u32 kFallbackCodepoint = 0xFFFFFFFFu;
|
||||
|
||||
EmbeddedMonoFont() noexcept = default;
|
||||
|
||||
/// Width of `utf8` in font pixels: the sum of the glyphs' advances, zero for
|
||||
/// the empty string and for null. Allocates nothing.
|
||||
f32 measure(const char* utf8) const override;
|
||||
|
||||
/// Cell height in font pixels: one line of text occupies this many pixels.
|
||||
f32 height() const override;
|
||||
/// Top of the cell to the baseline, in font pixels.
|
||||
f32 ascent() const override;
|
||||
/// `kEmPx`: the size the face was baked at. The renderer scales a run by
|
||||
/// `Style::axis_font_px / em_px()`, so the same backend draws the size the
|
||||
/// style asked for whatever it was baked at.
|
||||
f32 em_px() const override;
|
||||
|
||||
/// The glyph for `codepoint`: its atlas cell as a normalised rect, no extra
|
||||
/// bearing -- the bearing is inside the cell's ink -- and its advance. Never
|
||||
/// null: anything outside the coverage set maps to the fallback box.
|
||||
const Glyph* glyph(u32 codepoint) const override;
|
||||
|
||||
/// `(kCellW / atlas_width(), kCellH / atlas_height())`: the cell every glyph's
|
||||
/// rect is.
|
||||
Vec2 cell_uv() const override;
|
||||
|
||||
/// The packed atlas, GL_R8-ready, rows of `atlas_width()` bytes, top row
|
||||
/// first, one coverage byte per texel. Never null.
|
||||
const u8* atlas_pixels() const override;
|
||||
u32 atlas_width() const override;
|
||||
u32 atlas_height() const override;
|
||||
|
||||
/// Always zero: the atlas is a compile-time constant, so the renderer uploads
|
||||
/// it once and never re-uploads.
|
||||
u32 revision() const override;
|
||||
|
||||
/// Advance of a decoded codepoint, in font pixels. A codepoint outside the
|
||||
/// coverage set reports the fallback box's advance.
|
||||
static f32 advance_codepoint(u32 codepoint) noexcept;
|
||||
|
||||
/// Slot a codepoint maps to: printable ASCII is a direct index, the bakes SI
|
||||
/// and dash codepoints are a short switch, and everything else is the fallback
|
||||
/// box -- so a missing glyph is always visible, never blank.
|
||||
static u32 glyph_index(u32 codepoint) noexcept;
|
||||
|
||||
/// Number of cells in the atlas, the fallback included.
|
||||
static u32 glyph_count() noexcept;
|
||||
|
||||
/// The codepoint a slot was baked for, or `kFallbackCodepoint` for the
|
||||
/// fallback slot. Out-of-range slots report the fallback too.
|
||||
static u32 codepoint_of_slot(u32 slot) noexcept;
|
||||
|
||||
/// The fixed cell size, in atlas texels. Every glyph's rect is exactly this.
|
||||
static u32 cell_width() noexcept;
|
||||
static u32 cell_height() noexcept;
|
||||
|
||||
/// Texel origin of a slot's cell in the atlas; `glyph()` gives the same
|
||||
/// rectangle normalised, which is what the text pipeline stages.
|
||||
static Vec2 atlas_origin(u32 slot) noexcept;
|
||||
|
||||
/// The packed atlas image: the bytes `atlas_pixels()` returns, as a
|
||||
/// compile-time table. Never null.
|
||||
static const u8* atlas_image() noexcept;
|
||||
};
|
||||
|
||||
} // namespace plot::render
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
#include <plot/plot/plot.hpp>
|
||||
#include <plot/render/bitmap_font.hpp>
|
||||
#include <plot/render/embedded_mono_font.hpp>
|
||||
#include <plot/render/buffer.hpp>
|
||||
#include <plot/render/primitives.hpp>
|
||||
#include <plot/render/program.hpp>
|
||||
@@ -155,9 +155,11 @@ class Renderer {
|
||||
bool using_persistent_upload() const noexcept {
|
||||
return line_vao_.persistent() && text_vao_.persistent();
|
||||
}
|
||||
/// The axis text size the renderer actually draws with: `style().axis_font_px`
|
||||
/// rounded to the nearest whole multiple of `render::BitmapFont::kGlyphPx`,
|
||||
/// because a 1-bit glyph only scales cleanly by whole factors.
|
||||
/// The axis text size the renderer actually draws with: the on-screen cell
|
||||
/// height of the backend it draws with. `Style::axis_font_px` is an em size, so
|
||||
/// with an ascent-to-descent cell it is this that a host reads back as the line
|
||||
/// height it got -- and it is the backend's own `height()` scaled, so it always
|
||||
/// agrees with the metrics the plot laid the labels out with.
|
||||
f32 axis_font_px() const noexcept { return font_backend_.cell_px(); }
|
||||
|
||||
/// Writes the framebuffer's viewport region as a binary PPM (P6, 8 bits per
|
||||
@@ -174,23 +176,25 @@ class Renderer {
|
||||
bool dump_frame_ppm(const char* path, Status* st = nullptr) const;
|
||||
|
||||
private:
|
||||
/// The bitmap font as the plot layer sees it, at the size the text pipeline
|
||||
/// draws it: `measure`/`height`/`ascent` scale with the glyph cell, so a label
|
||||
/// can never be laid out for one size and drawn at another. The 8x8 font scales
|
||||
/// by whole multiples of its cell only -- anything else lands a 1-bit glyph's
|
||||
/// strokes unevenly -- so `reset()` picks the multiple nearest the requested
|
||||
/// size.
|
||||
/// The backend the plot layer sees, at the size the text pipeline draws it:
|
||||
/// `measure`/`height`/`ascent` scale with the run, so a label can never be laid
|
||||
/// out for one size and drawn at another.
|
||||
///
|
||||
/// It is also the backend the renderer uploads the atlas from, so it forwards
|
||||
/// the whole interface: the metrics and the glyph bearings and advances are
|
||||
/// scaled, while the artwork itself -- the atlas bytes, its size and its
|
||||
/// revision -- is the font's own and passes through untouched.
|
||||
/// It wraps a `TextBackend` and nothing more: the scale is
|
||||
/// `axis_font_px / em_px()`, a plain ratio, so an anti-aliased face is drawn at
|
||||
/// the size the style asked for rather than at a whole multiple of its cell,
|
||||
/// and any backend -- the embedded font, a host's TrueType face -- is drawn the
|
||||
/// same way. It forwards the whole interface: the metrics and the glyph
|
||||
/// bearings and advances are scaled, while the artwork itself -- the atlas
|
||||
/// bytes, its size and its revision -- is the backend's own and passes through
|
||||
/// untouched.
|
||||
class AxisFont final : public render::TextBackend {
|
||||
public:
|
||||
void reset(const render::BitmapFont& font, f32 requested_px) noexcept;
|
||||
void reset(const render::TextBackend& font, f32 requested_px) noexcept;
|
||||
f32 measure(const char* utf8) const override;
|
||||
f32 height() const override;
|
||||
f32 ascent() const override;
|
||||
f32 em_px() const override;
|
||||
/// The font's glyph at this wrapper's scale: the atlas rect is unchanged (it
|
||||
/// is scale-free) and the bearing and advance are multiplied by it, so one
|
||||
/// `Glyph` describes the glyph this backend draws. The result lives in the
|
||||
@@ -213,7 +217,7 @@ class Renderer {
|
||||
f32 scale() const noexcept { return scale_; }
|
||||
|
||||
private:
|
||||
const render::BitmapFont* font_ = nullptr;
|
||||
const render::TextBackend* font_ = nullptr;
|
||||
f32 cell_px_ = 1.0f;
|
||||
f32 scale_ = 1.0f;
|
||||
/// `glyph()`'s scaled result, returned by pointer because the interface is
|
||||
@@ -280,7 +284,10 @@ class Renderer {
|
||||
void draw_measures(const Plot& plot, const PlotLayout& layout);
|
||||
void draw_annotations(const Plot& plot);
|
||||
|
||||
render::BitmapFont font_;
|
||||
/// The built-in backend the renderer owns. Every use of it goes through the
|
||||
/// `TextBackend` interface -- `self`-typed nowhere -- so swapping it for
|
||||
/// another face is a change to this member alone.
|
||||
render::EmbeddedMonoFont default_font_;
|
||||
AxisFont font_backend_;
|
||||
render::Style style_;
|
||||
|
||||
|
||||
@@ -108,6 +108,18 @@ class TextBackend {
|
||||
/// text bottoms instead of cell bottoms.
|
||||
virtual f32 ascent() const = 0;
|
||||
|
||||
/// The face's em size in font pixels: the size the atlas was baked at, in the
|
||||
/// same units as `height()` and `ascent()`.
|
||||
///
|
||||
/// It exists so a run can be drawn at the size a caller asked for whatever the
|
||||
/// backend was baked at: the renderer scales by `requested_px / em_px()`, so a
|
||||
/// backend re-baked at another size -- or swapped for a different face -- keeps
|
||||
/// drawing `Style::axis_font_px`, and the renderer never has to know which font
|
||||
/// it holds. A backend with no meaningful em (one whose "font pixels" are an
|
||||
/// arbitrary unit) returns something proportional, `height()` being the
|
||||
/// natural choice.
|
||||
virtual f32 em_px() const = 0;
|
||||
|
||||
/// The glyph for `codepoint`, or null when this backend has none -- the caller
|
||||
/// then draws nothing for it (a backend that wants a visible missing-glyph box
|
||||
/// publishes one as the glyph for the codepoints it cannot render). The
|
||||
|
||||
Reference in new issue
Block a user