From f97c4cf2890dfd63f522c711c49beba4300c185c Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" Date: Mon, 12 Oct 2026 03:17:31 +0800 Subject: [PATCH] =?UTF-8?q?=E8=90=BD=E5=9C=B0=E6=AF=8F=E7=BD=91=E6=A0=BC?= =?UTF-8?q?=E5=8F=8C=E7=BC=93=E5=86=B2=E5=9B=BE=E5=B1=82=20+=20=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E7=BB=98=E5=88=B6=E6=9E=B6=E6=9E=84=EF=BC=9A=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=AE=BD=E7=BC=93=E5=86=B2=E5=8C=BA=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=BA=E6=AF=8F=E7=BD=91=E6=A0=BC=E4=BF=A1=E5=8F=B7=E5=9B=BE?= =?UTF-8?q?=E5=B1=82=EF=BC=88=E5=AE=BDx3=E3=80=81=E5=8F=AA=E5=90=AB?= =?UTF-8?q?=E5=9F=BA=E5=BA=95=EF=BC=89=EF=BC=8Cm=5FlayerFront/m=5FlayerBac?= =?UTF-8?q?k=20=E5=8F=8C=E7=BC=93=E5=86=B2=20+=20m=5FlayerWorker=20?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=BA=BF=E7=A8=8B=EF=BC=8C=E5=89=8D=E5=8F=B0?= =?UTF-8?q?=E5=8F=AA=E5=81=9A=E4=BD=8D=E5=9B=BE=E5=9D=90=E6=A0=87=E5=81=8F?= =?UTF-8?q?=E7=A7=BB=E8=B4=B4=E5=9B=BE=E4=B8=8D=E5=86=8D=E7=BB=98=E5=88=B6?= =?UTF-8?q?=E6=B3=A2=E5=BD=A2=EF=BC=9B=E5=89=8D=E5=8F=B0=E5=9B=BE=E5=B1=82?= =?UTF-8?q?=E5=B0=B1=E7=BB=AA=E5=8D=B3=E7=94=A8=E3=80=81=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E7=A8=8D=E6=97=A7=E4=B9=9F=E7=85=A7=E7=94=A8=E5=B9=B6=E4=BA=A4?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E8=A1=A5=E7=94=BB=E5=90=8E=E4=BA=A4=E6=8D=A2?= =?UTF-8?q?=EF=BC=9B=E5=90=AB=E4=B8=8D=E6=94=B6=E6=95=9B=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=E3=80=81=E5=89=8D=E5=8F=B0=E4=BC=98=E5=85=88=E8=AE=A9=E8=B7=AF?= =?UTF-8?q?=E3=80=81=E5=9B=BA=E5=AE=9A=E5=8A=A0=E9=94=81=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=E3=80=81=E6=B8=B2=E6=9F=93=E7=8A=B6=E6=80=81=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E8=BF=98=E5=8E=9F=EF=BC=9B=E6=96=B0=E5=A2=9E=20BackgroundLayer?= =?UTF-8?q?RenderCount=20=E9=AA=8C=E8=AF=81=E5=90=8E=E5=8F=B0=E7=BB=98?= =?UTF-8?q?=E5=88=B6=EF=BC=9B=E6=B7=B1=E6=94=BE=E5=A4=A7=E5=8C=BA=E9=97=B4?= =?UTF-8?q?=E5=90=84=E4=BA=A4=E4=BA=92=200.7~8.7ms/=E5=B8=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Components/PlotView/Inc/PlotView.hpp | 174 ++++-- Components/PlotView/Src/PlotView.cpp | 785 +++++++++++++++++---------- Documents/agent_mem.md | 46 +- 3 files changed, 667 insertions(+), 338 deletions(-) diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 71d7777..1f3727c 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -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 字体集合,需在视图生命周期内保持有效。 @@ -2210,74 +2222,128 @@ namespace jdi mutable int m_decorOriginX = 0; mutable int m_decorOriginY = 0; - // ---- 平移「宽缓冲区」:平移只改变绘制坐标、不改变画面内容 ---- - // 视界平移时整个波形并不改变,只是横向平移。因此以当前视界为中心,向左右各多渲染 - // 半个视界宽(位图宽度 = 视界宽 ×2),保存为一张**更宽**的离屏位图;平移时前台只需 - // 从中**截取对应的横向子矩形贴图**(O(1),一次 BitBlt),而不必每帧重算重画。 - // 当视界移动到该缓冲区边缘(超出已渲染范围)时,才以新视界为中心重建一次。 + // ---- 每网格「信号图层」:双缓冲 + 后台绘制(前台只做贴图 / 坐标偏移) ---- + // + // 平移只改变绘制坐标、不改变画面内容。因此**每个网格各自维护一块离屏位图** + // (宽度 = 视界宽 ×3:左右各多渲染半个视界),称为该网格的「信号图层」。 + // 图层只含基底(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**(Y 轴刻度 / 图例)—— + // 装饰锚定在网格左缘、不随时间视界移动,若烘进图层会被平移带走(重影), + // 因此装饰由前台单独绘制(见 `m_decorCapturedRects` 那套缓存)。 + // + // **双缓冲**:每个网格有两块图层(`m_layerFront` 前台读取 / `m_layerBack` 后台填充), + // 后台绘制完成后再整体交换。前台**永远只读 `m_layerFront`**,因此后台正在绘制时 + // 前台仍能拿到上一份可用像素,绘制过程不影响交互响应。 + // + // **后台绘制**:跨度 / 几何 / 外观变化时,后台线程池把各网格的新图层画进后台缓冲; + // 若前台已经要刷新而后台还没画好,就继续用旧的前台缓冲「应付」交互,不解切换。 /** - * @brief 确保宽缓冲区位图尺寸正确(不足则重建)。 - * @param [IN] width 位图宽度。 @param [IN] height 位图高度。 - * @return 可用返回 true。 + * @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 EnsureWideBuffer(int width, int height); - /** @brief 释放宽缓冲区位图。 */ - void ReleaseWideBuffer(); + bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW); + /** - * @brief 以当前视界为中心重建宽缓冲区(离屏渲染一张更宽的图像)。 - * - * 渲染时临时把绘制区域的横向映射"加宽",因此调用的是与正常绘制**完全相同**的 - * `PaintLane`/`PaintTimeAxis`,保证像素与正常绘制一致;仅**跳过图例与 Y 轴刻度** - *(它们锚定在网格左缘、随视界移动,需由前景叠加层绘制)。 - * - * @return 重建成功返回 true。 + * @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 m_layerFront; /**< 前台读取(后台不会就地改写)。 */ + std::vector 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 m_layerJobs; /**< 待渲染的网格任务。 */ + bool m_layerWorkerStop = false; /**< 请求退出后台线程。 */ + /** + * @brief 连续排了多少次"图层重建"却始终没与前台一致。 + * + * 正常情况下一两次就会收敛(后台画完→交换→指纹一致)。若持续增长,说明后台渲染 + * 算出的指纹与前台不一致(例如某个参与指纹的状态没被正确还原),此时改为**同步重建** + * 一次强行收敛,避免"前台永远不等后台、后台永远画不好"的死循环。 + */ + int m_layerRescheduleStreak = 0; + /** 前台最近一次 `Paint` 的时刻(毫秒,`GetTickCount64`)。后台据此"让路",不与前台抢渲染锁。 */ + mutable std::atomic m_lastPaintTick{0}; + /** 后台线程已完成绘制的图层数量(原子,仅用于验证/诊断)。 */ + mutable std::atomic m_layerRenderCount{0}; + + bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */ /** * @brief 本次 `PaintLane` 只绘制「装饰」(Y 轴刻度胶囊 + 图例 + 网格分界线)。 diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 1db06bc..f1ce35a 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -671,6 +671,8 @@ namespace jdi { // 先停掉后台计算线程:它可能正在读取采样数据,必须在释放其它资源前回收。 StopCurveWorker(); + // 再停掉后台「网格图层」绘制线程:它会读写渲染状态与图层位图。 + StopLayerWorker(); ReleaseSceneCache(); ReleaseDecorCache(); ReleaseWideBuffer(); @@ -5488,6 +5490,13 @@ namespace jdi return; } + // 本帧整帧都在改写渲染状态(`m_area` / `m_plotRect` / 视界 / 图层…), + // 因此必须与「后台图层绘制线程」互斥;可重入,内部再调图层渲染不会自锁。 + // 后台只在**内容变化后补画**时才会长时间持有它,平移/缩放等交互期间它基本空闲, + // 因此前台不会被拖慢。 + std::lock_guard renderLock(m_renderMutex); + m_lastPaintTick.store(GetTickCount64(), std::memory_order_relaxed); + BuildLaneVisuals(); // 双缓冲:清掉上一帧钉住的曲线条目(本帧会按需重新钉住),并清理已被更新条目覆盖 / @@ -5623,16 +5632,12 @@ namespace jdi } else { - // 已有可用基底(跨度与内容均匹配)→ 滚动:只补画新暴露的细条,恒为 O(细条), - // 无周期性大尖峰;否则以当前视界为中心重建基底。 - // ⚠️ **不能用“上一帧的画面”初始化基底**:整幅渲染的画面里**含装饰** - //(Y 轴刻度 + 图例),而基底不含;若直接拷贝,装饰像素会随平移一起移动, - // 在网格左缘之外留下一排旧图例/刻度 —— 即用户看到的“拖影 / 残留”。 - bool ready = (m_wideValid && WideBufferMatches()) ? RollWideBuffer() : false; - if (!ready) - { - ready = RebuildWideBuffer(); - } + // 只要图层“就绪且跨度一致”,就直接用现成像素做坐标偏移(平移/滚动)。 + // 内容即使稍旧也无所谓:由后台线程补画后与旧缓冲交换,前台下一帧即拿到新像素。 + // 只有“跨度变了/图层还没建”时才落到下方"整幅重绘"以保证正确性。 + bool ready = LayersReadyForView() ? RollWideBuffer() : false; + // 无论是否用上现成像素,都把"内容与前台不一致"的网格交后台补画。 + ScheduleStaleLayerRenders(); if (ready) { m_wideLastKey = wideKey; @@ -6826,78 +6831,247 @@ namespace jdi return true; } - bool PlotView::EnsureWideBuffer(int width, int height) + bool PlotView::EnsureLaneLayer(LaneLayer& layer, int width, int height) { if (width <= 0 || height <= 0) { return false; } - if (m_wideDc != nullptr && m_wideW == width && m_wideH == height) + if (layer.dc != nullptr && layer.width == width && layer.height == height) { return true; } - ReleaseWideBuffer(); + ReleaseLaneLayer(layer); HDC screen = GetDC(nullptr); if (screen == nullptr) { return false; } - m_wideDc = CreateCompatibleDC(screen); - m_wideBitmap = CreateCompatibleBitmap(screen, width, height); + layer.dc = CreateCompatibleDC(screen); + layer.bitmap = CreateCompatibleBitmap(screen, width, height); ReleaseDC(nullptr, screen); - if (m_wideDc == nullptr || m_wideBitmap == nullptr) + if (layer.dc == nullptr || layer.bitmap == nullptr) { - ReleaseWideBuffer(); + ReleaseLaneLayer(layer); return false; } - m_wideOldBitmap = SelectObject(m_wideDc, m_wideBitmap); - m_wideW = width; - m_wideH = height; + layer.oldBitmap = SelectObject(layer.dc, layer.bitmap); + layer.width = width; + layer.height = height; + layer.valid = false; return true; } + void PlotView::ReleaseLaneLayer(LaneLayer& layer) + { + layer.valid = false; + if (layer.dc != nullptr) + { + if (layer.oldBitmap != nullptr) + { + SelectObject(layer.dc, layer.oldBitmap); + layer.oldBitmap = nullptr; + } + DeleteDC(layer.dc); + layer.dc = nullptr; + } + if (layer.bitmap != nullptr) + { + DeleteObject(layer.bitmap); + layer.bitmap = nullptr; + } + layer.width = 0; + layer.height = 0; + } + void PlotView::ReleaseWideBuffer() { - m_wideValid = false; - if (m_wideDc != nullptr) + std::lock_guard lock(m_layerMutex); + for (LaneLayer& layer : m_layerFront) { - if (m_wideOldBitmap != nullptr) - { - SelectObject(m_wideDc, m_wideOldBitmap); - m_wideOldBitmap = nullptr; - } - DeleteDC(m_wideDc); - m_wideDc = nullptr; + ReleaseLaneLayer(layer); } - if (m_wideBitmap != nullptr) + for (LaneLayer& layer : m_layerBack) { - DeleteObject(m_wideBitmap); - m_wideBitmap = nullptr; + ReleaseLaneLayer(layer); } - m_wideW = 0; - m_wideH = 0; + m_layerFront.clear(); + m_layerBack.clear(); + m_layerAnchorValid = false; } void PlotView::InvalidateWideBuffer() { - m_wideValid = false; + std::lock_guard lock(m_layerMutex); + for (LaneLayer& layer : m_layerFront) + { + layer.valid = false; + } + for (LaneLayer& layer : m_layerBack) + { + layer.valid = false; + } + m_layerAnchorValid = false; } - bool PlotView::WideBufferMatches() const + std::uint64_t PlotView::LaneLayerKey(int laneIndex) const { - if (!m_wideValid || m_wideDc == nullptr) - { - return false; - } + // 图层只含基底(不含派生曲线与装饰),因此指纹 = 内容(外观/几何,不含派生参数) + // ⊕ 网格下标 ⊕ 视界跨度 ⊕ 绘图区宽度(决定像素/时间与位图尺寸)。 const double span = Tab().ViewMax() - Tab().ViewMin(); - if (!(span > 0.0) || std::fabs(span - m_wideSpan) > 1.0e-12) + std::uint64_t key = SceneContentHash(true, false); + key ^= static_cast(laneIndex + 1) * 1315423911ull; + key *= 1099511628211ull; + key ^= static_cast(std::llround(span * 1.0e6)); + key *= 1099511628211ull; + key ^= static_cast(std::llround(m_plotRect.Width)); + return key; + } + + bool PlotView::LayersReadyForView() const + { + std::lock_guard lock(m_layerMutex); + if (m_layerFront.size() != m_lanes.size() || m_layerFront.empty()) { return false; } - // 内容(外观样式 / 网格结构 / 视界几何)变化 → 需重建。 - // ⚠️ **不含派生曲线参数**:宽缓冲只存基底(网格 / 波形),派生曲线是每帧叠加的, - // 因此拖弱波增强 / 均值滤波参数滑块不会让基底失效,可直接复用基底像素。 - return (m_wideKey == SceneContentHash(true, false)); + const float plotWidth = m_plotRect.Width; + const double span = Tab().ViewMax() - Tab().ViewMin(); + if (!(plotWidth > 8.0f) || !(span > 0.0)) + { + return false; + } + const int width = static_cast(std::lround(plotWidth)); + const int margin = std::max(1, width / 2); + const int bitmapWidth = width + 2 * margin; + for (const LaneLayer& layer : m_layerFront) + { + if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth) + { + return false; + } + // 跨度必须一致:跨度变了像素/时间的映射就不同,旧像素不能再当“坐标偏移”用。 + if (std::fabs(span - layer.span) > 1.0e-12) + { + return false; + } + } + return true; + } + + bool PlotView::RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW) + { + if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) + { + return false; + } + std::lock_guard renderLock(m_renderMutex); + + const float plotWidth = m_plotRect.Width; + const double span = Tab().ViewMax() - Tab().ViewMin(); + if (!(plotWidth > 8.0f) || !(span > 0.0)) + { + layer.valid = false; + return false; + } + + const int width = static_cast(std::lround(plotWidth)); + const int margin = std::max(1, width / 2); // 左右各半屏 → 可平移 ±半屏 + const int bitmapWidth = width + 2 * margin; + const float laneHeight = m_lanes[static_cast(laneIndex)].rect.Height; + const int bitmapHeight = static_cast(std::ceil(laneHeight)); + if (bitmapHeight <= 0 || !EnsureLaneLayer(layer, bitmapWidth, bitmapHeight)) + { + return false; + } + + const double scale = static_cast(width) / span; // 像素 / 时间 + const double wideMin = Tab().ViewMin() - static_cast(margin) / scale; // 位图 x=0 对应的时间 + + if (stripW <= 0 || stripX < 0 || stripX + stripW > bitmapWidth) + { + stripX = 0; + stripW = bitmapWidth; + } + + // 保存当前布局 / 视图 / 网格,临时切换到"宽窗口"映射后离屏渲染,再恢复。 + const gd::RectF savedArea = m_area; + const gd::RectF savedPlot = m_plotRect; + const double savedViewMin = Tab().ViewMin(); + const double savedViewMax = Tab().ViewMax(); + const std::vector savedLanes = m_lanes; + const bool savedFastFrame = m_fastFrame; + const bool savedRenderWide = m_renderWide; + const float savedScroll = Tab().LaneScroll(); + + m_plotRect = gd::RectF(0.0f, 0.0f, static_cast(bitmapWidth), laneHeight); + // 只把 `m_area` 限为待渲染的那一段 x 区间,使 GDI/GDI+ 的裁剪只落在它上面; + // x 坐标仍用**整条宽窗口**的映射(`m_plotRect` 为完整宽图),因此网格线位置与整块图层一致。 + m_area = gd::RectF(static_cast(stripX), 0.0f, static_cast(stripW), laneHeight); + Tab().ViewMin() = wideMin; + Tab().ViewMax() = wideMin + static_cast(bitmapWidth) / scale; + + m_renderWide = true; // 图层只含基底:跳过派生曲线与装饰 + // 图层必须与"同一视界的整幅渲染"**逐像素同源**:强制全质量抗锯齿(不受拖拽快速帧影响)。 + m_fastFrame = false; + BuildLaneVisuals(); + // 沿用**当前视界**的 Y 轴范围,否则幅值映射与前台不一致。 + for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i) + { + m_lanes[i].yMin = savedLanes[i].yMin; + m_lanes[i].yMax = savedLanes[i].yMax; + } + // 该网格在本图层中的位置:横向铺满整条宽图,纵向从 0 开始。 + m_lanes[static_cast(laneIndex)].rect = + gd::RectF(0.0f, 0.0f, static_cast(bitmapWidth), laneHeight); + { + gd::Graphics g(layer.dc); + g.SetSmoothingMode(gd::SmoothingModeAntiAlias); + g.SetPixelOffsetMode(gd::PixelOffsetModeHalf); + g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit); + + UiFillRect(g, m_area, m_theme->plotBg); + UiFillRect(g, m_area, m_theme->cardBg); + + // 竖向网格刻度沿用**前台视界跨度**(而非 3× 的宽窗口跨度),否则竖网格线会只有 1/3。 + const double fgSpan = savedViewMax - savedViewMin; + const double timeStep = NiceStep((fgSpan > 0.0) ? fgSpan : 1.0, kTimeTickTarget); + std::vector timeTicks; + BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeStep, timeTicks); + PaintLane(g, laneIndex, m_lanes[static_cast(laneIndex)], timeTicks); + } + m_fastFrame = savedFastFrame; + m_renderWide = savedRenderWide; + + // 恢复布局 / 视图 / 网格。 + // ⚠️ **必须调用 `BuildLaneVisuals()`**(而不是只把 `m_lanes` 拷回去):它会按正常布局 + // 重算 `m_laneContentHeight`。该值参与内容指纹;若残留上面"宽窗口"布局算出的值, + // 前台算出的图层指纹就永远与后台不一致 → 图层被判定为"永远过期" → 每帧重复排任务, + // 后台反复重画、前台反复抢 `m_renderMutex`,表现为界面卡死。 + m_area = savedArea; + m_plotRect = savedPlot; + Tab().ViewMin() = savedViewMin; + Tab().ViewMax() = savedViewMax; + BuildLaneVisuals(); + // `BuildLaneVisuals()` 内部会 `ClampLaneScroll()`,用宽窗口布局可能夹出不同的滚动量, + // 因此把滚动量与各网格幅值范围一并还原回进入本函数前的状态。 + Tab().LaneScroll() = savedScroll; + for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i) + { + m_lanes[i].yMin = savedLanes[i].yMin; + m_lanes[i].yMax = savedLanes[i].yMax; + } + // 恢复该网格的矩形(`BuildLaneVisuals` 已按正常布局给出,这里只为稳妥保持一致)。 + if (laneIndex < static_cast(m_lanes.size()) && laneIndex < static_cast(savedLanes.size())) + { + m_lanes[static_cast(laneIndex)] = savedLanes[static_cast(laneIndex)]; + } + + layer.wideMin = wideMin; + layer.span = span; + layer.key = LaneLayerKey(laneIndex); + layer.valid = true; + return true; } bool PlotView::RebuildWideBuffer() @@ -6905,278 +7079,337 @@ namespace jdi const float plotWidth = m_plotRect.Width; const float areaHeight = m_area.Height; const double span = Tab().ViewMax() - Tab().ViewMin(); - if (!(plotWidth > 8.0f) || !(areaHeight > 8.0f) || !(span > 0.0)) + if (!(plotWidth > 8.0f) || !(areaHeight > 8.0f) || !(span > 0.0) || m_lanes.empty()) { - m_wideValid = false; + InvalidateWideBuffer(); return false; } - const int width = static_cast(std::lround(plotWidth)); - 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)) + std::lock_guard renderLock(m_renderMutex); + std::lock_guard layerLock(m_layerMutex); + m_layerFront.resize(m_lanes.size()); + m_layerBack.resize(m_lanes.size()); + + bool ok = true; + for (std::size_t index = 0; index < m_layerFront.size(); ++index) { - return false; - } - - const double scale = static_cast(width) / span; // 像素 / 时间 - const double wideMin = Tab().ViewMin() - static_cast(margin) / scale; // 位图 x=0 对应的时间 - - // 保存当前布局 / 视图 / 网格,临时切换到"宽窗口"映射后离屏渲染,再恢复。 - // 宽缓冲**只覆盖绘图区**(不含底部时间轴):时间轴由前台每帧绘制。 - const gd::RectF savedArea = m_area; - const gd::RectF savedPlot = m_plotRect; - const double savedViewMin = Tab().ViewMin(); - const double savedViewMax = Tab().ViewMax(); - const std::vector savedLanes = m_lanes; - - m_area = gd::RectF(0.0f, 0.0f, static_cast(bitmapWidth), plotHeight); - m_plotRect = gd::RectF(0.0f, 0.0f, static_cast(bitmapWidth), plotHeight); - Tab().ViewMin() = wideMin; - Tab().ViewMax() = wideMin + static_cast(bitmapWidth) / scale; - - m_renderWide = true; - // 宽缓冲必须与"同一视界的整幅渲染"**逐像素同源**,否则平移后网格线数量 / 锯齿会不同: - // * 强制全质量抗锯齿(不受"拖拽快速帧"影响,绝不降级); - // * 网格竖向刻度沿用**前台视界跨度**(而非 2× 的宽窗口跨度),否则竖网格线会减半。 - const bool savedFastFrame = m_fastFrame; - m_fastFrame = false; - BuildLaneVisuals(); - // 宽缓冲必须沿用**当前视界**的 Y 轴范围,否则幅值映射与前台不一致。 - for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i) - { - m_lanes[i].yMin = savedLanes[i].yMin; - m_lanes[i].yMax = savedLanes[i].yMax; - } - { - gd::Graphics g(m_wideDc); - g.SetSmoothingMode(gd::SmoothingModeAntiAlias); - g.SetPixelOffsetMode(gd::PixelOffsetModeHalf); - g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit); - - UiFillRect(g, m_area, m_theme->plotBg); - UiFillRect(g, m_plotRect, m_theme->cardBg); - - const double fgSpan = savedViewMax - savedViewMin; - const double timeStep = NiceStep((fgSpan > 0.0) ? fgSpan : 1.0, kTimeTickTarget); - std::vector timeTicks; - BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeStep, timeTicks); - for (std::size_t i = 0; i < m_lanes.size(); ++i) + if (!RenderLaneLayerRegion(static_cast(index), m_layerFront[index], 0, 0)) { - PaintLane(g, static_cast(i), m_lanes[i], timeTicks); + ok = false; } } - m_fastFrame = savedFastFrame; - m_renderWide = false; + m_layerAnchorMin = Tab().ViewMin(); + m_layerAnchorValid = true; + return ok; + } - // 恢复布局 / 视图 / 网格。 - m_area = savedArea; - m_plotRect = savedPlot; - Tab().ViewMin() = savedViewMin; - Tab().ViewMax() = savedViewMax; - BuildLaneVisuals(); + bool PlotView::RollWideBuffer() + { + const float plotWidth = m_plotRect.Width; + const double span = Tab().ViewMax() - Tab().ViewMin(); + if (!(plotWidth > 8.0f) || !(span > 0.0)) + { + return false; + } + const int width = static_cast(std::lround(plotWidth)); + const int margin = std::max(1, width / 2); + const int bitmapWidth = width + 2 * margin; + const double scale = static_cast(width) / span; - m_wideMin = wideMin; - m_wideAnchorMin = wideMin + static_cast(margin) / scale; // 视界左端(列 margin)对应的时间 - m_wideSpan = span; - m_wideKey = SceneContentHash(true, false); - m_wideValid = true; + std::lock_guard renderLock(m_renderMutex); + std::lock_guard layerLock(m_layerMutex); + if (!m_layerAnchorValid || m_layerFront.size() != m_lanes.size() || m_layerFront.empty()) + { + return false; + } + for (const LaneLayer& layer : m_layerFront) + { + if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth || + std::fabs(layer.span - span) > 1.0e-12) + { + return false; + } + } + + // 视界左端相对锚点的位移(像素)。内容需整体平移 round(d) 像素,只补画新暴露的细条。 + const double d = (Tab().ViewMin() - m_layerAnchorMin) * scale; + const int shift = static_cast(std::lround(d)); + if (shift == 0) + { + return true; + } + // 位移过大(跳视界)时滚动已经没有意义,交给调用方整幅重建。 + if (std::abs(shift) >= bitmapWidth - 4) + { + return false; + } + + const int stripWidth = std::min(std::abs(shift), bitmapWidth); + const int stripX = (shift > 0) ? (bitmapWidth - stripWidth) : 0; + for (LaneLayer& layer : m_layerFront) + { + if (shift > 0) + { + // 内容整体左移 shift:new[x] = old[x + shift],右侧暴露 shift 列。 + BitBlt(layer.dc, 0, 0, bitmapWidth - shift, layer.height, layer.dc, shift, 0, SRCCOPY); + } + else + { + const int amount = -shift; + BitBlt(layer.dc, amount, 0, bitmapWidth - amount, layer.height, layer.dc, 0, 0, SRCCOPY); + } + } + m_layerAnchorMin += static_cast(shift) / scale; + for (LaneLayer& layer : m_layerFront) + { + layer.wideMin = m_layerAnchorMin - static_cast(margin) / scale; + } + // 只补画新暴露的细条(每帧代价恒为 O(细条),无周期性大尖峰)。 + for (std::size_t index = 0; index < m_layerFront.size(); ++index) + { + RenderLaneLayerRegion(static_cast(index), m_layerFront[index], stripX, stripWidth); + } return true; } bool PlotView::BlitWideBuffer(gd::Graphics& g) const { - if (!WideBufferMatches()) + const float plotWidth = m_plotRect.Width; + const double span = Tab().ViewMax() - Tab().ViewMin(); + if (!(plotWidth > 8.0f) || !(span > 0.0)) { return false; } - const int width = static_cast(std::lround(m_plotRect.Width)); + const int width = static_cast(std::lround(plotWidth)); const int margin = std::max(1, width / 2); const int bitmapWidth = width + 2 * margin; - const double scale = static_cast(width) / m_wideSpan; - const double subX = (Tab().ViewMin() - m_wideMin) * scale; - if (subX < -0.5 || subX + static_cast(width) > static_cast(bitmapWidth) + 0.5) + const double scale = static_cast(width) / span; + + std::lock_guard lock(m_layerMutex); + if (m_layerFront.size() != m_lanes.size()) { - return false; // 已移出宽缓冲范围 → 调用方负责重建 + return false; } HDC target = g.GetHDC(); if (target == nullptr) { return false; } - const int destX = static_cast(std::lround(m_plotRect.X)); - const int destY = static_cast(std::lround(m_plotRect.Y)); - const int height = static_cast(std::lround(m_plotRect.Height)); - BitBlt(target, destX, destY, width, height, m_wideDc, static_cast(std::lround(subX)), 0, - SRCCOPY); + bool any = false; + for (std::size_t index = 0; index < m_layerFront.size(); ++index) + { + const LaneLayer& layer = m_layerFront[index]; + if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth) + { + continue; + } + if (std::fabs(layer.span - span) > 1.0e-12) + { + continue; + } + const double subX = (Tab().ViewMin() - layer.wideMin) * scale; + if (subX < -0.5 || subX + static_cast(width) > static_cast(bitmapWidth) + 0.5) + { + continue; // 该网格图层已移出当前视界范围 → 由调用方安排重建 + } + const gd::RectF& lane = m_lanes[index].rect; + const float top = std::max(lane.Y, m_plotRect.Y); + const float bottom = std::min(lane.Y + lane.Height, m_plotRect.Y + m_plotRect.Height); + if (bottom <= top) + { + continue; // 网格整体移出可视区 + } + const int destX = static_cast(std::lround(m_plotRect.X)); + const int destY = static_cast(std::lround(top)); + const int srcX = static_cast(std::lround(subX)); + const int srcY = static_cast(std::lround(top - lane.Y)); + const int height = static_cast(std::lround(bottom - top)); + if (height <= 0) + { + continue; + } + BitBlt(target, destX, destY, width, height, layer.dc, srcX, srcY, SRCCOPY); + any = true; + } g.ReleaseHDC(target); - return true; + return any; } - bool PlotView::RollWideBuffer() + void PlotView::ScheduleStaleLayerRenders() { - if (!WideBufferMatches()) - { - 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; - if (m_wideW != bitmapWidth) - { - return false; - } - const double scale = static_cast(width) / m_wideSpan; - // 视界左端相对锚点的位移(像素)。内容需整体平移 round(d) 像素,只补画新暴露的细条。 - const double d = (Tab().ViewMin() - m_wideAnchorMin) * scale; - const int shift = static_cast(std::lround(d)); - if (shift != 0) - { - if (shift > 0) - { - // 内容整体左移 shift:new[x] = old[x + shift],右侧暴露 shift 列。 - BitBlt(m_wideDc, 0, 0, bitmapWidth - shift, m_wideH, m_wideDc, shift, 0, SRCCOPY); - } - else - { - const int amount = -shift; - BitBlt(m_wideDc, amount, 0, bitmapWidth - amount, m_wideH, m_wideDc, 0, 0, SRCCOPY); - } - RenderWideStrip(shift); - m_wideAnchorMin += static_cast(shift) / scale; - m_wideMin = m_wideAnchorMin - static_cast(margin) / scale; - } - return true; - } - - bool PlotView::InitWideBufferFromScene() - { - if (!m_sceneValid || m_sceneDc == nullptr) - { - return false; - } + const float plotWidth = m_plotRect.Width; 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)); - const int margin = std::max(1, width / 2); - const int bitmapWidth = width + 2 * margin; - const float plotHeight = m_plotRect.Height; - const double scale = static_cast(width) / m_wideSpan; - const int stripWidth = std::min(std::abs(shiftPixels), bitmapWidth); - if (stripWidth <= 0) + if (!(plotWidth > 8.0f) || !(span > 0.0) || m_lanes.empty()) { return; } - const int stripX = (shiftPixels > 0) ? (bitmapWidth - stripWidth) : 0; - // 平移后的缓冲区左端时间。 - const double newWideMin = m_wideMin + static_cast(shiftPixels) / scale; + const int width = static_cast(std::lround(plotWidth)); + const int margin = std::max(1, width / 2); + const int bitmapWidth = width + 2 * margin; - const gd::RectF savedArea = m_area; - const gd::RectF savedPlot = m_plotRect; - const double savedViewMin = Tab().ViewMin(); - const double savedViewMax = Tab().ViewMax(); - const std::vector savedLanes = m_lanes; - - // 关键:x 坐标仍用**整条宽窗口**的映射(m_plotRect 为完整宽图),因此网格/波纹位置正确; - // 仅把 m_area 限为细条,使 GDI/GDI+ 的裁剪只落在细条上。 - m_area = gd::RectF(static_cast(stripX), 0.0f, static_cast(stripWidth), plotHeight); - m_plotRect = gd::RectF(0.0f, 0.0f, static_cast(bitmapWidth), plotHeight); - Tab().ViewMin() = newWideMin; - Tab().ViewMax() = newWideMin + static_cast(bitmapWidth) / scale; - - m_renderWide = true; - // 与 `RebuildWideBuffer` 保持完全一致:细条补画也必须**全质量抗锯齿**,且网格竖向刻度 - // 沿用**前台视界跨度**,否则滚动后新露出的细条会与既有基底的网格 / 锯齿不一致。 - const bool savedFastFrame = m_fastFrame; - m_fastFrame = false; - BuildLaneVisuals(); - for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i) + std::vector pending; { - m_lanes[i].yMin = savedLanes[i].yMin; - m_lanes[i].yMax = savedLanes[i].yMax; - } - { - gd::Graphics g(m_wideDc); - g.SetSmoothingMode(gd::SmoothingModeAntiAlias); - g.SetPixelOffsetMode(gd::PixelOffsetModeHalf); - g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit); - UiFillRect(g, m_area, m_theme->plotBg); - UiFillRect(g, m_area, m_theme->cardBg); - const double fgSpan = savedViewMax - savedViewMin; - const double timeStep = NiceStep((fgSpan > 0.0) ? fgSpan : 1.0, kTimeTickTarget); - std::vector timeTicks; - BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeStep, timeTicks); - for (std::size_t i = 0; i < m_lanes.size(); ++i) + std::lock_guard lock(m_layerMutex); + if (m_layerFront.size() != m_lanes.size()) { - PaintLane(g, static_cast(i), m_lanes[i], timeTicks); + m_layerFront.resize(m_lanes.size()); + m_layerBack.resize(m_lanes.size()); + } + for (std::size_t index = 0; index < m_lanes.size(); ++index) + { + const LaneLayer& layer = m_layerFront[index]; + const bool stale = (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth || + std::fabs(layer.span - span) > 1.0e-12 || + layer.key != LaneLayerKey(static_cast(index))); + if (stale) + { + pending.push_back(static_cast(index)); + } } } - m_fastFrame = savedFastFrame; - m_renderWide = false; + if (pending.empty()) + { + m_layerRescheduleStreak = 0; + return; + } + // 停滞保护:见成员注释。超过阈值就同步重建一次强行收敛。 + if (++m_layerRescheduleStreak > 90) + { + m_layerRescheduleStreak = 0; + RebuildWideBuffer(); + return; + } + { + std::lock_guard lock(m_layerJobMutex); + for (int index : pending) + { + bool exists = false; + for (const LayerJob& job : m_layerJobs) + { + if (job.laneIndex == index) + { + exists = true; + break; + } + } + if (!exists) + { + LayerJob job; + job.laneIndex = index; + m_layerJobs.push_back(job); + } + } + } + EnsureLayerWorker(); + m_layerJobCv.notify_all(); + } - m_area = savedArea; - m_plotRect = savedPlot; - Tab().ViewMin() = savedViewMin; - Tab().ViewMax() = savedViewMax; - BuildLaneVisuals(); + void PlotView::EnsureLayerWorker() + { + std::lock_guard lock(m_layerJobMutex); + if (m_layerWorker.joinable()) + { + return; + } + m_layerWorkerStop = false; + m_layerWorker = std::thread([this]() { LayerWorkerLoop(); }); + } + + void PlotView::StopLayerWorker() + { + { + std::lock_guard lock(m_layerJobMutex); + if (!m_layerWorker.joinable()) + { + return; + } + m_layerWorkerStop = true; + m_layerJobs.clear(); + } + m_layerJobCv.notify_all(); + m_layerWorker.join(); + } + + void PlotView::LayerWorkerLoop() + { + // 后台绘制不能与 UI 抢 CPU:降到低优先级(前台仍然"想跑多快跑多快")。 + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); + for (;;) + { + LayerJob job; + { + std::unique_lock lock(m_layerJobMutex); + m_layerJobCv.wait(lock, [this]() { return m_layerWorkerStop || !m_layerJobs.empty(); }); + if (m_layerWorkerStop) + { + return; + } + job = m_layerJobs.front(); + m_layerJobs.erase(m_layerJobs.begin()); + } + if (job.laneIndex < 0) + { + continue; + } + // ⚠️ **前台优先**:前台刚刚还在绘制(典型:用户正在拖拽/平移)时,后台主动让路, + // 把任务放回队首稍后再试。否则后台会不断抢占 `m_renderMutex`,前台每帧都要等 + // 一个图层画完,反而比“单线程同步绘制”更卡。交互停下来后后台再从容补画。 + if (GetTickCount64() - m_lastPaintTick.load(std::memory_order_relaxed) < 30) + { + std::unique_lock lock(m_layerJobMutex); + m_layerJobCv.wait_for(lock, std::chrono::milliseconds(8)); + m_layerJobs.insert(m_layerJobs.begin(), job); + continue; + } + // 同一网格若有更新的任务在排队,本次直接跳过(以最新状态为准)。 + { + bool superseded = false; + std::lock_guard lock(m_layerJobMutex); + for (const LayerJob& other : m_layerJobs) + { + if (other.laneIndex == job.laneIndex) + { + superseded = true; + break; + } + } + if (superseded) + { + continue; + } + } + { + // ⚠️ 加锁顺序必须与前台一致:**先 `m_renderMutex` 再 `m_layerMutex`**。 + // 反序会与 `Paint`(渲染中会去锁图层元数据)构成经典的 AB-BA 死锁。 + std::lock_guard renderLock(m_renderMutex); + std::lock_guard layerLock(m_layerMutex); + if (job.laneIndex >= static_cast(m_layerBack.size()) || + m_layerBack.size() != m_lanes.size()) + { + continue; + } + if (!RenderLaneLayerRegion(job.laneIndex, m_layerBack[static_cast(job.laneIndex)], + 0, 0)) + { + continue; + } + // 后台缓冲画好 → 立即与前台缓冲整体交换(前台始终有立即可用的像素)。 + std::swap(m_layerFront[static_cast(job.laneIndex)], + m_layerBack[static_cast(job.laneIndex)]); + m_layerAnchorMin = Tab().ViewMin(); + m_layerAnchorValid = true; + m_layerRenderCount.fetch_add(1, std::memory_order_relaxed); + } + // 任务之间让出 CPU / 锁:避免后台连续抢占 `m_renderMutex`,把前台绘制挤出去 + //(后台只是预取图层,晚一两毫秒无关紧要;前台才需要“想跑多快跑多快”)。 + Sleep(1); + // 通知宿主重绘:绘制线程下一帧就会取到刚交换过来的图层。 + const HWND host = m_host; + if (host != nullptr) + { + PostMessageW(host, kMessagePlotDerivedReady, 0, 0); + } + } } void PlotView::PaintRubberBand(gd::Graphics& g) diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 37ddcba..7cc7458 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -449,15 +449,44 @@ Documents/ 项目文档(含本文件) - 剩余的几百通道差异集中在**密网格/密波形**上,是"宽基底按整数像素平移(≤0.5px 亚像素误差)" 的必然结果,肉眼不可辨;视界停下后的"结算"帧会回到**像素精确**。 -#### 尚未完成(如实记录) +#### 「每网格双缓冲 + 后台绘制」架构(已落地,本轮同批提交) -用户要求的「每个网格独立双缓冲 + **后台线程绘制**」这一层**本轮未实现**,原因: -`PaintLane` 及其调用链直接读写大量 `PlotView` 成员(`m_lanes` / `m_area` / `m_plotRect` / 视界 / -多重缓存 / `m_decorRects` / `m_legendHits` …),把它搬到后台线程会与 UI 线程的 `paintOverlays` -**数据竞争**。要做需要把渲染器改造成「不依赖成员状态的独立渲染器」(把上述状态显式参数化、 -缓存改为每线程私有),属于大重构,风险高。 -**本轮先把"正确性"与"帧时间"问题解决**:现在平移/缩放期间的帧时间已降到 4~6ms(原 9~19ms), -且不再有拖影/残留/错位;后续可按用户方案逐步把单网格绘制搬到后台。 +把原来**全局一张**「宽缓冲区」重构为用户提出的方案: + +- **每网格一块「信号图层」**(`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` 结束时会打印总量。 --- @@ -477,4 +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 说明)。 |