滚动动画根治(巨型信号不再影响滚动)+「N 项」计数点击语义 + 浮动面板首现右上角 + 开关/关闭面板卡顿

一、滚动过渡动画在巨型信号处卡顿 —— 真因是「退化成整幅重绘」
滚动到**从未构建过图层的区域**时,可见网格一个都贴不出图层像素 ⇒ BlitWideBuffer() 返回 false
⇒ 落到 Paint() 末尾的整幅重绘 ⇒ 把本帧全部可见网格的波形(含千万级采样的巨型信号)每帧重画一遍。
实测(MF4 124 网格 + VERY_BIG_DATA 5 网格,点选最后一个信号):单帧 20~35ms,且「可见的巨型信号
网格数」越多越慢(0 个 ≈20ms、5 个 ≈34ms);对照手动拖滚动条只要 ~6ms。
修法:BlitWideBuffer 只要还能画网格底衬(backing 非空)就返回 true,
只有 blits 与 backing 都为空时才交给整幅重绘 —— 这正是「没来得及画信号时就按只有网格叠底的状态继续滚动」。
实测:动画均值 23.0→11.2ms、最坏 35.6→13.1ms;含 5 个巨型信号网格的帧 33→10.7~12.3ms。

二、面板动画帧只重绘面板(额外 −2.7ms/帧)
新增 Application::m_panelDirty:滚动定时器在只有面板动画在跑时置位,Render 走 else if (panelOnly)
只重绘面板 + 分隔条(SignalPanel::Paint 会铺满自己的区域)。m_chromeDirty(真内容变化)仍走完整装饰重绘。
⚠️ 回送仍是 if (fullFrame) {整窗} else {绘图区} 的无条件分支(遵守 §9.41)。
实测帧耗时拆解:plot 4.8ms / plot+panel 11.4ms / plot+全装饰 13.9ms。

三、分组行「N 项」计数:只折叠展开,绝不进入改名
新增 RowPart::GROUP_COUNT 与 SignalPanel::GroupStatsRect(row)(绘制与命中的唯一几何来源,命中矩形在
文本左右各外扩 3 逻辑像素)。RowHitPart 在名称区域之前判定计数区:
- OnLButtonDown:折叠/展开;OnDoubleClick:只折叠/展开,任何点击频率都不进入改名;
- DoubleClickIsPlainPress 返回 true ⇒ 快速连点每一击都生效。
⚠️「快速双击进改名」的计时只对 RowPart::BODY 记录(否则“点计数、紧接着点名称”会被误判成双击)。

四、浮动面板首次出现的位置:右上角
旧默认位置 top = plotTop + 16 + laneIndex*28:上百个网格时 laneIndex 很大 ⇒ 被夹到右下角
(实测 plotH=1081 时 topMargin=964)。改为按「当前真正显示出来的面板中排在本面板之前的个数」错开
(固定遍历顺序:网格下标 → 面板种类),并把错开量夹到“面板下沿不超过绘图区 45% 高度”以内:
第一个面板错开 0 ⇒ 正好贴在上右角(实测 topMargin=24、rightMargin=24),6 个面板时最坏 topMargin=234。
⚠️ 判定「面板是否显示」不能只看 *Panel.open —— LanePanelState::open 默认就是 true,必须同时看功能启用。

五、开关实验性功能 / 关闭面板卡顿 —— RebuildWideBuffer() 整批重建
四个 SetLane* 末尾都调 RebuildWideBuffer()(注释说是“预建宽缓冲基底”),而它同步重渲染全部网格的图层:
129 个网格(含 5 个千万级采样的巨型信号)整批重建要几百毫秒 ⇒ 点一下功能开关 / 关闭浮动面板就会卡住。
修法:新增 PlotView::EnsureLaneLayersPrefetched() —— 只有「图层数组与网格数量不符」(首次绘制 / 增删网格)
才同步整批建立;其余只 RollWideBuffer() 登记缺失/过期网格,由既有预算(每帧最多一个)逐帧补齐。
实测:几百毫秒 → ≤22ms;巨型信号网格上开关均值滤波同样 15ms 量级。

