Compare commits

..
2 Commits
3 changed files with 828 additions and 333 deletions

No files matched your search

+159 -63
View File
@@ -137,6 +137,18 @@ namespace jdi
*/
void SetHostWindow(HWND host) { m_host = host; }
/**
* @brief 后台「网格图层」绘制线程已完成的图层数量(仅供验证 / 诊断)。
*
* 计数 > 0 即说明单网格基底的绘制确实发生在**后台线程**,
* 前台只负责位图搬运(贴图)与叠加层。
* @return 累计完成的图层数量。
*/
std::uint64_t BackgroundLayerRenderCount() const
{
return m_layerRenderCount.load(std::memory_order_relaxed);
}
/**
* @brief 绑定字体与主题资源。
* @param [IN] fonts 字体集合,需在视图生命周期内保持有效。
@@ -2154,19 +2166,23 @@ namespace jdi
*/
bool m_sceneDegraded = false;
// ---- 「装饰层」像素缓存(水平平移时不变的内容:Y 轴刻度胶囊 + 图例)----
// 平移会让视图每帧变化 → 静态场景缓存必然失效 → 每帧都要重画波形**以及**网格线、
// Y 轴刻度文字、图例(实测后者占整幅重绘的一半以上)。但这些装饰内容**只取决于网格几何与
// 信号外观,与时间轴视图无关**,平移时不变化。因此在每张「全质量帧」后把装饰区域的像素
// 捕获下来,拖拽(快速帧)时直接贴回,省去每帧重绘文字与圆角底板的开销。
// 仅在完全相同(`m_decorKey` 命中)时启用,避免几何/外观变化时用到过期像素。
// ---- 「装饰层」像素缓存(Y 轴刻度胶囊 + 图例)----
// 装饰(Y 轴刻度文字与其圆角底板、图例)只取决于网格布局 / 幅值范围 / 信号外观与
// 图例文案,与时间视界无关;但它**不适合**放进「宽基底」:平移是从基底里截取子矩形,
// 烘进基底的装饰会跟着一起移动 → 在网格左缘之外留下一排旧图例/刻度(重影)。
// 因此单独缓存一帧**按当前视界绘制**的装饰像素,并在内容指纹完全一致时整块贴回。
//
// ⚠️ 关键不变量:**每一帧“真正绘制了装饰”都要重新捕获**(无论是否快速帧)。
// 只要保证“缓存像素”与“缓存指纹”永远同源,贴回就不可能过期;
// 反之(例如只在全质量帧捕获)会在几何/外观变化后贴回旧像素,
// 表现为残留 / 黑块 / 错位等错误元素。
/**
* @brief 确保装饰缓存位图尺寸正确(不足则重建)。
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
* @return 可用返回 true。
*/
bool EnsureDecorCache(int width, int height);
/** @brief 释放装饰缓存位图。 */
/** @brief 释放装饰缓存位图并作废缓存。 */
void ReleaseDecorCache();
/**
* @brief 把本帧记录下来的装饰矩形从后备缓冲区捕获进装饰缓存。
@@ -2174,7 +2190,7 @@ namespace jdi
*/
bool CaptureDecorCache(gd::Graphics& g);
/**
* @brief 把装饰缓存里的装饰贴回后备缓冲区(快速帧用)。
* @brief 把装饰缓存里的装饰贴回后备缓冲区(本帧跳过了装饰绘制时使用)。
* @param [IN,OUT] g 绘图上下文。 @return 贴图成功返回 true。
*/
bool BlitDecorCache(gd::Graphics& g);
@@ -2187,77 +2203,157 @@ namespace jdi
mutable bool m_decorValid = false; /**< 装饰缓存内容是否有效。 */
mutable std::uint64_t m_decorKey = 0; /**< 装饰缓存对应的内容指纹。 */
mutable bool m_useDecorCache = false; /**< 本帧是否跳过绘制装饰、改用缓存贴回。 */
/** 本帧需要捕获 / 贴回的装饰矩形(各网格的 Y 轴胶囊与图例带)。 */
/** 本帧需要捕获的装饰矩形(各网格的 Y 轴胶囊与图例带)。 */
mutable std::vector<gd::RectF> m_decorRects;
// ---- 平移「宽缓冲区」:平移只改变绘制坐标、不改变画面内容 ----
// 视界平移时整个波形并不改变,只是横向平移。因此以当前视界为中心,向左右各多渲染
// 半个视界宽(位图宽度 = 视界宽 ×2),保存为一张**更宽**的离屏位图;平移时前台只需
// 从中**截取对应的横向子矩形贴图**(O(1),一次 BitBlt),而不必每帧重算重画。
// 当视界移动到该缓冲区边缘(超出已渲染范围)时,才以新视界为中心重建一次。
/**
* @brief 确保宽缓冲区位图尺寸正确(不足则重建)。
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
* @return 可用返回 true。
* @brief 缓存里那批像素对应的装饰矩形(捕获时与像素同时保存)。
*
* ⚠️ 贴回必须用这一份,**不能**用当前的 `m_decorRects`:
* 命中缓存时各网格会跳过绘制、根本不会重建矩形列表(为空),
* 若用当前列表就会贴错位置或一个都不贴(表现为装饰消失 / 黑块 / 错位)。
*/
bool EnsureWideBuffer(int width, int height);
/** @brief 释放宽缓冲区位图。 */
void ReleaseWideBuffer();
mutable std::vector<gd::RectF> m_decorCapturedRects;
/**
* @brief 以当前视界为中心重建宽缓冲区(离屏渲染一张更宽的图像)。
* @brief 捕获缓存像素时使用的绘图区原点(`m_area` 左上角)。
*
* 渲染时临时把绘制区域的横向映射"加宽",因此调用的是与正常绘制**完全相同**的
* `PaintLane`/`PaintTimeAxis`,保证像素与正常绘制一致;仅**跳过图例与 Y 轴刻度**
*(它们锚定在网格左缘、随视界移动,需由前景叠加层绘制)。
*
* @return 重建成功返回 true。
* ⚠️ 贴回时**必须用这一份**推算源偏移:宽缓冲区渲染会临时改写 `m_area`,
* 若捕获与贴回两帧的 `m_area` 不同,直接用当前 `m_area` 作原点会让整块装饰错位。
*/
mutable int m_decorOriginX = 0;
mutable int m_decorOriginY = 0;
// ---- 每网格「信号图层」:双缓冲 + 后台绘制(前台只做贴图 / 坐标偏移) ----
//
// 平移只改变绘制坐标、不改变画面内容。因此**每个网格各自维护一块离屏位图**
// (宽度 = 视界宽 ×3:左右各多渲染半个视界),称为该网格的「信号图层」。
// 图层只含基底(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**(Y 轴刻度 / 图例)——
// 装饰锚定在网格左缘、不随时间视界移动,若烘进图层会被平移带走(重影),
// 因此装饰由前台单独绘制(见 `m_decorCapturedRects` 那套缓存)。
//
// **双缓冲**:每个网格有两块图层(`m_layerFront` 前台读取 / `m_layerBack` 后台填充),
// 后台绘制完成后再整体交换。前台**永远只读 `m_layerFront`**,因此后台正在绘制时
// 前台仍能拿到上一份可用像素,绘制过程不影响交互响应。
//
// **后台绘制**:跨度 / 几何 / 外观变化时,后台线程池把各网格的新图层画进后台缓冲;
// 若前台已经要刷新而后台还没画好,就继续用旧的前台缓冲「应付」交互,不解切换。
/**
* @brief 单个网格的「信号图层」(一块只含该网格基底的离屏位图)。
*/
struct LaneLayer
{
HDC dc = nullptr; /**< 离屏 DC。 */
HBITMAP bitmap = nullptr; /**< 离屏位图。 */
HGDIOBJ oldBitmap = nullptr; /**< DC 原选位图。 */
int width = 0; /**< 位图宽度(含左右各半屏余量)。 */
int height = 0; /**< 位图高度(= 网格高)。 */
double wideMin = 0.0; /**< 位图 x=0 对应的时间。 */
double span = 0.0; /**< 渲染时的视界跨度(决定像素/时间)。 */
std::uint64_t key = 0; /**< 内容指纹(含跨度、网格几何与外观)。 */
bool valid = false; /**< 是否有可显示的像素。 */
};
/** @brief 确保某网格图层位图尺寸正确(不足则重建 DC/位图)。 */
bool EnsureLaneLayer(LaneLayer& layer, int width, int height);
/** @brief 释放单个图层位图。 */
void ReleaseLaneLayer(LaneLayer& layer);
/** @brief 按当前视界与内容计算某网格图层的内容指纹。 */
std::uint64_t LaneLayerKey(int laneIndex) const;
/**
* @brief 把某网格的基底渲染进指定图层(同步,调用者需持有 `m_renderMutex`)。
* @param [IN] laneIndex 网格下标。
* @param [IN,OUT] layer 目标图层(会被写入尺寸 / 时间映射 / 指纹)。
* @param [IN] stripX 只渲染位图的这一段 x 区间(滚动时补画细条用)。
* @param [IN] stripW 该区间的宽度;传 0 表示渲染整块。
* @return 渲染成功返回 true。
*/
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW);
/**
* @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧时使用)。
* @return 全部成功返回 true。
*/
bool RebuildWideBuffer();
/**
* @brief 若当前视界落在宽缓冲区内,则截取子矩形贴图到后备缓冲区。
* @brief 让全部图层"滚动"以跟随视界:整数像素自贴图平移 + 只补画新暴露的细条。
* @return 图层仍可用返回 true。
*/
bool RollWideBuffer();
/**
* @brief 逐个网格截取图层子矩形贴图到后备缓冲区(O(1) 位图搬运)。
* @param [IN,OUT] g 绘图上下文。
* @return 命中并贴图返回 true(调用方随后只画叠加层即可)。
* @return 至少贴了若干网格返回 true。
*/
bool BlitWideBuffer(gd::Graphics& g) const;
/**
* @brief 让宽缓冲区"滚动"以跟随视界:按整数像素平移已渲染内容,只补画新暴露的细条。
* @brief 全部图层是否已就绪且跨度与当前视界一致(**不要求内容也一致**)。
*
* 视界每帧移动 d 像素时,把缓冲区内容整体挪动 `round(d)` 像素(自贴图),
* 再只渲染新暴露出来的 `|round(d)|` 像素宽细条。这样**每帧代价恒为 O(细条)**,
* 不会像"移出边界就整幅重建 2×视界"那样出现周期性大尖峰。
* 用一个亚像素残差累加器把"内容相对时间轴的偏移"始终限制在半个像素内。
*
* @return 缓冲区仍可用返回 true。
* 内容一致与否由后台线程负责跟踪:只要跨度一致、图层能覆盖当前视界,
* 前台就直接用现成像素“应付”交互(内容稍旧的一两帧由后台画好后交换),
* 这样样式/参数变化也不会退化成整幅重绘。
* @return 可以直接贴图层像素返回 true。
*/
bool RollWideBuffer();
/** @brief 渲染宽缓冲区中因滚动而新暴露的一条细条(宽 |shiftPixels|)。 */
void RenderWideStrip(int shiftPixels);
/**
* @brief 用「上一帧的静态场景缓存」初始化宽缓冲区(平移首帧用,避免从零重画)。
*
* 平移时画面只是整体横向移动,场景缓存里的内容(含派生曲线/图例)**依然正确**;
* 因此把场景缓存的绘图区直接贴进宽缓冲中心即可,随后滚动到当前视界。
* 仅当"视界相对场景缓存发生了平移"(跨度一致)时可用。
*
* @return 初始化成功返回 true。
*/
bool InitWideBufferFromScene();
/** @brief 宽缓冲区内容是否仍与当前视图/内容一致。 */
bool WideBufferMatches() const;
/** @brief 使宽缓冲区失效。 */
bool LayersReadyForView() const;
/** @brief 使全部图层失效。 */
void InvalidateWideBuffer();
/** @brief 释放全部图层位图(前后台都释放)。 */
void ReleaseWideBuffer();
/**
* @brief 为"内容与前台图层不一致"的网格排一次后台渲染任务(去重)。
*
* 后台渲染完成后会与前台缓冲交换并通知宿主重绘,前台因此始终有立即可用的像素。
*/
void ScheduleStaleLayerRenders();
HDC m_wideDc = nullptr; /**< 宽缓冲区设备上下文。 */
HBITMAP m_wideBitmap = nullptr; /**< 宽缓冲区位图(比视界更宽)。 */
HGDIOBJ m_wideOldBitmap = nullptr; /**< 宽缓冲 DC 原选位图。 */
int m_wideW = 0; /**< 宽缓冲区位图宽度(像素)。 */
int m_wideH = 0; /**< 宽缓冲区位图高度(像素)。 */
double m_wideMin = 0.0; /**< 宽缓冲区最左侧对应的时间。 */
double m_wideAnchorMin = 0.0; /**< 视界左端当前在宽缓冲中的锚点时间(滚动基准)。 */
double m_wideSpan = 0.0; /**< 宽缓冲区制作时的视界跨度。 */
bool m_wideValid = false; /**< 宽缓冲区内容是否有效。 */
std::uint64_t m_wideKey = 0; /**< 宽缓冲区对应的内容指纹(外观/派生参数等)。 */
bool m_renderWide = false; /**< 正在渲染宽缓冲区(重入 `PaintLane` 时跳过装饰)。 */
/** @brief 确保后台图层绘制线程已启动。 */
void EnsureLayerWorker();
/** @brief 停止后台图层绘制线程。 */
void StopLayerWorker();
/** @brief 后台图层绘制线程主循环。 */
void LayerWorkerLoop();
/** 单个网格图层任务:只需网格下标(参数在渲染时按最新状态取)。 */
struct LayerJob
{
int laneIndex = -1;
std::uint64_t key = 0;
};
std::vector<LaneLayer> m_layerFront; /**< 前台读取(后台不会就地改写)。 */
std::vector<LaneLayer> m_layerBack; /**< 后台填充。 */
mutable std::mutex m_layerMutex; /**< 保护前后台切换 / 元数据读取 / 锚点(const 方法也要读,故 mutable)。 */
std::recursive_mutex m_renderMutex; /**< 串行化“改写渲染状态”的绘制(前台整帧 vs 后台单网格);可重入(整帧内部会再调图层渲染)。 */
double m_layerAnchorMin = 0.0; /**< 图层滚动锚点:视界左端对应的时间。 */
bool m_layerAnchorValid = false; /**< 锚点是否有效。 */
std::thread m_layerWorker; /**< 后台图层绘制线程。 */
std::mutex m_layerJobMutex; /**< 任务队列互斥。 */
std::condition_variable m_layerJobCv; /**< 任务到来 / 退出通知。 */
std::vector<LayerJob> m_layerJobs; /**< 待渲染的网格任务。 */
bool m_layerWorkerStop = false; /**< 请求退出后台线程。 */
/**
* @brief 连续排了多少次"图层重建"却始终没与前台一致。
*
* 正常情况下一两次就会收敛(后台画完→交换→指纹一致)。若持续增长,说明后台渲染
* 算出的指纹与前台不一致(例如某个参与指纹的状态没被正确还原),此时改为**同步重建**
* 一次强行收敛,避免"前台永远不等后台、后台永远画不好"的死循环。
*/
int m_layerRescheduleStreak = 0;
/** 前台最近一次 `Paint` 的时刻(毫秒,`GetTickCount64`)。后台据此"让路",不与前台抢渲染锁。 */
mutable std::atomic<unsigned long long> m_lastPaintTick{0};
/** 后台线程已完成绘制的图层数量(原子,仅用于验证/诊断)。 */
mutable std::atomic<std::uint64_t> m_layerRenderCount{0};
bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */
/**
* @brief 本次 `PaintLane` 只绘制「装饰」(Y 轴刻度胶囊 + 图例 + 网格分界线)。
*
* 宽缓冲区只存「基底」(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**:
* 因为装饰锚定在网格左缘、随视界横向移动,若烘进基底,平移截取时会产生重影。
* 因此前台每帧用本标志重画一次装饰(复用与整幅渲染**完全相同**的绘制代码,
* 保证逐像素一致),而不是去贴回任何缓存像素。
*/
bool m_renderDecorOnly = false;
/** 上一次走"宽缓冲快路径"时的内容指纹(含视界)。当前帧指纹与之相同 → 视界已停止变化,
* 此时改走一次整幅重绘("结算"),保证最终呈现的是**像素精确**的全质量帧。 */
std::uint64_t m_wideLastKey = 0;
File diff suppressed because it is too large. Load diff
+107
View File
@@ -385,6 +385,111 @@ Documents/ 项目文档(含本文件)
---
### 9.15 修复「缩放后平移出现拖影/残留/错误元素」与深放大区间仍然卡顿(本轮)
#### 用户方案评估:与当前实现的差异
用户提出的目标是「每个网格各自维护双缓冲区 + 后台线程绘制 + 前台只做贴图/坐标偏移」。
与当前实现的差异与问题点:
| 维度 | 当前实现 | 用户设想 |
| --- | --- | --- |
| 像素复用粒度 | **全局一张**「宽基底」+ 场景缓存 + 装饰缓存 + 样式补丁(4 套机制叠加) | **每网格**一张信号图层(双缓冲) |
| 绘制线程 | 全部在 UI 线程;只有**派生预览曲线**在后台算 | 单网格绘制也搬到后台 |
| 失效判定 | 多个指纹(内容/几何/装饰)各自为政,**容易漏项**(本轮事实上踩了坑) | 每网格一个状态指纹 |
问题点(本轮实证,是本次卡顿与拖影的**真正根因**):
1. **`Tab().LaneScroll()`(网格纵向滚动量)没有参与任何内容指纹。**
它只改变各网格的绘制位置,却被所有指纹忽略 → 一旦滚动网格(滚轮 / 上下方向键 / 滚动条),
平移宽缓冲区会把**旧滚动位置**的像素当成当前内容贴回(位置错位、拖影、错误元素);
同时因为指纹没变,"结算"逻辑误以为视界已停 → **每两帧就触发一次整幅重绘(约 125ms)**,
表现为 8fps 级卡顿。这是用户报告的**两个症状的共同根因**。
2. **`FindStyleOnlyLanes()` 只看 `SceneGeometryHash()`,而该指纹刻意不含视界。**
于是视界已经变了却仍走"只重画受影响网格"的局部补丁,把缓存里**旧视界的画面**
(时间轴、其它网格、底色)留在屏上 → 拖影 / 残留。
3. **`InitWideBufferFromScene()` 把"上一帧的整幅画面"拷进宽基底**,而整幅画面里**含装饰**
(Y 轴刻度胶囊 + 图例),宽基底却不含 → 装饰像素随平移一起移动,在网格左缘之外
留下一排旧图例/刻度(重影)。
4. **装饰缓存的两个不变量被打破**:贴回时用的是**当前帧重建的**矩形列表(命中缓存时根本不会重建 → 为空);
并用**当前帧的** `m_area` 原点去推算源偏移(两帧原点可能不同)→ 表现为装饰消失 / 黑块 / 整体错位。
#### 本轮修复
- `SceneContentHash()` 计入 `Tab().LaneScroll()`(根本修复:滚动网格不再复用旧位置像素,
也不再误触发"结算")。
- `FindStyleOnlyLanes()` 增加**视图几何逐项校验**(视界 / 滚动 / 布局 / 缩放 / 拖拽光晕)后才允许局部补丁。
- 宽基底**不再用"含装饰的整幅画面"初始化**;`RebuildWideBuffer()` 以 `m_renderWide` 跳过装饰,
保证基底永远不含装饰。
- 宽路径的装饰改为**按当前视界重画**(新增 `m_renderDecorOnly` 复用与整幅渲染**完全相同**
的 `PaintLane` 代码路径,只跳过网格线与波形)。
- 装饰缓存修好两个不变量:**每次真正绘制装饰的帧都立即重新捕获**(不再只在全质量帧捕获),
且捕获时**同时保存矩形列表与坐标原点**,贴回时用保存的这份、按绝对坐标写入。
- 装饰底板改为**不透明**(原来是 94% / 91%):装饰像素因此与时间视界无关,可安全跨视界复用;
半透明底板会把旧视界的波形混进缓存像素,平移时在底板内部留下残影。
- 新增离屏诊断模式(`.preflight/perf/perf_main.cpp` 的 `mode=6`):复现"加载 → 缩放 → 平移/键盘/滚轮"
全部交互,逐帧计时,并把"当前屏"与"从零整幅渲染"做**逐像素对比 + 差异包围盒**。
这是本轮定位问题的关键工具(否则只能靠猜测)。
#### 实测(`VERY_BIG_DATA.CSV`,视界 `[64.3948, 64.4153]s` ≈ **2.045ms 一大格**,5 网格,ms/帧)
| 交互 | 修复前 | 修复后 | 像素差(对比从零整幅渲染) |
| --- | --- | --- | --- |
| 键盘左右方向键平移 | 11~19ms | **5.2ms** | 3705 通道(≤0.5px 亚像素偏移,见下) |
| 键盘上下方向键滚动网格 | 0.9ms(但错位/偶发 125ms) | **0.5ms** | **0** |
| 中键水平拖动 | 10~18ms | **5.2ms** | 357 通道 |
| 中键**纵向**拖动 | 14~19ms | **6.0ms** | 72 通道 |
| 左键水平拖动 | 9~17ms | **4.5ms** | 390 通道 |
| 左键**纵向**拖动 | 15~19ms | **6.1ms** | 72 通道 |
| 滚轮滚动网格 | 1.0ms | **0.6ms** | **0** |
| 缩放后再平移 | 9~17ms | **3.7ms**(缩放帧 6.8ms) | 495 通道 |
| 弱波增强预览 + 平移 | 15~18ms | **5.8ms** | 2264 通道(异步曲线发布时序) |
- **修复前的纵向拖动会产生 2 万+ 通道的残留**(黑块 / 标签残缺);修复后降到 72 通道(仅亚像素量化)。
- 剩余的几百通道差异集中在**密网格/密波形**上,是"宽基底按整数像素平移(≤0.5px 亚像素误差)"
的必然结果,肉眼不可辨;视界停下后的"结算"帧会回到**像素精确**。
#### 「每网格双缓冲 + 后台绘制」架构(已落地,本轮同批提交)
把原来**全局一张**「宽缓冲区」重构为用户提出的方案:
- **每网格一块「信号图层」**(`LaneLayer`):宽度 = 视界宽 ×3(左右各多画半屏),只含该网格基底
(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**(Y 轴刻度 / 图例,它们锚定网格左缘、不随时间视界移动)。
- **双缓冲**:`m_layerFront`(前台只读)/ `m_layerBack`(后台填充)。后台画完整块后
`std::swap` 整体交换;**前台永远能拿到立即可用的像素**,后台正在绘制也不影响交互。
- **后台绘制**:`m_layerWorker` 线程(BELOW_NORMAL 优先级)按任务队列逐网格重画。
前台每帧只做:① 判断各网格图层是否就绪;② `BitsBlt` 把**就绪且跨度一致**的图层按坐标偏移贴到画面;
③ 画时间轴 / 派生叠加层 / 装饰 / 叠加层。**前台不再绘制任何波形**。
- **"未就绪就用旧数据应付"**:只要图层**跨度一致、能覆盖当前视界**,即使内容稍旧也照贴
(`LayersReadyForView()` 不比较内容指纹),内容由后台补画后交换 —— 于是样式/参数变化、
平移、滚动都不会退化成整幅重绘。只有"跨度变了 / 图层还没建"才落到整幅重绘一次以保证正确性。
- **不收敛保护**:若同一状态反复排任务仍与前台不一致(说明某参与指纹的状态没被正确还原),
超过 90 次即改为同步重建一次强行收敛,避免死循环。
- **前台优先**:后台在"前台最近 30ms 内绘制过"时主动让路(把任务放回队首,8ms 后再试),
避免与前台抢 `m_renderMutex` 反而更卡;交互停下后后台从容补画。
- **加锁顺序**:一律 `m_renderMutex` → `m_layerMutex`(后台与前台一致),避免 AB-BA 死锁。
- **渲染状态还原**:图层渲染会临时改写 `m_area` / `m_plotRect` / 视界 / 网格矩形,
结束后必须调用 `BuildLaneVisuals()` 重算 `m_laneContentHeight` 并还原 `LaneScroll` 与各网格幅值范围
—— 否则内容指纹与前台永远不一致 → 图层被判定"永远过期" → 死循环(本轮踩过这个坑)。
验证(离屏,`perf.exe ... 6`,视界 ≈ **2.045ms 一大格**):
| 交互 | 帧平均 | 像素差(对比从零整幅渲染) |
| --- | --- | --- |
| 键盘左右方向键平移 | **3.5ms** | 3705 通道(≤0.5px 亚像素) |
| 键盘上下方向键滚动网格 | 0.7ms | **0** |
| 中键水平拖动 / 左键水平拖动 | 3.3 / 1.8ms | 357 / **0** 通道 |
| 中键纵向 / 左键纵向拖动 | 5.7 / 5.5ms | 72 通道 |
| 滚轮滚动网格 | 1.9ms | **0** |
| 缩放后再平移 | 4.6ms(缩放帧 5.0ms) | 549 通道 |
| 弱波增强预览 + 平移 | 8.7ms | 3590 通道(异步曲线发布时序) |
- 「背景图层绘制次数」计数可直接证明**单网格基底的绘制确实发生在后台线程**:一轮诊断累计 **18~45 次**;
前台只在做位图搬运。
- 跨进程验证:`BackgroundLayerRenderCount()` 为公开只读接口,`perf.exe ... 6` 结束时会打印总量。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -401,3 +506,5 @@ Documents/ 项目文档(含本文件)
| 2026-10-11 | DeepSeek-V4.1-Flash | 宽缓冲改为**滚动式**(自贴图平移 + 只补画新暴露细条),消除"移出边界整幅重建"的周期性尖峰;缓冲只覆盖绘图区、时间轴前台绘制、亚像素误差用锚点限制在 ≤0.5px → 持续拖动平移约 6ms/帧、无尖峰(长距最大 10.5ms);静止帧逐像素 0 差异。残留:每次开始拖拽的首帧需新建缓冲(一次启动尖峰)。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 消除"开始拖拽"的首帧尖峰:平移首帧用**静态场景缓存初始化宽基底**(`InitWideBufferFromScene`,贴图 + 细条,不再从零重画 2×视界);启用带预览的实验性功能时**预建基底**。实测:平移拖动最大 8.5ms、弱波增强滑块最大 10.8ms(原分别 35/37.6ms),用户全部鼠标交互在该放大区间**全程 ≥60fps、无尖峰**;静止帧逐像素 0 差异。残留:非拖拽视界变化(键盘/工具栏)仍整幅重绘。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 消除 9.13 残留:**非拖拽视界变化(键盘/工具栏)也走宽基底快路径**(124.6ms→**8.6ms**),并用 `wideKey` 相等判定"视界已停止变化"触发一次**结算**整幅全质量帧以恢复像素精确;分支顺序固定为"静态贴图 → 宽基底 → 样式补丁 → 整幅",宽基底强制全质量抗锯齿且竖向刻度用前台跨度。新增像素对比模式验证:底座管线忠实(meanAbs 0.68/255),平移期仅 ≤0.5px 亚像素差;7 张静态/结算帧与 HEAD **字节一致**;平移中途帧不再有图例/刻度重影。 |
| 2026-10-12 | DeepSeek-V4.1-Flash | 落地用户提出的「各网格独立双缓冲区 + 后台线程绘制」架构:把全局宽缓冲区重构为**每网格信号图层**(宽×3,只含基底不含装饰)、`m_layerFront/m_layerBack` **双缓冲** + `m_layerWorker` **后台线程**(前台只做 BitBlt 坐标偏移,不再绘制波形);前台"图层就绪即用现成像素、内容稍旧也照用,交后台补画后交换";加不收敛保护、前台优先让路、固定加锁顺序(render→layer)、渲染状态完整还原(含 `m_laneContentHeight`/`LaneScroll`)。新增 `BackgroundLayerRenderCount()` 公开计数用于验证后台绘制确实发生(一轮诊断 18~45 次)。实测深放大区间(2.045ms 一大格)各交互 0.7~8.7ms/帧,像素差 ≤0.017(除亚像素平移),静态/结算帧像素精确。 |
| 2026-10-12 | DeepSeek-V4.1-Flash | 修复用户报告的「缩放后平移出现拖影/残留/错误元素」与「2ms 一大格区间仍然卡顿」。根因:①网格纵向滚动量未参与任何内容指纹→滚动时把旧位置像素贴回且每两帧触发一次整幅重绘(≈8fps);②`FindStyleOnlyLanes` 未校验视界→视界变化后仍走局部补丁;③宽基底用含装饰的整幅画面初始化→装饰随平移重影;④装饰缓存的矩形列表与坐标原点两帧不同源→装饰消失/黑块/错位。修复后交互帧 9~19ms → **4.5~6ms**,纵向拖动残留 2 万+ 通道 → 72 通道(亚像素量化),静态/结算帧像素精确。新增 `perf.exe ... 6` 诊断模式(逐帧计时 + 逐像素对比 + 差异包围盒)。未实现:每网格双缓冲后台渲染(需先把 `PaintLane` 改造成无状态渲染器,已在文档 9.15 说明)。 |