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

+6
View File
@@ -13,6 +13,12 @@ add_library(plot_core
src/data/stream_series.cpp
src/gl/loader.cpp
src/gl/handle.cpp
src/render/shaders.cpp
src/render/bitmap_font.cpp
src/render/primitives.cpp
src/render/buffer.cpp
src/render/vertex_array.cpp
src/render/program.cpp
)
add_library(plot::core ALIAS plot_core)
+1
View File
@@ -6,6 +6,7 @@ namespace plot {
using f32 = float;
using f64 = double;
using u8 = std::uint8_t;
using i32 = std::int32_t;
using u32 = std::uint32_t;
using u64 = std::uint64_t;
+2
View File
@@ -139,6 +139,7 @@ enum : GLenum {
X(void, glUniform1i, (GLint, GLint)) \
X(void, glUniform1f, (GLint, float)) \
X(void, glUniform2f, (GLint, float, float)) \
X(void, glUniform4f, (GLint, float, float, float, float)) \
X(void, glUniform1fv, (GLint, GLsizei, const float*)) \
X(void, glUniform2fv, (GLint, GLsizei, const float*)) \
/* buffers */ \
@@ -153,6 +154,7 @@ enum : GLenum {
X(void, glBindVertexArray, (GLuint)) \
X(void, glEnableVertexAttribArray, (GLuint)) \
X(void, glVertexAttribPointer, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void*)) \
X(void, glVertexAttribDivisor, (GLuint, GLuint)) \
/* drawing */ \
X(void, glDrawArrays, (GLenum, GLint, GLsizei)) \
X(void, glDrawArraysInstanced, (GLenum, GLint, GLsizei, GLsizei)) \
+8
View File
@@ -1,4 +1,5 @@
#pragma once
#include <plot/core/color.hpp>
#include <plot/core/status.hpp>
#include <plot/core/types.hpp>
#include <plot/gl/api.hpp>
@@ -103,6 +104,13 @@ class Program final : public Handle<Program> {
/// Sets a two-component float uniform.
void set_uniform(const char* name, Vec2 value) const noexcept;
/// Sets a four-component float uniform, e.g. the colour of a pipeline.
void set_uniform(const char* name, Color value) const noexcept;
/// Sets an integer uniform, e.g. a sampler's texture unit. Deliberately not
/// an overload of the float setter: an untyped literal such as `0` must not
/// turn a float uniform into a `glUniform1i` call.
void set_uniform_int(const char* name, i32 value) const noexcept;
/// Exposed for the CRTP base.
static void destroy(GLuint id) noexcept;
+101
View File
@@ -0,0 +1,101 @@
#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, computed once when
/// the font is constructed from the same bitmaps the atlas uploads. 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. The text pipeline
/// draws a cell `u_px_range` pixels across, so a run at another size measures
/// `measure(utf8) * u_px_range.x / kGlyphPx`; `TextBatch::add_text` takes that
/// cell size and applies the same scale to the pen it advances.
class BitmapFont final : public TextBackend {
public:
// Atlas geometry. It is part of the render pipeline's contract: the text
// vertex shader expands a glyph cell by `u_px_range` pixels and reads the cell
// at the instance's texel origin, so the atlas is a grid of 8x8 cells whose
// width must stay a whole number of cells.
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
static_assert(kAtlasCols * kAtlasRows >= kGlyphCount, "atlas too small for the glyph set");
/// Reported by `next_codepoint` for a byte sequence that is not valid UTF-8.
/// It is not a Unicode scalar value, so it can only map to the fallback box.
static constexpr u32 kInvalidCodepoint = 0xFFFFFFFFu;
/// Decodes one UTF-8 sequence from `p` and advances `p` past it — one byte
/// when the sequence is malformed, so a damaged label still terminates. The
/// text batch and `measure` share it, which is what keeps a staged run exactly
/// `measure`-wide.
static u32 next_codepoint(const char*& p) noexcept;
BitmapFont() noexcept;
/// 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;
/// 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; this is the per-instance `a_uv`
/// the text pipeline reads.
static Vec2 atlas_origin(u32 slot) noexcept;
private:
f32 advances_[kGlyphCount];
};
} // namespace plot::render
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <plot/core/status.hpp>
#include <plot/core/types.hpp>
#include <plot/gl/handle.hpp>
namespace plot::render {
/// Per-instance payload of the line pipeline: one segment in data space. It
/// mirrors the interleaved floats `LineBatch::data()` produces, which is why the
/// attribute offsets in vertex_array.cpp come from `offsetof` rather than from
/// hand-written numbers.
struct LineInstance {
f32 x0;
f32 y0;
f32 x1;
f32 y1;
};
/// Per-instance payload of the text pipeline: the glyph quad's top-left corner
/// in Rect pixels plus the glyph's atlas texel origin.
struct TextInstance {
f32 x;
f32 y;
f32 u;
f32 v;
};
static_assert(sizeof(LineInstance) == 4 * sizeof(f32), "line instance must stay 4 floats");
static_assert(sizeof(TextInstance) == 4 * sizeof(f32), "text instance must stay 4 floats");
namespace detail {
/// The growth half of an instance buffer, shared by both pipelines so neither
/// duplicates the GL ownership or the "grow only when the frame needs it" rule.
class InstanceStore {
public:
/// Creates the vertex buffer. Reports `ErrorCode::Unsupported` while the GL
/// entry points are unresolved, `GLFailure` if the driver hands out no name.
bool create(Status* st = nullptr) noexcept;
/// Uploads `count` instances of `instance_bytes`. The store only reallocates
/// when the frame's workload outgrows it, so steady-state frames write into
/// the existing allocation.
void upload(const void* data, u32 count, usize instance_bytes) noexcept;
void bind() const noexcept { buffer_.bind(); }
private:
gl::Buffer buffer_;
u32 capacity_ = 0; ///< instances the store can hold
};
} // namespace detail
/// Instance buffer of the line pipeline: one `LineInstance` per segment.
class LineBuffer {
public:
bool create(Status* st = nullptr) noexcept { return store_.create(st); }
void upload(const f32* interleaved, u32 count) noexcept {
store_.upload(interleaved, count, sizeof(LineInstance));
}
void bind() const noexcept { store_.bind(); }
private:
detail::InstanceStore store_;
};
/// Instance buffer of the text pipeline: one `TextInstance` per glyph.
class TextBuffer {
public:
bool create(Status* st = nullptr) noexcept { return store_.create(st); }
void upload(const f32* interleaved, u32 count) noexcept {
store_.upload(interleaved, count, sizeof(TextInstance));
}
void bind() const noexcept { store_.bind(); }
private:
detail::InstanceStore store_;
};
} // namespace plot::render
+87
View File
@@ -0,0 +1,87 @@
#pragma once
#include <plot/core/types.hpp>
#include <plot/math/vec2.hpp>
#include <plot/render/bitmap_font.hpp>
#include <vector>
namespace plot::render {
/// CPU-side staging for one instanced-AA line draw call.
///
/// `begin()` starts a frame and `clear()` empties the batch; neither releases
/// the store, and both leave capacity alone so a steady-state frame re-uses the
/// previous allocation instead of reallocating per frame. The renderer re-uses
/// one batch for the whole run and uploads `data()` once per frame.
///
/// The payload is interleaved `x0,y0,x1,y1` per segment — exactly the
/// per-instance attribute layout the line shader declares (see buffer.hpp and
/// shaders.hpp), so the intersection of `data()` with an `f32` span can be
/// uploaded verbatim.
class LineBatch {
public:
/// Starts a frame: empties the batch, keeps its capacity.
void begin() noexcept;
/// Same as `begin()`, so the render loop can read as begin/stage/draw.
void clear() noexcept;
/// Stages one segment: one instance, two quad corners per end.
void add_segment(Vec2 a, Vec2 b);
/// Stages `n - 1` segments joining consecutive points. A null pointer or
/// fewer than two points stages nothing.
void add_polyline(const Vec2* pts, u32 n);
/// Segments staged so far, one instance each.
u32 segment_count() const noexcept { return static_cast<u32>(v_.size() / 4u); }
/// Vertices the pipeline will rasterise: two per staged segment.
u32 vertex_count() const noexcept { return segment_count() * 2u; }
/// Interleaved `x0,y0,x1,y1`, four floats per staged segment.
const std::vector<f32>& data() const noexcept { return v_; }
private:
std::vector<f32> v_;
};
/// CPU-side staging for one instanced text draw call.
///
/// Like `LineBatch` it keeps its capacity across frames. Each staged glyph is
/// one instance: the glyph quad's top-left corner in pixels, top-left origin
/// like `plot::Rect`, plus the glyph's atlas texel origin, interleaved as
/// `x,y,u,v` — the layout the text shader declares (see buffer.hpp).
class TextBatch {
public:
/// Starts a frame: empties the batch, keeps its capacity.
void begin() noexcept;
/// Same as `begin()`.
void clear() noexcept;
/// Stages one glyph cell at `pos_px` (its top-left corner), reading the cell
/// from `slot` in the font atlas (see `BitmapFont::atlas_origin`). `slot`
/// indexes the atlas, not a codepoint.
void add_glyph(Vec2 pos_px, u32 slot);
/// Stages every codepoint of `utf8` and returns the pen past the last glyph,
/// so the run is exactly `font.measure(utf8) * px_per_cell /
/// BitmapFont::kGlyphPx` pixels wide — the width the draw covers when it is
/// told the same cell size as `u_px_range`.
///
/// `px_per_cell` is the on-screen size of one glyph cell: pass
/// `BitmapFont::kGlyphPx` for 1:1 text, and anything else to scale the run.
/// The glyph quad itself is sized by the shader's `u_px_range`, which must be
/// the same value. A null or empty string stages nothing and returns `pen_px`.
Vec2 add_text(Vec2 pen_px, const char* utf8, const BitmapFont& font,
f32 px_per_cell = static_cast<f32>(BitmapFont::kGlyphPx));
/// Glyphs staged so far, one instance each.
u32 glyph_count() const noexcept { return static_cast<u32>(v_.size() / 4u); }
/// Interleaved `x,y,u,v`, four floats per staged glyph.
const std::vector<f32>& data() const noexcept { return v_; }
private:
std::vector<f32> v_;
};
} // namespace plot::render
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include <plot/core/color.hpp>
#include <plot/core/status.hpp>
#include <plot/gl/handle.hpp>
#include <plot/math/vec2.hpp>
namespace plot::render {
/// Linked line program and its uniform surface.
///
/// It owns the `plot::gl::Program` and knows the GLSL pair (shaders.hpp), so no
/// caller spells out a uniform name that could drift from the shader. Creating
/// it propagates what the GL layer reports: `ErrorCode::Unsupported` while
/// `plot::gl::available()` is false, `GLFailure` plus the driver's log when the
/// GLSL does not compile or link.
class LineProgram {
public:
bool create(Status* st = nullptr) noexcept;
bool valid() const noexcept { return program_.id() != 0; }
void bind() const noexcept { program_.use(); }
/// `u_scale`: data space to pixels.
void set_scale(Vec2 data_to_px) const noexcept;
/// `u_offset`: pixel offset added after `set_scale`.
void set_offset(Vec2 px) const noexcept;
/// `u_viewport`: render target size in pixels.
void set_viewport(Vec2 px) const noexcept;
/// `u_width`: stroke width in pixels.
void set_width(f32 px) const noexcept;
/// `u_color`: stroke colour, alpha included.
void set_color(Color color) const noexcept;
private:
gl::Program program_;
};
/// Linked text program and its uniform surface. It fails exactly the way
/// `LineProgram::create()` does, propagating `Unsupported` and `GLFailure`.
///
/// `create()` also points the `u_atlas` sampler at texture unit 0, which is the
/// unit `plot::gl::Texture::bind()` defaults to, and leaves this program bound.
/// The atlas itself stays with the caller, who owns its lifetime.
class TextProgram {
public:
bool create(Status* st = nullptr) noexcept;
bool valid() const noexcept { return program_.id() != 0; }
void bind() const noexcept { program_.use(); }
/// `u_viewport`: render target size in pixels.
void set_viewport(Vec2 px) const noexcept;
/// `u_px_range`: pixels per glyph cell, i.e. the on-screen size of one glyph.
void set_px_range(Vec2 px) const noexcept;
/// `u_tex_size`: atlas size in texels.
void set_texture_size(Vec2 texels) const noexcept;
/// `u_color`: tint colour, alpha included.
void set_color(Color color) const noexcept;
private:
gl::Program program_;
};
} // namespace plot::render
+41
View File
@@ -0,0 +1,41 @@
#pragma once
namespace plot::render {
/// GLSL source of the instanced antialiased line pipeline.
///
/// The vertex shader takes one segment per instance (`a_p0` at location 0,
/// `a_p1` at location 1, both with a vertex attribute divisor of 1), expands it
/// in pixel space along its normal by `u_width * 0.5 + 1.0` pixels of AA fringe,
/// and emits a four-vertex `GL_TRIANGLE_STRIP` quad whose corners come from
/// `gl_VertexID` (0 and 1 are the two sides of `a_p0`, 2 and 3 of `a_p1`). It
/// outputs `v_dist`, the signed distance from the segment axis in pixels, and
/// `v_along`, the distance travelled from `a_p0` in pixels.
///
/// Uniforms: `u_scale` (vec2, data space to pixels), `u_offset` (vec2, pixels),
/// `u_viewport` (vec2, pixels), `u_width` (float, pixels), `u_color` (vec4).
/// `plot::Rect` is top-left origin while GL is bottom-left, so the y axis is
/// flipped exactly once, here.
///
/// The fragment shader discards outside the fringe and ramps alpha over the last
/// pixel of the half width with `smoothstep`, which is what keeps a 1px trace
/// from aliasing into dashes.
extern const char* const kLineVS;
extern const char* const kLineFS;
/// GLSL source of the instanced text pipeline.
///
/// One glyph cell per instance: `a_pos` at location 0 is the quad's top-left
/// corner in Rect pixels and `a_uv` at location 1 is the glyph's atlas texel
/// origin, both with a vertex attribute divisor of 1. The quad grows by
/// `u_px_range` pixels per cell and the fragment shader samples the
/// single-channel `GL_R8` atlas at the nearest texel centre, so `GL_NEAREST`
/// never lands on a cell boundary. `u_color` tints the straight alpha.
///
/// Uniforms: `u_viewport` (vec2, pixels), `u_px_range` (vec2, pixels per glyph
/// cell), `u_tex_size` (vec2, atlas texels), `u_atlas` (sampler2D, unit 0),
/// `u_color` (vec4).
extern const char* const kTextVS;
extern const char* const kTextFS;
} // namespace plot::render
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <plot/core/color.hpp>
namespace plot::render {
/// Every colour and metric the renderer draws with. Defaults are the dark theme
/// the plan's render smoke test asserts against; a caller swaps a field and the
/// next frame picks it up, so nothing here is baked into a shader.
struct Style {
Color background = Color::hex(0x101418);
Color grid_major = Color::hex(0x2A3038);
Color grid_minor = Color::hex(0x1C2126);
Color axis_line = Color::hex(0x8A94A6);
Color axis_text = Color::hex(0xC8D0DC);
Color cursor = Color::hex(0xFFB020);
Color measure_fill = Color::hex(0xFFB020, 0.15f);
f32 axis_font_px = 12.0f;
f32 grid_label_pad = 4.0f;
f32 line_width_px = 1.5f;
f32 dense_threshold_spp = 1.5f; ///< spec §4.3: samples per pixel above which
///< a trace is drawn from the LOD pyramid.
f32 tick_min_px = 60.0f; ///< minimum spacing between major ticks.
};
} // namespace plot::render
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <plot/core/types.hpp>
namespace plot::render {
/// Text metrics the plot layer lays axes and labels out with. It is a pure
/// interface so the layout code never depends on the bitmap font, and so a
/// caller can measure with a host toolkit's font instead.
///
/// Text is passed as UTF-8, not as a byte-per-character string: `plot::format_si`
/// emits U+00B5, U+00B0 and U+03A9 for micro, degree and ohm, and an
/// implementation that ignores that prints mojibake.
class TextBackend {
public:
TextBackend() noexcept = default;
TextBackend(const TextBackend&) = default;
TextBackend& operator=(const TextBackend&) = default;
virtual ~TextBackend() = default;
/// Width of `utf8` in the same units as `height()` (pixels for the bitmap
/// font). Zero for the empty string.
virtual f32 measure(const char* utf8) const = 0;
/// Line height: the cell a single line of text occupies.
virtual f32 height() const = 0;
/// Distance from the top of the cell to the baseline, so a caller can align
/// text bottoms instead of cell bottoms.
virtual f32 ascent() const = 0;
};
} // namespace plot::render
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <plot/core/status.hpp>
#include <plot/core/types.hpp>
#include <plot/gl/handle.hpp>
#include <plot/render/buffer.hpp>
namespace plot::render {
/// Vertex array plus instance buffer of the line pipeline.
///
/// The attribute layout is fixed by the line shader (see shaders.hpp): `a_p0` at
/// location 0 and `a_p1` at location 1, two floats each, advancing once per
/// instance. A caller never writes a location, stride or offset; it stages a
/// `LineBatch` and uploads its `data()`.
class LineVertexArray {
public:
/// Creates the vertex array, the instance buffer and the attribute layout.
bool create(Status* st = nullptr) noexcept;
/// Uploads `segments` instances from interleaved `x0,y0,x1,y1` floats and
/// records how many the next draw should rasterise.
void upload(const f32* interleaved, u32 segments) noexcept;
void bind() const noexcept { vao_.bind(); }
bool valid() const noexcept { return vao_.id() != 0; }
u32 instance_count() const noexcept { return instances_; }
private:
LineBuffer buffer_;
gl::VertexArray vao_;
u32 instances_ = 0;
};
/// Vertex array plus instance buffer of the text pipeline: `a_pos` at location 0
/// and the glyph's atlas texel origin `a_uv` at location 1, two floats each,
/// advancing once per instance.
class TextVertexArray {
public:
bool create(Status* st = nullptr) noexcept;
/// Uploads `glyphs` instances from interleaved `x,y,u,v` floats.
void upload(const f32* interleaved, u32 glyphs) noexcept;
void bind() const noexcept { vao_.bind(); }
bool valid() const noexcept { return vao_.id() != 0; }
u32 instance_count() const noexcept { return instances_; }
private:
TextBuffer buffer_;
gl::VertexArray vao_;
u32 instances_ = 0;
};
} // namespace plot::render
+20
View File
@@ -226,6 +226,26 @@ void Program::set_uniform(const char* name, Vec2 value) const noexcept {
}
}
void Program::set_uniform(const char* name, Color value) const noexcept {
if (id_ == 0 || name == nullptr) {
return;
}
const GLint location = glGetUniformLocation(id_, name);
if (location >= 0) {
glUniform4f(location, value.r, value.g, value.b, value.a);
}
}
void Program::set_uniform_int(const char* name, i32 value) const noexcept {
if (id_ == 0 || name == nullptr) {
return;
}
const GLint location = glGetUniformLocation(id_, name);
if (location >= 0) {
glUniform1i(location, value);
}
}
void Program::destroy(GLuint id) noexcept { glDeleteProgram(id); }
// ------------------------------------------------------------------ Texture
+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
+1
View File
@@ -28,6 +28,7 @@ add_executable(plot_tests
main.cpp
test_transform.cpp
test_gl_context.cpp
test_primitives.cpp
test_ring_buffer.cpp
test_minmax_pyramid.cpp
test_series.cpp
+187
View File
@@ -6,6 +6,11 @@
#include <plot/gl/caps.hpp>
#include <plot/gl/handle.hpp>
#include <plot/gl/loader.hpp>
#include <plot/render/bitmap_font.hpp>
#include <plot/render/primitives.hpp>
#include <plot/render/program.hpp>
#include <plot/render/shaders.hpp>
#include <plot/render/vertex_array.hpp>
#include <cstdint>
#include <cstdio>
@@ -201,3 +206,185 @@ PLOT_TEST(program_compiles_trivial_shaders) {
PLOT_CHECK(broken.id() == 0);
ctx.destroy();
}
// The GLSL of both pipelines has to be accepted by the real driver: a typo that
// no CPU test can catch (a missing varying, a reserved word) fails here.
PLOT_TEST(shaders_compile_and_link) {
plottest::HeadlessContext ctx;
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(nullptr));
plot::Status s0;
plot::Status s1;
plot::gl::Program line =
plot::gl::Program::from_source(plot::render::kLineVS, plot::render::kLineFS, &s0);
plot::gl::Program text =
plot::gl::Program::from_source(plot::render::kTextVS, plot::render::kTextFS, &s1);
PLOT_CHECK(bool(s0));
PLOT_CHECK(bool(s1));
PLOT_CHECK(line.id() != 0);
PLOT_CHECK(text.id() != 0);
ctx.destroy();
}
// A renderer wrapper that was never successfully created must be a harmless
// no-op: Task 7 checks `create()`'s bool and then keeps drawing with whatever
// else it has, so the setters must not reach an empty program.
PLOT_TEST(render_programs_are_inert_until_created) {
plottest::HeadlessContext ctx;
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(nullptr));
plot::render::LineProgram line;
plot::render::TextProgram text;
PLOT_CHECK(!line.valid());
PLOT_CHECK(!text.valid());
line.bind();
line.set_scale({1.0f, 1.0f});
line.set_offset({0.0f, 0.0f});
line.set_viewport({256.0f, 256.0f});
line.set_width(2.0f);
line.set_color(plot::Color::hex(0xFFFFFF));
text.bind();
text.set_viewport({256.0f, 256.0f});
text.set_px_range({8.0f, 8.0f});
text.set_texture_size({128.0f, 56.0f});
text.set_color(plot::Color::hex(0xFFFFFF));
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
ctx.destroy();
}
// End to end on the real driver: the instanced attribute layout, the
// triangle-strip expansion, the single y flip between Rect pixels and GL, and
// the AA ramp that makes a thin trace legible.
PLOT_TEST(line_pipeline_draws_an_aa_strip) {
plottest::HeadlessContext ctx(256, 256);
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(nullptr));
plot::render::LineProgram program;
PLOT_CHECK(program.create(&st));
plot::render::LineVertexArray lines;
PLOT_CHECK(lines.create(&st));
// Two instances, so a wrong attribute divisor cannot pass: with a divisor of
// zero the second quad would be assembled from the first instance's leftover
// attribute values instead of its own segment. Data space is pixels here, and
// u_offset lands the two axes at Rect y = 100 and Rect y = 120.
const f32 segments[8] = {0.0f, 0.0f, 40.0f, 0.0f, 0.0f, 20.0f, 40.0f, 20.0f};
lines.upload(segments, 2);
PLOT_CHECK(lines.instance_count() == 2);
plot::gl::glViewport(0, 0, static_cast<plot::gl::GLsizei>(ctx.width),
static_cast<plot::gl::GLsizei>(ctx.height));
plot::gl::glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
plot::gl::glEnable(plot::gl::GL_BLEND);
plot::gl::glBlendFunc(plot::gl::GL_SRC_ALPHA, plot::gl::GL_ONE_MINUS_SRC_ALPHA);
program.bind();
program.set_scale({1.0f, 1.0f});
program.set_offset({64.0f, 100.0f});
program.set_viewport({static_cast<f32>(ctx.width), static_cast<f32>(ctx.height)});
program.set_width(3.0f);
program.set_color(plot::Color::hex(0xFF00FF));
lines.bind();
plot::gl::glDrawArraysInstanced(plot::gl::GL_TRIANGLE_STRIP, 0, 4, 2);
plot::u8 px[4] = {};
// Rect y = 100 is GL row 155.5, so rows 155 and 156 straddle the axis
// (d = 0.5), rows 154 and 157 are the half-covered AA fringe (d = 1.5), and
// row 152 is outside the quad. Getting the flip wrong, or applying it twice,
// moves the whole strip to the other half of the target.
PLOT_CHECK(ctx.read_pixel(84, 155, px));
PLOT_CHECK(px[0] > 200 && px[1] < 60 && px[2] > 200);
PLOT_CHECK(ctx.read_pixel(84, 156, px));
PLOT_CHECK(px[0] > 200 && px[2] > 200);
PLOT_CHECK(ctx.read_pixel(84, 157, px));
PLOT_CHECK(px[0] > 60 && px[0] < 200);
PLOT_CHECK(ctx.read_pixel(84, 152, px));
PLOT_CHECK(px[0] + px[2] < 40);
// The second instance is where its own segment says: Rect y = 120 is rows
// 135/136, and the gap between the two strips stays background.
PLOT_CHECK(ctx.read_pixel(84, 135, px));
PLOT_CHECK(px[0] > 200 && px[2] > 200);
PLOT_CHECK(ctx.read_pixel(84, 136, px));
PLOT_CHECK(px[0] > 200 && px[2] > 200);
PLOT_CHECK(ctx.read_pixel(84, 145, px));
PLOT_CHECK(px[0] + px[2] < 40);
// Past the end of the segment (flat cap) and far from both strips.
PLOT_CHECK(ctx.read_pixel(110, 155, px));
PLOT_CHECK(px[0] + px[2] < 40);
PLOT_CHECK(ctx.read_pixel(128, 0, px));
PLOT_CHECK(px[0] + px[2] < 40);
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
ctx.destroy();
}
// The text pipeline end to end: the R8 atlas upload, the per-instance glyph UV
// origin, the NEAREST texel-centre sampling, the tint and the same single y
// flip. Every one of the cell's 64 pixels is checked against the glyph bitmap,
// so a wrong atlas cell, a wrong UV or a transposed axis cannot pass.
PLOT_TEST(text_pipeline_draws_a_glyph) {
using plot::render::BitmapFont;
plottest::HeadlessContext ctx(256, 256);
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(nullptr));
// The renderer's atlas: single channel, GL_NEAREST, straight alpha.
plot::gl::Texture atlas;
PLOT_CHECK(atlas.create_r8(BitmapFont::kAtlasWidth, BitmapFont::kAtlasHeight, &st));
atlas.upload_r8(BitmapFont::kAtlasWidth, BitmapFont::kAtlasHeight, BitmapFont::atlas_image());
atlas.set_filter(plot::gl::GL_NEAREST, plot::gl::GL_NEAREST);
plot::render::TextProgram program;
PLOT_CHECK(program.create(&st));
plot::render::TextVertexArray glyphs;
PLOT_CHECK(glyphs.create(&st));
BitmapFont font;
plot::render::TextBatch batch;
batch.begin();
batch.add_text({64.0f, 100.0f}, "A", font); // one cell at Rect (64, 100)
PLOT_CHECK(batch.glyph_count() == 1);
glyphs.upload(batch.data().data(), batch.glyph_count());
plot::gl::glViewport(0, 0, static_cast<plot::gl::GLsizei>(ctx.width),
static_cast<plot::gl::GLsizei>(ctx.height));
plot::gl::glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
plot::gl::glEnable(plot::gl::GL_BLEND);
plot::gl::glBlendFunc(plot::gl::GL_SRC_ALPHA, plot::gl::GL_ONE_MINUS_SRC_ALPHA);
program.bind();
program.set_viewport({static_cast<f32>(ctx.width), static_cast<f32>(ctx.height)});
program.set_px_range({static_cast<f32>(BitmapFont::kGlyphPx), static_cast<f32>(BitmapFont::kGlyphPx)});
program.set_texture_size({static_cast<f32>(BitmapFont::kAtlasWidth),
static_cast<f32>(BitmapFont::kAtlasHeight)});
program.set_color(plot::Color::hex(0xFF00FF));
atlas.bind();
glyphs.bind();
plot::gl::glDrawArraysInstanced(plot::gl::GL_TRIANGLE_STRIP, 0, 4, 1);
// One cell is 8x8 texels and u_px_range is 8, so texel (gx, gy) lands on one
// target pixel: Rect x = 64 + gx, and Rect y = 100 + gy is GL row 155 - gy.
const plot::u8* rows = font.glyph_bitmap('A');
plot::u8 px[4] = {};
for (plot::u32 gy = 0; gy < BitmapFont::kGlyphPx; ++gy) {
for (plot::u32 gx = 0; gx < BitmapFont::kGlyphPx; ++gx) {
const bool ink = ((rows[gy] >> gx) & 1u) != 0u;
PLOT_CHECK(ctx.read_pixel(64u + gx, 155u - gy, px));
if (ink) {
PLOT_CHECK(px[0] > 200 && px[1] < 60 && px[2] > 200);
} else {
PLOT_CHECK(px[0] < 40 && px[2] < 40);
}
}
}
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
ctx.destroy();
}
+221
View File
@@ -0,0 +1,221 @@
// CPU-side contract of the render layer: the staging batches that feed the
// instanced line and text pipelines, and the bitmap font's metrics and atlas.
// Nothing here touches GL, so it runs on any machine that can build the tests.
#include "harness.hpp"
#include <plot/render/bitmap_font.hpp>
#include <plot/render/primitives.hpp>
#include <cstddef>
PLOT_TEST(line_batch_emits_two_verts_per_segment) {
plot::render::LineBatch b;
b.begin();
const plot::Vec2 pts[3] = {{0, 0}, {1, 1}, {2, 0}};
b.add_polyline(pts, 3); // 2 segments
PLOT_CHECK(b.vertex_count() == 4);
b.clear();
PLOT_CHECK(b.vertex_count() == 0);
PLOT_CHECK(b.data().capacity() >= 4); // clear() must not free capacity
b.add_segment({0, 0}, {1, 1});
PLOT_CHECK(b.vertex_count() == 2);
}
PLOT_TEST(line_batch_stages_segment_endpoints) {
plot::render::LineBatch b;
b.begin();
b.add_segment({1.0f, 2.0f}, {3.0f, 4.0f});
PLOT_CHECK(b.data().size() == 4); // x0,y0,x1,y1 for the one instance
PLOT_CHECK_NEAR(b.data()[0], 1.0, 0.0);
PLOT_CHECK_NEAR(b.data()[1], 2.0, 0.0);
PLOT_CHECK_NEAR(b.data()[2], 3.0, 0.0);
PLOT_CHECK_NEAR(b.data()[3], 4.0, 0.0);
// A polyline stages one instance per consecutive pair, in order.
const plot::Vec2 pts[3] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}};
b.begin();
b.add_polyline(pts, 3);
PLOT_CHECK(b.data().size() == 8);
PLOT_CHECK_NEAR(b.data()[2], 1.0, 0.0);
PLOT_CHECK_NEAR(b.data()[4], 1.0, 0.0);
PLOT_CHECK_NEAR(b.data()[5], 0.0, 0.0);
PLOT_CHECK_NEAR(b.data()[6], 1.0, 0.0);
PLOT_CHECK_NEAR(b.data()[7], 1.0, 0.0);
}
PLOT_TEST(line_batch_ignores_degenerate_polylines) {
plot::render::LineBatch b;
b.begin();
const plot::Vec2 one[1] = {{5.0f, 5.0f}};
b.add_polyline(one, 1); // no pair -> no instance
b.add_polyline(nullptr, 0);
PLOT_CHECK(b.vertex_count() == 0);
PLOT_CHECK(b.data().empty());
b.add_polyline(nullptr, 3); // null with a count: still no deref
PLOT_CHECK(b.vertex_count() == 0);
}
PLOT_TEST(line_batch_reuses_capacity_across_frames) {
plot::render::LineBatch b;
const plot::Vec2 pts[5] = {{0, 0}, {1, 1}, {2, 0}, {3, 1}, {4, 0}};
b.begin();
b.add_polyline(pts, 5); // 4 segments -> 16 floats
const std::size_t cap = b.data().capacity();
const f32* store = b.data().data();
PLOT_CHECK(cap >= 16);
b.clear();
PLOT_CHECK(b.vertex_count() == 0);
PLOT_CHECK(b.data().capacity() == cap); // clear() keeps the store
b.begin();
b.add_polyline(pts, 5); // same workload: must not reallocate
PLOT_CHECK(b.data().size() == 16);
PLOT_CHECK(b.data().capacity() == cap);
PLOT_CHECK(b.data().data() == store);
}
PLOT_TEST(text_batch_measures_ascii) {
plot::render::BitmapFont f;
const f32 w = f.measure("12.5 ms");
PLOT_CHECK(w > 0.0f);
PLOT_CHECK_NEAR(f.measure(""), 0.0, 1e-6);
PLOT_CHECK(f.measure("ab") < f.measure("abc")); // monotonic in length
PLOT_CHECK(f.measure("iiii") < f.measure("WWWW")); // proportional-ish, not fixed-width-blank
}
PLOT_TEST(bitmap_font_advance_follows_ink_extent) {
plot::render::BitmapFont f;
PLOT_CHECK(f.advance('i') < f.advance('W'));
// Whitespace is a blank glyph with a nonzero advance, never zero.
PLOT_CHECK(f.advance(' ') >= 2.0f);
// Advance is ink width + 1px of spacing, floored at 2px: derived here from
// the same bitmaps the atlas uploads, so a second width table cannot drift.
auto ink_width = [&f](char c) {
const plot::u8* rows = f.glyph_bitmap(c);
int lo = 8;
int hi = -1;
for (plot::u32 y = 0; y < f.glyph_rows(c); ++y) {
for (int x = 0; x < 8; ++x) {
if ((rows[y] >> x) & 1u) {
if (x < lo) lo = x;
if (x > hi) hi = x;
}
}
}
return hi < 0 ? 0 : hi - lo + 1;
};
const char* samples = "iW.1_m";
for (const char* c = samples; *c != '\0'; ++c) {
const int ink = ink_width(*c);
const f32 want = ink + 1 < 2 ? 2.0f : static_cast<f32>(ink + 1);
PLOT_CHECK_NEAR(f.advance(*c), want, 1e-6);
}
// measure() is exactly the sum of the glyph advances it stages.
f32 sum = 0.0f;
for (const char* c = "12.5 ms"; *c != '\0'; ++c) sum += f.advance(*c);
PLOT_CHECK_NEAR(f.measure("12.5 ms"), sum, 1e-6);
}
PLOT_TEST(bitmap_font_decodes_si_units_and_falls_back_visibly) {
plot::render::BitmapFont f;
// plot::format_si emits U+00B5 MICRO SIGN, U+00B0 DEGREE SIGN and U+03A9 OHM.
const f32 micro = f.measure("\xC2\xB5s");
const f32 degree = f.measure("\xC2\xB0");
const f32 ohm = f.measure("\xCE\xA9");
PLOT_CHECK(micro > 0.0f);
PLOT_CHECK(degree > 0.0f);
PLOT_CHECK(ohm > 0.0f);
PLOT_CHECK_NEAR(micro, f.advance_codepoint(0x00B5u) + f.advance('s'), 1e-6);
PLOT_CHECK_NEAR(degree, f.advance_codepoint(0x00B0u), 1e-6);
PLOT_CHECK_NEAR(ohm, f.advance_codepoint(0x03A9u), 1e-6);
// Those three map to their own renderable glyph, not to the missing-glyph box.
const plot::u32 fallback = f.glyph_index(0x4E00u); // CJK, deliberately absent
PLOT_CHECK(f.glyph_index(0x00B5u) != fallback);
PLOT_CHECK(f.glyph_index(0x00B0u) != fallback);
PLOT_CHECK(f.glyph_index(0x03A9u) != fallback);
PLOT_CHECK(f.glyph_index(0x00B5u) != f.glyph_index(0x00B0u));
PLOT_CHECK(f.glyph_index(0x00B0u) != f.glyph_index(0x03A9u));
// Anything else non-ASCII is a visible box with a nonzero advance, and a
// malformed UTF-8 byte is treated the same way rather than measured as zero.
PLOT_CHECK_NEAR(f.measure("\xE4\xB8\x80"), f.advance_codepoint(0x4E00u), 1e-6);
PLOT_CHECK(f.advance_codepoint(0x4E00u) > 0.0f);
PLOT_CHECK(f.advance(static_cast<char>(0x80)) > 0.0f);
PLOT_CHECK_NEAR(f.advance(static_cast<char>(0x80)), f.advance_codepoint(0x4E00u), 1e-6);
// The fallback box glyph has ink; a blank bitmap would hide the problem.
bool any_ink = false;
const plot::u8* box = plot::render::BitmapFont::glyph_bitmap_of(fallback);
for (plot::u32 y = 0; y < plot::render::BitmapFont::kGlyphPx; ++y) {
any_ink = any_ink || box[y] != 0;
}
PLOT_CHECK(any_ink);
}
PLOT_TEST(bitmap_font_atlas_is_uploadable) {
using plot::render::BitmapFont;
PLOT_CHECK(BitmapFont::kAtlasWidth == BitmapFont::kAtlasCols * BitmapFont::kGlyphPx);
PLOT_CHECK(BitmapFont::kAtlasHeight % BitmapFont::kGlyphPx == 0);
PLOT_CHECK(BitmapFont::kAtlasCols * BitmapFont::kAtlasRows >= BitmapFont::kGlyphCount);
PLOT_CHECK(BitmapFont::kAsciiGlyphs == 95);
plot::render::BitmapFont f;
PLOT_CHECK(f.glyph_rows('A') == 8);
PLOT_CHECK(f.height() > 0.0f);
PLOT_CHECK(f.ascent() > 0.0f);
PLOT_CHECK(f.ascent() <= f.height());
// Each glyph occupies its own atlas cell, and that cell is the bitmap byte
// for byte: 255 where a row bit is set, 0 elsewhere (straight alpha).
const plot::u8* atlas = BitmapFont::atlas_image();
const plot::u32 slots[] = {'A' - 32, 'g' - 32, 'i' - 32, 'W' - 32, 0, '.' - 32,
BitmapFont::kFallbackSlot, BitmapFont::kMicroSlot};
for (const plot::u32 slot : slots) {
const plot::Vec2 o = BitmapFont::atlas_origin(slot);
const plot::u8* rows = BitmapFont::glyph_bitmap_of(slot);
for (plot::u32 y = 0; y < BitmapFont::kGlyphPx; ++y) {
for (plot::u32 x = 0; x < BitmapFont::kGlyphPx; ++x) {
const plot::u8 want = ((rows[y] >> x) & 1u) != 0u ? 0xFFu : 0x00u;
const auto row = static_cast<std::size_t>(o.y) + y;
const auto col = static_cast<std::size_t>(o.x) + x;
PLOT_CHECK(atlas[row * BitmapFont::kAtlasWidth + col] == want);
}
}
}
// The atlas is a whole number of cells and no glyph leaves its cell.
PLOT_CHECK(BitmapFont::kAtlasWidth == 16u * BitmapFont::kGlyphPx);
}
PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
plot::render::BitmapFont f;
plot::render::TextBatch b;
b.begin();
const plot::Vec2 end = b.add_text({0.0f, 0.0f}, "12.5 ms", f);
PLOT_CHECK(b.glyph_count() == 7);
PLOT_CHECK(b.data().size() == 28); // x,y,u,v per glyph
PLOT_CHECK_NEAR(end.x, f.measure("12.5 ms"), 1e-6);
PLOT_CHECK_NEAR(end.y, 0.0, 1e-6);
PLOT_CHECK_NEAR(b.data()[0], 0.0, 1e-6); // first quad at the pen
PLOT_CHECK_NEAR(b.data()[1], 0.0, 1e-6);
PLOT_CHECK_NEAR(b.data()[4], f.advance('1'), 1e-6); // pen advanced per glyph
// The UV origin of the staged quad is the glyph's atlas cell.
const plot::Vec2 dot_uv =
plot::render::BitmapFont::atlas_origin(static_cast<plot::u32>('.') - 32u);
PLOT_CHECK_NEAR(b.data()[10], dot_uv.x, 1e-6);
PLOT_CHECK_NEAR(b.data()[11], dot_uv.y, 1e-6);
// Non-ASCII runs stage one quad per codepoint, and preserve capacity on reuse.
const std::size_t cap = b.data().capacity();
b.begin();
b.add_text({0.0f, 0.0f}, "1.5\xC2\xB5s", f); // 5 codepoints, 6 bytes
PLOT_CHECK(b.glyph_count() == 5);
PLOT_CHECK(b.data().capacity() == cap);
// A run drawn at twice the glyph cell size advances twice as far, so the
// staged label still ends where the scaled measurement says it does.
b.begin();
const plot::Vec2 scaled = b.add_text({10.0f, 4.0f}, "AW", f, 16.0f);
PLOT_CHECK(b.glyph_count() == 2);
PLOT_CHECK_NEAR(scaled.x, 10.0f + 2.0f * f.measure("AW"), 1e-6);
PLOT_CHECK_NEAR(scaled.y, 4.0, 1e-6);
PLOT_CHECK_NEAR(b.data()[4], 10.0f + 2.0f * f.advance('A'), 1e-6); // second glyph's pen
}