fix(plotview): 修复面板条目栏悬停时预览曲线高亮丢失 + 极端工况绘制管线提速

一、悬停高亮回归修复
- 根因:高亮画在预览曲线绘制路径里,而该路径在命中静态场景缓存时完全不会被调用
  (悬停态又不参与 SceneContentHash),因此缓存命中帧永远看不到高亮。
- 修法:拆出 PaintDerivedCurvesFor(hoverOnly) 两态,新增 PaintDerivedHoverHighlight,
  在 paintOverlays() 中紧跟 PaintHoverHighlight 以前景方式绘制(面板之下、光标之上)。
  高亮仍按面板种类(均值滤波/中心轴/弱峰调整)分别判定。

二、预览曲线绘制重写(单条 15ms → 1.7ms)
- 稠密区间改用与波形基底同一套「逐像素列 min/max 包络带」+ 一次 FillPath;
  旧抽稀折线在稠密视界下每段都是竖直满幅,GDI+ 描边代价正比于覆盖像素数
  (3200 顶点满幅锯齿抗锯齿 128ms / 无抗锯齿 31ms,包络带 1.9ms)。
- 列密度自适应:中等密度 2px/列,极密(>8 采样/像素)4px/列。
- 稀疏区间保持逐点折线;附带修复「等间隔抽样漏掉尖峰」。

三、预览曲线像素缓存层(Picture cache + pan roll)
- 45 条曲线画进带 alpha 的 32bpp DIB,正常帧只做一次 AlphaBlend(1~2ms)。
- 纯平移时整行 memmove 一个 dx 并只重画新露出的竖带(与宽缓冲区滚动同构),
  因此拖动视界的每帧代价与信号数无关。
- 内容指纹 = SceneContentHash(含样式/派生参数/网格几何) ⊕ 曲线发布序号;高亮不进指纹。

四、其他热点
- 光标取值胶囊不再逐帧重算并发布 45 条曲线(CurveSpanFor 新增 computeIfMissing),
  只取已覆盖的缓存曲线,未命中退到最新曲线或本帧不标注。
- 样式补丁路径不再同步重建网格图层(宽基底 2 倍宽 × 2 倍高,实测单次 2.2 秒),
  且开启派生功能时直接走整幅重绘(补丁会覆盖缓存中已烘入的曲线像素)。

实测(15 信号合并一条网格 + 全部实验性功能,1600x900 离屏探针):
拖动视界 264→5.9ms,双光标拖动 187→20.9ms,弱峰滑块 555→89ms,
改信号颜色 2661→200ms,悬停面板行 225→38ms,深放大拖动 207→23.8ms。
全屏 1920x1080:拖动视界 9.3ms。

验证:新增离屏探针 p27 8/8(悬停高亮在缓存命中帧可见、平移/缩放/改颜色后的叠加层
与「完全重画」参考帧逐像素一致 diff=0);p20 9/9、p21 9/9、p22 14/14、p23 8/8、p25 12/12;
make release / make debug 零 warning;Release 814,080 B。规则已同步到 Documents/agent_mem.md。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-02 18:47:21 +08:00
1 parent ea704b03ea
commit 0c320cf1b3
6 files changed
+797 -33

No files matched your search

