mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-13 01:29:41 +01:00
Merge pull request #22719 from hrydgard/text-measure-cache
TextDrawer: Cache wrapped text, hash the string caches
This commit is contained in:
5 files changed
+185
-50
No files matched your search
@@ -33,6 +33,10 @@ struct FontStyle {
|
||||
FontStyleFlags flags = FontStyleFlags::Default;
|
||||
};
|
||||
|
||||
inline constexpr bool operator==(const FontStyle &a, const FontStyle &b) {
|
||||
return a.family == b.family && a.sizePts == b.sizePts && a.flags == b.flags;
|
||||
}
|
||||
|
||||
inline constexpr bool operator<(const FontStyle &a, const FontStyle &b) {
|
||||
if (a.family != b.family)
|
||||
return a.family < b.family;
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "ext/xxhash.h"
|
||||
#include "Common/System/Display.h"
|
||||
#include "Common/GPU/thin3d.h"
|
||||
#include "Common/Data/Hash/Hash.h"
|
||||
@@ -41,6 +44,15 @@ static bool IsRenderDocLoaded() {
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t TextCacheKeyHash::operator()(const TextCacheKey &key) const {
|
||||
uint32_t maxWidthBits, scaleBits;
|
||||
memcpy(&maxWidthBits, &key.maxWidth, sizeof(maxWidthBits));
|
||||
memcpy(&scaleBits, &key.scale, sizeof(scaleBits));
|
||||
uint64_t seed = ((uint64_t)key.style.sizePts << 48) | ((uint64_t)key.style.family << 40) | ((uint64_t)key.style.flags << 32) | (uint32_t)key.flags;
|
||||
seed ^= (((uint64_t)maxWidthBits << 32) | scaleBits) * 0x9E3779B97F4A7C15ULL;
|
||||
return (size_t)XXH3_64bits_withSeed(key.text.data(), key.text.size(), seed);
|
||||
}
|
||||
|
||||
TextDrawer::TextDrawer(Draw::DrawContext *draw) : draw_(draw) {
|
||||
// These probably shouldn't be state.
|
||||
dpiScale_ = CalculateDPIScale();
|
||||
@@ -52,9 +64,25 @@ float TextDrawerWordWrapper::MeasureWidth(std::string_view str) {
|
||||
return w;
|
||||
}
|
||||
|
||||
void TextDrawer::WrapString(std::string &out, std::string_view str, float maxW, int flags) {
|
||||
TextDrawerWordWrapper wrapper(this, str, maxW, flags);
|
||||
out = wrapper.Wrapped();
|
||||
const TextCacheKey &TextDrawer::LookupKey(std::string_view str, int flags, float maxWidth, float scale) {
|
||||
lookupKey_.text.assign(str.data(), str.size());
|
||||
lookupKey_.style = fontStyle_;
|
||||
lookupKey_.flags = flags;
|
||||
lookupKey_.maxWidth = maxWidth;
|
||||
lookupKey_.scale = scale;
|
||||
return lookupKey_;
|
||||
}
|
||||
|
||||
const std::string &TextDrawer::WrapString(std::string_view str, float maxW, int flags) {
|
||||
// The wrapper measures in scaled units, so the result depends on the scale too.
|
||||
auto [iter, inserted] = wrapCache_.try_emplace(LookupKey(str, flags, maxW, fontScaleX_));
|
||||
TextWrapEntry &entry = iter->second;
|
||||
entry.lastUsedFrame = frameCount_;
|
||||
if (inserted) {
|
||||
TextDrawerWordWrapper wrapper(this, str, maxW, flags);
|
||||
entry.wrapped = wrapper.Wrapped();
|
||||
}
|
||||
return entry.wrapped;
|
||||
}
|
||||
|
||||
void TextDrawer::SetFontScale(float xscale, float yscale) {
|
||||
@@ -74,13 +102,12 @@ void TextDrawer::DrawString(DrawBuffer &target, std::string_view str, float x, f
|
||||
return;
|
||||
}
|
||||
|
||||
const CacheKeyType key{ std::string(str), fontStyle_ };
|
||||
target.Flush(true);
|
||||
|
||||
TextStringEntry *entry;
|
||||
|
||||
auto iter = cache_.find(key);
|
||||
if (iter != cache_.end()) {
|
||||
auto [iter, inserted] = cache_.try_emplace(LookupKey(str));
|
||||
if (!inserted) {
|
||||
entry = iter->second.get();
|
||||
entry->lastUsedFrame = frameCount_;
|
||||
if (!entry->texture) {
|
||||
@@ -104,15 +131,15 @@ void TextDrawer::DrawString(DrawBuffer &target, std::string_view str, float x, f
|
||||
texFormat = Draw::DataFormat::R8G8B8A8_UNORM;
|
||||
}
|
||||
|
||||
entry = new TextStringEntry(frameCount_);
|
||||
iter->second = std::make_unique<TextStringEntry>(frameCount_);
|
||||
entry = iter->second.get();
|
||||
|
||||
// Convert the bitmap to a Thin3D compatible array of 16-bit pixels. Can't use a single channel format
|
||||
// because we need white. Well, we could using swizzle, but not all our backends support that.
|
||||
TextureDesc desc{};
|
||||
std::vector<uint8_t> bitmapData;
|
||||
if (!DrawStringBitmap(bitmapData, *entry, texFormat, str, align, emoji)) {
|
||||
// Nothing drawn. Store that fact in the cache.
|
||||
cache_[key] = std::unique_ptr<TextStringEntry>(entry);
|
||||
// Nothing drawn. The entry without a texture records that fact.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,7 +154,6 @@ void TextDrawer::DrawString(DrawBuffer &target, std::string_view str, float x, f
|
||||
desc.tag = "TextDrawer";
|
||||
desc.swizzle = texFormat == Draw::DataFormat::R8_UNORM ? Draw::TextureSwizzle::R8_AS_PREMUL_ALPHA : Draw::TextureSwizzle::DEFAULT,
|
||||
entry->texture = draw_->CreateTexture(desc);
|
||||
cache_[key] = std::unique_ptr<TextStringEntry>(entry);
|
||||
}
|
||||
|
||||
_dbg_assert_(entry->texture);
|
||||
@@ -156,26 +182,20 @@ void TextDrawer::MeasureString(std::string_view str, float *w, float *h) {
|
||||
str = str.substr(0, MAX_TEXT_LENGTH);
|
||||
}
|
||||
|
||||
const CacheKeyType key{std::string(str), fontStyle_};
|
||||
|
||||
TextMeasureEntry *entry;
|
||||
auto iter = sizeCache_.find(key);
|
||||
if (iter != sizeCache_.end()) {
|
||||
entry = iter->second.get();
|
||||
} else {
|
||||
entry = new TextMeasureEntry();
|
||||
auto [iter, inserted] = sizeCache_.try_emplace(LookupKey(str));
|
||||
TextMeasureEntry &entry = iter->second;
|
||||
if (inserted) {
|
||||
float extW, extH;
|
||||
MeasureStringInternal(str, &extW, &extH);
|
||||
entry->width = extW;
|
||||
entry->height = extH;
|
||||
entry.width = extW;
|
||||
entry.height = extH;
|
||||
// Hm, use the old calculation?
|
||||
// int h = i == lines.size() - 1 ? entry->height : metrics.tmHeight + metrics.tmExternalLeading;
|
||||
sizeCache_[key] = std::unique_ptr<TextMeasureEntry>(entry);
|
||||
}
|
||||
|
||||
entry->lastUsedFrame = frameCount_;
|
||||
*w = entry->width * fontScaleX_ * dpiScale_;
|
||||
*h = entry->height * fontScaleY_ * dpiScale_;
|
||||
entry.lastUsedFrame = frameCount_;
|
||||
*w = entry.width * fontScaleX_ * dpiScale_;
|
||||
*h = entry.height * fontScaleY_ * dpiScale_;
|
||||
}
|
||||
|
||||
void TextDrawer::MeasureStringRect(std::string_view str, float maxWidth, float *w, float *h, int align) {
|
||||
@@ -190,9 +210,7 @@ void TextDrawer::MeasureStringRect(std::string_view str, float maxWidth, float *
|
||||
MeasureString(str, &plainW, &plainH);
|
||||
|
||||
if (wrap && plainW > maxWidth) {
|
||||
std::string toMeasure = std::string(str);
|
||||
WrapString(toMeasure, toMeasure, maxWidth, wrap);
|
||||
MeasureString(toMeasure, w, h);
|
||||
MeasureString(WrapString(str, maxWidth, wrap), w, h);
|
||||
} else {
|
||||
*w = plainW;
|
||||
*h = plainH;
|
||||
@@ -221,12 +239,8 @@ void TextDrawer::DrawStringRect(DrawBuffer &target, std::string_view str, const
|
||||
str = str.substr(0, MAX_TEXT_LENGTH);
|
||||
}
|
||||
|
||||
std::string toDraw(str);
|
||||
int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT);
|
||||
if (wrap) {
|
||||
WrapString(toDraw, str, bounds.w, wrap);
|
||||
}
|
||||
DrawString(target, toDraw, x, y, color, align);
|
||||
const int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT);
|
||||
DrawString(target, wrap ? std::string_view(WrapString(str, bounds.w, wrap)) : str, x, y, color, align);
|
||||
}
|
||||
|
||||
bool TextDrawer::DrawStringBitmapRect(std::vector<uint8_t> &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, std::string_view str, const Bounds &bounds, int align, bool fullColor) {
|
||||
@@ -235,12 +249,8 @@ bool TextDrawer::DrawStringBitmapRect(std::vector<uint8_t> &bitmapData, TextStri
|
||||
str = str.substr(0, MAX_TEXT_LENGTH);
|
||||
}
|
||||
|
||||
std::string toDraw(str);
|
||||
int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT);
|
||||
if (wrap) {
|
||||
WrapString(toDraw, str, bounds.w, wrap);
|
||||
}
|
||||
return DrawStringBitmap(bitmapData, entry, texFormat, toDraw, align, fullColor);
|
||||
const int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT);
|
||||
return DrawStringBitmap(bitmapData, entry, texFormat, wrap ? std::string_view(WrapString(str, bounds.w, wrap)) : str, align, fullColor);
|
||||
}
|
||||
|
||||
void TextDrawer::ClearCache() {
|
||||
@@ -250,6 +260,7 @@ void TextDrawer::ClearCache() {
|
||||
}
|
||||
cache_.clear();
|
||||
sizeCache_.clear();
|
||||
wrapCache_.clear();
|
||||
fontStyle_ = {};
|
||||
}
|
||||
|
||||
@@ -277,19 +288,29 @@ void TextDrawer::OncePerFrame() {
|
||||
}
|
||||
|
||||
for (auto iter = sizeCache_.begin(); iter != sizeCache_.end(); ) {
|
||||
if (frameCount_ - iter->second->lastUsedFrame > 100) {
|
||||
if (frameCount_ - iter->second.lastUsedFrame > 100) {
|
||||
iter = sizeCache_.erase(iter);
|
||||
} else {
|
||||
iter++;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto iter = wrapCache_.begin(); iter != wrapCache_.end(); ) {
|
||||
if (frameCount_ - iter->second.lastUsedFrame > 100) {
|
||||
iter = wrapCache_.erase(iter);
|
||||
} else {
|
||||
iter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t TextDrawer::GetCacheDataSize() const {
|
||||
size_t sz = 0;
|
||||
for (const auto &iter : cache_) {
|
||||
sz += iter.second->texture->DataSize();
|
||||
if (iter.second->texture) {
|
||||
sz += iter.second->texture->DataSize();
|
||||
}
|
||||
}
|
||||
return sz;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "Common/Data/Text/WrapText.h"
|
||||
#include "Common/Render/DrawBuffer.h"
|
||||
@@ -41,6 +42,28 @@ struct TextMeasureEntry {
|
||||
int lastUsedFrame;
|
||||
};
|
||||
|
||||
struct TextWrapEntry {
|
||||
std::string wrapped;
|
||||
int lastUsedFrame;
|
||||
};
|
||||
|
||||
struct TextCacheKey {
|
||||
std::string text;
|
||||
FontStyle style;
|
||||
int flags = 0;
|
||||
// Wrapping only.
|
||||
float maxWidth = 0.0f;
|
||||
float scale = 0.0f;
|
||||
|
||||
bool operator==(const TextCacheKey &other) const {
|
||||
return text == other.text && style == other.style && flags == other.flags && maxWidth == other.maxWidth && scale == other.scale;
|
||||
}
|
||||
};
|
||||
|
||||
struct TextCacheKeyHash {
|
||||
size_t operator()(const TextCacheKey &key) const;
|
||||
};
|
||||
|
||||
class TextDrawer {
|
||||
public:
|
||||
virtual ~TextDrawer() = default;
|
||||
@@ -85,7 +108,8 @@ protected:
|
||||
virtual bool SupportsColorEmoji() const = 0;
|
||||
virtual void ClearFonts() = 0;
|
||||
|
||||
void WrapString(std::string &out, std::string_view str, float maxWidth, int flags);
|
||||
// Wraps and/or ellipsizes, caching the result. The reference stays valid until the next OncePerFrame.
|
||||
const std::string &WrapString(std::string_view str, float maxWidth, int flags);
|
||||
|
||||
// v has the 8-bit alpha.
|
||||
static u16 AlphaToPremul4444(u32 v) {
|
||||
@@ -114,8 +138,6 @@ protected:
|
||||
return (a << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
typedef std::pair<std::string, FontStyle> CacheKeyType;
|
||||
|
||||
Draw::DrawContext *draw_;
|
||||
|
||||
int frameCount_ = 0;
|
||||
@@ -128,8 +150,14 @@ protected:
|
||||
// We will clamp strings to this length to avoid various degenenerate behaviors.k
|
||||
static constexpr size_t MAX_TEXT_LENGTH = 16384;
|
||||
|
||||
std::map<CacheKeyType, std::unique_ptr<TextStringEntry>> cache_;
|
||||
std::map<CacheKeyType, std::unique_ptr<TextMeasureEntry>> sizeCache_;
|
||||
// Fills in and returns lookupKey_, which reuses its string buffer, so a cache hit doesn't allocate.
|
||||
const TextCacheKey &LookupKey(std::string_view str, int flags = 0, float maxWidth = 0.0f, float scale = 0.0f);
|
||||
|
||||
TextCacheKey lookupKey_;
|
||||
|
||||
std::unordered_map<TextCacheKey, std::unique_ptr<TextStringEntry>, TextCacheKeyHash> cache_;
|
||||
std::unordered_map<TextCacheKey, TextMeasureEntry, TextCacheKeyHash> sizeCache_;
|
||||
std::unordered_map<TextCacheKey, TextWrapEntry, TextCacheKeyHash> wrapCache_;
|
||||
};
|
||||
|
||||
class TextDrawerWordWrapper : public WordWrapper {
|
||||
|
||||
+16
-4
@@ -273,10 +273,16 @@ void UIContext::MeasureText(const FontStyle &style, float scaleX, float scaleY,
|
||||
Draw()->SetFontScale(scaleX * sizeFactor, scaleY * sizeFactor);
|
||||
Draw()->MeasureText(AtlasFontFromStyle(style), str, x, y);
|
||||
} else {
|
||||
textDrawer_->SetOrCreateFont(style);
|
||||
// Switching fonts costs two lookups, and most measuring is done in the current style.
|
||||
const bool switchFont = !(style == *fontStyle_);
|
||||
if (switchFont) {
|
||||
textDrawer_->SetOrCreateFont(style);
|
||||
}
|
||||
textDrawer_->SetFontScale(scaleX, scaleY);
|
||||
textDrawer_->MeasureString(str, x, y);
|
||||
textDrawer_->SetOrCreateFont(*fontStyle_);
|
||||
if (switchFont) {
|
||||
textDrawer_->SetOrCreateFont(*fontStyle_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,10 +293,16 @@ void UIContext::MeasureTextRect(const FontStyle &style, float scaleX, float scal
|
||||
Draw()->SetFontScale(scaleX * sizeFactor, scaleY * sizeFactor);
|
||||
Draw()->MeasureTextRect(AtlasFontFromStyle(style), str, maxWidth, x, y, align);
|
||||
} else {
|
||||
textDrawer_->SetOrCreateFont(style);
|
||||
// Switching fonts costs two lookups, and most measuring is done in the current style.
|
||||
const bool switchFont = !(style == *fontStyle_);
|
||||
if (switchFont) {
|
||||
textDrawer_->SetOrCreateFont(style);
|
||||
}
|
||||
textDrawer_->SetFontScale(scaleX, scaleY);
|
||||
textDrawer_->MeasureStringRect(str, maxWidth, x, y, align);
|
||||
textDrawer_->SetOrCreateFont(*fontStyle_);
|
||||
if (switchFont) {
|
||||
textDrawer_->SetOrCreateFont(*fontStyle_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
|
||||
#include "Common/File/AndroidContentURI.h"
|
||||
#include "Common/System/Request.h"
|
||||
#include "Common/Render/Text/draw_text.h"
|
||||
|
||||
#include "unittest/JitHarness.h"
|
||||
#include "unittest/TestVertexJit.h"
|
||||
@@ -3475,6 +3476,74 @@ bool TestFatShortNames() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A TextDrawer whose "font" is ten units per byte and twenty per line, so we can test the
|
||||
// shared caching and wrapping logic without a platform font backend.
|
||||
class FakeTextDrawer : public TextDrawer {
|
||||
public:
|
||||
FakeTextDrawer() : TextDrawer(nullptr) {}
|
||||
void SetOrCreateFont(const FontStyle &style) override { fontStyle_ = style; }
|
||||
bool DrawStringBitmap(std::vector<uint8_t> &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, std::string_view str, int align, bool fullColor) override { return false; }
|
||||
|
||||
int measureCalls = 0;
|
||||
|
||||
protected:
|
||||
void MeasureStringInternal(std::string_view str, float *w, float *h) override {
|
||||
measureCalls++;
|
||||
std::vector<std::string_view> lines;
|
||||
SplitString(str, '\n', lines);
|
||||
int maxLen = 0;
|
||||
for (auto line : lines) {
|
||||
maxLen = std::max(maxLen, (int)line.size());
|
||||
}
|
||||
*w = maxLen * 10.0f;
|
||||
*h = lines.size() * 20.0f;
|
||||
}
|
||||
bool SupportsColorEmoji() const override { return false; }
|
||||
void ClearFonts() override {}
|
||||
};
|
||||
|
||||
bool TestTextDrawerCache() {
|
||||
FakeTextDrawer drawer;
|
||||
drawer.SetForcedDPIScale(1.0f);
|
||||
drawer.SetOrCreateFont(FontStyle(FontFamily::SansSerif, 20, FontStyleFlags::Default));
|
||||
|
||||
float w, h;
|
||||
drawer.MeasureString("hello", &w, &h);
|
||||
EXPECT_EQ_FLOAT(w, 50.0f);
|
||||
EXPECT_EQ_FLOAT(h, 20.0f);
|
||||
drawer.MeasureString("hello", &w, &h);
|
||||
EXPECT_EQ_INT(drawer.measureCalls, 1);
|
||||
|
||||
// A different style is a different cache entry.
|
||||
drawer.SetOrCreateFont(FontStyle(FontFamily::SansSerif, 30, FontStyleFlags::Default));
|
||||
drawer.MeasureString("hello", &w, &h);
|
||||
EXPECT_EQ_INT(drawer.measureCalls, 2);
|
||||
|
||||
const std::string_view para = "The quick brown fox jumps over the lazy dog, and then it does it again, and again, until the dog finally gets up and leaves.";
|
||||
drawer.MeasureStringRect(para, 200.0f, &w, &h, FLAG_WRAP_TEXT);
|
||||
// The trailing space of a wrapped line is measured too.
|
||||
EXPECT_TRUE(w <= 210.0f);
|
||||
EXPECT_TRUE(h > 20.0f);
|
||||
float w2, h2;
|
||||
int callsBefore = drawer.measureCalls;
|
||||
drawer.MeasureStringRect(para, 200.0f, &w2, &h2, FLAG_WRAP_TEXT);
|
||||
EXPECT_EQ_FLOAT(w, w2);
|
||||
EXPECT_EQ_FLOAT(h, h2);
|
||||
EXPECT_EQ_INT(drawer.measureCalls, callsBefore);
|
||||
|
||||
// Scaling the font changes where the text wraps.
|
||||
drawer.SetFontScale(0.5f, 0.5f);
|
||||
drawer.MeasureStringRect(para, 200.0f, &w2, &h2, FLAG_WRAP_TEXT);
|
||||
EXPECT_TRUE(h2 < h * 0.5f);
|
||||
drawer.SetFontScale(1.0f, 1.0f);
|
||||
|
||||
drawer.MeasureStringRect(para, 200.0f, &w, &h, FLAG_ELLIPSIZE_TEXT);
|
||||
EXPECT_EQ_FLOAT(h, 20.0f);
|
||||
EXPECT_TRUE(w <= 200.0f);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tab/Shift+Tab focus navigation walks the view hierarchy in declaration order rather than by
|
||||
// geometry, so what it does is entirely determined by CollectTabOrder - which is worth pinning
|
||||
// down, since the interesting cases (nesting, hidden tabs, disabled items) are all structural.
|
||||
@@ -3786,6 +3855,7 @@ TestItem availableTests[] = {
|
||||
TEST_ITEM(TextureReplacer),
|
||||
TEST_ITEM(UITabOrder),
|
||||
TEST_ITEM(UIFocusNavigation),
|
||||
TEST_ITEM(TextDrawerCache),
|
||||
TEST_ITEM(FatShortNames),
|
||||
TEST_ITEM(GEDumpGameID),
|
||||
TEST_ITEM(QueueBuf),
|
||||
|
||||
Reference in new issue
Block a user