修复悬停高亮变长方形色块/联动失效 + 信号列表动画卡顿真因(行位图缓存)
一、悬停高亮恢复"整条曲线发光"(上一轮裁成鼠标附近一段 ⇒ 满幅方波上就是个长方形色块) 光晕的语义是"整条曲线一起发光",对编码器这种满幅方波,一列包络本来就跨满网格高, 裁成 144px 宽 × 满高的窗口在屏幕上就是一个像选区框的色块(用户 2026-10-18 第二轮报告)。 修法(PlotViewPaint.cpp): - PaintSignalHaloFor 恢复**整条**几何(包络带 FillPath / 折线 DrawLines); - 新增 PaintSignalHaloCached:光晕几何只取决于「信号 + 视界 + 网格几何 + 样式」,**与鼠标无关** ⇒ 渲染一次进离屏位图,之后每帧只做一次 AlphaBlend(源位图预乘 alpha,用 256×256 查表); - 缓存键与 AcquirePolyline 同源,**不含网格屏幕位置**(滚动网格只按新位置贴同一张位图)。 210ms/帧(整条描边)→ 一次性 15ms 渲染 + 每帧 ~2ms 贴图。 halocheck 校验:悬停必命中(60/60 位置)、光晕贴合带宽(outsideBand=0)、不越出网格、 bbox 覆盖整条曲线(不是中段方块)、列表行悬停联动仍生效。 二、信号列表动画卡顿的三个真因(探针 panelstutter:129 个可见网格 + 反复"展开→切组复选框→收起") 1) 面板每帧重画 ~36 行 × ~0.35ms(十来个 GDI+ 原语)⇒ panel.Paint 24~35ms; 2) 动画帧里仍在预热网格图层:每帧重建 1 个网格图层,2500 万采样信号 10~30ms ⇒ 帧被拉长; 3) 勾选分组会把上百个**离屏**网格排进待重画队列,随后每帧补 1 个 ⇒ 数秒持续掉帧。 修法: 1) 行位图缓存(SignalPanel::PaintRowCached):按"行的可见内容"做键,整行渲染进离屏 DIB, 命中时一次 BitBlt。panel.Paint 24~35ms → 5~6ms。 ⚠️ 必须 DIB + 内存 DC(gd::Bitmap 上 GDI 会关闭 ClearType,整列文字退化); ⚠️ 行内几何在 RowRect 里平移(GDI 文本不认 GDI+ 世界变换,否则文字画到小位图之外); ⚠️ 渲染到行位图时不按列表矩形裁剪,裁剪放到贴图时的 DC(GDI 不认 GDI+ 裁剪, 必须自己 SaveDC + IntersectClipRect,折起动画时行不许画到组名上); ⚠️ 缓存上限 192 行(≈10MB)。静态画面逐像素一致(面板区域 54/275200 不同)。 2) 新增 PlotView::SetAnimationBusy:UI 动画期间不预热网格图层(队列保留,动画结束后继续)。 3) 待重画队列只保留可视 + 预取带内的网格;RollWideBuffer 不再登记离屏网格。 实测:展开 p50 35ms → 13ms、收起 33ms → 13ms、动画帧内图层重建 56 次 → 0 次。 三、试过并放弃(避免以后重走) - ExtTextOutW 替代 DrawTextW:快 2~4 倍但光栅化不同(面板 10% 像素差异)⇒ 观感变化,放弃; - 用 gd::Bitmap 做行位图:文字丢失 ClearType,放弃。 四、验证(全部离屏,不占用鼠标/键盘/屏幕) - 新增 .preflight/perf/panelstutter_main.cpp(8/8)、halocheck_main.cpp(16/16); - 回归 verify 49/49、panelanim 158/158、hoverdrag 20/20、gridanim 93/93、lanescroll 23/23、 datacheck 69/69、edgecheck 46/46、errcheck 19/19、panflash 19/19、stress 6/6; - make release / make debug 零警告(TARGET_FILE_NAME=TargetCheck/TargetCheckD)。 文档:Documents/agent_mem.md 追加本轮小节(含 4 条新规则:高亮语义不得为性能让步、 GDI 文本的三个隐形陷阱、面板卡顿先量"每行成本×可见行数"与"本帧是否顺带做绘图区重活"、 动画帧内绝不做可延迟的整帧级工作)。
This commit is contained in:
1 parent
0efaa8c0ca
commit
24f0bf849d
11 files changed
+853
-197
No files matched your search
+64
-43
@@ -123,8 +123,64 @@ namespace jdi
|
||||
};
|
||||
/** 上限保护:超过时整体清空(缓存只是加速手段,清空后重新填充即可)。 */
|
||||
constexpr std::size_t kMeasureCacheLimit = 4096;
|
||||
std::unordered_map<MeasureKey, float, MeasureKeyHash> g_measureCache;
|
||||
|
||||
/** 文本的像素宽度与字符单元高度(后者与 `DT_VCENTER` 的居中口径一致)。 */
|
||||
struct TextExtent
|
||||
{
|
||||
float width = 0.0f; /**< 像素宽度。 */
|
||||
float height = 0.0f; /**< 字符单元高度。 */
|
||||
};
|
||||
|
||||
std::unordered_map<MeasureKey, TextExtent, MeasureKeyHash> g_measureCache;
|
||||
std::mutex g_measureMutex;
|
||||
|
||||
/**
|
||||
* @brief 取文本的宽 / 高(带缓存)。
|
||||
*
|
||||
* ⚠️ 必须走 GDI(`GetDC` + `GetTextExtentPoint32W` + `ReleaseDC`),单次约 10µs;
|
||||
* 而布局与绘制每帧会把同一批字符串**重复测量成百上千次**,因此按 (字体, 文本) 缓存。
|
||||
* 高度也一并缓存:`DrawUiText` 的快速路径要用它做垂直居中(见那里的说明)。
|
||||
*/
|
||||
TextExtent MeasureText(HFONT font, const std::wstring& text)
|
||||
{
|
||||
if (text.empty() || font == nullptr)
|
||||
{
|
||||
return TextExtent();
|
||||
}
|
||||
const MeasureKey key{font, text};
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_measureMutex);
|
||||
const auto found = g_measureCache.find(key);
|
||||
if (found != g_measureCache.end())
|
||||
{
|
||||
return found->second;
|
||||
}
|
||||
}
|
||||
|
||||
HDC hdc = GetDC(nullptr);
|
||||
if (hdc == nullptr)
|
||||
{
|
||||
return TextExtent();
|
||||
}
|
||||
HGDIOBJ oldFont = SelectObject(hdc, font);
|
||||
SIZE size = {};
|
||||
GetTextExtentPoint32W(hdc, text.c_str(), static_cast<int>(text.size()), &size);
|
||||
SelectObject(hdc, oldFont);
|
||||
ReleaseDC(nullptr, hdc);
|
||||
|
||||
TextExtent extent;
|
||||
extent.width = static_cast<float>(size.cx);
|
||||
extent.height = static_cast<float>(size.cy);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_measureMutex);
|
||||
if (g_measureCache.size() >= kMeasureCacheLimit)
|
||||
{
|
||||
g_measureCache.clear();
|
||||
}
|
||||
g_measureCache.emplace(key, extent);
|
||||
}
|
||||
return extent;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void UiClearMeasureCache()
|
||||
@@ -413,6 +469,10 @@ namespace jdi
|
||||
rc.right = static_cast<LONG>(std::ceil(rect.X + rect.Width));
|
||||
rc.bottom = static_cast<LONG>(std::ceil(rect.Y + rect.Height));
|
||||
|
||||
// ⚠️ 这里必须继续用 `DrawTextW`:试过改用 `ExtTextOutW` 的"快速路径"(约快 2~4 倍),
|
||||
// 但同一段文字的**光栅化结果并不相同**(实测面板区域 10% 的像素有差异,
|
||||
// 亚像素边缘与基线差 1px)—— 全体界面文字观感都会变,因此放弃。
|
||||
// 列表卡顿改由"行位图缓存 + 动画期间不预热网格图层"解决(见 `SignalPanel::PaintRowCached`)。
|
||||
UINT flags = DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS;
|
||||
switch (align)
|
||||
{
|
||||
@@ -455,48 +515,9 @@ namespace jdi
|
||||
|
||||
float UiMeasureTextWidth(HFONT font, const std::wstring& text)
|
||||
{
|
||||
if (text.empty() || font == nullptr)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// 记忆化:文本宽度只能走 GDI(GetDC + GetTextExtentPoint32W + ReleaseDC),
|
||||
// 单次约 10µs;而布局代码每帧会把同一批字符串**重复测量成百上千次**——
|
||||
// 例如浮动面板的几何是按「网格数 × 面板种类」反复重算的(5 网格 × 4 类 = 20 次,
|
||||
// 每次十来个字符串),实测光这一项就占 3ms 以上。字体与文本都相同的测量结果
|
||||
// 必然一致,因此按 (字体, 文本) 做一层缓存;字体被重建时由 `UiClearMeasureCache` 作废。
|
||||
const MeasureKey key{font, text};
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_measureMutex);
|
||||
const auto found = g_measureCache.find(key);
|
||||
if (found != g_measureCache.end())
|
||||
{
|
||||
return found->second;
|
||||
}
|
||||
}
|
||||
|
||||
HDC hdc = GetDC(nullptr);
|
||||
if (hdc == nullptr)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
HGDIOBJ oldFont = SelectObject(hdc, font);
|
||||
SIZE size = {};
|
||||
GetTextExtentPoint32W(hdc, text.c_str(), static_cast<int>(text.size()), &size);
|
||||
SelectObject(hdc, oldFont);
|
||||
ReleaseDC(nullptr, hdc);
|
||||
|
||||
const float width = static_cast<float>(size.cx);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_measureMutex);
|
||||
if (g_measureCache.size() >= kMeasureCacheLimit)
|
||||
{
|
||||
g_measureCache.clear();
|
||||
}
|
||||
g_measureCache.emplace(key, width);
|
||||
}
|
||||
return width;
|
||||
// 记忆化(键 = 字体 + 文本,值 = 宽 / 高):布局代码每帧会把同一批字符串
|
||||
// **重复测量成百上千次**,而 `GetTextExtentPoint32W` 单次约 10µs。
|
||||
return MeasureText(font, text).width;
|
||||
}
|
||||
|
||||
std::wstring UiEllipsize(const std::wstring& text, HFONT font, float maxWidth)
|
||||
|
||||
Reference in new issue
Block a user