127 lines
5.2 KiB
C++
127 lines
5.2 KiB
C++
//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;
|
||
}
|