Crafter.Graphics/tests/FontAtlasDirtyRect/main.cpp

127 lines
5.2 KiB
C++
Raw Normal View History

2026-07-22 18:09:06 +02:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Regression test for issue #51: FontAtlas::Update used to re-upload the
// whole 1024×1024 atlas every time a single glyph was rasterized. It now
// accumulates a dirty bounding box (FontAtlas::DirtyRect) and copies only
// that sub-rect. The bookkeeping is pure CPU math, so this test drives it
// directly — no Vulkan device or WebGPU context needed:
//
// - a fresh rect is empty; the first Add seeds exact bounds
// - further Adds grow the half-open union, never shrink it
// - degenerate glyphs (w<=0 / h<=0) leave the rect untouched
// - UploadBox converts (min,max) → (origin, extent) and clamps to the
// atlas, and reports emptiness so Update can early-out
// - FontAtlas::MarkDirty keeps the public `dirty` flag in lockstep with
// the rect, and Reset() clears both notions of dirtiness
//
// The actual GPU copy params (bufferOffset / bufferRowLength on Vulkan,
// dst origin / bytesPerRow on WebGPU) are exercised end-to-end by rendering
// text in the HelloUI example on both backends.
#include <cstdlib>
import Crafter.Graphics;
import std;
using namespace Crafter;
namespace {
int failures = 0;
void Check(bool ok, std::string_view what) {
std::println("{} {}", ok ? "PASS" : "FAIL", what);
if (!ok) ++failures;
}
// Read UploadBox into locals and compare against an expected (x,y,w,h).
bool BoxIs(const FontAtlas::DirtyRect& r, int limit,
std::uint32_t ex, std::uint32_t ey, std::uint32_t ew, std::uint32_t eh) {
std::uint32_t x = 99999, y = 99999, w = 99999, h = 99999;
if (!r.UploadBox(limit, x, y, w, h)) return false;
return x == ex && y == ey && w == ew && h == eh;
}
} // namespace
int main() {
// --- DirtyRect: empty / seed / grow ------------------------------------
{
FontAtlas::DirtyRect r;
Check(r.Empty(), "fresh rect is empty");
std::uint32_t x, y, w, h;
Check(!r.UploadBox(1024, x, y, w, h), "UploadBox on empty rect returns false");
r.Add(10, 20, 4, 6);
Check(!r.Empty(), "rect is non-empty after first Add");
Check(BoxIs(r, 1024, 10, 20, 4, 6), "first Add seeds exact (origin, extent)");
// A box fully inside the current one must not change anything.
r.Add(11, 21, 1, 1);
Check(BoxIs(r, 1024, 10, 20, 4, 6), "contained Add does not shrink or move the box");
// Grow up-left and down-right; result is the union.
r.Add(5, 8, 2, 2); // extends min corner to (5,8)
r.Add(100, 200, 10, 10); // extends max corner to (110,210)
Check(BoxIs(r, 1024, 5, 8, 105, 202), "Adds grow to the half-open union");
}
// --- DirtyRect: degenerate glyphs are no-ops ---------------------------
{
FontAtlas::DirtyRect r;
r.Add(3, 3, 0, 5);
r.Add(3, 3, 5, 0);
r.Add(3, 3, -1, -1);
Check(r.Empty(), "zero/negative-extent Adds leave the rect empty");
r.Add(3, 3, 5, 5);
r.Add(50, 50, 0, 0); // must not move the max corner out
Check(BoxIs(r, 1024, 3, 3, 5, 5), "degenerate Add after a real one is ignored");
}
// --- DirtyRect: clamp to atlas extent ----------------------------------
{
FontAtlas::DirtyRect r;
r.Add(-4, -4, 8, 8); // straddles the top-left corner
Check(BoxIs(r, 1024, 0, 0, 4, 4), "negative origin clamps to 0, extent trimmed");
FontAtlas::DirtyRect r2;
r2.Add(1020, 1020, 16, 16); // straddles the bottom-right corner
Check(BoxIs(r2, 1024, 1020, 1020, 4, 4), "over-extent box clamps to the atlas edge");
}
// --- FontAtlas::MarkDirty keeps `dirty` and the rect in sync -----------
{
FontAtlas atlas;
Check(!atlas.dirty && atlas.dirtyRect.Empty(), "default FontAtlas is clean");
atlas.MarkDirty(0, 0, FontAtlas::kAtlasSize, FontAtlas::kAtlasSize);
Check(atlas.dirty, "MarkDirty sets the public dirty flag");
Check(BoxIs(atlas.dirtyRect, FontAtlas::kAtlasSize,
0, 0, FontAtlas::kAtlasSize, FontAtlas::kAtlasSize),
"full-atlas MarkDirty covers the whole extent (the Initialize clear)");
atlas.MarkDirty(8, 8, 16, 16); // contained — full extent already dirty
Check(BoxIs(atlas.dirtyRect, FontAtlas::kAtlasSize,
0, 0, FontAtlas::kAtlasSize, FontAtlas::kAtlasSize),
"subsequent contained MarkDirty keeps the full extent");
// Simulate the reset Update performs after a successful upload.
atlas.dirty = false;
atlas.dirtyRect.Reset();
Check(!atlas.dirty && atlas.dirtyRect.Empty(), "post-upload reset clears dirtiness");
// After reset, a single glyph marks only its own rect.
atlas.MarkDirty(64, 128, 12, 20);
Check(atlas.dirty, "a glyph after reset re-arms dirty");
Check(BoxIs(atlas.dirtyRect, FontAtlas::kAtlasSize, 64, 128, 12, 20),
"post-reset MarkDirty tracks just the new glyph, not the old full extent");
}
if (failures != 0) {
std::println("{} check(s) failed", failures);
return EXIT_FAILURE;
}
std::println("all checks passed");
return EXIT_SUCCESS;
}