diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 9c977d2..8a281ad 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -1076,6 +1076,18 @@ namespace jdi void PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane, const std::vector& timeTicks); + /** + * @brief 绘制某网格的全部**派生预览曲线**(均值滤波 / 中心轴 / 弱波增强),叠加在原信号之上。 + * + * 从 `PaintLane` 抽出,供「宽缓冲区」前台路径复用:宽缓冲只存"基底"(网格/波形), + * 派生曲线每帧作为叠加层单独绘制,因此拖派生参数滑块时无需重画波形。 + * + * @param [IN,OUT] g 绘图上下文。 + * @param [IN] laneIndex 网格下标。 + * @param [IN] lane 网格绘制信息。 + */ + void PaintLaneDerivedOverlays(gd::Graphics& g, int laneIndex, const LaneVisual& lane) const; + /** * @brief 绘制底部时间轴。 * @param [IN,OUT] g 绘图上下文。 @@ -1933,6 +1945,13 @@ namespace jdi float opacity = 1.0f; /**< 不透明度。 */ float lineWidth = 1.0f; /**< 线宽。 */ int mode = 0; /**< 绘制模式。 */ + + /** @brief 相等比较(用于判断"逐信号外观样式是否变化")。 */ + bool operator==(const SceneStyleEntry& other) const + { + return color == other.color && opacity == other.opacity && + lineWidth == other.lineWidth && mode == other.mode; + } }; /** diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 6ee8b9b..f8a8bcf 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -5562,23 +5562,34 @@ namespace jdi } }; - // 视界平移(左键/中键拖动):画面内容不变、只是横向平移。直接复用「宽缓冲区」 - // 已渲染的像素,截取子矩形贴图 —— 前台 O(1)(一次 BitBlt),不重画波形, - // 因此无论放大到多深的区间,平移都与全局显示时一样流畅。 - if (m_fastFrame && m_dragMode == PlotDragMode::PAN && m_decorValid && HasDrawableContent()) + // 视界平移 / 派生预览曲线参数变化:画面**基底**(网格/波形)不变,只是横向平移或 + // 叠加的派生曲线变化。直接复用「宽缓冲区」已渲染的**基底**像素,只把派生曲线作为 + // 叠加层重画 —— 前台 O(1),不重画波形,因此无论放大到多深都保持跟手。 + // 判定:网格结构未变(几何指纹一致)且**逐信号外观样式未变** → 属视图/派生变化,可用宽基底; + // 外观样式(颜色/粗细/透明度/模式)变化 → 交给下方"样式补丁"分支局部重画。 + bool wideUsable = false; + if (m_fastFrame && m_decorValid && HasDrawableContent() && m_sceneValid && + m_sceneGeometryHash == SceneGeometryHash()) + { + std::vector> currentStyles; + CollectSceneStyles(currentStyles); + wideUsable = (currentStyles == m_sceneStyles); + } + if (wideUsable) { bool blitted = BlitWideBuffer(g); - if (!blitted) + if (!blitted && RebuildWideBuffer()) { - // 视界移出了宽缓冲范围,或内容/跨度变化 → 以当前视界为中心重建一次。 - if (RebuildWideBuffer()) - { - blitted = BlitWideBuffer(g); - } + blitted = BlitWideBuffer(g); } if (blitted) { - // 宽缓冲不含图例与 Y 轴刻度(二者锚定网格左缘、随视界移动),由装饰缓存贴回。 + // 宽缓冲只含基底:派生预览曲线作为叠加层重画(很轻)。 + for (std::size_t i = 0; i < m_lanes.size(); ++i) + { + PaintLaneDerivedOverlays(g, static_cast(i), m_lanes[i]); + } + // 图例与 Y 轴刻度(锚定网格左缘、随视界移动)由装饰缓存贴回。 BlitDecorCache(g); paintOverlays(); return; @@ -6786,8 +6797,10 @@ namespace jdi { return false; } - // 内容(外观样式 / 派生曲线参数 / 网格结构 / 视界几何)变化 → 需重建。 - return (m_wideKey == SceneContentHash(true)); + // 内容(外观样式 / 网格结构 / 视界几何)变化 → 需重建。 + // ⚠️ **不含派生曲线参数**:宽缓冲只存基底(网格 / 波形),派生曲线是每帧叠加的, + // 因此拖弱波增强 / 均值滤波参数滑块不会让基底失效,可直接复用基底像素。 + return (m_wideKey == SceneContentHash(true, false)); } bool PlotView::RebuildWideBuffer() @@ -6870,7 +6883,7 @@ namespace jdi m_wideMin = wideMin; m_wideSpan = span; - m_wideKey = SceneContentHash(true); + m_wideKey = SceneContentHash(true, false); m_wideValid = true; return true; } @@ -7152,15 +7165,8 @@ namespace jdi // 只有拖拽中的信号光晕可以进入静态场景缓存(悬停光晕见 PaintHoverHighlight)。 const SignalRef haloRef = SceneHaloRef(); - // 「边沿计数(中心轴)」开启时,为**被勾选**的信号各叠加一条均值滤波曲线 - //(参数 = 该信号自己的条目:均值点数 X + 迭代次数 N,与「均值滤波」完全同一套语义)。 - const bool centerOn = Tab().Lanes()[static_cast(laneIndex)].edgeCenter; - // 「均值滤波」开启时,为**被勾选**的信号各叠加一条多次迭代滑动平均曲线 - //(均值点数 X、迭代次数 N 由该信号自己的条目给出)。 - const bool meanFilterOn = Tab().Lanes()[static_cast(laneIndex)].meanFilter; - // 「弱波增强」开启时同样只为**被勾选**的信号叠加曲线 - //(增强等级取整个网格共用的 `weakBoostLevel`)。 - const bool weakBoostOn = Tab().Lanes()[static_cast(laneIndex)].weakBoost; + // 派生预览曲线(中心轴 / 均值滤波 / 弱波增强)在下方"第二遍"统一叠加绘制, + // 见 `PaintLaneDerivedOverlays`(抽出以便宽缓冲区前台路径复用)。 for (const SignalRef& ref : laneSignals) { @@ -7286,55 +7292,12 @@ namespace jdi } // 第二遍:把所有**派生预览曲线**统一绘制在**本网格全部原信号之上**。 - // 若在同一个信号循环里就地叠加,后绘制的原信号会把先前信号的预览曲线盖住 - //(用户明确要求"预览信号处于原信号上层不要被覆盖")。 - for (const SignalRef& ref : laneSignals) + // 渲染「宽缓冲区」时跳过:宽缓冲只存"基底"(网格/波形,不含派生曲线与装饰), + // 派生曲线改由前台叠加层绘制(见 `PaintLaneDerivedOverlays`), + // 这样拖派生参数滑块时基底不变、可复用宽缓冲像素。 + if (!m_renderWide) { - const ResolvedSignal resolved = ResolveSignal(ref); - if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible) - { - continue; - } - // 中心轴均值滤波曲线(「实验性功能 → 边沿计数(中心轴)」):与「均值滤波」同源, - // 只有被勾选的信号才绘制。 - if (centerOn) - { - const Lane::EdgeCenterItem* centerItem = - Tab().Lanes()[static_cast(laneIndex)].FindEdgeCenterItem(ref); - if (centerItem != nullptr) - { - PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations); - } - } - // 均值滤波预览曲线:只有被勾选的信号才绘制(参数取自它自己的条目)。 - if (meanFilterOn) - { - const Lane::MeanFilterItem* item = - Tab().Lanes()[static_cast(laneIndex)].FindMeanFilterItem(ref); - if (item != nullptr) - { - PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations); - } - } - // 弱波增强预览曲线:只有被勾选的信号才绘制(增强等级取自整个网格)。 - if (weakBoostOn) - { - const Lane::WeakBoostItem* item = - Tab().Lanes()[static_cast(laneIndex)].FindWeakBoostItem(ref); - if (item != nullptr) - { - const int levelPercent = static_cast(std::lround( - std::max(0.0, std::min(1.0, - Tab().Lanes()[static_cast(laneIndex)] - .weakBoostLevel)) * - 100.0)); - const int iterations = std::max( - kWeakBoostIterationsMin, - std::min(kWeakBoostIterationsMax, - Tab().Lanes()[static_cast(laneIndex)].weakBoostIterations)); - PaintWeakBoostCurve(g, resolved, lane, levelPercent, iterations); - } - } + PaintLaneDerivedOverlays(g, laneIndex, lane); } // g.Restore 也会还原绘制状态,此处直接恢复图形状态即可。 @@ -7465,6 +7428,59 @@ namespace jdi return SignalRef(); } + void PlotView::PaintLaneDerivedOverlays(gd::Graphics& g, int laneIndex, const LaneVisual& lane) const + { + if (m_tab == nullptr || laneIndex < 0 || + laneIndex >= static_cast(m_tab->Lanes().size())) + { + return; + } + const Lane& tabLane = Tab().Lanes()[static_cast(laneIndex)]; + const bool centerOn = tabLane.edgeCenter; + const bool meanFilterOn = tabLane.meanFilter; + const bool weakBoostOn = tabLane.weakBoost; + if (!centerOn && !meanFilterOn && !weakBoostOn) + { + return; + } + const int levelPercent = static_cast( + std::lround(std::max(0.0, std::min(1.0, tabLane.weakBoostLevel)) * 100.0)); + const int boostIterations = + std::max(kWeakBoostIterationsMin, std::min(kWeakBoostIterationsMax, tabLane.weakBoostIterations)); + for (const SignalRef& ref : tabLane.signals) + { + const ResolvedSignal resolved = ResolveSignal(ref); + if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible) + { + continue; + } + if (centerOn) + { + const Lane::EdgeCenterItem* centerItem = tabLane.FindEdgeCenterItem(ref); + if (centerItem != nullptr) + { + PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations); + } + } + if (meanFilterOn) + { + const Lane::MeanFilterItem* item = tabLane.FindMeanFilterItem(ref); + if (item != nullptr) + { + PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations); + } + } + if (weakBoostOn) + { + const Lane::WeakBoostItem* item = tabLane.FindWeakBoostItem(ref); + if (item != nullptr) + { + PaintWeakBoostCurve(g, resolved, lane, levelPercent, boostIterations); + } + } + } + } + void PlotView::PaintLaneLegend(gd::Graphics& g, int laneIndex, const LaneVisual& lane, float visibleTop, float visibleBottom) { diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index cf006d5..43094e3 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -303,6 +303,25 @@ Documents/ 项目文档(含本文件) - **显示效果**:**静止/结算帧与修复前逐像素 0 差异**(`LockBits` 对照);平移拖动帧为快速帧质量(关闭抗锯齿,与既有"拖动时低分辨率、松手补全质量"策略一致),松手后立即恢复全质量。 - **说明**:宽缓冲区重建目前在前台进行(每平移约半屏一次);若日后要连这一次重建也移出交互帧,需把 `PaintLane` 的成员缓存重构为可复制的渲染上下文再搬后台(较大改动)。弱波增强参数滑块仍有一次"重绘受影响网格 + 浮动面板"的开销(负载下约 55ms),是本轮未彻底消除的残留。 +### 9.11 把「宽缓冲区」推广为**基底复用**:平移 + 拖参数滑块都降为 O(1) +承接 9.10,进一步把「宽缓冲区」从"只用于平移"推广为**通用基底复用**,彻底解决拖派生参数滑块的问题: +- **抽取派生曲线绘制**:把 `PaintLane` 里叠加派生预览曲线(均值滤波/中心轴/弱波增强)的"第二遍"抽成 `PaintLaneDerivedOverlays`。宽缓冲区渲染时**跳过派生曲线**,因此宽缓冲只存**基底**(网格/波形/时间轴,不含派生曲线与图例/Y 轴刻度)。 +- **基底键与派生参数解耦**:宽缓冲的校验键改为 `SceneContentHash(true, false)`(含外观样式、**不含派生参数**)→ 拖弱波增强等级/迭代次数滑块**不会让基底失效**,可直接复用基底像素。 +- **统一前台路径**:判定"网格结构未变(几何指纹一致)且逐信号外观样式未变"(即只有**视图**或**派生参数**变化)时 → 贴宽缓冲基底像素 + **只重画派生曲线叠加层** + 装饰层 → 前台 O(1),不重画波形。 + - 外观样式(颜色/粗细/透明度/模式)变化仍走"样式补丁"(局部重画受影响网格)。 +- **实测**(`VERY_BIG_DATA` 全信号,视界 `[40.1756, 40.1885]s`,5 网格;负载机 ms/帧): + + | 场景 | 9.10 后 | 本次后 | + | --- | --- | --- | + | 中键平移拖动 | 1.3ms | **1.3ms** | + | 弱波增强滑块 | 55ms | **5.2ms** | + | 透明度/粗细/颜色 | 8ms | 8ms | + +- **显示效果**:静止/结算帧与 HEAD **逐像素 0 差异**(`LockBits` 验证)。拖拽/拖参数帧为快速帧质量(关抗锯齿),松手后恢复全质量。 +- **仍存在的残留**(如实记录): + 1. **宽缓冲区重建**在视界移出缓冲范围(约每平移 1 屏一次)时于**前台**进行,负载下约 30–50ms 的**一次**尖峰。彻底消除需把"基底渲染"做成**不依赖成员状态的独立渲染器**再搬后台线程(`PaintLane` 会写成员缓存,直接后台化有数据竞争,属较大重构)。 + 2. **非拖拽的视界变化**(键盘方向键 / 工具栏)仍走整幅重绘(约 125ms);仅**鼠标左/中键拖动**走宽缓冲快路径。若需让键盘平移也快,需允许非拖拽帧复用"整数像素对齐"的宽缓冲(当前为避免 ≤0.5px 亚像素偏差而未启用)。 + --- ## 10. 变更日志(本文件维护记录) @@ -317,3 +336,4 @@ Documents/ 项目文档(含本文件) | 2026-10-11 | DeepSeek-V4.1-Flash | 预览曲线改为**按整条信号计算**,与「构建为新信号」逐位一致;新增**平移装饰层像素缓存**(图例 + Y 轴刻度在拖拽时直接贴回,2ms/大格拖拽 13ms→约 5ms);后台任务按 (信号,种类) 去重以防拖滑块淹没线程池。 | | 2026-10-11 | DeepSeek-V4.1-Flash | 探针定位"样式/参数滑块在深放大下极卡":根因是**样式补丁用了抗锯齿**(≈72ms/网格)且**后台曲线发布时整幅作废**导致每帧整幅重绘。修复:样式补丁强制快速帧、拖拽中不整幅作废、交互期省略面板投影 → 透明度/粗细/颜色 77ms→16ms、弱波增强滑块 96–176ms→26ms,静止/结算帧逐像素不变。 | | 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 差异。残留:宽缓冲重建在前台(约每屏一次)、非拖拽视界变化仍整幅重绘。 |