验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/perf/lanescroll_main.cpp(lanescroll.exe)23/23:逐帧耗时 + 「可见巨型信号网格数」关联、
  「N 项」计数热区与双击不进改名、面板首现位置与多面板错开、开着巨型信号的开关/关闭面板耗时。
- 既有回归:verify 49/49、panelanim 158/158、datacheck 69/69、edgecheck 46/46、errcheck 19/19、
  gridanim 93/93。
- make release / make debug 零错误零警告(用 TARGET_FILE_NAME=TargetCheck/TargetCheckD 避免与运行中实例冲突)。

文档:Documents/agent_mem.md 新增 §9.44、§11.2 第 134–140 条与变更日志一行。
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-10-17 09:56:45 +08:00
1 parent 436dcbfd52
commit 6b15575829
10 files changed
+352 -38

No files matched your search

+117
View File
@@ -2744,6 +2744,94 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
---
### 9.44 滚动动画不再受巨型信号影响 + 「N 项」计数的点击语义 + 浮动面板首现位置与开关卡顿(本轮)
> 用户 2026-10-17 第八轮(本机)。四项需求:
> ① 快速点击分组行右侧的「N 项」计数会**进入组名改名** —— 它应只响应展开/收起;
> ② 滚动过渡动画在**巨型信号网格**(`VERY_BIG_DATA`)处卡顿,本不该受"网格内信号是否画好"影响;
> ③ 在下方的网格里开启实验性功能,浮动面板**首次出现在右下角**(应在右上角);
> ④ 面板出现/关闭卡顿(三明治加载时更明显,关面板要等一会才消失)。
> 离屏验证:新增 `.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` 零警告。
#### 一、滚动动画卡顿的**真正根因**:`BlitWideBuffer` 返回 false ⇒ 退化成「整幅重绘」
⚠️⚠️ 上一轮(§9.43)修的是"动画期间的图层重建"与"定时器失效范围",但**没有**解决主要矛盾。
本轮用**逐阶段计时**(临时插桩,测完已删除)定位到:
- 滚动到**从未构建过图层的区域**时,可见网格一个都贴不出像素 ⇒ `BlitWideBuffer()` 返回 `false`
⇒ 落到 `Paint()` 末尾的**整幅重绘** ⇒ 把**本帧全部可见网格的波形**(含千万级采样的巨型信号)每帧重画一遍。
- 实测(MF4 124 网格 + `VERY_BIG_DATA` 5 网格,点选最后一个信号):
单帧 **20~35ms**,且「可见的巨型信号网格数」越多越慢(0 个 ≈ 20ms、5 个 ≈ 34ms);
对照**手动拖滚动条**只要 ~6ms。
- ✓ 修法:`BlitWideBuffer` 只要还能画**网格底衬**就返回 `true`(`backing` 非空):
```cpp
if (blits.empty() && backing.empty()) { return false; } // 连底衬都没得画才交给整幅重绘
```
这正是用户要求的语义:“没来得及画信号时就按**只有网格叠底**的状态继续滚动”。
**最终实测**(同一工况,逐帧):均值 **23.0ms → 11.2ms**、最坏 **35.6ms → 13.1ms**;
含 5 个巨型信号网格的帧 **33ms → 10.7~12.3ms**(与不含巨型信号的帧一致);
手动拖滚动条参照 4.5ms。
#### 二、`WM_TIMER` 帧的「只重画面板」快路径(额外 −2.7ms/帧)
- 面板滚动动画期间 `m_chromeDirty` 会把**标签栏 / 工具栏 / 整窗底色**一起重绘(实测 +2.7ms)。
- ✓ 新增 `Application::m_panelDirty`:滚动定时器在**只有面板动画在跑**时置位,
`Render` 走 `else if (panelOnly)` 只重绘**面板 + 分隔条**(`SignalPanel::Paint` 会铺满自己的区域);
`m_chromeDirty`(真内容变化)仍走完整装饰重绘。
- ⚠️ **回送**仍是 `if (fullFrame) {整窗} else {绘图区}` 的**无条件分支**(遵守 §9.41 的教训:
绝不让"更新区域"决定画什么/回送什么;这里决定的是"重画笔哪一层",且由**改变像素的那条路径**显式置位)。
- 实测帧耗时拆解:`plot 4.8ms` / `plot+panel 11.4ms` / `plot+全装饰 13.9ms`。
#### 三、分组行「N 项」计数:只折叠展开,**绝不**进入改名(用户要求)
- 新增 `RowPart::GROUP_COUNT` 与 `SignalPanel::GroupStatsRect(row)`(**绘制与命中的唯一几何来源**,
命中矩形在文本左右各外扩 3 逻辑像素)。
- `RowHitPart` 在**名称区域之前**判定计数区 ⇒ 点击计数返回 `GROUP_COUNT`:
* `OnLButtonDown`:折叠/展开(与三角、「组X」、名称本体一致);
* `OnDoubleClick`:**只折叠/展开**,任何点击频率都不进入改名;
* `DoubleClickIsPlainPress` 对它返回 **true** ⇒ 快速连点每一击都生效。
- ⚠️ 「快速双击进改名」的计时只对 `RowPart::BODY`(名称本体)记录 —— 否则“点一次计数、紧接着点一次名称”
会被误判成双击。
#### 四、浮动面板**首次出现**的位置:右上角(用户要求)
- 旧默认位置:`left = 右侧 - 16`、`top = plotTop + 16 + laneIndex*28`。
⚠️ **上百个网格时 `laneIndex` 很大** ⇒ `16 + laneIndex*28` 早就超过绘图区高度 ⇒ 被夹到**右下角**
(实测 plotH=1081 时 `topMargin=964`)。
- ✓ 改为按「**当前真正显示出来的面板**中排在本面板之前的个数」错开(固定遍历顺序:网格下标 → 面板种类),
并把错开量夹到“面板下沿不超过绘图区 45% 高度”以内:
* 第一个面板 ⇒ 错开 0 ⇒ **正好贴在上右角**(实测 `topMargin=24`、`rightMargin=24`);
* 依次下移 28 逻辑像素,6 个面板时最坏 `topMargin=234`(仍在顶部 22%)。
- ⚠️ 判定「面板是否显示」**不能只看 `*Panel.open`** —— `LanePanelState::open` **默认就是 true**,
必须同时看功能是否启用(与 `EdgePanelLayoutFor` 的 `enabled && open` 一致)。
#### 五、开关实验性功能 / 关闭面板卡顿的根因:`RebuildWideBuffer()` 整批重建
- ⚠️⚠️ 四个 `SetLane*`(边沿计数 / 中心轴 / 均值滤波 / 弱峰调整)在末尾都调 `RebuildWideBuffer()`
(注释说是“预建宽缓冲基底”),而它**同步重渲染全部网格的图层**:
129 个网格(含 5 个千万级采样的巨型信号)整批重建要**几百毫秒**
⇒ 点一下功能开关 / 关闭浮动面板就会“卡住一段时间”(用户报告,三明治加载时更明显)。
- ✓ 新增 `PlotView::EnsureLaneLayersPrefetched()`:
* **只有**「图层数组与网格数量不符」(首次绘制 / 增删网格)才同步整批建立;
* 其余只 `RollWideBuffer()` 把缺失 / 过期的网格登记进待重画队列,由既有预算(每帧最多一个)逐帧补齐。
这已足够达成原本目的(拖参数滑块时视界在变 ⇒ 本就不重建、直接用现有基底)。
- 实测(关/开面板,网格内含巨型信号):**几百毫秒 → 22ms 以内**;巨型信号网格上开关均值滤波同样 15ms 量级。
#### 六、⚠️ 方法论教训
1. **“修完感觉没变”时要重新插桩定位,而不是继续猜**:上一轮改的两处(动画期禁重建、定时器失效范围)
合计只省了 ~8ms,真正的 20ms 来自 `BlitWideBuffer` 退化成整幅重绘。
2. **逐阶段计时(临时插桩)比"改代码做 A/B"快得多**:在 `Paint` 里插 5 个 `QueryPerformanceCounter`
就定位到"宽基底分支内部几乎不耗时 ⇒ 一定是走了别的分支"。
3. **`LanePanelState::open` 默认 true** 这类"默认值"极易让统计类判据出错(本轮“已打开面板数”一度算成 8)。
4. **面板首现位置用 `laneIndex` 做错开量**是典型「小数据量下正确、上百网格时崩掉」的写法;
任何与 `laneIndex` 线性相关的屏幕坐标都必须重新审视。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -2806,6 +2894,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 2026-10-17 | DeepSeek-V4.1-Flash(**本机**) | **滚动过渡动画根治 + 快速连点跟手 + 分组名单击展开 + 光标吸附同源 + 按钮淡出对齐 + 滚动位置复位(详见 §9.43)**。① **滚动动画卡顿(三处根因)**:**(a)** `WM_TIMER(kScrollTimerId)` 无条件整窗失效 ⇒ `WM_PAINT` 把 `m_chromeDirty` 置真 ⇒ **每帧重绘标签栏/工具栏/面板**(绘图区动画帧 13.6→**3.5ms**;手动拖滚动条参照 4.2ms);**(b)** 图层预热不足 ⇒ 长距离滚动扫进未建图层的网格、每帧靠底衬重画十几份网格(+8ms/帧)⇒ 可视判定加 **±1 屏预取带** + `PrefetchStepAllowed()` 限速后台预热(`lane-only+VERY_BIG_DATA` 13.9→**5.3ms**);**(c)** 网格滚动量参与宽基底可用性判据(上一轮已修)。② **光标吸附改用「自动点线」同一判定**:`AutoPointLineNeeded` 改名 `MinorCellSparse`,吸附与自动点线共用;判定**不看绘制方式** ⇒ 用户改过样式也吸附;删除旧整屏判据 `SignalSnapEnabled`。③ **「回到最上方」按钮淡出与浮动提示一致**:`kBackToTopFadeOutMs = kHintFadeMs`(500ms) + **线性** alpha + 到达顶部立即淡出(实测 50% 时刻 alpha=0.500,旧 ease-out 为 0.875)。④ **快速连点跟手**:`CS_DBLCLKS` 会把快速连点的第 2 次按下合成为 `WM_LBUTTONDBLCLK`,旧实现只给工具栏打补丁 ⇒ 复选框/全部显示隐藏/绝对值/组X/折叠三角/浮动面板复选框的按下**丢失**;新增 `DoubleClickIsPlainPress(x,y)`(`PlotView` + `SignalPanel`),宿主据此选择走 `OnLButtonDown`。⑤ **分组行:名称恢复单击展开收起**(推翻 §9.37 ①),只有**快速双击**(`kGroupRenameDoubleClickMs=300`,比系统 500ms 紧)才进入改名,且进入前**撤销**按下那次的折叠切换。⑥ **信号重现时网格滚动回到最上方**(`SynchronizeView` 首次出现可见信号分支里 `LaneScroll()=0`)。验证:`gridanim` **93/93**(真实消息循环帧距 + 逐帧耗时对比 + 行为断言)、`verify` 49/49、`panelanim` **158/158**(按新规则更新 8 条断言)、`datacheck` 69/69、`edgecheck` 46/46、`errcheck` 19/19;`make release`/`make debug` 零警告。§11.2 新增第 125–133 条。
| 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 条。 |
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -3456,6 +3546,33 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
133. ⚠️ **动画性能 A/B 必须在同一二进制、同一起始状态下对比**:靠“改代码重编”做 A/B 会被
**两次运行之间不同的预热状态**污染(本轮因此得出过“底衬兜底 +8ms”的误导结论);
应加**运行时开关**在**同一次运行**内切换。
134. ⚠️⚠️ **`BlitWideBuffer()` 只要还能画网格底衬就必须返回 true,绝不能因为“一个网格都贴不出图层像素”
就退化成整幅重绘**:滚动到从未建过图层的区域时这是必现情形,退化成整幅重绘会把**本帧全部可见网格的
波形**(可能含千万级采样的巨型信号)每帧重画一遍 —— 实测单帧 **20~35ms**(用户 2026-10-17 报告:
滚动动画在巨型信号处卡顿,“本不该受网格内信号绘制的影响”)。
✓ 只有 `blits` 与 `backing` **都为空**时才返回 false。这正是“没来得及画信号就按只有网格叠底的状态继续滚动”。
实测:动画均值 23→**11.2ms**、最坏 35.6→**13.1ms**;含 5 个巨型信号网格的帧 33→**~11ms**。
135. ⚠️ **面板滚动动画期间只重绘面板,不要重绘标签栏/工具栏/整窗底色**:
由滚动定时器置 `Application::m_panelDirty`,`Render` 走 `else if (panelOnly)` 分支
(`SignalPanel::Paint` 会铺满自己的区域)。实测 −2.7ms/帧。
⚠️ 这不是 §9.41 禁止的“按更新区域决定画什么”:**像素由谁改变,就由那条路径显式置位**;
**回送**仍是 `if (fullFrame) {整窗} else {绘图区}` 的无条件分支。
136. ⚠️ **`LanePanelState::open` 的默认值是 `true`**:任何“哪些面板正在显示”的统计都必须同时检查
功能是否启用(`lane.edgeCount / edgeCenter / meanFilter / weakPeak`),否则会把**所有**网格都算成“已打开”。
137. ⚠️ **浮动面板首次出现的位置必须锚定在右上角**:旧实现用 `top = plotTop + 16 + laneIndex*28` 错开,
上百个网格时 `laneIndex` 很大 ⇒ 被夹到**右下角**(用户 2026-10-17 报告)。
✓ 改用「当前真正显示出来的面板中排在本面板之前的个数」错开,并把错开量夹到
“面板下沿不超过绘图区 45% 高度”以内 ⇒ 第一个面板正好上右角(`topMargin/rightMargin = 16*scale`)。
⚠️ 任何与 `laneIndex` 线性相关的屏幕坐标都必须重新审视。
138. ⚠️⚠️ **`SetLane*`(实验性功能开关)末尾绝不能调用 `RebuildWideBuffer()`**:它**同步重渲染全部网格图层**,
129 个网格(含千万级采样的巨型信号)整批重建要**几百毫秒** ⇒ 点一下开关 / 关闭浮动面板就“卡住一段时间”
(用户 2026-10-17 报告)。✓ 用 `EnsureLaneLayersPrefetched()`:只有图层数与网格数不符才整批建立,
其余只登记进待重画队列、由既有预算(每帧最多一个)逐帧补齐。实测几百毫秒 → **≤22ms**。
139. ⚠️ **分组行的「N 项」计数是独立热区 `RowPart::GROUP_COUNT`**(`GroupStatsRect` 为唯一几何来源):
**任何**点击频率都只折叠/展开、绝不进入改名(用户 2026-10-17 要求)。
⚠️ 「快速双击进改名」的计时只对 `RowPart::BODY` 记录 —— 否则“点计数、紧接着点名称”会被误判成双击。
140. ⚠️ **“修完感觉没变”时要重新插桩定位**:本轮在 `Paint` 里插 5 个 `QueryPerformanceCounter` 才定位到
“宽基底分支内部几乎不耗时 ⇒ 一定走了别的分支(整幅重绘)”。逐阶段计时比“改代码做 A/B”快得多。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)