fix(plotview): 修复面板条目栏悬停时预览曲线高亮丢失 + 极端工况绘制管线提速
一、悬停高亮回归修复 - 根因:高亮画在预览曲线绘制路径里,而该路径在命中静态场景缓存时完全不会被调用 (悬停态又不参与 SceneContentHash),因此缓存命中帧永远看不到高亮。 - 修法:拆出 PaintDerivedCurvesFor(hoverOnly) 两态,新增 PaintDerivedHoverHighlight, 在 paintOverlays() 中紧跟 PaintHoverHighlight 以前景方式绘制(面板之下、光标之上)。 高亮仍按面板种类(均值滤波/中心轴/弱峰调整)分别判定。 二、预览曲线绘制重写(单条 15ms → 1.7ms) - 稠密区间改用与波形基底同一套「逐像素列 min/max 包络带」+ 一次 FillPath; 旧抽稀折线在稠密视界下每段都是竖直满幅,GDI+ 描边代价正比于覆盖像素数 (3200 顶点满幅锯齿抗锯齿 128ms / 无抗锯齿 31ms,包络带 1.9ms)。 - 列密度自适应:中等密度 2px/列,极密(>8 采样/像素)4px/列。 - 稀疏区间保持逐点折线;附带修复「等间隔抽样漏掉尖峰」。 三、预览曲线像素缓存层(Picture cache + pan roll) - 45 条曲线画进带 alpha 的 32bpp DIB,正常帧只做一次 AlphaBlend(1~2ms)。 - 纯平移时整行 memmove 一个 dx 并只重画新露出的竖带(与宽缓冲区滚动同构), 因此拖动视界的每帧代价与信号数无关。 - 内容指纹 = SceneContentHash(含样式/派生参数/网格几何) ⊕ 曲线发布序号;高亮不进指纹。 四、其他热点 - 光标取值胶囊不再逐帧重算并发布 45 条曲线(CurveSpanFor 新增 computeIfMissing), 只取已覆盖的缓存曲线,未命中退到最新曲线或本帧不标注。 - 样式补丁路径不再同步重建网格图层(宽基底 2 倍宽 × 2 倍高,实测单次 2.2 秒), 且开启派生功能时直接走整幅重绘(补丁会覆盖缓存中已烘入的曲线像素)。 实测(15 信号合并一条网格 + 全部实验性功能,1600x900 离屏探针): 拖动视界 264→5.9ms,双光标拖动 187→20.9ms,弱峰滑块 555→89ms, 改信号颜色 2661→200ms,悬停面板行 225→38ms,深放大拖动 207→23.8ms。 全屏 1920x1080:拖动视界 9.3ms。 验证:新增离屏探针 p27 8/8(悬停高亮在缓存命中帧可见、平移/缩放/改颜色后的叠加层 与「完全重画」参考帧逐像素一致 diff=0);p20 9/9、p21 9/9、p22 14/14、p23 8/8、p25 12/12; make release / make debug 零 warning;Release 814,080 B。规则已同步到 Documents/agent_mem.md。
This commit is contained in:
1 parent
ea704b03ea
commit
0c320cf1b3
6 files changed
+797
-33
No files matched your search
@@ -1751,6 +1751,32 @@ namespace jdi
|
||||
*/
|
||||
void PaintLaneDerivedOverlays(gd::Graphics& g, int laneIndex, const LaneVisual& lane) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制全部派生预览曲线(可只画悬停高亮的那一条)。
|
||||
*
|
||||
* `PaintLaneDerivedOverlays`(基底/缓存路径,不带高亮)与
|
||||
* `PaintDerivedHoverHighlight`(前景高亮)共用本函数,保证两者的几何完全一致。
|
||||
*
|
||||
* ⚠️ **高亮绝不能画进静态场景缓存**(用户 2026-11-02 报告“悬停时预览曲线的高亮丢失”):
|
||||
* 悬停状态不参与 `SceneContentHash`,命中场景缓存时叠加层根本不会被重画 ——
|
||||
* 于是“悬停 → 高亮”永远看不到;而一旦参与哈希,又会退化成“每动一下鼠标就整幅重绘”。
|
||||
* 正确做法:缓存里只画普通曲线,高亮那一帧在前景(贴图之后)单独叠加。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] lane 网格绘制信息。
|
||||
* @param [IN] hoverOnly true = 只画 `m_meanRowHoverRef` 在 `m_meanRowHoverKind` 面板里的那条曲线
|
||||
* (并带高亮);false = 画全部曲线且不带高亮。
|
||||
*/
|
||||
void PaintDerivedCurvesFor(gd::Graphics& g, int laneIndex, const LaneVisual& lane,
|
||||
bool hoverOnly) const;
|
||||
|
||||
/**
|
||||
* @brief 在**前景**叠加绘制“鼠标所在面板条目栏”对应的预览曲线高亮。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void PaintDerivedHoverHighlight(gd::Graphics& g) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制底部时间轴。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
@@ -2332,13 +2358,22 @@ namespace jdi
|
||||
* 而是交后台线程算,本帧先用已算好的旧曲线顶替(双缓冲),保证交互始终跟手。
|
||||
* 仅供**预览绘制**传 true;需要精确取值的统计(中心轴边沿计数、取值胶囊)
|
||||
* 传 false,走同步精确计算。
|
||||
* @param [IN] computeIfMissing 缓存未命中时是否**进行计算**并发布新条目。
|
||||
*
|
||||
* ⚠️ 光标取值胶囊必须传 false(用户 2026-11-02 反馈“拖光标仍有明显卡顿”):
|
||||
* 胶囊只要“光标所在的那两个采样”,而缓存键含请求窗口、窗口随光标每帧变化 ⇒
|
||||
* 每帧都未命中 ⇒ 每帧同步计算 + 发布。15 信号 × 3 种曲线 = 每帧 45 次发布,
|
||||
* 条目列表与线性搜索同步膨胀,实测拖动光标单帧 200ms(其中约 150ms 在这里)。
|
||||
* 传 false 后:命中就取(绘制路径发布的窗口带保护带、覆盖整个可见区间,
|
||||
* 正常情况必然命中),未命中则退到同 (信号, 种类) 的最新曲线,再无则本帧不标注。
|
||||
* @return 曲线数组指针(覆盖 `[spanBegin, spanBegin + size())`);暂无可显示曲线时返回 nullptr。
|
||||
*/
|
||||
const std::vector<float>* CurveSpanFor(DerivedCurveKind kind, const SignalData& signal,
|
||||
int points, int iterations, int amountPercent,
|
||||
std::size_t begin, std::size_t end,
|
||||
std::size_t& spanBegin,
|
||||
bool allowAsyncCompute = false) const;
|
||||
bool allowAsyncCompute = false,
|
||||
bool computeIfMissing = true) const;
|
||||
|
||||
/**
|
||||
* @brief 派生曲线缓存的键(决定一条派生曲线数值的全部输入)。
|
||||
@@ -3926,6 +3961,69 @@ namespace jdi
|
||||
*/
|
||||
mutable std::vector<gd::RectF> m_decorCapturedRects;
|
||||
|
||||
// ---- 「派生预览曲线」像素缓存(均值滤波 / 中心轴边沿计数 / 弱峰调整的叠加曲线)----
|
||||
//
|
||||
// 为什么需要单独一层像素缓存(用户 2026-11-02 反馈“极端工况下所有交互都卡”):
|
||||
// 每条预览曲线都要按「逐像素列包络带」填充,代价 ∝ **覆盖像素数**。15 个信号 ×
|
||||
// 3 个功能 = 45 条曲线、哪怕单条只要 2ms,一帧也要 90ms —— 而其中**绝大多数帧
|
||||
// 画面根本没变**(拖动光标、悬停、静止)。因此与装饰层同一套思路:
|
||||
// 曲线画进一张带 alpha 的 32bpp DIB,正常帧只做一次 `AlphaBlend`(约 1~2ms)。
|
||||
//
|
||||
// 平移复用(关键):视界**只平移不改跨度**时,画面内容只是横向平移 —— 于是把 DIB
|
||||
// 的像素整体 memmove 一个 dx,再只重画**新露出的那条竖带**即可(与宽缓冲区
|
||||
// 的滚动逻辑同构),因此拖动视界的每帧代价与信号数无关。
|
||||
//
|
||||
// ⚠️ 与装饰层的区别:本层的**内容随视界变化**,因此必须记录渲染时的视界
|
||||
// (`m_derivedViewMin/Max`);跨度一变就必须整块重画。
|
||||
/**
|
||||
* @brief 确保派生曲线缓存位图尺寸正确(不足则重建为 32bpp DIB 段)。
|
||||
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
|
||||
* @return 可用返回 true。
|
||||
*/
|
||||
bool EnsureDerivedCache(int width, int height);
|
||||
/** @brief 释放派生曲线缓存位图并作废缓存。 */
|
||||
void ReleaseDerivedCache();
|
||||
/**
|
||||
* @brief 把全部派生预览曲线绘制进透明缓存位图。
|
||||
*
|
||||
* `m_derivedStripActive == true` 时只重画 `[m_derivedStripX0, m_derivedStripX1)` 这段竖带
|
||||
* (平移后的“新露出”区域),否则整块重画。
|
||||
* @return 成功返回 true。
|
||||
*/
|
||||
bool RenderDerivedCache();
|
||||
/**
|
||||
* @brief 把派生曲线缓存按 `AlphaBlend` 贴回后备缓冲区(仅绘图区)。
|
||||
* @param [IN,OUT] g 绘图上下文。 @return 贴图成功返回 true。
|
||||
*/
|
||||
bool BlitDerivedCache(gd::Graphics& g);
|
||||
/**
|
||||
* @brief 派生曲线缓存的内容指纹(**不含视界**:视界单独比对,用于判断能否滚动复用)。
|
||||
* @return 指纹。
|
||||
*/
|
||||
std::uint64_t DerivedOverlayKey() const;
|
||||
/**
|
||||
* @brief 按当前视界/内容刷新派生曲线缓存,并把结果贴回后备缓冲区。
|
||||
*
|
||||
* 三种情形:内容与视界都相同 → 只贴图;内容相同且跨度不变(纯平移)→ 滚动 + 补条带;
|
||||
* 其余 → 整块重画。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void PaintDerivedOverlayLayer(gd::Graphics& g);
|
||||
|
||||
mutable HDC m_derivedDc = nullptr; /**< 派生曲线缓存设备上下文。 */
|
||||
mutable HBITMAP m_derivedBitmap = nullptr; /**< 派生曲线缓存位图。 */
|
||||
mutable HGDIOBJ m_derivedOldBitmap = nullptr; /**< DC 原选位图。 */
|
||||
mutable void* m_derivedBits = nullptr; /**< 位图像素(32bpp DIB 段)。 */
|
||||
mutable int m_derivedW = 0; /**< 缓存宽度。 */
|
||||
mutable int m_derivedH = 0; /**< 缓存高度。 */
|
||||
mutable bool m_derivedValid = false; /**< 缓存内容是否有效。 */
|
||||
mutable std::uint64_t m_derivedContentKey = 0; /**< 缓存对应的内容指纹(不含视界)。 */
|
||||
mutable double m_derivedViewMin = 0.0; /**< 缓存渲染时的视界下限。 */
|
||||
mutable double m_derivedViewMax = 0.0; /**< 缓存渲染时的视界上限。 */
|
||||
mutable bool m_derivedStripActive = false; /**< 当前是否只重画一条竖带。 */
|
||||
mutable float m_derivedStripX0 = 0.0f; /**< 竖带左边界(客户区像素)。 */
|
||||
mutable float m_derivedStripX1 = 0.0f; /**< 竖带右边界(客户区像素)。 */
|
||||
|
||||
// ---- 每网格「信号图层」(前台同步维护;交互帧只做贴图 / 坐标偏移) ----
|
||||
//
|
||||
// 平移只改变绘制坐标、不改变画面内容。因此**每个网格各自维护一块离屏位图**
|
||||
|
||||
@@ -57,6 +57,32 @@ namespace jdi
|
||||
inline const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数(上限保护)。 */
|
||||
inline const size_t kPolylinePointsPerPixel = 2; /**< 每个像素列允许的折线顶点数(上/下包络两顶点)。 */
|
||||
inline const size_t kMinPolylinePoints = 512; /**< 折线最少顶点数(窗口很窄时仍保留细节)。 */
|
||||
/**
|
||||
* @brief 派生预览曲线走「逐像素列包络带」时,每列抽样几个采样。
|
||||
*
|
||||
* 稠密视界(可见采样数 > 像素列数)下预览曲线若按抽稀折线绘制,相邻顶点的 y
|
||||
* 会整幅跳变 ⇒ GDI+ 描边代价 ∝ 覆盖像素数,实测单条曲线 76~128ms。
|
||||
* 改为逐列记录上下包络后一次 FillPath,单条曲线降到约 2ms(约 60 倍)。
|
||||
* 每列抽样 4 个点即可得到饱满的包络(再多只影响饱满度,不影响代价量级)。
|
||||
*/
|
||||
inline const size_t kDerivedCurveSamplesPerColumn = 4;
|
||||
/**
|
||||
* @brief 派生预览曲线包络带的**列密度分母**:每 N 个设备像素一列。
|
||||
*
|
||||
* 包络带的填充代价主要取决于**边数**与**覆盖面积**(实测同一形状 1600 列 ≈ 24ms、
|
||||
* 400 列 ≈ 6.4ms):预览曲线是半透明叠加层、且本来就是“逐列 min/max”的聚合结果,
|
||||
* 横向 4px 的分辨率损失在稠密视界下不可辨(尖峰仍不会丢:每列取的是该列的极值)。
|
||||
* 稀疏区间仍走逐点折线(见 `PaintDerivedCurve`),不受影响。
|
||||
*/
|
||||
inline const size_t kDerivedCurveColumnDivisor = 4;
|
||||
/**
|
||||
* @brief 判定“极密视界”的采样密度阈值(采样/设备像素),超过时包络带列密度降到
|
||||
* `kDerivedCurveColumnDivisor`,否则用 2 像素一列。
|
||||
*
|
||||
* 中等密度(1~8 采样/像素)下预览曲线形状本身还有细节,用 2 像素一列即可保住观感;
|
||||
* 极密视界下每列内部就是一条满幅噪声带,4 像素一列肉眼无差别、代价再降一半。
|
||||
*/
|
||||
inline const size_t kDerivedCurveVeryDenseSamplesPerPixel = 8;
|
||||
/**
|
||||
* @brief 图层**纵向滚动**的量化阈值(位图像素)。
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user