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
|
|
@ -574,6 +574,14 @@ export namespace Crafter {
|
|||
// cross-platform. Browser key repeat is delivered through the
|
||||
// KeyboardEvent.repeat flag directly — no manual synthesis.
|
||||
static void TickKeyRepeats() {}
|
||||
|
||||
// The frame clock, as on native: bumped at the end of every
|
||||
// Window::Render. The font atlas evicts by it (#55). One frame in
|
||||
// flight: a frame's queue.writeTexture runs before that frame's own
|
||||
// submit but after every earlier one, so only the frame being built
|
||||
// can still sample a texel the atlas writes.
|
||||
inline static std::atomic<std::uint64_t> frameCounter = 0;
|
||||
inline static std::uint8_t framesInFlight = 1;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,31 @@ import :WebGPU;
|
|||
#endif
|
||||
|
||||
export namespace Crafter {
|
||||
// A glyph's cell in the atlas, as of when the glyph got it. Evicting the
|
||||
// glyph bumps the cell's generation, so a handle kept across frames (a
|
||||
// shaped run keeps one per glyph it draws) can tell that its cell, and the
|
||||
// UVs into it, now belong to another glyph.
|
||||
struct GlyphHandle {
|
||||
static constexpr std::uint32_t kNone = ~std::uint32_t{0};
|
||||
std::uint32_t slot = kNone;
|
||||
std::uint32_t generation = 0;
|
||||
};
|
||||
|
||||
// Per-glyph metrics. UVs are 0..1 in atlas space; on-screen sizes /
|
||||
// offsets / advance are in *atlas pixels at the base size* and scale
|
||||
// linearly with the requested font size at draw time.
|
||||
// linearly with the requested font size at draw time. w and h are 0 for a
|
||||
// glyph with no ink (a space). A glyph with ink that is not Resident()
|
||||
// found no room in the atlas: its UVs mean nothing, it draws blank, and its
|
||||
// advance and box still hold, so the text around it keeps its layout.
|
||||
struct Glyph {
|
||||
float u0 = 0, v0 = 0;
|
||||
float u1 = 0, v1 = 0;
|
||||
float w = 0, h = 0;
|
||||
float xoff = 0, yoff = 0;
|
||||
float advance = 0;
|
||||
GlyphHandle handle;
|
||||
|
||||
bool Resident() const noexcept { return handle.slot != GlyphHandle::kNone; }
|
||||
};
|
||||
|
||||
class FontAtlas {
|
||||
|
|
@ -80,6 +96,121 @@ export namespace Crafter {
|
|||
}
|
||||
};
|
||||
|
||||
// Where glyphs go in the atlas, and which of them give up their cells
|
||||
// once it is full (#55). Glyphs sit on shelves: rows as tall as the
|
||||
// glyph that opened them, filled left to right. A glyph goes into the
|
||||
// first gap wide enough on a shelf at least as tall, else onto a new
|
||||
// shelf below the last. When there is neither, it takes the same
|
||||
// columns on one shelf at least as tall, or on several in a row that
|
||||
// are as tall together (the free rows below the last shelf counting
|
||||
// as one more), from the first one's top: free texels, and, when the
|
||||
// caller allows it, the cells of glyphs no frame in flight uses, the
|
||||
// least recently used first. A glyph spanning shelves is a glyph on
|
||||
// each. Without that, a glyph taller than the shelves opened before
|
||||
// the rows ran out would never fit, and a few glyphs drawn every
|
||||
// frame would pin every shelf they sit on rather than just their
|
||||
// columns.
|
||||
// A glyph stays in flight for framesInFlight + 1 frames after it was
|
||||
// used, the frame of its use included: a frame's GlyphItems hold its
|
||||
// UVs, and its uploads and dispatches may not have run until the
|
||||
// frame loop has waited its fence, at the start of the frame
|
||||
// framesInFlight later. Device::frameCounter already reads that frame
|
||||
// from the end of the frame before, when the deletion queue has not
|
||||
// yet moved on, and text can be shaped in between (an input callback,
|
||||
// onBeforeUpdate). Pure CPU and pixel-free, so a test drives it with
|
||||
// made-up boxes; FontAtlas rasterises into the cells and clears what
|
||||
// it reports.
|
||||
class Packer {
|
||||
public:
|
||||
struct Cell { int x = 0, y = 0, w = 0, h = 0; };
|
||||
|
||||
explicit Packer(int size = kAtlasSize) : size_(size) {}
|
||||
|
||||
// A w×h cell for a glyph used in frame `now`, as a slot, or
|
||||
// GlyphHandle::kNone when nothing can make room. With `evict`, it
|
||||
// may take cold glyphs' cells: Evicted() then lists the slots it
|
||||
// took, whose glyphs are gone, and Cleared() the texels they
|
||||
// covered, for the caller to zero, since the new glyph covers only
|
||||
// part of them and a neighbour's filtering must not pick up a stale
|
||||
// glyph. The shelves' dirty spans already include both. A failure
|
||||
// evicts nothing.
|
||||
std::uint32_t Allocate(int w, int h, std::uint64_t now, std::uint32_t framesInFlight, bool evict);
|
||||
std::span<const std::uint32_t> Evicted() const noexcept { return evicted_; }
|
||||
std::span<const Cell> Cleared() const noexcept { return cleared_; }
|
||||
|
||||
// The glyph in `slot` was used in frame `now`.
|
||||
void Touch(std::uint32_t slot, std::uint64_t now) noexcept { slots_[slot].lastUse = now; }
|
||||
// The same for the glyph of every handle that still holds its
|
||||
// cell; false when one doesn't. All of them, not only those before
|
||||
// the first that doesn't: the caller shapes the run again, and the
|
||||
// run's own glyphs must not then look cold to its missing ones.
|
||||
bool Touch(std::span<const GlyphHandle> handles, std::uint64_t now) noexcept;
|
||||
|
||||
GlyphHandle Handle(std::uint32_t slot) const noexcept { return { slot, slots_[slot].generation }; }
|
||||
const Cell& CellOf(std::uint32_t slot) const noexcept { return slots_[slot].cell; }
|
||||
|
||||
// Hands `upload` each shelf's span of texels changed since it was
|
||||
// last uploaded; `upload` copies the span up and resets it.
|
||||
template<typename F>
|
||||
void ForEachDirty(F&& upload) {
|
||||
for (Shelf& s : shelves_) upload(s.dirty);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
Cell cell;
|
||||
std::uint64_t lastUse = 0;
|
||||
std::uint32_t generation = 0;
|
||||
};
|
||||
// A span per shelf (#129) stays a short run of rows, where one box
|
||||
// over every shelf written in a frame would take in the rows
|
||||
// between them too.
|
||||
struct Shelf {
|
||||
int y = 0;
|
||||
int height = 0;
|
||||
// Its glyphs, left to right: each covers the shelf's top row,
|
||||
// so their columns never overlap.
|
||||
std::vector<std::uint32_t> slots;
|
||||
DirtyRect dirty;
|
||||
};
|
||||
|
||||
int size_;
|
||||
// The shelves cover rows [0, nextShelfY_), each starting where the
|
||||
// one above ends, and stay as they are opened; the rows below are
|
||||
// free, and zero.
|
||||
int nextShelfY_ = 0;
|
||||
std::vector<Shelf> shelves_;
|
||||
std::vector<Slot> slots_;
|
||||
std::vector<std::uint32_t> freeSlots_;
|
||||
std::vector<std::uint32_t> evicted_;
|
||||
std::vector<Cell> cleared_;
|
||||
std::vector<std::uint32_t> run_; // Allocate's scratch: the glyphs on a run of shelves
|
||||
// The boxes that found no room in this frame, none holding another,
|
||||
// with eviction and without. Within a frame no glyph turns cold,
|
||||
// a placement only turns free or cold texels into its own glyph's,
|
||||
// and a shelf opens on free rows that the runs above it could
|
||||
// already reach (a box that fails is taller than they are), so the
|
||||
// room a box could get only shrinks. A box at least as wide and as
|
||||
// tall as one of these fails at once, so the glyphs of a frame that
|
||||
// doesn't fit don't each search every shelf. An eviction frees room
|
||||
// for placements without one, which forget theirs.
|
||||
struct Box { int w = 0, h = 0; };
|
||||
std::vector<Box> failedEvicting_;
|
||||
std::vector<Box> failedFree_;
|
||||
std::uint64_t failedFrame_ = ~std::uint64_t{0};
|
||||
std::uint32_t failedDepth_ = 0;
|
||||
|
||||
// A cell at column x on shelves [first, last], from first's top.
|
||||
std::uint32_t Insert(std::size_t first, std::size_t last, int x, int w, int h, std::uint64_t now);
|
||||
// Takes the glyph in `slot` off every shelf it is on, lists its
|
||||
// cell as cleared and dirty, and frees the slot for another glyph.
|
||||
void Evict(std::uint32_t slot);
|
||||
// Opens a shelf `height` rows tall below the last.
|
||||
void OpenShelf(int height);
|
||||
// The shelf whose rows hold row y.
|
||||
std::size_t ShelfAt(int y) const noexcept;
|
||||
};
|
||||
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
ImageVulkan<std::uint8_t> image;
|
||||
#else
|
||||
|
|
@ -90,7 +221,7 @@ export namespace Crafter {
|
|||
// renderer polls each frame; it is the OR of every dirty span Update
|
||||
// copies. `dirtyRect` is the *whole-atlas* span — used only for the
|
||||
// one-shot zero-clear in Initialize; per-glyph dirt is tracked tight
|
||||
// per shelf (Shelf::dirty) instead of inflating one tall union box
|
||||
// per shelf (in the Packer) instead of inflating one tall union box
|
||||
// across scattered shelves (#129). `dirty` and the spans are always
|
||||
// armed and cleared together.
|
||||
bool dirty = false;
|
||||
|
|
@ -116,13 +247,38 @@ export namespace Crafter {
|
|||
// Same shape on both backends.
|
||||
std::uint8_t* PixelPtr() noexcept;
|
||||
|
||||
// The glyph for `codepoint`: its metrics, cached per (Font*,
|
||||
// codepoint), and, when it has ink, a cell it is rasterised into on
|
||||
// first use if there is free room. Marks the glyph used in the current
|
||||
// frame (Device::frameCounter), which keeps its cell its own while a
|
||||
// frame in flight may sample it. Never fails: a glyph that finds no
|
||||
// free room comes back not Resident(), keeping its advance and box,
|
||||
// and draws blank. It waits for the next frame, whose first call here,
|
||||
// to Touch or to Update gives it a cell, evicting what the Packer
|
||||
// allows, once every glyph the frame before drew has been marked: an
|
||||
// eviction made while the frame is still asking for glyphs would take
|
||||
// ones it draws further down, which would then take others in turn.
|
||||
// The atlas keeps the Font* of a waiting glyph until then, so call
|
||||
// InvalidateFont before destroying a Font. The reference holds for
|
||||
// the frame: a glyph is evicted no sooner than framesInFlight + 1
|
||||
// frames after its last use.
|
||||
const Glyph& EnsureAndGet(Font& font, std::uint32_t codepoint);
|
||||
// EnsureAndGet, saying whether the glyph can be drawn: it has a cell,
|
||||
// or no ink to put in one.
|
||||
bool Ensure(Font& font, std::uint32_t codepoint);
|
||||
// Ensure the glyph is in the atlas and return a pointer to it in one
|
||||
// hash+probe. Returns nullptr only when a brand-new glyph cannot be
|
||||
// placed (ShelfPlace failure); a glyph with no SDF coverage (e.g.
|
||||
// space) still returns a valid pointer.
|
||||
const Glyph* EnsureAndGet(Font& font, std::uint32_t codepoint);
|
||||
// The cached glyph, or nullptr; neither places it nor marks it used.
|
||||
// The pointer holds until the next EnsureAndGet, Touch or Update,
|
||||
// which may evict the glyph and drop it.
|
||||
const Glyph* Lookup(Font& font, std::uint32_t codepoint) const;
|
||||
// Marks used in the current frame the glyphs whose handles a caller
|
||||
// kept (UIRenderer's shaped runs). False when one of them has been
|
||||
// evicted since: whatever was built from its UVs must be built again.
|
||||
bool Touch(std::span<const GlyphHandle> handles);
|
||||
// Forgets every glyph of `font`: its metrics, and the glyphs waiting
|
||||
// for a cell, whose Font* the atlas would otherwise read next frame.
|
||||
// Their cells stay until they are evicted, as a frame in flight may
|
||||
// still draw them. UIRenderer::InvalidateFont calls it.
|
||||
void InvalidateFont(const Font& font) noexcept;
|
||||
void Update(GraphicsCommandBuffer cmd);
|
||||
|
||||
// Flag the [x, x+w) × [y, y+h) atlas region as needing re-upload,
|
||||
|
|
@ -132,15 +288,13 @@ export namespace Crafter {
|
|||
dirty = !dirtyRect.Empty();
|
||||
}
|
||||
|
||||
private:
|
||||
// A shelf packs glyphs left-to-right at a fixed top (`y`). Its `dirty`
|
||||
// span therefore stays naturally tight: a contiguous X run capped by
|
||||
// the shelf height — far smaller than a union box spanning every
|
||||
// shelf a frame happened to touch.
|
||||
struct Shelf { int y = 0; int height = 0; int cursorX = 0; DirtyRect dirty; };
|
||||
std::vector<Shelf> shelves_;
|
||||
int nextShelfY_ = 0;
|
||||
// How many glyphs have been rasterised into a cell, counting a glyph
|
||||
// again each time it comes back after an eviction. For tests: a glyph
|
||||
// that finds no room must not be rasterised for nothing, nor one the
|
||||
// same frame draws further down evicted for it.
|
||||
std::uint64_t GlyphsRasterized() const noexcept { return rasterized_; }
|
||||
|
||||
private:
|
||||
struct Key {
|
||||
const Font* font;
|
||||
std::uint32_t cp;
|
||||
|
|
@ -153,10 +307,28 @@ export namespace Crafter {
|
|||
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
|
||||
}
|
||||
};
|
||||
std::unordered_map<Key, Glyph, KeyHash> cache_;
|
||||
struct Entry {
|
||||
Glyph glyph;
|
||||
std::uint64_t waiting = ~std::uint64_t{0}; // the frame it last joined waiting_ in
|
||||
};
|
||||
// A glyph with no cell stays here, measured, so that it keeps its
|
||||
// layout and is not measured again; an evicted glyph is dropped.
|
||||
std::unordered_map<Key, Entry, KeyHash> cache_;
|
||||
// owners_[slot]: the glyph holding a cell, to drop when it is evicted.
|
||||
std::vector<Key> owners_;
|
||||
// The glyphs asked for since the last Settle that found no free room,
|
||||
// in the order they were first asked for.
|
||||
std::vector<Key> waiting_;
|
||||
std::uint64_t settled_ = ~std::uint64_t{0}; // the frame Settle last ran for
|
||||
Packer packer_;
|
||||
std::uint64_t rasterized_ = 0;
|
||||
|
||||
// On success, outShelf is the index into shelves_ of the placed
|
||||
// glyph, so the caller can mark that shelf's dirty span.
|
||||
bool ShelfPlace(int w, int h, int& outX, int& outY, int& outShelf);
|
||||
// Gives the glyphs in waiting_ cells, evicting for them, at a frame's
|
||||
// first EnsureAndGet, Touch or Update.
|
||||
void Settle(std::uint64_t now);
|
||||
// Asks the packer for a cell for `e`, drops and clears the glyphs it
|
||||
// evicted for it, and rasterises `e` into the cell it got. False when
|
||||
// it got none.
|
||||
bool Place(const Font& font, const Key& key, Entry& e, std::uint64_t now, bool evict);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,10 +296,24 @@ export namespace Crafter {
|
|||
//
|
||||
// The origin-relative shaped run is memoized in a CPU cache keyed by
|
||||
// (Font*, pxSize, color, utf8); a hit skips UTF-8 decode, glyph
|
||||
// lookup, and layout, translating the cached run into `out` instead.
|
||||
// Caching is byte-equivalent — the glyph buffer is fully rewritten
|
||||
// and re-flushed every frame regardless. Call InvalidateFont before
|
||||
// destroying a Font (the cache keys on the Font pointer).
|
||||
// lookup, and layout, translating the cached run into `out` instead,
|
||||
// and marks the run's glyphs used in this frame so the atlas keeps
|
||||
// them. Caching is byte-equivalent — the glyph buffer is fully
|
||||
// rewritten and re-flushed every frame regardless. A run is shaped
|
||||
// again when the atlas has evicted one of its glyphs, or when it was
|
||||
// shaped while a glyph had no room (which draws blank but keeps its
|
||||
// advance, and gets a cell when the next frame begins). Call
|
||||
// InvalidateFont before destroying a Font (the cache keys on the Font
|
||||
// pointer).
|
||||
//
|
||||
// The GlyphItems written are good for the frame they are shaped for:
|
||||
// once the atlas is full it gives a glyph's cell to another glyph
|
||||
// framesInFlight + 1 frames after the glyph was last used. Shape text
|
||||
// again every frame, as the Tier 3 components do (a hit costs a
|
||||
// copy), into an item buffer of the frame's own, one per frame in
|
||||
// flight by UIBuildArgs::frameIdx: a frame in flight reads its items
|
||||
// as it runs, and a later frame's items written over them can name
|
||||
// cells that frame drew other glyphs in.
|
||||
std::uint32_t ShapeText(Font& font, float pxSize,
|
||||
float x, float baselineY,
|
||||
std::string_view utf8,
|
||||
|
|
@ -308,10 +322,12 @@ export namespace Crafter {
|
|||
float* outAdvance = nullptr,
|
||||
TextAlign align = TextAlign::Left);
|
||||
|
||||
// Drop every cached shaped run that references `font`. Must be called
|
||||
// before a Font is destroyed: the run cache (and FontAtlas's glyph
|
||||
// cache) key on the Font pointer, so a Font freed and a new one
|
||||
// reallocated at the same address would otherwise alias stale runs.
|
||||
// Drop every cached shaped run that references `font`, and the font
|
||||
// atlas's glyphs of it (FontAtlas::InvalidateFont). Must be called
|
||||
// before a Font is destroyed or replaced: both caches key on the Font
|
||||
// pointer, so a Font at the same address would otherwise alias stale
|
||||
// runs and glyphs, and the atlas keeps the Font* of a glyph waiting
|
||||
// for a cell until the next frame.
|
||||
void InvalidateFont(const Font& font) noexcept;
|
||||
|
||||
// Shaped-run cache introspection (issue #123). Hits/misses are
|
||||
|
|
@ -400,11 +416,21 @@ export namespace Crafter {
|
|||
struct ShapedRun {
|
||||
std::vector<GlyphItem> glyphs; // origin-relative
|
||||
float advance = 0;
|
||||
// The atlas cells `glyphs` sample, for a hit to mark them used
|
||||
// and to find out whether one has gone to another glyph (#55).
|
||||
std::vector<GlyphHandle> cells;
|
||||
// False when a glyph with ink had no room in the atlas and draws
|
||||
// blank: the run is shaped again on every hit until it fits.
|
||||
bool complete = true;
|
||||
// Position of this run's key in shapedRunsLru_ (front = most
|
||||
// recently used). Lets a hit splice the entry to the front and an
|
||||
// overflow pop the least-recently-used entry, both O(1).
|
||||
std::list<const ShapedRunKey*>::iterator lruIt{};
|
||||
};
|
||||
// Shapes `utf8` into `run` from scratch, origin-relative, asking the
|
||||
// atlas for every glyph.
|
||||
void ShapeRun(Font& font, float pxSize, std::string_view utf8,
|
||||
std::array<float,4> color, ShapedRun& run);
|
||||
// Soft cap. A pathological caller drawing a fresh string every frame
|
||||
// would grow this without bound; on overflow we evict the single
|
||||
// least-recently-used run (issue #123) rather than clearing the whole
|
||||
|
|
|
|||
Loading…
Reference in a new issue