多层加载拖拽卡顿根治 + 高亮命中线段中段 / 散点高亮 + 选区放大也高亮 + 自动点线判定与规则修正

一、三明治加载后拖拽视界卡顿(39.2ms → 8.2ms 均值,47.9ms → 10.3ms 最坏)—— 四个叠加根因
1) RenderLaneLayerRegion 每次「横向补条」都调用两次 BuildLaneVisuals()(对全部网格算
   ComputeLaneRange,O(网格数))⇒ 253 网格时单帧 RollWideBuffer = 26.8ms。
   修法:进入前快照 m_lanes / m_laneContentHeight,结束时直接拷回(它只渲染一个网格)。
2) 横向「重新居中」本来就不是正确性要求:BlitWideBuffer 按 (ViewMin - layer.wideMin) * scale
   算源偏移,不滚动也画得对 ⇒ 新增 kLayerRollQuantumX=32(不足一个量子不滚)+
   kLayerRollsPerFrame=3(每帧最多重居中 3 个网格,分帧摊销)。
3) 位移过大时旧实现「立刻整块重画」:巨型信号(25M 采样)一次 20~130ms,实测拖拽最坏帧 28ms。
   修法:kLayerRollMaxShiftPixels=160,超出就作废图层 + 登记待重画,本帧由 BlitWideBuffer 的
   网格底衬兜底(正是用户要求的“没来得及画信号就按只有网格叠底继续拖”);
   并且 RollWideBuffer 只处理可视/预取带内的网格(离屏网格不做像素工作)。
4) 包络带路径也支持横向补条裁剪:包络是逐像素列独立计算的,只构建条带覆盖的列即可。
   旧实现对包络带总是构建整幅可见范围 ⇒ 巨型信号一条 32px 竖带要 5ms。

二、鼠标悬停「两点之间的连线」无法高亮(放大到自动点线后)
根因:HitTestSignal 的横向窗口用 upper_bound(fx+tol) 取顶点区间,只扫 [first, last) 的线段;
放大后相邻点相距几十像素,鼠标落在两点中间时两端都在窗口外 ⇒ windowFirst >= windowLast ⇒ 跳过。
修法:线段扫描上界改为 min(stop, windowLast + 1)(必须包含“一端在窗口内、另一端在窗口外”的线段);
散点命中的顶点范围也向两侧各扩一个。实测 11 段长线段中点命中 0/11 → 11/11。

三、散点模式补充高亮(用户要求:原线段/点线高亮“去掉数据点之间连线的部分”)
PaintSignalHalo 原来对 DrawMode::SCATTER 直接 return ⇒ 只有拖拽提示、没有高亮。
现在给每个参与绘制的散点套一圈半透明强调色圆环(步长与绘制同源 kMaxScatterPoints)。
线段 / 点线保持原有的折线光晕。列表 / 图例 / 样式区「目标」行联动高亮走同一条路径,自动生效。

四、「选区放大」模式恢复悬停高亮(仍不允许拖信号)
OnMouseMove 去掉 ZoomMode() == NONE 的门控;OnLButtonDown 在 SELECT 模式下仍直接进入 RUBBER_SELECT。
该模式下的提示文案只显示信号名(去掉 “· 按住拖拽可移入其它网格” 尾缀)。

五、自动点线误判(MF4 + VERY_BIG_DATA 同时加载时 MF4 信号默认就被切点线)
根因:MinorCellSparse 把整个视界切成小格。VERY_BIG_DATA 跨度 ~130s、MF4 数据只覆盖 ~11s ⇒
MF4 信号在 90% 的格子里本来就没有任何采样点 ⇒ “存在一个格子内 < 5 个点”恒成立。
(用户猜测是“用了抽稀点”,实际是“空格子”,但现象一致。)
修法:先用可见数据的显示时间跨度 [dataMin, dataMax] 收窄参与的格子(firstCell..lastCell),
鸽巢判定的分母改用参与格子数;端点 NaN 时做 64 步有界探测并回退到轴区间。
实测:248 个 MF4/中等信号被误判的数量 248 → 0。

