perf(ui): transparent shaped-run cache lookup, no string copy on hit (#122) #141
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
|
// 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
|
// translate below. The whole string is shaped (no outCapacity limit) so
|
||||||
// a later call with a larger buffer still gets the full run.
|
// a later call with a larger buffer still gets the full run.
|
||||||
ShapedRunKey key{&font, pxSize, color, std::string(utf8)};
|
// Probe with a borrowing view key (text is a string_view onto utf8) so a
|
||||||
auto it = shapedRuns_.find(key);
|
// 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()) {
|
if (it == shapedRuns_.end()) {
|
||||||
// Miss: shape from scratch. Ensure() each glyph so brand-new ones
|
// Miss: shape from scratch. Ensure() each glyph so brand-new ones
|
||||||
// rasterise into the atlas and mark it dirty for the next upload —
|
// 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;
|
run.advance = cursor;
|
||||||
|
|
||||||
if (shapedRuns_.size() >= kMaxShapedRuns) shapedRuns_.clear();
|
if (shapedRuns_.size() >= kMaxShapedRuns) shapedRuns_.clear();
|
||||||
|
ShapedRunKey key{&font, pxSize, color, std::string(utf8)};
|
||||||
it = shapedRuns_.emplace(std::move(key), std::move(run)).first;
|
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
|
// only needs a translate by (x + alignShift, baselineY). The full
|
||||||
// string is part of the key (compared on lookup) so hash collisions
|
// string is part of the key (compared on lookup) so hash collisions
|
||||||
// can't return the wrong run.
|
// 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 {
|
struct ShapedRunKey {
|
||||||
const Font* font;
|
const Font* font;
|
||||||
float pxSize;
|
float pxSize;
|
||||||
std::array<float, 4> color;
|
std::array<float, 4> color;
|
||||||
std::string text;
|
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 {
|
struct ShapedRunKeyHash {
|
||||||
std::size_t operator()(const ShapedRunKey& k) const noexcept {
|
using is_transparent = void;
|
||||||
std::size_t h = std::hash<const void*>{}(k.font);
|
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 {
|
auto mix = [&h](std::size_t v) noexcept {
|
||||||
h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2);
|
h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2);
|
||||||
};
|
};
|
||||||
mix(std::hash<std::uint32_t>{}(std::bit_cast<std::uint32_t>(k.pxSize)));
|
mix(std::hash<std::uint32_t>{}(std::bit_cast<std::uint32_t>(pxSize)));
|
||||||
for (float c : k.color)
|
for (float c : color)
|
||||||
mix(std::hash<std::uint32_t>{}(std::bit_cast<std::uint32_t>(c)));
|
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;
|
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 {
|
struct ShapedRun {
|
||||||
std::vector<GlyphItem> glyphs; // origin-relative
|
std::vector<GlyphItem> glyphs; // origin-relative
|
||||||
|
|
@ -367,7 +405,7 @@ export namespace Crafter {
|
||||||
// would grow this without bound; on overflow we drop everything and
|
// would grow this without bound; on overflow we drop everything and
|
||||||
// rebuild (correctness is unaffected, only the hit rate after a flush).
|
// rebuild (correctness is unaffected, only the hit rate after a flush).
|
||||||
static constexpr std::size_t kMaxShapedRuns = 8192;
|
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 outImageSlot_;
|
||||||
ImageSlot fontAtlasImageSlot_;
|
ImageSlot fontAtlasImageSlot_;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue