fix(font-atlas): give glyphs that find no room a cell when the next frame begins, evicting the least recently drawn
The atlas packed glyphs onto shelves and never gave a place back (#55). Once its 1024x1024 image was full, EnsureAndGet returned nullptr for each new glyph before caching anything, so every new string holding one rasterised it again, and ShapeText dropped it together with its advance: the rest of the line closed up over the gap, and stayed that way. FontAtlas - A glyph is measured, not rasterised, until it gets a cell. One that finds no free room keeps its advance and box and draws blank for a frame. EnsureAndGet always returns the glyph, now a reference; Glyph::Resident() says whether it has a cell, Glyph::handle which one (slot and generation). - Evictions wait for the next frame: its first EnsureAndGet, Touch or Update places the glyphs that waited, once every glyph the frame before drew has been marked. Evicting while a frame is still asking for glyphs took ones it drew further down, which then took others in turn: a page switched back to could rasterise most of itself again rather than what it was missing, and on the DOM backend an over-full view could do so every frame. - A glyph keeps its cell for framesInFlight + 1 frames after it was drawn. The frame loop waits frame u's fence at the start of frame u + framesInFlight, but Device::frameCounter reads that from the end of the frame before, and input callbacks and onBeforeUpdate run in between. - FontAtlas::Packer places glyphs. Until the rows run out it fills shelves exactly as before. Then a glyph takes the same columns of one shelf at least as tall, or of several in a row as tall together, the free rows below the last shelf counting as one more: free texels, and, when the frame begins, glyphs nothing in flight draws, the least recently drawn first. A glyph spanning shelves lets in one taller than every shelf, and a few glyphs drawn every frame pin only their columns. Merging whole shelves instead, tried first, left a random workload without room for a glyph while a median 43% of the atlas was in flight; spanning, at 65%. - A box that found no room in a frame is remembered, with every box at least as big, as a small set per mode, until the frame ends or (without eviction) something is evicted: a saturated atlas no longer searches every shelf for each of its missing glyphs. - An evicted glyph is forgotten, and its texels are zeroed and uploaded with the glyph that takes its place: a neighbour's filtering at its edge must read padding or zero, not a stale glyph. - Touch(handles) marks every glyph whose handle still holds its cell, and says whether all did. InvalidateFont(font) forgets the font's glyphs and those waiting for a cell, whose Font* the next frame would read. The glyph index is looked up at rasterisation, so a Font replaced in place never gets one past its glyphs. GlyphsRasterized() counts, for tests. UIRenderer - A shaped run keeps a handle per glyph it draws. A hit touches them all, so the atlas keeps what is drawn every frame, and shapes the run again when one was evicted or one had no room. A glyph without room draws blank but advances the pen. - InvalidateFont clears the atlas's glyphs of the font too, as its comment and AGENTS.md always said it did. - Record calls the atlas's Update before onBuild, every frame, so the waiting glyphs are placed and uploaded first. Dispatch uploads the atlas before any kernel, a Tier 1 one sampling FontAtlasImageSlot() included: a glyph placed earlier in onBuild can sit in a cell another glyph had. DispatchText and DispatchFused no longer do it themselves. Device, Window (DOM) - Device::frameCounter and Device::framesInFlight exist on the DOM backend too, the counter bumped at the end of Window::Render, with one frame in flight: a frame's writeTexture lands before that frame's submit but after every earlier one. HelloUI - One set of item buffers and slots per frame in flight, by frameIdx. A frame still on the GPU read the items the next frame's onBuild was rewriting; with cells changing hands, those can name cells it drew other glyphs in. Tests - FontAtlasPacker: the packer with made-up boxes and no device: the fill order, least recently drawn first, framesInFlight + 1 frames held, nothing without evict, wide glyphs, tall glyphs spanning shelves and the free rows, Touch past a stale handle, stale handles, dirty spans, and the invariants over a random workload placed the way the atlas places. - FontAtlasEviction: fills the atlas through ShapeText frame by frame. The line that no longer fits keeps its advance and its glyphs' places; its missing glyphs aren't rasterised while nothing can go; when only line 0 is cold, only its glyphs give way; a frame's own misses evict nothing in it; a frame later the line and twenty more are whole, pixel for pixel, line 0 losing a larger share than the glyphs drawn after it; a stale cached run is shaped again; no stale texel is left; InvalidateFont reaches the atlas. Against master, with its two calls to the new API stubbed out, an earlier version failed ten checks. - UIDispatchAtlasFlush: a custom ui.Dispatch after ShapeText leaves the atlas uploaded. Needs a compositor. - ShapeTextCache: a hit rasterises nothing; after InvalidateFont the re-shaped run draws the same, from new cells. Docs - README and AGENTS.consumer.md: the atlas's eviction, the frame a glyph waits, shaping text every frame into per-frame item buffers, InvalidateFont, EnsureAndGet's signature and Lookup's lifetime, and that the atlas, like the deletion queue, assumes one rendering window and evicts nothing without one. - AGENTS.md: FontAtlas::Packer among the pure-logic helpers, eviction without a Window, and InvalidateFont. Breaking: FontAtlas::EnsureAndGet returns const Glyph& and never fails; check Glyph::Resident() before drawing from its UVs. FontAtlas::Touch is no longer noexcept. A GlyphItem is good for the frame it was shaped for, once the atlas has filled up, and item buffers must be per frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
578a3c6dc4
commit
a4be6f1a9c
17 changed files with 1632 additions and 187 deletions
359
tests/FontAtlasPacker/main.cpp
Normal file
359
tests/FontAtlasPacker/main.cpp
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
//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;
|
||||
}
|
||||
Loading…
Reference in a new issue