perf(ui): transparent shaped-run cache lookup, no string copy on hit (#122)
ShapeText built ShapedRunKey{..., std::string(utf8)} before find()
unconditionally — even on cache hits, on the per-frame onBuild path.
N labels meant N string copies + hashes (+ frees for non-SSO strings)
every frame, partially defeating the run cache.
Add is_transparent hash + equality functors on shapedRuns_ and probe
with a borrowing ShapedRunViewKey {const Font*, float, array<float,4>,
string_view}. The owning std::string is now materialised only on a
miss, for emplace. Cache hits copy and hash no string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2783e47674
commit
c8495f548b
2 changed files with 51 additions and 9 deletions
|
|
@ -75,8 +75,11 @@ std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
|||
// the pen starting at x=0, baseline=0 so a cache hit only needs the
|
||||
// translate below. The whole string is shaped (no outCapacity limit) so
|
||||
// a later call with a larger buffer still gets the full run.
|
||||
ShapedRunKey key{&font, pxSize, color, std::string(utf8)};
|
||||
auto it = shapedRuns_.find(key);
|
||||
// Probe with a borrowing view key (text is a string_view onto utf8) so a
|
||||
// cache hit copies and hashes no string. The owning std::string is only
|
||||
// built on a miss, below, for the emplace.
|
||||
ShapedRunViewKey viewKey{&font, pxSize, color, utf8};
|
||||
auto it = shapedRuns_.find(viewKey);
|
||||
if (it == shapedRuns_.end()) {
|
||||
// Miss: shape from scratch. Ensure() each glyph so brand-new ones
|
||||
// rasterise into the atlas and mark it dirty for the next upload —
|
||||
|
|
@ -110,6 +113,7 @@ std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
|||
run.advance = cursor;
|
||||
|
||||
if (shapedRuns_.size() >= kMaxShapedRuns) shapedRuns_.clear();
|
||||
ShapedRunKey key{&font, pxSize, color, std::string(utf8)};
|
||||
it = shapedRuns_.emplace(std::move(key), std::move(run)).first;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -339,25 +339,63 @@ export namespace Crafter {
|
|||
// only needs a translate by (x + alignShift, baselineY). The full
|
||||
// string is part of the key (compared on lookup) so hash collisions
|
||||
// can't return the wrong run.
|
||||
//
|
||||
// The map owns its key string, but lookups happen every frame on the
|
||||
// onBuild path. To avoid copying the string into a key on each cache
|
||||
// hit, the hash and equality functors are transparent (is_transparent)
|
||||
// and accept a borrowing view key whose text is a std::string_view;
|
||||
// the owning std::string is only materialised on a miss, for emplace.
|
||||
struct ShapedRunKey {
|
||||
const Font* font;
|
||||
float pxSize;
|
||||
std::array<float, 4> color;
|
||||
std::string text;
|
||||
bool operator==(const ShapedRunKey&) const = default;
|
||||
};
|
||||
struct ShapedRunViewKey {
|
||||
const Font* font;
|
||||
float pxSize;
|
||||
std::array<float, 4> color;
|
||||
std::string_view text;
|
||||
};
|
||||
struct ShapedRunKeyHash {
|
||||
std::size_t operator()(const ShapedRunKey& k) const noexcept {
|
||||
std::size_t h = std::hash<const void*>{}(k.font);
|
||||
using is_transparent = void;
|
||||
static std::size_t Hash(const Font* font, float pxSize,
|
||||
const std::array<float, 4>& color,
|
||||
std::string_view text) noexcept {
|
||||
std::size_t h = std::hash<const void*>{}(font);
|
||||
auto mix = [&h](std::size_t v) noexcept {
|
||||
h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2);
|
||||
};
|
||||
mix(std::hash<std::uint32_t>{}(std::bit_cast<std::uint32_t>(k.pxSize)));
|
||||
for (float c : k.color)
|
||||
mix(std::hash<std::uint32_t>{}(std::bit_cast<std::uint32_t>(pxSize)));
|
||||
for (float c : color)
|
||||
mix(std::hash<std::uint32_t>{}(std::bit_cast<std::uint32_t>(c)));
|
||||
mix(std::hash<std::string_view>{}(k.text));
|
||||
mix(std::hash<std::string_view>{}(text));
|
||||
return h;
|
||||
}
|
||||
std::size_t operator()(const ShapedRunKey& k) const noexcept {
|
||||
return Hash(k.font, k.pxSize, k.color, k.text);
|
||||
}
|
||||
std::size_t operator()(const ShapedRunViewKey& k) const noexcept {
|
||||
return Hash(k.font, k.pxSize, k.color, k.text);
|
||||
}
|
||||
};
|
||||
struct ShapedRunKeyEqual {
|
||||
using is_transparent = void;
|
||||
static bool Eq(const Font* fa, float pa, const std::array<float, 4>& ca,
|
||||
std::string_view ta,
|
||||
const Font* fb, float pb, const std::array<float, 4>& cb,
|
||||
std::string_view tb) noexcept {
|
||||
return fa == fb && pa == pb && ca == cb && ta == tb;
|
||||
}
|
||||
bool operator()(const ShapedRunKey& a, const ShapedRunKey& b) const noexcept {
|
||||
return Eq(a.font, a.pxSize, a.color, a.text, b.font, b.pxSize, b.color, b.text);
|
||||
}
|
||||
bool operator()(const ShapedRunViewKey& a, const ShapedRunKey& b) const noexcept {
|
||||
return Eq(a.font, a.pxSize, a.color, a.text, b.font, b.pxSize, b.color, b.text);
|
||||
}
|
||||
bool operator()(const ShapedRunKey& a, const ShapedRunViewKey& b) const noexcept {
|
||||
return Eq(a.font, a.pxSize, a.color, a.text, b.font, b.pxSize, b.color, b.text);
|
||||
}
|
||||
};
|
||||
struct ShapedRun {
|
||||
std::vector<GlyphItem> glyphs; // origin-relative
|
||||
|
|
@ -367,7 +405,7 @@ export namespace Crafter {
|
|||
// would grow this without bound; on overflow we drop everything and
|
||||
// rebuild (correctness is unaffected, only the hit rate after a flush).
|
||||
static constexpr std::size_t kMaxShapedRuns = 8192;
|
||||
std::unordered_map<ShapedRunKey, ShapedRun, ShapedRunKeyHash> shapedRuns_;
|
||||
std::unordered_map<ShapedRunKey, ShapedRun, ShapedRunKeyHash, ShapedRunKeyEqual> shapedRuns_;
|
||||
|
||||
ImageSlot outImageSlot_;
|
||||
ImageSlot fontAtlasImageSlot_;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue