359 lines
18 KiB
C++
359 lines
18 KiB
C++
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
|||
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|||
|
|
|
|||
|
|
// Issue #55: FontAtlas::Packer decides where a glyph goes in the atlas and,
|
|||
|
|
// once the atlas is full, which glyphs give up their cells. It knows no
|
|||
|
|
// pixels, so this drives it with made-up boxes and frames, without a device:
|
|||
|
|
// - until something is evicted it fills shelves as the old allocator did:
|
|||
|
|
// the first shelf tall enough with room at its end, else a new shelf
|
|||
|
|
// - while every glyph was used within framesInFlight + 1 frames, a full
|
|||
|
|
// packer gives nothing away; after that it does, the least recently used
|
|||
|
|
// first, and touching a glyph keeps it; without `evict` it never does
|
|||
|
|
// - a glyph wider than a cell takes the coldest adjacent cells, never one
|
|||
|
|
// whose glyph is in flight
|
|||
|
|
// - a glyph taller than every shelf spans several, taking free columns
|
|||
|
|
// across them before it evicts anything, and the free rows below the
|
|||
|
|
// last shelf too, and blocks just its columns on each; glyphs in flight
|
|||
|
|
// on every shelf block just theirs
|
|||
|
|
// - Touch marks every handle that still holds its cell, even after one
|
|||
|
|
// that doesn't, so the glyphs of a run shaped again stay
|
|||
|
|
// - a handle whose cell went to another glyph touches nothing
|
|||
|
|
// - every cell handed out and every region cleared is in a dirty span
|
|||
|
|
// - over a long random workload, placed the way FontAtlas places glyphs
|
|||
|
|
// (free room within a frame, evictions when the next one begins): cells
|
|||
|
|
// stay inside the atlas and apart, only cold glyphs are evicted, the
|
|||
|
|
// cleared regions cover the evicted cells and no live one, and a failure
|
|||
|
|
// or a placement without `evict` evicts nothing
|
|||
|
|
//
|
|||
|
|
// Pure CPU: no Vulkan device or WebGPU context at runtime.
|
|||
|
|
|
|||
|
|
#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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
using Packer = FontAtlas::Packer;
|
|||
|
|
using Cell = Packer::Cell;
|
|||
|
|
|
|||
|
|
constexpr std::uint32_t kNone = GlyphHandle::kNone;
|
|||
|
|
constexpr std::uint32_t kDepth = 3; // frames in flight, as a Window has them
|
|||
|
|
constexpr std::uint64_t kHold = kDepth + 1; // frames a glyph keeps its cell after its use
|
|||
|
|
|
|||
|
|
std::uint32_t Place(Packer& p, int w, int h, std::uint64_t frame) {
|
|||
|
|
return p.Allocate(w, h, frame, kDepth, /*evict*/ true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool SameCell(const Cell& a, const Cell& b) {
|
|||
|
|
return a.x == b.x && a.y == b.y && a.w == b.w && a.h == b.h;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool Overlap(const Cell& a, const Cell& b) {
|
|||
|
|
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool Contains(const Cell& outer, const Cell& inner) {
|
|||
|
|
return inner.x >= outer.x && inner.y >= outer.y
|
|||
|
|
&& inner.x + inner.w <= outer.x + outer.w && inner.y + inner.h <= outer.y + outer.h;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Some shelf's pending span covers `c`. Reads the spans without uploading.
|
|||
|
|
bool Dirty(Packer& p, const Cell& c) {
|
|||
|
|
bool covered = false;
|
|||
|
|
p.ForEachDirty([&](FontAtlas::DirtyRect& r) {
|
|||
|
|
covered = covered || (r.active && c.x >= r.minX && c.y >= r.minY
|
|||
|
|
&& c.x + c.w <= r.maxX && c.y + c.h <= r.maxY);
|
|||
|
|
});
|
|||
|
|
return covered;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Upload(Packer& p) {
|
|||
|
|
p.ForEachDirty([](FontAtlas::DirtyRect& r) { r.Reset(); });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// `count` cells of w×h, all used in frame `frame`.
|
|||
|
|
std::vector<std::uint32_t> Fill(Packer& p, int count, int w, int h, std::uint64_t frame) {
|
|||
|
|
std::vector<std::uint32_t> slots;
|
|||
|
|
for (int i = 0; i < count; ++i) slots.push_back(Place(p, w, h, frame));
|
|||
|
|
return slots;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool AllPlaced(std::span<const std::uint32_t> slots) {
|
|||
|
|
return std::ranges::none_of(slots, [](std::uint32_t s) { return s == kNone; });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
struct WorkloadResult {
|
|||
|
|
std::size_t placed = 0, evicted = 0, failed = 0, settled = 0;
|
|||
|
|
bool inside = true, apart = true, onlyCold = true, covers = true, spares = true;
|
|||
|
|
bool cleanFailures = true, freeEvictsNothing = true, dirty = true, handles = true;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Glyphs of random sizes, a few to a dozen a frame, in epochs of short ones
|
|||
|
|
// and of tall ones, so the short ones fill the rows with low shelves that the
|
|||
|
|
// tall ones need more than. Drawn each frame, as on a screen: what came in
|
|||
|
|
// the last twenty frames, and twenty glyphs from the first frames that never
|
|||
|
|
// stop being drawn (a UI's fixed labels), spread over the first shelves. A
|
|||
|
|
// new glyph gets free room in its frame, or waits: the next frame begins by
|
|||
|
|
// placing the waiting ones with evictions, as FontAtlas does.
|
|||
|
|
WorkloadResult Workload(int size, std::uint32_t seed) {
|
|||
|
|
Packer p(size);
|
|||
|
|
std::mt19937 rng(seed);
|
|||
|
|
struct Live { Cell cell; std::uint64_t born; std::uint64_t lastUse; GlyphHandle handle; bool fixed; };
|
|||
|
|
std::unordered_map<std::uint32_t, Live> live;
|
|||
|
|
std::vector<std::pair<int, int>> waiting;
|
|||
|
|
std::size_t fixed = 0;
|
|||
|
|
WorkloadResult r;
|
|||
|
|
auto take = [&](std::uint32_t s, int w, int h, std::uint64_t frame, bool evict) {
|
|||
|
|
if (s == kNone) {
|
|||
|
|
r.cleanFailures = r.cleanFailures && p.Evicted().empty() && p.Cleared().empty();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
r.freeEvictsNothing = r.freeEvictsNothing && (evict || p.Evicted().empty());
|
|||
|
|
for (std::uint32_t v : p.Evicted()) {
|
|||
|
|
auto it = live.find(v);
|
|||
|
|
if (it == live.end()) { r.onlyCold = false; continue; }
|
|||
|
|
r.onlyCold = r.onlyCold && it->second.lastUse + kHold <= frame;
|
|||
|
|
r.covers = r.covers && std::ranges::any_of(p.Cleared(), [&](const Cell& c) { return Contains(c, it->second.cell); });
|
|||
|
|
live.erase(it);
|
|||
|
|
++r.evicted;
|
|||
|
|
}
|
|||
|
|
const Cell cell = p.CellOf(s);
|
|||
|
|
r.inside = r.inside && cell.w == w && cell.h == h && cell.x >= 0 && cell.y >= 0
|
|||
|
|
&& cell.x + w <= size && cell.y + h <= size;
|
|||
|
|
for (const auto& [slot, l] : live) r.apart = r.apart && !Overlap(cell, l.cell);
|
|||
|
|
for (const Cell& c : p.Cleared()) {
|
|||
|
|
for (const auto& [slot, l] : live) r.spares = r.spares && !Overlap(c, l.cell);
|
|||
|
|
r.dirty = r.dirty && Dirty(p, c);
|
|||
|
|
}
|
|||
|
|
r.dirty = r.dirty && Dirty(p, cell);
|
|||
|
|
const bool isFixed = frame < 30 && fixed < 20 && rng() % 2 == 0;
|
|||
|
|
fixed += isFixed;
|
|||
|
|
live[s] = { cell, frame, frame, p.Handle(s), isFixed };
|
|||
|
|
++r.placed;
|
|||
|
|
return true;
|
|||
|
|
};
|
|||
|
|
for (std::uint64_t frame = 10; frame < 4010; ++frame) {
|
|||
|
|
for (auto [w, h] : std::exchange(waiting, {})) {
|
|||
|
|
if (take(p.Allocate(w, h, frame, kDepth, /*evict*/ true), w, h, frame, true)) ++r.settled;
|
|||
|
|
else ++r.failed;
|
|||
|
|
}
|
|||
|
|
for (auto& [slot, l] : live) {
|
|||
|
|
if (l.fixed || l.born + 20 > frame) { p.Touch(slot, frame); l.lastUse = frame; }
|
|||
|
|
}
|
|||
|
|
const int maxH = (frame / 500) % 2 == 0 ? 12 : 40;
|
|||
|
|
const int n = static_cast<int>(rng() % 12);
|
|||
|
|
for (int k = 0; k < n; ++k) {
|
|||
|
|
const int w = 4 + static_cast<int>(rng() % 37);
|
|||
|
|
const int h = 4 + static_cast<int>(rng() % static_cast<std::uint32_t>(maxH - 3));
|
|||
|
|
if (!take(p.Allocate(w, h, frame, kDepth, /*evict*/ false), w, h, frame, false)) waiting.emplace_back(w, h);
|
|||
|
|
}
|
|||
|
|
if (frame % 50 == 0) {
|
|||
|
|
// Every live handle still holds its cell. Touching it in its own
|
|||
|
|
// last frame leaves the workload as it was.
|
|||
|
|
for (const auto& [slot, l] : live) {
|
|||
|
|
r.handles = r.handles && p.Touch(std::span{ &l.handle, 1 }, l.lastUse);
|
|||
|
|
}
|
|||
|
|
Upload(p);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return r;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} // namespace
|
|||
|
|
|
|||
|
|
int main() {
|
|||
|
|
// ── 1. Filling: the first shelf tall enough with room, else a new one. ──
|
|||
|
|
{
|
|||
|
|
Packer p(100);
|
|||
|
|
const std::uint32_t a = Place(p, 30, 10, 0);
|
|||
|
|
const std::uint32_t b = Place(p, 30, 10, 0);
|
|||
|
|
const std::uint32_t c = Place(p, 30, 10, 0);
|
|||
|
|
const std::uint32_t d = Place(p, 30, 10, 0);
|
|||
|
|
const std::uint32_t e = Place(p, 10, 8, 0);
|
|||
|
|
const std::uint32_t f = Place(p, 20, 12, 0);
|
|||
|
|
Check(SameCell(p.CellOf(a), {0, 0, 30, 10}) && SameCell(p.CellOf(b), {30, 0, 30, 10})
|
|||
|
|
&& SameCell(p.CellOf(c), {60, 0, 30, 10}), "a shelf fills left to right");
|
|||
|
|
Check(SameCell(p.CellOf(d), {0, 10, 30, 10}), "a glyph without room on a shelf opens the next one");
|
|||
|
|
Check(SameCell(p.CellOf(e), {90, 0, 10, 8}), "a shorter glyph takes the first shelf with room");
|
|||
|
|
Check(SameCell(p.CellOf(f), {0, 20, 20, 12}), "a taller glyph opens a shelf of its own height");
|
|||
|
|
Check(p.Evicted().empty() && p.Cleared().empty(), "filling evicts nothing");
|
|||
|
|
Check(Dirty(p, p.CellOf(a)) && Dirty(p, p.CellOf(f)), "every cell handed out is in a dirty span");
|
|||
|
|
Check(Place(p, 0, 10, 0) == kNone && Place(p, 10, 101, 0) == kNone,
|
|||
|
|
"an empty box, or one bigger than the atlas, gets no cell");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 2. Full: nothing to give while every glyph is in flight. ───────────
|
|||
|
|
{
|
|||
|
|
Packer p(64);
|
|||
|
|
const auto cells = Fill(p, 16, 16, 16, 1);
|
|||
|
|
Check(AllPlaced(cells), "sixteen 16x16 cells fill a 64x64 atlas");
|
|||
|
|
bool held = true;
|
|||
|
|
for (std::uint64_t frame = 1; frame < 1 + kHold; ++frame) {
|
|||
|
|
held = held && Place(p, 16, 16, frame) == kNone && p.Evicted().empty();
|
|||
|
|
}
|
|||
|
|
Check(held, "for framesInFlight + 1 frames after their use, the glyphs keep their cells");
|
|||
|
|
Check(p.Allocate(16, 16, 1 + kHold, kDepth, /*evict*/ false) == kNone && p.Evicted().empty(),
|
|||
|
|
"then, without evict, still none gives way");
|
|||
|
|
const std::uint32_t s = Place(p, 16, 16, 1 + kHold);
|
|||
|
|
Check(s != kNone && p.Evicted().size() == 1, "with it, one gives way, and only one");
|
|||
|
|
Check(p.Cleared().size() == 1 && SameCell(p.Cleared()[0], p.CellOf(s)) && Dirty(p, p.Cleared()[0]),
|
|||
|
|
"its texels are cleared, and the clearing is in a dirty span");
|
|||
|
|
Check(Place(p, 8, 8, 1 + kHold) != kNone && p.Evicted().size() == 1,
|
|||
|
|
"a smaller glyph takes one cold cell too");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 3. The least recently used goes first; a touch keeps a glyph. ──────
|
|||
|
|
{
|
|||
|
|
Packer p(64);
|
|||
|
|
std::vector<std::uint32_t> cells;
|
|||
|
|
for (std::uint64_t frame = 1; frame <= 16; ++frame) cells.push_back(Place(p, 16, 16, frame));
|
|||
|
|
p.Touch(cells[0], 19); // frame 1's glyph, used again in frame 19
|
|||
|
|
std::vector<std::uint32_t> order;
|
|||
|
|
for (int i = 0; i < 15; ++i) {
|
|||
|
|
if (Place(p, 16, 16, 20) == kNone || p.Evicted().size() != 1) break;
|
|||
|
|
order.push_back(p.Evicted()[0]);
|
|||
|
|
}
|
|||
|
|
Check(order == std::vector<std::uint32_t>(cells.begin() + 1, cells.end()),
|
|||
|
|
"full, the glyphs give way in the order they were last used");
|
|||
|
|
Check(Place(p, 16, 16, 20) == kNone, "the glyph touched in a frame still in flight stays");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 4. A wide glyph takes adjacent cold cells, never one in flight. ────
|
|||
|
|
{
|
|||
|
|
Packer p(64);
|
|||
|
|
const std::uint32_t a = Place(p, 16, 16, 1);
|
|||
|
|
const std::uint32_t b = Place(p, 16, 16, 9);
|
|||
|
|
const std::uint32_t c = Place(p, 16, 16, 2);
|
|||
|
|
const std::uint32_t d = Place(p, 16, 16, 3);
|
|||
|
|
Check(AllPlaced(Fill(p, 12, 16, 16, 9)), "three more shelves, in flight in frame 10");
|
|||
|
|
const std::uint32_t s = Place(p, 32, 16, 10);
|
|||
|
|
Check(s != kNone && SameCell(p.CellOf(s), {32, 0, 32, 16}),
|
|||
|
|
"a 32-wide glyph takes the two cold cells beside each other");
|
|||
|
|
const auto evicted = p.Evicted();
|
|||
|
|
Check(evicted.size() == 2 && std::ranges::count(evicted, c) == 1 && std::ranges::count(evicted, d) == 1,
|
|||
|
|
"it evicts just those two");
|
|||
|
|
Check(std::ranges::count(evicted, a) == 0 && std::ranges::count(evicted, b) == 0,
|
|||
|
|
"the colder glyph beside the one in flight stays, as does that one");
|
|||
|
|
Check(p.Cleared().size() == 2 && Dirty(p, p.Cleared()[0]) && Dirty(p, p.Cleared()[1]),
|
|||
|
|
"both cells are cleared, in a dirty span");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 5. A glyph taller than every shelf spans several. ──────────────────
|
|||
|
|
{
|
|||
|
|
Packer p(64);
|
|||
|
|
// Eight shelves of eight 8x8 glyphs; shelf 1's are in flight.
|
|||
|
|
for (int shelf = 0; shelf < 8; ++shelf) Fill(p, 8, 8, 8, shelf == 1 ? 9 : 1);
|
|||
|
|
Upload(p);
|
|||
|
|
const std::uint32_t s = Place(p, 16, 24, 10);
|
|||
|
|
Check(s != kNone && SameCell(p.CellOf(s), {0, 16, 16, 24}),
|
|||
|
|
"a 24-tall glyph spans three 8-row shelves, past the one in flight");
|
|||
|
|
Check(p.Evicted().size() == 6, "it evicts the six glyphs in its columns");
|
|||
|
|
bool cleared = p.Cleared().size() == 6;
|
|||
|
|
for (const Cell& c : p.Cleared()) cleared = cleared && Contains(p.CellOf(s), c) && Dirty(p, c);
|
|||
|
|
Check(cleared, "their cells are cleared, in a dirty span");
|
|||
|
|
const std::uint32_t t = Place(p, 40, 20, 10);
|
|||
|
|
Check(t != kNone && SameCell(p.CellOf(t), {16, 16, 40, 20}),
|
|||
|
|
"a glyph spanning shelves blocks its columns on each, and no more");
|
|||
|
|
}
|
|||
|
|
{
|
|||
|
|
Packer p(64);
|
|||
|
|
// Eight 8-row shelves, each holding one 40x8 glyph, all in flight:
|
|||
|
|
// columns 40 to 63 are free on every shelf.
|
|||
|
|
Check(AllPlaced(Fill(p, 8, 40, 8, 1)), "eight 40-wide glyphs open eight shelves");
|
|||
|
|
const std::uint32_t s = p.Allocate(16, 16, 1, kDepth, /*evict*/ false);
|
|||
|
|
Check(s != kNone && SameCell(p.CellOf(s), {40, 0, 16, 16}) && p.Evicted().empty(),
|
|||
|
|
"a 16-tall glyph takes free columns across two shelves, without evict too");
|
|||
|
|
}
|
|||
|
|
{
|
|||
|
|
Packer p(64);
|
|||
|
|
// Eight shelves of eight 8x8 glyphs, and on shelf k the glyph in
|
|||
|
|
// column k is in flight: every shelf holds one, every pair of shelves
|
|||
|
|
// has them in two columns side by side.
|
|||
|
|
std::vector<std::uint32_t> cells;
|
|||
|
|
for (int shelf = 0; shelf < 8; ++shelf) {
|
|||
|
|
for (std::uint32_t c : Fill(p, 8, 8, 8, 1)) cells.push_back(c);
|
|||
|
|
}
|
|||
|
|
for (std::uint32_t c : cells) {
|
|||
|
|
if (p.CellOf(c).x == p.CellOf(c).y) p.Touch(c, 9);
|
|||
|
|
}
|
|||
|
|
const std::uint32_t s = Place(p, 16, 16, 10);
|
|||
|
|
Check(s != kNone && SameCell(p.CellOf(s), {16, 0, 16, 16}) && p.Evicted().size() == 4,
|
|||
|
|
"glyphs in flight on every shelf block only their own columns");
|
|||
|
|
}
|
|||
|
|
{
|
|||
|
|
Packer p(64);
|
|||
|
|
// Six full shelves of 8x8 glyphs in flight, a seventh holding three
|
|||
|
|
// in columns 0 to 23, and 8 free rows below it: a 12-tall glyph is
|
|||
|
|
// taller than the free rows and than any shelf.
|
|||
|
|
Check(AllPlaced(Fill(p, 51, 8, 8, 9)), "six and a bit 8-row shelves, in flight in frame 10");
|
|||
|
|
const std::uint32_t s = p.Allocate(10, 12, 10, kDepth, /*evict*/ false);
|
|||
|
|
Check(s != kNone && SameCell(p.CellOf(s), {24, 48, 10, 12}) && p.Evicted().empty(),
|
|||
|
|
"a glyph taller than the free rows spans the last shelf and them");
|
|||
|
|
const std::uint32_t t = Place(p, 64, 4, 10);
|
|||
|
|
Check(t != kNone && SameCell(p.CellOf(t), {0, 60, 64, 4}),
|
|||
|
|
"the free rows it left below take later glyphs");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 6. Touch marks every handle that still holds its cell. ─────────────
|
|||
|
|
{
|
|||
|
|
Packer p(32);
|
|||
|
|
const auto cells = Fill(p, 4, 16, 16, 1);
|
|||
|
|
std::vector<GlyphHandle> run;
|
|||
|
|
for (std::uint32_t c : cells) run.push_back(p.Handle(c));
|
|||
|
|
const std::uint32_t n = Place(p, 16, 16, 1 + kHold); // takes cells[0], the first found
|
|||
|
|
Check(n == cells[0] && p.Evicted().size() == 1, "one of four cold glyphs gives way");
|
|||
|
|
Check(!p.Touch(run, 2 + kHold), "a run whose first glyph went touches false");
|
|||
|
|
// Only the new glyph is cold kHold frames on: the run's other three
|
|||
|
|
// were marked although the first handle no longer held its cell.
|
|||
|
|
const std::uint32_t m = Place(p, 16, 16, 1 + 2 * kHold);
|
|||
|
|
Check(m != kNone && p.Evicted().size() == 1 && p.Evicted()[0] == n,
|
|||
|
|
"the run's other glyphs were marked used past the stale handle");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 7. A handle whose cell went to another glyph touches nothing. ──────
|
|||
|
|
{
|
|||
|
|
Packer p(32);
|
|||
|
|
const std::uint32_t a = Place(p, 32, 32, 1);
|
|||
|
|
const std::array handles{ p.Handle(a) };
|
|||
|
|
Check(p.Touch(handles, 2), "a handle to a live cell touches its glyph");
|
|||
|
|
const std::uint32_t b = Place(p, 32, 32, 2 + kHold);
|
|||
|
|
Check(b == a && p.Evicted().size() == 1 && p.Evicted()[0] == a, "the cell goes to another glyph");
|
|||
|
|
Check(p.Handle(b).generation != handles[0].generation, "with a new generation");
|
|||
|
|
Check(!p.Touch(handles, 3 + kHold), "the old handle no longer touches it");
|
|||
|
|
Check(Place(p, 32, 32, 2 + 2 * kHold) != kNone,
|
|||
|
|
"and did not keep the new glyph from being evicted in its turn");
|
|||
|
|
const std::array foreign{ GlyphHandle{ 999, 0 } };
|
|||
|
|
Check(!p.Touch(foreign, 9), "a handle to no slot touches nothing");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 8. A long random workload. ─────────────────────────────────────────
|
|||
|
|
{
|
|||
|
|
const WorkloadResult r = Workload(256, 55);
|
|||
|
|
std::println("random workload: {} placed ({} when a frame began), {} evicted, {} failed",
|
|||
|
|
r.placed, r.settled, r.evicted, r.failed);
|
|||
|
|
Check(r.evicted > 1000 && r.settled > 1000, "the workload evicts, when frames begin");
|
|||
|
|
Check(r.inside, "every cell is the size asked for and inside the atlas");
|
|||
|
|
Check(r.apart, "no cell overlaps a live one");
|
|||
|
|
Check(r.onlyCold, "only glyphs no frame in flight uses are evicted");
|
|||
|
|
Check(r.covers, "the cleared regions cover every evicted cell");
|
|||
|
|
Check(r.spares, "and no live one");
|
|||
|
|
Check(r.dirty, "every cell handed out and every region cleared is in a dirty span");
|
|||
|
|
Check(r.cleanFailures, "a failure evicts nothing");
|
|||
|
|
Check(r.freeEvictsNothing, "a placement without evict evicts nothing");
|
|||
|
|
Check(r.handles, "every live glyph's handle still holds its cell");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (failures == 0) std::println("\nAll font atlas packer checks passed.");
|
|||
|
|
else std::println("\n{} font atlas packer check(s) FAILED.", failures);
|
|||
|
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|||
|
|
}
|