perf(ui): 缓存卡片阴影、TextBlock 字体格式和候选列表文字度量

- Card:每帧为每道阴影新建离屏位图、画刷和高斯模糊特效。改为把全部阴影
  合成到一张位图,按尺寸、圆角、DPI、阴影参数和 render target 复用;
  尺寸不变的重绘(选中项移动、译文与云候选补齐)直接贴图。合成失败时仍
  走原来的逐帧绘制。
- TextBlock:每次测量都新建 IDWriteTextFormat,自定义回退字体还要把系统
  回退表整份复制进新 builder。格式与文字、光标无关,改为只在字体、字号、
  对齐或回退字体变化时重建。
- CandidateList:一帧里会测量多次(自然尺寸、保留尺寸、按最终宽度排版),
  每次都为每段文字新建 DirectWrite layout,一页九项每键约一两百个。文字
  宽高只取决于文字、字号、宽度和字体,改为记忆化,字体或外观变化时清空,
  超过上限整体重来。空的辅助码和译文不再创建 layout。
This commit is contained in:
fanlusky committed 2026-09-27 15:03:16 +08:00
1 parent f87809cf59
commit 7bbb9042fc
4 files changed
+263 -42

No files matched your search

+11
View File
@@ -5,6 +5,7 @@
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <wrl/client.h>
@@ -630,10 +631,20 @@ class CandidateList : public Visual
size_t HitTestItem(const PointF &point) const;
float EstimateTextWidth(const std::wstring &text, float fontSize) const;
RectF ItemRect(size_t index) const;
const std::wstring &ResolvedFontFamily() const;
// Results of EstimateTextWidth / MeasureTextHeight. A frame measures the
// list several times (natural size, sticky size, arrange at the final
// width) and each measurement used to build a fresh DirectWrite layout per
// string; the metrics depend only on the text, size, width and font, so
// they are memoized until the font changes.
bool LookupTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width, float &value) const;
void StoreTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width, float value) const;
std::vector<Item> items_;
std::vector<ItemLayoutCache> layoutCache_;
std::vector<ItemGeometry> itemGeometry_;
mutable std::unordered_map<std::wstring, float> textMetricCache_;
mutable std::wstring textMetricFamily_;
float layoutWidth_ = 0.0f;
Appearance appearance_{};
Orientation orientation_ = Orientation::Vertical;
+24
View File
@@ -368,6 +368,9 @@ class Card : public Panel
void Render(DeviceResources &deviceResources) override;
private:
void RenderShadow(ID2D1RenderTarget *target);
bool RenderCachedShadow(ID2D1RenderTarget *target, const ShadowPass *passes, size_t count);
Brush brush_;
float padding_ = 0.0f;
float shadowScale_ = 1.0f;
@@ -375,6 +378,21 @@ class Card : public Panel
std::vector<ShadowPass> shadowPasses_;
bool shadowEnabled_ = true;
SizeF childSize_ = {};
// All blur passes composed into one bitmap. Rebuilding the blur inputs and
// effects on every frame dominated repaints that do not change the card's
// size (selection moves, late candidate enrichment); the bitmap is reused
// while the key below matches and belongs to cachedShadowTarget_.
Microsoft::WRL::ComPtr<ID2D1RenderTarget> cachedShadowTarget_;
Microsoft::WRL::ComPtr<ID2D1Bitmap> cachedShadowBitmap_;
SizeF cachedShadowSize_ = {};
float cachedShadowRadius_ = 0.0f;
float cachedShadowScale_ = 0.0f;
float cachedShadowOpacity_ = 0.0f;
float cachedShadowDpiX_ = 0.0f;
float cachedShadowDpiY_ = 0.0f;
float cachedShadowPad_ = 0.0f;
std::vector<ShadowPass> cachedShadowPasses_;
};
class TextBlock : public Visual
@@ -399,6 +417,7 @@ class TextBlock : public Visual
private:
void InvalidateTextLayoutCache();
void InvalidateTextFormatCache();
std::wstring text_;
float fontSize_ = 16.0f;
@@ -417,6 +436,11 @@ class TextBlock : public Visual
std::wstring cachedFontFamily_;
float cachedLayoutWidth_ = -1.0f;
Microsoft::WRL::ComPtr<IDWriteTextLayout> cachedTextLayout_;
// The format (family, size, weight, alignment, fallback) outlives text and
// caret changes; building it, and a custom fallback chain in particular,
// cost more than the layout itself on every keystroke.
std::wstring cachedFormatFamily_;
Microsoft::WRL::ComPtr<IDWriteTextFormat> cachedTextFormat_;
};
class Spacer : public Visual
+89 -13
View File
@@ -7,7 +7,9 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <d2d1_3.h>
#include <limits>
#include <utility>
@@ -2705,9 +2707,70 @@ void CandidateList::SetAppearance(Appearance appearance)
{
appearance_ = appearance;
InvalidateLayoutCache();
textMetricCache_.clear();
InvalidateMeasure();
}
const std::wstring &CandidateList::ResolvedFontFamily() const
{
const Theme &theme = ThemeManager::GetCurrent();
return appearance_.fontFamily.empty() ? theme.textInputFontFamily : appearance_.fontFamily;
}
namespace
{
std::wstring TextMetricKey(wchar_t kind, const std::wstring &text, float fontSize, float width)
{
uint32_t sizeBits = 0;
uint32_t widthBits = 0;
static_assert(sizeof(float) == sizeof(uint32_t));
std::memcpy(&sizeBits, &fontSize, sizeof(sizeBits));
std::memcpy(&widthBits, &width, sizeof(widthBits));
std::wstring key;
key.reserve(text.size() + 5);
key.push_back(kind);
key.push_back(static_cast<wchar_t>(sizeBits & 0xFFFF));
key.push_back(static_cast<wchar_t>(sizeBits >> 16));
key.push_back(static_cast<wchar_t>(widthBits & 0xFFFF));
key.push_back(static_cast<wchar_t>(widthBits >> 16));
key.append(text);
return key;
}
// Enough for several pages of candidates with labels, annotations and
// translations; past it the cache simply starts over.
constexpr size_t kMaxTextMetricEntries = 2048;
} // namespace
bool CandidateList::LookupTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width,
float &value) const
{
const std::wstring &family = ResolvedFontFamily();
if (textMetricFamily_ != family)
{
textMetricCache_.clear();
textMetricFamily_ = family;
return false;
}
const auto found = textMetricCache_.find(TextMetricKey(kind, text, fontSize, width));
if (found == textMetricCache_.end())
{
return false;
}
value = found->second;
return true;
}
void CandidateList::StoreTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width,
float value) const
{
if (textMetricCache_.size() >= kMaxTextMetricEntries)
{
textMetricCache_.clear();
}
textMetricCache_[TextMetricKey(kind, text, fontSize, width)] = value;
}
void CandidateList::SetOrientation(Orientation orientation)
{
orientation_ = orientation;
@@ -2732,10 +2795,14 @@ float CandidateList::EstimateTextWidth(const std::wstring &text, float fontSize)
return 0.0f;
}
float cached = 0.0f;
if (LookupTextMetric(L'w', text, fontSize, 0.0f, cached))
{
return cached;
}
IDWriteFactory *factory = GetSharedDWriteFactory();
const Theme &theme = ThemeManager::GetCurrent();
const std::wstring &fontFamily =
appearance_.fontFamily.empty() ? theme.textInputFontFamily : appearance_.fontFamily;
const std::wstring &fontFamily = ResolvedFontFamily();
ComPtr<IDWriteTextLayout> layout = CreateCachedTextLayout(
factory, fontFamily, text, fontSize, DWRITE_FONT_WEIGHT_NORMAL, 4096.0f, std::max(fontSize * 2.0f, 1.0f),
DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_PARAGRAPH_ALIGNMENT_CENTER, DWRITE_WORD_WRAPPING_NO_WRAP,
@@ -2754,22 +2821,29 @@ float CandidateList::EstimateTextWidth(const std::wstring &text, float fontSize)
layout->GetMetrics(&metrics);
DWRITE_OVERHANG_METRICS overhang = {};
layout->GetOverhangMetrics(&overhang);
return std::ceil(metrics.widthIncludingTrailingWhitespace + std::max(overhang.right, 0.0f) + 1.0f);
const float width = std::ceil(metrics.widthIncludingTrailingWhitespace + std::max(overhang.right, 0.0f) + 1.0f);
StoreTextMetric(L'w', text, fontSize, 0.0f, width);
return width;
}
float CandidateList::MeasureTextHeight(const std::wstring &text, float fontSize, float width) const
{
if (text.empty())
return 0.0f;
const Theme &theme = ThemeManager::GetCurrent();
const auto &family = appearance_.fontFamily.empty() ? theme.textInputFontFamily : appearance_.fontFamily;
const auto layout =
CreateCachedTextLayout(GetSharedDWriteFactory(), family, text, fontSize, DWRITE_FONT_WEIGHT_NORMAL, width,
65536.0f, DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_PARAGRAPH_ALIGNMENT_NEAR,
DWRITE_WORD_WRAPPING_WRAP, appearance_.fallbackFontFamilies);
float cached = 0.0f;
if (LookupTextMetric(L'h', text, fontSize, width, cached))
return cached;
const auto layout = CreateCachedTextLayout(GetSharedDWriteFactory(), ResolvedFontFamily(), text, fontSize,
DWRITE_FONT_WEIGHT_NORMAL, width, 65536.0f,
DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_PARAGRAPH_ALIGNMENT_NEAR,
DWRITE_WORD_WRAPPING_WRAP, appearance_.fallbackFontFamilies);
DWRITE_TEXT_METRICS metrics{};
if (layout && SUCCEEDED(layout->GetMetrics(&metrics)))
return std::ceil(metrics.height);
{
const float height = std::ceil(metrics.height);
StoreTextMetric(L'h', text, fontSize, width, height);
return height;
}
return std::ceil(EstimateTextWidth(text, fontSize) / width) * fontSize * 1.25f;
}
@@ -3017,7 +3091,8 @@ void CandidateList::Render(DeviceResources &deviceResources)
cache.textWidth = textRect.width;
cache.fontFamily = fontFamily;
}
if (cache.fontFamily != fontFamily || cache.annotationWidth != annotationRect.width)
if (!items_[index].annotation.empty() &&
(cache.fontFamily != fontFamily || cache.annotationWidth != annotationRect.width))
{
cache.annotationLayout = CreateCachedTextLayout(
factory, fontFamily, items_[index].annotation, appearance_.annotationFontSize,
@@ -3027,7 +3102,8 @@ void CandidateList::Render(DeviceResources &deviceResources)
cache.annotationWidth = annotationRect.width;
cache.fontFamily = fontFamily;
}
if (cache.fontFamily != fontFamily || cache.translationWidth != translationRect.width)
if (!items_[index].translation.empty() &&
(cache.fontFamily != fontFamily || cache.translationWidth != translationRect.width))
{
cache.translationLayout = CreateCachedTextLayout(
factory, fontFamily, items_[index].translation, translationFontSize, DWRITE_FONT_WEIGHT_NORMAL,
+139 -29
View File
@@ -1912,15 +1912,7 @@ void Card::Render(DeviceResources &deviceResources)
if (shadowEnabled_)
{
if (shadowPasses_.empty())
{
DrawWin11WindowShadow(target, bounds_, brush_.radiusX, shadowScale_, shadowOpacity_);
}
else
{
DrawGaussianPasses(target, bounds_, brush_.radiusX, shadowPasses_.data(), shadowPasses_.size(),
shadowScale_, shadowOpacity_);
}
RenderShadow(target);
}
const auto roundedRect =
@@ -1935,6 +1927,110 @@ void Card::Render(DeviceResources &deviceResources)
}
}
void Card::RenderShadow(ID2D1RenderTarget *target)
{
const bool builtIn = shadowPasses_.empty();
const ShadowPass *passes = builtIn ? kMistShadowPasses : shadowPasses_.data();
const size_t count = builtIn ? std::size(kMistShadowPasses) : shadowPasses_.size();
if (RenderCachedShadow(target, passes, count))
{
return;
}
// Uncached path, unchanged from before the cache existed.
cachedShadowBitmap_.Reset();
cachedShadowTarget_.Reset();
if (builtIn)
{
DrawWin11WindowShadow(target, bounds_, brush_.radiusX, shadowScale_, shadowOpacity_);
}
else
{
DrawGaussianPasses(target, bounds_, brush_.radiusX, passes, count, shadowScale_, shadowOpacity_);
}
}
bool Card::RenderCachedShadow(ID2D1RenderTarget *target, const ShadowPass *passes, size_t count)
{
if (bounds_.width <= 0.0f || bounds_.height <= 0.0f || count == 0)
{
return false;
}
float dpiX = 96.0f;
float dpiY = 96.0f;
target->GetDpi(&dpiX, &dpiY);
const bool hit = cachedShadowBitmap_ && cachedShadowTarget_.Get() == target &&
IsSameSize(cachedShadowSize_, {bounds_.width, bounds_.height}) &&
cachedShadowRadius_ == brush_.radiusX && cachedShadowScale_ == shadowScale_ &&
cachedShadowOpacity_ == shadowOpacity_ && cachedShadowDpiX_ == dpiX && cachedShadowDpiY_ == dpiY &&
cachedShadowPasses_.size() == count &&
std::equal(cachedShadowPasses_.begin(), cachedShadowPasses_.end(), passes);
if (!hit)
{
cachedShadowBitmap_.Reset();
cachedShadowTarget_.Reset();
const float s = std::max(shadowScale_, 0.15f);
float pad = 0.0f;
for (size_t i = 0; i < count; ++i)
{
// Same margin DrawGaussianShadowPass gives each pass, plus that pass's offset.
const float passPad = passes[i].sigma * s * 3.0f + 4.0f +
std::max(std::fabs(passes[i].offsetX), std::fabs(passes[i].offsetY)) * s;
pad = std::max(pad, passPad);
}
pad = std::ceil(pad);
ComPtr<ID2D1BitmapRenderTarget> composed;
if (FAILED(target->CreateCompatibleRenderTarget({bounds_.width + pad * 2.0f, bounds_.height + pad * 2.0f},
composed.GetAddressOf())))
{
return false;
}
ComPtr<ID2D1DeviceContext> composedDc;
if (FAILED(composed.As(&composedDc)))
{
return false;
}
composed->BeginDraw();
composed->Clear(D2D1::ColorF(0, 0.0f));
const RectF local = {pad, pad, bounds_.width, bounds_.height};
bool drew = false;
for (size_t i = 0; i < count; ++i)
{
if (DrawGaussianShadowPass(composed.Get(), composedDc.Get(), local, brush_.radiusX, passes[i].sigma * s,
passes[i].alpha * shadowOpacity_, passes[i].offsetX * s, passes[i].offsetY * s))
{
drew = true;
}
}
ComPtr<ID2D1Bitmap> bitmap;
if (FAILED(composed->EndDraw()) || !drew || FAILED(composed->GetBitmap(bitmap.GetAddressOf())))
{
return false;
}
cachedShadowTarget_ = target;
cachedShadowBitmap_ = bitmap;
cachedShadowSize_ = {bounds_.width, bounds_.height};
cachedShadowRadius_ = brush_.radiusX;
cachedShadowScale_ = shadowScale_;
cachedShadowOpacity_ = shadowOpacity_;
cachedShadowDpiX_ = dpiX;
cachedShadowDpiY_ = dpiY;
cachedShadowPad_ = pad;
cachedShadowPasses_.assign(passes, passes + count);
}
const float pad = cachedShadowPad_;
target->DrawBitmap(cachedShadowBitmap_.Get(),
D2D1::RectF(bounds_.x - pad, bounds_.y - pad, bounds_.x + bounds_.width + pad,
bounds_.y + bounds_.height + pad),
1.0f, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR);
return true;
}
TextBlock::TextBlock(std::wstring text, float fontSize, D2D1_COLOR_F color, bool bold)
: text_(std::move(text)), fontSize_(fontSize), color_(color), caretColor_(color), bold_(bold)
{
@@ -1960,7 +2056,7 @@ void TextBlock::SetFontSize(float fontSize)
return;
}
fontSize_ = fontSize;
InvalidateTextLayoutCache();
InvalidateTextFormatCache();
InvalidateMeasure();
}
@@ -1972,7 +2068,7 @@ void TextBlock::SetTextAlignment(DWRITE_TEXT_ALIGNMENT alignment)
}
textAlignment_ = alignment;
InvalidateTextLayoutCache();
InvalidateTextFormatCache();
InvalidateVisual();
}
@@ -1984,7 +2080,7 @@ void TextBlock::SetFontFamily(std::wstring fontFamily)
}
fontFamilyOverride_ = std::move(fontFamily);
InvalidateTextLayoutCache();
InvalidateTextFormatCache();
InvalidateMeasure();
}
@@ -1995,13 +2091,20 @@ void TextBlock::InvalidateTextLayoutCache()
cachedLayoutWidth_ = -1.0f;
}
void TextBlock::InvalidateTextFormatCache()
{
cachedTextFormat_.Reset();
cachedFormatFamily_.clear();
InvalidateTextLayoutCache();
}
void TextBlock::SetFallbackFontFamilies(std::vector<std::wstring> families)
{
if (hasCustomFontFallback_ && fallbackFontFamilies_ == families)
return;
hasCustomFontFallback_ = true;
fallbackFontFamilies_ = std::move(families);
InvalidateTextLayoutCache();
InvalidateTextFormatCache();
InvalidateMeasure();
}
@@ -2066,26 +2169,33 @@ SizeF TextBlock::Measure(const SizeF &availableSize)
const bool needsLayout = !cachedTextLayout_ || cachedLayoutWidth_ != maxWidth || cachedFontFamily_ != fontFamily;
if (needsLayout)
{
ComPtr<IDWriteTextFormat> format;
if (FAILED(dwriteFactory->CreateTextFormat(
fontFamily.c_str(), nullptr, bold_ ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, fontSize_, L"", format.GetAddressOf())))
if (!cachedTextFormat_ || cachedFormatFamily_ != fontFamily)
{
ComPtr<IDWriteTextFormat> format;
if (FAILED(dwriteFactory->CreateTextFormat(
UiFontFallbackFamily(), nullptr, bold_ ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL,
fontFamily.c_str(), nullptr, bold_ ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, fontSize_, L"", format.GetAddressOf())))
{
measured_ = {maxWidth, fontSize_ + textLayoutPadding_.top + textLayoutPadding_.bottom};
return measured_;
if (FAILED(dwriteFactory->CreateTextFormat(
UiFontFallbackFamily(), nullptr,
bold_ ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL, fontSize_, L"", format.GetAddressOf())))
{
measured_ = {maxWidth, fontSize_ + textLayoutPadding_.top + textLayoutPadding_.bottom};
return measured_;
}
}
}
format->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP);
format->SetTextAlignment(textAlignment_);
if (hasCustomFontFallback_)
ApplyFontFallback(dwriteFactory, format.Get(), fallbackFontFamilies_);
else
ApplyUiFontFallback(dwriteFactory, format.Get());
format->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP);
format->SetTextAlignment(textAlignment_);
if (hasCustomFontFallback_)
ApplyFontFallback(dwriteFactory, format.Get(), fallbackFontFamilies_);
else
ApplyUiFontFallback(dwriteFactory, format.Get());
cachedTextFormat_ = format;
cachedFormatFamily_ = fontFamily;
}
IDWriteTextFormat *format = cachedTextFormat_.Get();
const UINT32 caretPos = static_cast<UINT32>((std::min)(caretIndex_, text_.size()));
const bool insertSlot = showCaret_ && caretPos < text_.size();
@@ -2095,8 +2205,8 @@ SizeF TextBlock::Measure(const SizeF &availableSize)
layoutText.insert(layoutText.begin() + static_cast<std::ptrdiff_t>(caretPos), kCaretSlotChar);
}
if (FAILED(dwriteFactory->CreateTextLayout(layoutText.c_str(), static_cast<UINT32>(layoutText.size()),
format.Get(), maxWidth, std::numeric_limits<float>::max(),
if (FAILED(dwriteFactory->CreateTextLayout(layoutText.c_str(), static_cast<UINT32>(layoutText.size()), format,
maxWidth, std::numeric_limits<float>::max(),
cachedTextLayout_.ReleaseAndGetAddressOf())))
{
measured_ = {maxWidth, fontSize_ + textLayoutPadding_.top + textLayoutPadding_.bottom};