六、自动点线规则收窄(用户本次要求)
只有 DrawMode::LINE 的信号在放大时自动切 LINE_SCATTER;SCATTER 不切换(散点已足够提示数据点位置)、
LINE_SCATTER 不动;用户手动改过样式(autoScatterUserOverride)时不切换,避免“改完下一帧又被改回去”。
退出时(用户未手动改过)恢复原来的 LINE。
实测:LINE → 放大后 mode=2 auto=1 → 适应视图后 mode=0 auto=0;SCATTER → 放大后 mode=1 auto=0。

验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/perf/hoverdrag_main.cpp(hoverdrag.exe)20/20:三明治加载后的点选跳转 + 拖拽逐帧耗时、
  线段中点命中、散点高亮像素、选区放大悬停命中、自动点线判定与规则。
- 既有回归:verify 49/49、panelanim 158/158、datacheck 69/69、edgecheck 46/46、errcheck 19/19、
  gridanim 93/93、lanescroll 23/23。
- 探针的绝对毫秒阈值改为相对基准(同一基线代码在不同时段可差 2~3 倍,避免假失败)。
- make release / make debug 零错误零警告(用 TARGET_FILE_NAME=TargetCheck/TargetCheckD 避免与运行中实例冲突)。

文档:Documents/agent_mem.md 新增 §9.45、§11.2 第 141–150 条与变更日志一行。
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-10-18 01:08:11 +08:00
1 parent 6b15575829
commit fd606a6f80
5 files changed
+394 -70

No files matched your search

