修复悬停高亮变长方形色块/联动失效 + 信号列表动画卡顿真因(行位图缓存)

一、悬停高亮恢复"整条曲线发光"(上一轮裁成鼠标附近一段 ⇒ 满幅方波上就是个长方形色块)
光晕的语义是"整条曲线一起发光",对编码器这种满幅方波,一列包络本来就跨满网格高,
裁成 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:
DeepSeek-V4.1-Flash committed 2026-10-18 21:40:56 +08:00
1 parent 0efaa8c0ca
commit 24f0bf849d
11 files changed
+853 -197

No files matched your search

@@ -806,6 +806,76 @@ namespace jdi
*/
void PaintSignalRow(gd::Graphics& g, int row);
/**
* @brief 把一行(分组行 / 信号行)**绘制进离屏位图缓存**,再整块贴到列表上。
*
* ⚠️ 一行的绘制要调用十来个 GDI+ 原语(圆角托底 / 复选框 / 色块 / 两段文字…),
* 实测 **0.35ms/行**;列表一屏能放 35~40 行 ⇒ 单帧 **12~15ms** 全花在重复绘制
* **内容完全没变**的行上(分组折起/展开动画只是在整行平移)。
* 因此按"行的可见内容"做键缓存位图:动画帧只需把命中的位图按新位置贴出来。
*
* ⚠️ 缓存期间行的绘制**不能**裁到列表矩形(`m_rowClipRender`):位图是整行内容,
* 裁剪由贴图时的 GDI+ 裁剪区负责 —— 否则部分滚出上边界的行会永久丢掉顶部像素。
*
* @param [IN,OUT] g 绘图上下文(调用方已把裁剪区设为列表矩形)。
* @param [IN] row 行下标。
*/
void PaintRowCached(gd::Graphics& g, int row);
/**
* @brief 计算一行的缓存键(涵盖影响该行像素的全部状态)。
* @param [IN] row 行下标。
* @return 键值。
*/
std::uint64_t RowCacheKey(int row) const;
/** @brief 作废全部行位图缓存(行集合 / 字体 / 主题变化时调用)。 */
void InvalidateRowCache();
/** @brief 一行的离屏位图缓存项。 */
struct RowBitmapCache
{
/**
* @brief 该行的像素(DIB 内存 DC,已铺面板底色)。
*
* ⚠️ 必须是 **DIB + 内存 DC**,不能用 `gd::Bitmap`:GDI 在 GDI+ 位图的临时 HDC 上
* 会**关闭 ClearType**(退化为灰度抗锯齿),列表文字因此变细、观感与正文不一致
*(实测:整列文字丢失亚像素渲染)。DIB 内存 DC 上 ClearType 与屏幕一致。
*/
HDC dc = nullptr;
HBITMAP dib = nullptr;
std::uint64_t key = 0; /**< 内容键(不一致即重建)。 */
int width = 0; /**< 位图宽度(= 列表宽度)。 */
int height = 0; /**< 位图高度(= 行高)。 */
};
std::vector<RowBitmapCache> m_rowBitmapCache; /**< 与 `m_rows` 同序的行位图缓存。 */
/**
* @brief 行位图缓存的**数量上限**(超过即整体作废重建)。
*
* 一行位图 ≈ 列表宽 × 行高 × 4B(430×30×4 ≈ 51KB)。用户滚动浏览上千行时,
* 缓存会按"画过的行下标"逐渐长大,必须有上限(这里 192 行 ≈ 10MB)。
*/
static constexpr std::size_t kRowBitmapCacheLimit = 192;
std::size_t m_rowBitmapLiveCount = 0; /**< 当前持有的行位图数量。 */
/** @brief 行绘制正被渲染进离屏位图(此时禁止按列表矩形裁剪)。 */
bool m_rowClipRender = false;
/**
* @brief 行位图缓存渲染期间的**几何平移量**(把整行搬到"位图坐标系")。
*
* ⚠️ 不能用 `gd::Graphics::TranslateTransform`:行内**文本走 GDI `DrawTextW`**,
* 而 GDI 文本**不遵循 GDI+ 的世界变换**(`Graphics::GetHDC` 拿到的 DC 只有裁剪区),
* 于是文字会被画到绝对坐标处、落到小位图之外(实测量产现象:行里只剩复选框/色块,
* 文字全部消失)。因此在**几何层**平移:`RowRect` 是行内一切矩形的唯一来源,
* 它统一减去该原点即可(`SignalBadgeRect` / ✕ / 名称框等全部自动跟随)。
*/
bool m_rowRenderActive = false;
float m_rowRenderOriginX = 0.0f; /**< 位图左上角(屏幕 x)。 */
float m_rowRenderOriginY = 0.0f; /**< 位图左上角(屏幕 y)。 */
/**
* @brief 判断哪个分组行需要「悬浮」在列表顶部(返回行下标,无需悬浮时返回 -1)。
*