feat(render): anti-aliased embedded Adwaita Mono atlas, generated and committed

This commit is contained in:
Martino Ferrari committed 2026-09-28 10:40:23 +02:00
1 parent 3ff7b9c978
commit 5ca3587325
17 files changed
+3422 -627

No files matched your search

+36 -20
View File
@@ -6,13 +6,14 @@
#include <plot/gl/caps.hpp>
#include <plot/gl/handle.hpp>
#include <plot/gl/loader.hpp>
#include <plot/render/bitmap_font.hpp>
#include <plot/render/embedded_mono_font.hpp>
#include <plot/render/primitives.hpp>
#include <plot/render/program.hpp>
#include <plot/render/shaders.hpp>
#include <plot/render/text_atlas.hpp>
#include <plot/render/vertex_array.hpp>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
@@ -341,10 +342,10 @@ PLOT_TEST(line_pipeline_draws_an_aa_strip) {
// The text pipeline end to end: the R8 atlas upload, the per-instance glyph UV
// rect, 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.
// Every texel of the cell is checked against the backend's own coverage byte, 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;
using plot::render::EmbeddedMonoFont;
plottest::HeadlessContext ctx(256, 256);
plot::Status st;
PLOT_CHECK(ctx.create(&st));
@@ -352,12 +353,12 @@ PLOT_TEST(text_pipeline_draws_a_glyph) {
// The backend's atlas, through the owner the renderer uses: single channel,
// GL_NEAREST, straight alpha, uploaded from the backend's own bytes.
BitmapFont font;
EmbeddedMonoFont font;
plot::render::TextAtlas atlas;
PLOT_CHECK(atlas.sync(font, &st));
PLOT_CHECK(!atlas.empty());
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(BitmapFont::kAtlasWidth), 1e-6);
PLOT_CHECK_NEAR(atlas.texel_size().y, static_cast<plot::f32>(BitmapFont::kAtlasHeight), 1e-6);
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(font.atlas_width()), 1e-6);
PLOT_CHECK_NEAR(atlas.texel_size().y, static_cast<plot::f32>(font.atlas_height()), 1e-6);
plot::render::TextProgram program;
PLOT_CHECK(program.create(&st));
@@ -383,29 +384,44 @@ PLOT_TEST(text_pipeline_draws_a_glyph) {
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)});
// The backend's own cell, on screen at 1:1: the draw's one rect size.
const plot::f32 cell_w = static_cast<plot::f32>(EmbeddedMonoFont::cell_width());
const plot::f32 cell_h = static_cast<plot::f32>(EmbeddedMonoFont::cell_height());
program.set_px_range({cell_w, cell_h});
program.set_glyph_uv(font.cell_uv());
program.set_texture_size(atlas.texel_size());
program.set_color(plot::Color::hex(0xFF00FF));
// White, so the blended pixel value *is* the coverage byte: the shader emits
// `vec4(rgb, a * coverage)`, and a white tint over the black clear lands at
// `1.0 * coverage` in every channel.
program.set_color(plot::Color::hex(0xFFFFFF));
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');
// The cell is cell_w x cell_h texels and u_px_range is the same, so texel
// (gx, gy) lands on exactly one target pixel: Rect x = 64 + gx, and Rect y =
// 100 + gy is GL row 155 - gy. A transposed axis, a wrong cell or a wrong UV
// all move ink away from where the coverage byte says it is.
const plot::u32 slot = EmbeddedMonoFont::glyph_index(static_cast<plot::u32>('A'));
const plot::Vec2 origin = EmbeddedMonoFont::atlas_origin(slot);
const plot::u8* atlas_bytes = font.atlas_pixels();
plot::u32 antialiased = 0;
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;
for (plot::u32 gy = 0; gy < EmbeddedMonoFont::cell_height(); ++gy) {
for (plot::u32 gx = 0; gx < EmbeddedMonoFont::cell_width(); ++gx) {
const plot::u32 ax = static_cast<plot::u32>(origin.x) + gx;
const plot::u32 ay = static_cast<plot::u32>(origin.y) + gy;
const plot::u8 want = atlas_bytes[static_cast<std::size_t>(ay) * font.atlas_width() + ax];
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_NEAR(px[0], static_cast<plot::f32>(want), 2.0);
PLOT_CHECK_NEAR(px[1], static_cast<plot::f32>(want), 2.0);
PLOT_CHECK_NEAR(px[2], static_cast<plot::f32>(want), 2.0);
if (want > 0u && want < 255u) ++antialiased;
}
}
// 'A' has grey edge texels and they reached the framebuffer: a 1-bit atlas
// could only ever have produced 0 or 255 here.
PLOT_CHECK(antialiased > 0u);
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
ctx.destroy();
}