143 lines
5.8 KiB
C++
143 lines
5.8 KiB
C++
|
|
/*
|
|||
|
|
Crafter®.Graphics
|
|||
|
|
Copyright (C) 2026 Catcrafts®
|
|||
|
|
catcrafts.net
|
|||
|
|
|
|||
|
|
This library is free software; you can redistribute it and/or
|
|||
|
|
modify it under the terms of the GNU Lesser General Public
|
|||
|
|
License version 3.0 as published by the Free Software Foundation;
|
|||
|
|
|
|||
|
|
This library is distributed in the hope that it will be useful,
|
|||
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|||
|
|
Lesser General Public License for more details.
|
|||
|
|
|
|||
|
|
You should have received a copy of the GNU Lesser General Public
|
|||
|
|
License along with this library; if not, write to the Free Software
|
|||
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// 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;
|
|||
|
|
}
|