Crafter.Graphics/tests/FontAtlasDirtyRect/main.cpp

127 lines
5.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//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;
}