+95
View File
@@ -9403,3 +9403,98 @@ make release / make debug 零 warning;Release 808,960 B
否则会因"扫描早已完成"而假失败(真实场景的更新路径是同一条)。
* ⚠️ PowerShell 5.1 按 ANSI 读 `.ps1`:脚本里**不能出现中文**(连正则里带中文都会解析失败),
要把中文内容放进 UTF-8 数据文件再读取。
---
## 2026-11-02 第二十三轮(本机):悬停高亮回归修复 + 「极端工况」绘制管线性能改造
### 1. ⚠️ 悬停高亮"丢失"的根因与修复
现象(用户报):鼠标停在**带预览曲线的实验性功能面板的信号名条目栏**上,对应的预览曲线不再高亮。
根因:高亮被画在 `PaintLaneDerivedOverlays`(预览曲线绘制)内部,而这条绘制路径在
**命中静态场景缓存时根本不会被调用**(`SceneCacheMatches() && BlitSceneCache(g)` 直接
`paintOverlays(); return;`)。悬停态又不参与 `SceneContentHash`(参与的话每动一下鼠标就整幅重绘)
⇒ 缓存命中帧永远看不到高亮。旧探针之所以"能过",是因为它每帧都整幅重绘。
修复(结构性,不是补丁):**高亮一律画在贴图之后的前景**(与图例高亮同一套思路)。
* `PaintDerivedCurvesFor(g, lane, hoverOnly)`:`hoverOnly == false`(基底/缓存路径)只画普通曲线、
不带高亮;`hoverOnly == true` 只画悬停那一条并带光晕。
* 新函数 `PaintDerivedHoverHighlight(g)`,在 `paintOverlays()` 里紧跟 `PaintHoverHighlight`
调用(在面板之下、光标之上)。
* 高亮仍**必须按面板种类判定**(MEAN / CENTER / PEAK 各自只看自己面板的悬停行)。
### 2. ⭐⭐ 极端工况的性能改造(3 次 VERY_BIG_DATA + 全部信号并一条网格 + 全部实验性功能)
改造前实测(离屏探针 `p26f`,15 信号 × 3 功能 = 45 条预览曲线,1600×900):
| 场景 | 改造前 | 改造后 |
| --- | --- | --- |
| 中键拖动视界 | ~264 ms | **5.9 ms** |
| 双光标拖动 | ~187 ms | **20.9 ms** |
| 弱峰滑块拖动 | ~555 ms | **89 ms** |
| 改信号颜色 | ~2661 ms | **200 ms** |
| 悬停面板行 | ~225 ms | **38 ms** |
| 深放大后拖动视界 | ~207 ms | **23.8 ms** |
| 静止帧 | 7.8 ms | **0.6 ms** |
#### 2.1 预览曲线改「逐像素列包络带」(单条 15ms → 1.7ms)
旧实现把派生曲线用 `stride` 抽稀成折线后 `DrawLines`。视界覆盖百万采样时相邻抽稀点 y **整幅跳变**
(几乎每段都是竖直满幅),而 GDI+ 描边代价 ∝ **覆盖像素数**:实测 3200 顶点满幅锯齿折线
**128ms(抗锯齿)/ 31ms(无抗锯齿)**,同样画面的逐列 min/max 包络带一次 `FillPath` 只要 **1.9ms**。
现在稠密区间(可见采样数 > 像素列数)改用与波形基底同一套 `EmitEnvelopeBand`(逐列极值 +
线宽补偿),稀疏区间仍走逐点折线。附带好处:**不再漏掉尖峰**(等间隔抽样会整段跳过)。
列密度:中等密度 2px/列,极密(> 8 采样/px)4px/列(`kDerivedCurveColumnDivisor` /
`kDerivedCurveVeryDenseSamplesPerPixel`)—— 填充代价主要取决于边数与面积。
#### 2.2 ⭐ 预览曲线改「像素缓存层 + 平移滚动」(这是把拖动降到 6ms 的关键)
`PaintDerivedOverlayLayer`:45 条曲线画进一张带 alpha 的 32bpp DIB(与装饰层同一套配方),
正常帧只做一次 `AlphaBlend`(约 1~2ms)。三种情形:
* 内容与视界都相同 → 只贴图(静止 / 悬停 / 光标动画的帧全部走这条);
* 内容相同、跨度不变(纯平移)→ **像素整行 memmove 一个 dx + 只重画新露出的竖带**
(与宽缓冲区滚动同构)⇒ 拖动视界每帧代价与信号数无关;
* 其余(缩放 / 参数 / 样式变化)→ 整块重画,并记下渲染时的视界。
内容指纹 `DerivedOverlayKey()` = `SceneContentHash(true,true,true,true)` ⊕ `m_curveSerial`
(后台曲线到达时必须刷新);高亮状态**不进**指纹(前景绘制)。
⚠️ 滚动时 `memmove` 的方向必须分开写:内容左移(`shift<0`)时目标不能越过行首,否则越界崩溃
(本轮真实踩到)。
#### 2.3 ⚠️ 光标取值胶囊不再"每帧重算并发布 45 条曲线"
`CurveSpanFor` 新增 `computeIfMissing`。胶囊只要光标所在那两个采样,而缓存键含请求窗口、
窗口随光标每帧变化 ⇒ 每帧 45 次未命中 ⇒ 45 次同步计算 + `PublishCurve`(条目列表与线性搜索
同步膨胀)。实测占拖动光标单帧 **约 150ms**。现在胶囊只取"已覆盖"的曲线(绘制路径发布的窗口
带保护带、覆盖整个可见区间),未命中退到同 (信号, 种类) 的最新曲线,再无则本帧不标注。
#### 2.4 ⚠️⚠️ 样式补丁路径不再同步重建网格图层("改颜色卡到无法操作"的根因)
样式补丁(拖颜色/粗细/透明度)每帧 `QueueLaneRebuild` + `RenderPendingLaneRebuilds(1)`。
一个网格图层是 **2 倍宽 × 2 倍高**的整块重绘,15 条稠密信号实测 **2.2 秒**
(整幅重绘只要 ~0.4 秒)。现在:
* **不再同步重建**(只登记待重建;`LaneLayerHash` 含颜色/线宽,宽基底快路径会自动跳过它);
* 开启了派生功能时**直接不走补丁路径**(补丁会把缓存里已烘进去的曲线像素覆盖掉),
改为整幅重绘 —— 实测 2.2s → 0.2s。
### 3. 验证(离屏探针,`SW_HIDE` + 合成消息,不占用鼠标/键盘/屏幕)
```
.preflight/p27(新增,8/8)
静态帧命中场景缓存;预览曲线在静态帧上可见;**悬停面板行在缓存命中帧上仍高亮**
平移后的叠加层与"完全重画"的参考帧逐像素一致(diff=0)
缩放后 / 改颜色后同样逐像素一致(diff=0,说明滚动/重建都正确)
无功能时缓存被释放、重新开启后重建
.preflight/p20 9/9 · p21 9/9 · p22 14/14 · p23 8/8 · p25 12/12
make release / make debug 零 warning;Release 814,080 B
```
### 4. 踩坑(重要)
* ⚠️⚠️ **离屏探针必须把画面画进 DIB 段的 HDC**(`CreateDIBSection` + `CreateCompatibleDC`),
与真实后备缓冲区一致。画进 `gd::Bitmap` 时每次 `Graphics::GetHDC()`(`UiText` 每次都会调)
都要锁/刷新软件表面 ⇒ 实测**单次文字 2.5ms**,会把这个成本错误地算到"文字/光标"头上
(同一份代码换 DIB 后单次文字 0.04ms)。换成 DIB 后 `paintOverlays` 里 30 个取值标注
从 200ms 降到 2.7ms —— **之前所有"光标拖动很慢"的结论都是探针假象**。
* ⚠️ 探针里 `OnMouseMove(cursorX, cy)` 很容易命中信号曲线而变成 **MOVE_SIGNAL** 拖拽
(该模式每帧让场景缓存失效);测光标拖动必须先断言 `m_dragMode == CURSOR_MOVE`。
* ⚠️ 场景缓存命中帧不会再走预览曲线绘制路径 —— 任何"随鼠标变化"的叠加内容都必须画在
`paintOverlays()` 里,否则只会在整幅重绘的那一帧出现。
* ⚠️ PowerShell 5.1 的 `-replace`/多行字符串在含中文/引号时会把命令搞坏;改文件内容优先
用编辑工具或 `.ps1` + `[System.IO.File]::WriteAllText`。