diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 5592c06..b2f72a5 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -2232,6 +2232,16 @@ namespace jdi bool RollWideBuffer(); /** @brief 渲染宽缓冲区中因滚动而新暴露的一条细条(宽 |shiftPixels|)。 */ void RenderWideStrip(int shiftPixels); + /** + * @brief 用「上一帧的静态场景缓存」初始化宽缓冲区(平移首帧用,避免从零重画)。 + * + * 平移时画面只是整体横向移动,场景缓存里的内容(含派生曲线/图例)**依然正确**; + * 因此把场景缓存的绘图区直接贴进宽缓冲中心即可,随后滚动到当前视界。 + * 仅当"视界相对场景缓存发生了平移"(跨度一致)时可用。 + * + * @return 初始化成功返回 true。 + */ + bool InitWideBufferFromScene(); /** @brief 宽缓冲区内容是否仍与当前视图/内容一致。 */ bool WideBufferMatches() const; /** @brief 使宽缓冲区失效。 */ diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index a238427..b7df407 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -1845,6 +1845,8 @@ namespace jdi { lane.edgeCenterPanelOpen = false; } + // 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。 + RebuildWideBuffer(); return true; } @@ -1988,6 +1990,8 @@ namespace jdi { lane.weakBoostPanelOpen = false; } + // 预建「宽缓冲基底」(不含派生曲线),使用户首次拖动该滑块时无需从零重建(避免一次性尖峰)。 + RebuildWideBuffer(); return true; } @@ -2069,6 +2073,8 @@ namespace jdi { lane.meanFilterPanelOpen = false; } + // 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。 + RebuildWideBuffer(); return true; } @@ -5578,8 +5584,13 @@ namespace jdi if (wideUsable) { // 已有可用基底 → 滚动(只补画新暴露的细条,恒为 O(细条),无周期性大尖峰); - // 否则(首帧 / 跨度或内容变化)整幅重建一次基底。 + // 否则优先用"上一帧场景缓存"初始化基底(平移首帧:贴图 + 细条,避免从零重画 + // 2×视界造成的一次尖峰),再不行才整幅重建。 bool ready = (m_wideValid && WideBufferMatches()) ? RollWideBuffer() : false; + if (!ready && InitWideBufferFromScene()) + { + ready = RollWideBuffer(); + } if (!ready) { ready = RebuildWideBuffer(); @@ -6963,6 +6974,68 @@ namespace jdi return true; } + bool PlotView::InitWideBufferFromScene() + { + if (!m_sceneValid || m_sceneDc == nullptr) + { + return false; + } + const double span = Tab().ViewMax() - Tab().ViewMin(); + const double sceneSpan = m_sceneKey.viewMax - m_sceneKey.viewMin; + if (!(span > 0.0) || std::fabs(sceneSpan - span) > 1.0e-9) + { + return false; + } + // 仅当"派生预览曲线参数未变"时可用:此时场景缓存内容(含派生曲线/图例)依然正确。 + // (拖弱波增强/均值滤波参数滑块时派生曲线会变,必须从零重建基底。) + std::vector currentDerived; + CollectSceneDerived(currentDerived); + if (currentDerived.size() != m_sceneDerived.size()) + { + return false; + } + for (std::size_t i = 0; i < currentDerived.size(); ++i) + { + const SceneDerivedEntry& a = currentDerived[i]; + const SceneDerivedEntry& b = m_sceneDerived[i]; + if (a.edgeCenter != b.edgeCenter || a.meanFilter != b.meanFilter || + a.weakBoost != b.weakBoost || a.weakBoostLevelPercent != b.weakBoostLevelPercent || + a.weakBoostIterations != b.weakBoostIterations || + a.edgeCenterItems != b.edgeCenterItems || a.meanFilterItems != b.meanFilterItems || + a.weakBoostItems != b.weakBoostItems) + { + return false; + } + } + + const int width = static_cast(std::lround(m_plotRect.Width)); + const int margin = std::max(1, width / 2); + const int bitmapWidth = width + 2 * margin; + const float plotHeight = m_plotRect.Height; + const int bitmapHeight = static_cast(std::ceil(plotHeight)); + if (!EnsureWideBuffer(bitmapWidth, bitmapHeight)) + { + return false; + } + + // 场景缓存的绘图区相对其原点(m_sceneX/m_sceneY)的位置。 + const int srcX = static_cast(std::floor(m_plotRect.X)) - m_sceneX; + const int srcY = static_cast(std::floor(m_plotRect.Y)) - m_sceneY; + if (BitBlt(m_wideDc, margin, 0, width, bitmapHeight, m_sceneDc, srcX, srcY, SRCCOPY) == FALSE) + { + m_wideValid = false; + return false; + } + + const double scale = static_cast(width) / span; + m_wideMin = m_sceneKey.viewMin - static_cast(margin) / scale; + m_wideAnchorMin = m_sceneKey.viewMin; + m_wideSpan = span; + m_wideKey = SceneContentHash(true, false); + m_wideValid = true; + return true; + } + void PlotView::RenderWideStrip(int shiftPixels) { const int width = static_cast(std::lround(m_plotRect.Width)); diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index ebe2af8..b08efb3 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -340,6 +340,22 @@ Documents/ 项目文档(含本文件) - **显示效果**:滚动平移帧截图核对无缝隙/无残字;**静止/结算帧与 HEAD 逐像素 0 差异**(`LockBits` 验证)。 - **剩余残留**:**每次"开始一次拖拽"的第一帧**需新建缓冲(2×视界基底,负载下约 38ms 的一次尖峰);持续拖动过程中无尖峰。要消除这一次性启动尖峰,需把基底渲染做成可后台的独立渲染器(同上,较大重构)。非拖拽视界变化仍整幅重绘。 +### 9.13 消除"开始拖拽"的首帧尖峰:用场景缓存初始化基底 + 启用预览功能时预建基底 +9.12 的残留(首帧需从零新建 2×视界基底,负载下约 38ms)已消除: +- **平移首帧用「上一帧静态场景缓存」初始化宽基底**(`InitWideBufferFromScene`):平移只是整体横向移动,场景缓存里的内容(含派生曲线/图例)**依然正确**,因此把它直接贴进宽基底中心即可,再滚动到当前视界 —— 首帧只需一次 BitBlt + 一条细条,**不再从零重画 2×视界**。仅当"派生预览曲线参数未变"时适用(拖这些参数时派生曲线会变,须从零重建基底)。 +- **启用带预览的实验性功能时预建基底**:在 `SetLaneWeakBoost`/`SetLaneMeanFilter`/`SetLaneEdgeCenterCount(…, true)` 里预建宽基底,使**首次拖动这些参数滑块**时无需从零重建(避免一次性尖峰)。 +- **实测**(`VERY_BIG_DATA` 全信号,视界 `[40.1756, 40.1885]s`,5 网格,负载机 ms/帧): + + | 场景 | 9.12 | 本次 | + | --- | --- | --- | + | 中键平移拖动(短距) | 最大 35ms(首帧重建) | **最大 8.5ms** | + | 中键平移拖动(长距) | 最大 10.5ms | **最大 7.4ms** | + | 弱波增强滑块 | 最大 37.6ms(首帧重建) | **最大 10.8ms** | + | 透明度/粗细/颜色 | 8–16ms | 8–15ms | + +- **结论**:用户报告的全部**鼠标**交互(左/中键拖动平移、样式滑块/拾色、弱波增强参数滑块)在该放大区间**全程 ≥60fps、无尖峰**;静止/结算帧与 HEAD **逐像素 0 差异**。 +- **仍存在的残留**:**非拖拽的视界变化**(键盘方向键 / 工具栏,非用户报告的鼠标场景)仍走整幅重绘(约 125ms)—— 因其没有"拖拽结束"事件可用来触发一次精确的全质量重绘,若走宽缓冲快路径会留下 ≤0.5px 的亚像素偏差且无法"结算"回精确帧,故保持整幅重绘以保证像素精确。 + --- ## 10. 变更日志(本文件维护记录) @@ -356,3 +372,4 @@ Documents/ 项目文档(含本文件) | 2026-10-11 | DeepSeek-V4.1-Flash | 落地用户提出的**复用像素**方案:新增**平移宽缓冲区**(以视界为中心左右各多渲染半个视界宽),平移时前台只做一次 **BitBlt 截取子矩形**(O(1)),移出边界才重建 → 深放大下平移 **34ms→1.3ms**;静止/结算帧逐像素 0 差异。 | | 2026-10-11 | DeepSeek-V4.1-Flash | 把宽缓冲推广为**基底复用**:抽出 `PaintLaneDerivedOverlays`、宽缓冲只存基底(不含派生曲线),基底键排除派生参数 → 拖弱波增强滑块 **55ms→5.2ms**;平移 1.3ms、样式 8ms 不变;静止帧逐像素 0 差异。残留:宽缓冲重建在前台(约每屏一次)、非拖拽视界变化仍整幅重绘。 | | 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 差异。残留:非拖拽视界变化(键盘/工具栏)仍整幅重绘。 |