网格底衬永不空白 + 动画流畅性根治 + 光标随首个信号重现 + 「放大自动点线」新判据

一、网格底衬消失(半屏/局部/全屏,且要等一会才刷新好)—— 两个机制叠加
1) BlitWideBuffer 对「跨度已过期」的图层原用 StretchBlt 放大占位:深度放大时把一小段像素放大
   几十倍 ⇒ 整块网格变成一片无网格的模糊色块(用户看到的就是「网格叠底消失、没有任何内容」)。
   修法:删除占位拉伸;对拿不到(完整)图层像素的网格【就地补画网格底衬】
   (新增 PaintLaneGridBacking + m_renderGridOnly,复用 PaintLane 的网格部分,跳过波形/散点/
   派生曲线/Y 轴刻度/图例)。底衬代价极低,可每帧执行 ⇒ 网格永不空白/闪烁。
   ⚠️ 顺序:先画底衬(GDI+)再 g.GetHDC() 做 BitBlt;只有全部网格都贴不出像素时才交给整幅重绘。
2) 待重画队列按下标顺序消费(一次 1 个)⇒ 上百个网格时可视网格要等上百帧。
   修法:RenderPendingLaneRebuilds 里做稳定划分,【可视网格优先】(LaneVisuallyRelevant);
   「结算」判据由 LanesAllReadyForView() 改为 LanesVisiblyReadyForView()。

二、过渡动画卡顿/不平顺/直接闪到终态 —— 三处根因
1) 「m_layerRebuildPending 非空就每帧请求再送一帧」:上百个网格时 700ms 内挤出 124 次重绘,
   而 WM_TIMER 优先级低于投递消息 ⇒ 动画定时器被饿死(实测 frames=124 / timerTicks=1)。
   修法:新增 AnyVisibleLanePending(),只用「还有没有可视网格待补」决定是否请求再送一帧;
   离屏网格改为机会式预热(实测 frames=25 / timerTicks=16)。
2) SetTimer 的间隔会被量化到系统 tick(默认 15.6ms):请求 16ms 实际等到下一个 tick
   ⇒ 实测帧距 ~26ms(只有约 38fps)。修法:kScrollTimerPeriodMs 16 → 10(实测 ~15.5ms ≈ 64fps;
   真实消息循环帧距 ~25ms → ~16ms)。实测映射:8→15.8 / 10→15.5 / 12→16.5 / 16→26.4 / 31→37.9。
   ⚠️ timeBeginPeriod(1) 在本机无效(已 A/B 验证)。
3) 网格滚动联动 / 「回到最上方」每帧都改 LaneScroll(),而它原本参与「宽基底可用性」判据
   (SceneGeometryHash)⇒ 每一帧都退化成整幅重绘。修法:SceneContentHash 新增 includeLaneScroll 形参,
   宽基底可用性判据不计网格滚动量/各网格屏幕纵向位置(它们只是图层的贴图落点);
   静态场景缓存的指纹仍然计入。实测网格滚动动画单帧 18.36ms → 1.19ms。

三、光标工具激活且没有任何信号绘制时,首个信号出现后按规则重新出现(用户要求)
新增 PlotView::ResetCursorPlacementOnFreshContent()(规则与 CycleCursorMode() 的「首次出现」同源:
单光标置于视图中心;双光标第二条紧邻第一条且落在可见范围内),由 Application::SynchronizeView
的「首次出现可见信号」分支在 FitAll() 之后调用。
实测:单光标 stale=4.60 → 新的视图中心 20.92;双光标 stale=(2.89,7.18) → (20.92,26.07)。

四、「放大自动点线」规则改为:任一小格内数据点少于 5 个即自动切换(用户重新定义)
新增 PlotView::AutoPointLineNeeded()(推翻此前「一屏内数据点 < N×4」):按 GridMinorCellCount()
切成横向小格,只要存在任何一个小格内有效数据点 < kAutoPointLineMinPointsPerCell(=5) 就切「点线」。
实现:先「总有效点 < N×5」鸽巢 O(1) 充分判定;单调时间轴逐小格两次二分取区间(单格累计到 5 个即早退,
另给 kAutoPointLineCellScanCap=512 封顶);非单调时间轴一次有界扫描分桶。保留「视界内至少一个可见点」保护。
⚠️ 光标吸附仍用旧的整屏判据 SignalSnapEnabled(用户明确要求「其它规则不变」)。

验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/perf/gridanim_main.cpp(gridanim.exe)55/55:
  [A] 缩放/适应视图/「预热未完成再滚动」等每个用例逐帧断言「每个可视网格都有网格底衬
      (有竖直锐边、无模糊占位、无未绘制带)」;[B] 直接驱动 + 真实消息循环两条路径统计每帧耗时、
      帧距均值/最大与进度单调性;[C] 光标重现;[D] 自动点线新判据(稠密不切、稀疏切换、恢复原样式)。
- 既有回归:verify 49/49、panelanim 156/156、datacheck 69/69、edgecheck 46/46、errcheck 19/19。
- make release 零错误零警告(使用 TARGET_FILE_NAME=TargetCheck 以避免与用户正在运行的实例冲突)。

文档:Documents/agent_mem.md 新增 §9.42、§11.2 第 117–124 条与变更日志一行。
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-10-17 00:56:19 +08:00
1 parent f845616938
commit 34375481de
7 files changed
+702 -168

No files matched your search

@@ -120,6 +120,10 @@ namespace jdi
// 首次出现可见信号时做一次自适应。单点信号(时间/幅值范围退化成 0)由
// `FitAll` 内部按"单值散点"的规则处理(居中 + 一大格 1s),这里不再分开判断。
m_plot.FitAll();
// 用户 2026-10-17 要求:光标工具处于激活状态(单 / 双光标)而之前**没有任何信号绘制**时,
// 第一个信号出现后光标要按“光标出现的规则”重新出现一次,而不是停在旧的残留位置。
// 规则与 `CycleCursorMode()` 首次启用完全同源;光标工具未激活时本调用无副作用。
m_plot.ResetCursorPlacementOnFreshContent();
}
const bool laneCountChanged = (laneCount != previousLaneCount);
@@ -74,7 +74,16 @@ namespace jdi
* 按经过时间推进,这里只负责按帧驱动它(约 60fps)。
*/
inline const UINT_PTR kScrollTimerId = 3;
inline const UINT kScrollTimerPeriodMs = 16;
/**
* @brief 信号 / 绘图区动画的定时器周期(毫秒)。
*
* ⚠️⚠️ **必须是「不超过一个系统 tick」的值**:`SetTimer` 的间隔会被量化到系统 tick
*(默认 15.6ms)的整数倍上,因此请求 16ms 实际会等到**下一个** tick ⇒ 实测帧距
* **≈ 26ms(只有约 38fps)**,动画明显卡顿(用户 2026-10-17 报告:所有过渡动画都不平顺)。
* 请求 10ms 时量化结果正好是一个 tick ⇒ 实测帧距 **≈ 15.5ms(约 64fps)**,
* 回到“约 60fps 平滑过渡”的设计目标。
*/
inline const UINT kScrollTimerPeriodMs = 10;
/**
* @brief 查询本进程当前占用的内存(**私有工作集**,与任务管理器"内存"列同口径)。