feat(render): style, GLSL line/text pipelines, staging batches, bitmap font
This commit is contained in:
1 parent
9a086b7d69
commit
7cbc1c9955
22 files changed
+1535
No files matched your search
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user