+134
View File
@@ -2832,6 +2832,103 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
---
### 9.45 多层加载时拖拽卡顿根治 + 高亮命中「线段中段」/ 散点高亮 + 选区放大也要高亮 + 自动点线判定修正与规则收窄(本轮)
> 用户 2026-10-17 第九轮(本机)。四项需求:
> ① 三明治加载(MF4 + `VERY_BIG_DATA` + MF4,全部显示)后点选信号跳转到中间的巨型网格,
> 用左/中键拖拽视界明显卡顿,且之后在任何位置拖拽都卡;
> ② 放大到自动点线后,鼠标只有靠近**数据点**才高亮,**两点之间的连线**不行;散点模式只有拖拽提示没有高亮;
> ③ 「选区放大」模式下完全没有悬停高亮(旧规则是刻意禁掉的);
> ④ 同时加载 MF4 + `VERY_BIG_DATA` 时 MF4 信号在**默认视图**就被误判成稀疏、自动切点线。
> 离屏验证:新增 `.preflight/perf/hoverdrag_main.cpp`(`hoverdrag.exe`,**20/20**);
> 既有 `verify` 49/49、`panelanim` 158/158、`datacheck` 69/69、`edgecheck` 46/46、
> `errcheck` 19/19、`gridanim` 93/93、`lanescroll` 23/23;`make release`/`make debug` 零警告。
#### 一、拖拽卡顿的**三个叠加根因**(用逐阶段计时定位,实测 39ms → 8.8ms/帧)
1. ⚠️⚠️ **每次「横向补条」都重跑 `BuildLaneVisuals()`(O(网格数))**:
`RollWideBuffer` 每帧把每个**可见**网格的图层滚回“居中”位置并补一条 2~8px 宽的竖带,
而 `RenderLaneLayerRegion` 内部有**两次** `BuildLaneVisuals()`(每次都对全部网格算 `ComputeLaneRange`)。
253 个网格 × 30 个可见网格 ⇒ 单帧 `RollWideBuffer` **26.8ms**。
✓ `RenderLaneLayerRegion` 不再调用 `BuildLaneVisuals()`:进入前完整快照 `m_lanes` 与
`m_laneContentHeight`,结束时直接拷回(`m_lanes = savedLanes`)。它只渲染**一个**网格,
其余网格的几何保持原值即可。
2. ⚠️ **重新居中本来就不是必需的**:`BlitWideBuffer` 贴图时按 `(ViewMin - layer.wideMin) * scale`
算源偏移,只要还在位图覆盖范围内,**不滚动也画得对**。因此改为:
* `kLayerRollQuantumX = 32`:位移不足一个量子就不滚;
* `kLayerRollsPerFrame = 3`:每帧最多重居中 3 个网格(分摊到多帧,纯优化可以随便分摊)。
3. ⚠️⚠️ **位移过大时“立刻整块重画”**是巨型信号的灾难:离屏网格被跳过像素跟随,位移会累积;
一旦它进入预取带,旧实现会**同步整块重画**(25M 采样 ⇒ **20~130ms**,实测拖拽最坏帧 28ms)。
✓ `kLayerRollMaxShiftPixels = 160`:超出就**作废图层 + 登记待重画**,本帧由 `BlitWideBuffer`
的**网格底衬**兜底(正是用户要求的“没来得及画信号就按只有网格叠底继续拖”)。
同时 `RollWideBuffer` **只处理可视/预取带内的网格**(离屏网格不做任何像素工作)。
4. 附带:**包络带路径也支持横向补条裁剪** —— 旧实现只对“逐点描边”路径按条带裁剪,
包络带总是构建**整幅可见范围**的包络 ⇒ 巨型信号一条 32px 竖带要 **5ms**。包络是**逐像素列独立**
计算的,因此只构建条带覆盖的列即可(像素与整块渲染一致)。
**实测(`hoverdrag`,MF4 + VERY_BIG_DATA + MF4 = 253 网格,点选后拖拽 40 帧)**:
| 指标 | 修前 | 修后 |
| --- | --- | --- |
| 拖拽平均帧 | **39.2ms** | **8.2ms** |
| 拖拽最坏帧 | **47.9ms** | **10.3ms** |
| `RollWideBuffer` | 26.8ms | 0.03ms |
| 远离巨型网格处拖拽 | 43.0ms | 9.4ms |
#### 二、高亮命中「两点之间的连线」(用户报告:只有数据点附近才高亮)
- ⚠️ 根因在 `HitTestSignal` 的**横向窗口**:它用 `lower_bound(fx-tol)` / `upper_bound(fx+tol)`
取顶点区间,然后只扫 `[windowFirst, windowLast)` 的线段。放大到点线模式后相邻点相距几十像素,
鼠标落在两点**中间**时两个端点都在窗口外 ⇒ `windowFirst >= windowLast` ⇒ 直接跳过。
- ✓ 修法:线段扫描上界改为 `min(stop, windowLast + 1)`(必须包含“一端在窗口内、另一端在窗口外”
的跨窗口线段);散点命中的顶点范围也向两侧各扩一个。
- 实测:11 段长线段的中点,命中 **0/11 → 11/11**。
#### 三、散点模式的高亮 = 数据点高亮环(不含连线)
- `PaintSignalHalo` 原来对 `DrawMode::SCATTER` 直接 `return` ⇒ 散点信号悬停**只有提示、没有高亮**。
- ✓ 现在散点模式给每个**参与绘制**的散点套一圈半透明强调色圆环(步长与绘制同源 `kMaxScatterPoints`);
线段 / 点线仍用原来的折线光晕(用户:“原来的线段和点线模式的高亮去掉数据点之间连线的部分似乎就还不错”)。
- ⚠️ 信号列表 / 图例 / 样式区「目标」行的联动高亮走的是**同一条** `PaintSignalHalo` 路径(`m_externalHoverSignal`),
因此散点信号在列表里悬停时同样会亮起数据点环。
#### 四、「选区放大」模式:保留悬停高亮,但不允许拖信号
- ⚠️ 旧规则(§9.34 之前)在 `OnMouseMove` 里直接 `if (ZoomMode == NONE)` 才做 `HitTestSignal`。
- ✓ 现在**任何缩放模式**都做悬停命中;`OnLButtonDown` 在 `SELECT` 模式下仍然直接进入 `RUBBER_SELECT`
(信号拖拽只在 `NONE` 模式下成立),因此“能高亮、能看提示、但拖不动”。
- ✓ 提示文案在 `SELECT` 模式下**只显示信号名**(去掉“· 按住拖拽可移入其它网格”的尾缀)。
#### 五、自动点线误判:判定范围必须限制在「该信号真正有数据的时间段」
- ⚠️⚠️ **根因**:`MinorCellSparse` 把**整个视界**切成小格。同时加载 `VERY_BIG_DATA`(跨度 ~130s)
与 MF4(数据只覆盖 ~11s)时,MF4 信号在 **90% 的格子里本来就没有任何采样点** ⇒
“存在一个格子内 < 5 个点”恒成立 ⇒ **默认视图就被判成稀疏、自动切点线**。
(用户猜测是“用了缺省绘制的抽稀点”—— 实际不是抽稀,是**空格子**;但现象与猜测一致。)
实测:248 个 MF4/中等信号,**248 → 0** 个被误判。
- ✓ 修法:先用「可见数据的显示时间跨度」`[dataMin, dataMax]` 收窄参与的格子(`firstCell..lastCell`),
鸽巢判定也用**参与格子数**作分母;端点无效(NaN)时做 64 步有界探测并回退到轴区间。
#### 六、自动点线**只对「线段」模式生效**(用户本次要求)
- ✔ `DrawMode::LINE` → 放大时自动切 `LINE_SCATTER`;退出时(用户没手动改过)恢复 `LINE`。
- ✘ `DrawMode::SCATTER`:**不**自动切换(散点已足够提示数据点位置,再叠连线反而干扰)。
- ✘ 已经是 `LINE_SCATTER` 的:不动。
- ⚠️ 用户手动改过样式(`autoScatterUserOverride`)时**不再自动切换**,避免“改完下一帧又被改回去”。
- 实测:LINE → 放大后 `mode=2 auto=1` → 适应视图后 `mode=0 auto=0`;SCATTER → 放大后 `mode=1 auto=0`。
#### 七、⚠️ 方法论教训
1. **绝对毫秒阈值会被机器状态污染**:本轮同一份**未改动的基线提交**在几小时后跑同一探针,
数值从 11ms 变成 26ms(普通整帧从 8ms 变成 21ms)。⇒ 断言必须用**相对基准**
(如“同一时段的普通整帧耗时”或“同一时段的手动拖滚动条耗时”),否则假失败。
2. **printf 的参数个数必须与格式串匹配**:本轮探针漏传参数导致读栈上垃圾,
把 `roll=130.49`(其实是 span 值)当成真实耗时,误导了排查方向。
3. **先分离“是否真的贵”再优化**:滚动只是“把位图重新居中”,而贴图本来就按 `wideMin` 算偏移 ⇒
**不滚动也画得对**。识别出这一点后,量子化 + 每帧限量就都是安全的。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -2896,6 +2993,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 2026-10-17 | DeepSeek-V4.1-Flash(**本机**) | **滚动动画根治(巨型信号不再影响滚动)+「N 项」计数点击语义 + 浮动面板首现右上角 + 开关/关闭面板卡顿(详见 §9.44)**。① **滚动卡顿真因**:滚动到**从未建过图层**的区域时 `BlitWideBuffer()` 一个网格都贴不出像素 ⇒ 返回 `false` ⇒ 退化成**整幅重绘** ⇒ 每帧重画全部可见网格的波形(含千万级采样的巨型信号),实测 **20~35ms**(手动拖滚动条只要 ~6ms)。修法:只要还能画**网格底衬**(`backing` 非空)就返回 true ⇒ 动画均值 23→**11.2ms**、最坏 35.6→**13.1ms**,含 5 个巨型信号网格的帧 33→**~11ms**。② 新增 `Application::m_panelDirty`:面板动画帧只重绘面板+分隔条(−2.7ms/帧)。③ **「N 项」计数**独立为 `RowPart::GROUP_COUNT`(`GroupStatsRect` 为唯一几何来源),任何点击频率只折叠展开、绝不进改名;改名计时只对 `BODY` 记录。④ **浮动面板首现位置**改用「已显示面板序数」错开(旧实现用 `laneIndex`,上百网格时被夹到右下角),实测第一个面板 `topMargin=rightMargin=16*scale`(正上右角),6 个面板仍在顶部 22%。⑤ **开关/关闭面板卡顿真因**:`SetLane*` 末尾的 `RebuildWideBuffer()` **同步重渲染全部网格图层**(129 网格含巨型信号 ⇒ 几百毫秒);改用 `EnsureLaneLayersPrefetched()`(只在图层数与网格数不符时整批建立,其余登记待重画队列)⇒ 几百毫秒 → **≤22ms**。验证:新增 `.preflight/perf/lanescroll_main.cpp`(`lanescroll.exe`)**23/23**(逐帧耗时 + 巨型网格可见数关联、计数热区、面板位置、开着巨型信号的开关/关闭耗时);`verify` 49/49、`panelanim` 158/158、`datacheck` 69/69、`edgecheck` 46/46、`errcheck` 19/19、`gridanim` 93/93;`make release`/`make debug` 零警告。§11.2 新增第 134–140 条。 |
| 2026-10-17 | DeepSeek-V4.1-Flash(**本机**) | **多层加载拖拽卡顿根治 + 高亮命中线段中段 / 散点高亮 + 选区放大也高亮 + 自动点线判定与规则修正(详见 §9.45)**。① **拖拽卡顿(39.2→8.2ms 均值、47.9→10.3ms 最坏)**:**(a)** `RenderLaneLayerRegion` 每次补条都调两次 `BuildLaneVisuals()`(O(网格数))⇒ 253 网格时 `RollWideBuffer` 单帧 **26.8ms**;改为快照/拷回 `m_lanes`;**(b)** 横向“重新居中”纯属优化 ⇒ `kLayerRollQuantumX=32` + `kLayerRollsPerFrame=3`;**(c)** 位移过大时旧实现**同步整块重画**(巨型信号 20~130ms)⇒ `kLayerRollMaxShiftPixels=160` 作废图层交给底衬;`RollWideBuffer` 跳过离屏网格;**(d)** 包络带路径也支持补条裁剪(巨型信号一条 32px 竖带 5ms → 亚毫秒)。② **高亮命中**:`HitTestSignal` 横向窗口少算一个顶点 ⇒ 放大后两点**中间的连线**完全点不中(0/11 → 11/11);散点模式新增“数据点高亮环”(不含连线),列表/图例联动同源。③ **选区放大**恢复悬停高亮(仍不可拖信号),提示只显示信号名。④ **自动点线误判**:判定范围未限制在信号有数据的时段 ⇒ 长跨度文件 + 短信号时 MF4 信号默认视图全部误判(248→0);并改为**只对 `DrawMode::LINE` 生效**(SCATTER 不切换、用户手动改过不切换)。验证:新增 `.preflight/perf/hoverdrag_main.cpp` **20/20**;`verify` 49/49、`panelanim` 158/158、`datacheck` 69/69、`edgecheck` 46/46、`errcheck` 19/19、`gridanim` 93/93、`lanescroll` 23/23;`make release`/`make debug` 零警告。⚠️ 探针的绝对毫秒阈值改为**相对基准**(同一基线代码在不同时段可差 2~3 倍)。§11.2 新增第 141–150 条。 |
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -3573,6 +3672,41 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
⚠️ 「快速双击进改名」的计时只对 `RowPart::BODY` 记录 —— 否则“点计数、紧接着点名称”会被误判成双击。
140. ⚠️ **“修完感觉没变”时要重新插桩定位**:本轮在 `Paint` 里插 5 个 `QueryPerformanceCounter` 才定位到
“宽基底分支内部几乎不耗时 ⇒ 一定走了别的分支(整幅重绘)”。逐阶段计时比“改代码做 A/B”快得多。
141. ⚠️⚠️ **`RenderLaneLayerRegion` 内部绝不能调用 `BuildLaneVisuals()`**(本轮实测:253 网格时
`RollWideBuffer` 单帧 **26.8ms**,全是“每次补条 × 2 次 O(网格数) 的 `ComputeLaneRange`”):
它只渲染**一个**网格,进入前快照 `m_lanes` / `m_laneContentHeight`,结束时直接拷回即可。
142. ⚠️ **图层“横向重新居中”是纯优化,不是正确性要求**:`BlitWideBuffer` 按
`(ViewMin - layer.wideMin) * scale` 算源偏移 ⇒ 不滚动也画得对(只要还在位图覆盖内)。
因此必须:`kLayerRollQuantumX = 32`(不足一个量子不滚)+ `kLayerRollsPerFrame = 3`(分帧摊销)。
143. ⚠️⚠️ **位移过大时绝不可以在前台同步“整块重画”**:巨型信号(25M 采样)一次整块重画
**20~130ms**(实测拖拽最坏帧 28ms)。超出 `kLayerRollMaxShiftPixels = 160` 就**作废图层 + 登记待重画**,
本帧由 `BlitWideBuffer` 的网格底衬兜底。
⚠️ 同时 `RollWideBuffer` 必须**跳过不在可视/预取带内的网格**(离屏网格做像素工作纯属浪费)。
144. ⚠️ **包络带路径也要支持横向补条裁剪**:包络是**逐像素列独立**计算的,只构建条带覆盖的列即可;
旧实现对包络带总是构建整幅可见范围 ⇒ 巨型信号一条 32px 竖带要 5ms。
145. ⚠️⚠️ **折线命中测试的横向窗口必须包含“跨窗口线段”**:`upper_bound(fx+tol)` 给出的是
**第一个超出容差的顶点**,而“一端在窗口内、另一端在窗口外”的线段正是鼠标所在的那一段 ⇒
线段扫描上界必须是 `min(stop, windowLast + 1)`。否则放大到点线模式后
**两点之间的连线完全点不中**(用户 2026-10-17 报告)。
146. ⚠️ **散点模式也要有悬停高亮**(用户 2026-10-17 要求):`PaintSignalHalo` 对 `DrawMode::SCATTER`
不能再直接 `return`,而应给每个参与绘制的散点套强调色圆环(步长与绘制同源 `kMaxScatterPoints`)。
⚠️ 列表 / 图例 / 样式区「目标」行的联动高亮走**同一条** `PaintSignalHalo` 路径,改一处即可全覆盖。
147. ⚠️ **「选区放大」模式必须保留悬停高亮**(用户 2026-10-17 修订 §9.34 的旧规则):
`OnMouseMove` 里不应再有 `ZoomMode() == NONE` 的门控;`OnLButtonDown` 在 `SELECT` 模式下仍然直接
进入 `RUBBER_SELECT`(拖信号只在 `NONE` 模式成立)⇒ “能高亮、能看提示、但拖不动”。
该模式下的提示文案**只显示信号名**(去掉“· 按住拖拽可移入其它网格”尾缀)。
148. ⚠️⚠️ **「每小格内数据点 < N」的判定必须限制在【该信号真正有数据的时间段】内**:
否则同时加载“跨度很长的大文件 + 跨度很短的信号”时,短信号在**大部分格子里本来就没有采样点** ⇒
“存在空格子”恒成立 ⇒ **默认视图就被误判成稀疏、自动切点线**(用户 2026-10-17 报告;
实测 248 个 MF4/中等信号全部误判)。✓ 用可见数据的 `[dataMin, dataMax]` 收窄参与的格子
(`firstCell..lastCell`),鸽巢判定的分母也改用**参与格子数**。
149. ⚠️ **「放大自动点线」只对 `DrawMode::LINE` 生效**(用户 2026-10-17 要求):
`SCATTER` 不换(散点已足够提示数据点位置)、`LINE_SCATTER` 不动;用户手动改过
(`autoScatterUserOverride`)时也不切换,避免“改完下一帧又被改回去”。
150. ⚠️⚠️ **探针的绝对毫秒阈值不可靠**:同一份基线代码在不同时段跑,数值可差 2~3 倍
(本轮 11ms → 26ms,普通整帧 8ms → 21ms)⇒ 断言必须用**同一时段测得的相对基准**
(普通整帧耗时 / 手动拖滚动条耗时)。⚠️ 另外 **printf 的参数必须与格式串数量一致**,
否则读到栈上垃圾值(本轮把 `130.49` 当成真实耗时,误导排查)。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)