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 图层**纵向滚动**的量化阈值(位图像素)。
|
||||
*
|
||||
|
||||
@@ -2020,7 +2020,8 @@ namespace jdi
|
||||
int points, int iterations, int amountPercent,
|
||||
std::size_t begin, std::size_t end,
|
||||
std::size_t& spanBegin,
|
||||
bool allowAsyncCompute) const
|
||||
bool allowAsyncCompute,
|
||||
bool computeIfMissing) const
|
||||
{
|
||||
spanBegin = 0;
|
||||
const std::size_t count = signal.values.size();
|
||||
@@ -2058,6 +2059,34 @@ namespace jdi
|
||||
return &hit->values;
|
||||
}
|
||||
|
||||
// 1b) 「只读」模式(光标取值胶囊):**绝不新建计算任务 / 条目**。
|
||||
if (!computeIfMissing)
|
||||
{
|
||||
if (const CurveEntry* stale = AcquireNewestCurve(key, begin, end, spanBegin))
|
||||
{
|
||||
return &stale->values;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 1b) “只读”模式(光标取值胶囊):**绝不新建计算任务/条目**。
|
||||
//
|
||||
// ⚠️⚠️ 为什么必须这么做(用户 2026-11-02 反馈“拖光标时有明显卡顿”):
|
||||
// 胶囊只为“光标所在的那两个采样”取值,请求窗口只有几个采样 —— 但缓存键里含窗口,
|
||||
// 而窗口随光标每帧都在变 ⇒ 每帧都未命中 ⇒ 每帧同步计算并 `PublishCurve` 一次。
|
||||
// 15 信号 × 3 种曲线 = 每帧 45 次发布,条目列表不断变长,`Acquire*` 的线性搜索
|
||||
// 也随之变慢(实测拖动光标单帧 200ms,其中约 150ms 来自这 45 次重算 + 发布)。
|
||||
// 现在:命中就取(绘制路径发布的窗口**带保护带**、覆盖整个可见区间,正常情况一定命中),
|
||||
// 没命中就退到同 (信号, 种类) 的最新曲线,再没有就本帧不标注。
|
||||
if (!computeIfMissing)
|
||||
{
|
||||
if (const CurveEntry* stale = AcquireNewestCurve(key, begin, end, spanBegin))
|
||||
{
|
||||
return &stale->values;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 2) 代价较大时交后台计算,本帧先用最新旧曲线顶替(双缓冲)。
|
||||
// 这样绘制线程永远不做重计算,拖动滑块/光标/视界/缩放的反馈因此始终跟手。
|
||||
//
|
||||
|
||||
@@ -940,6 +940,10 @@ namespace jdi
|
||||
//(用户 2026-10-29 报告的两处现象)。
|
||||
const int textLayer = UiGetTextLayer();
|
||||
PaintHoverHighlight(g);
|
||||
// 实验性功能面板条目栏的悬停 → 对应**预览曲线**的高亮。
|
||||
// ⚠️ 必须在贴图之后画(见 `PaintDerivedCurvesFor` 的说明):静态场景缓存里
|
||||
// 只有普通曲线,高亮随鼠标变化,烘进缓存就会“悬停看不到 / 移开不消失”。
|
||||
PaintDerivedHoverHighlight(g);
|
||||
PaintLegendHighlight(g);
|
||||
PaintRubberBand(g);
|
||||
PaintCursor(g, m_lanes);
|
||||
@@ -1119,11 +1123,8 @@ namespace jdi
|
||||
// 网格分界线:图层里**不含**它(它锚定在网格的屏幕位置,会被纵向偏移贴图带走),
|
||||
// 因此在这里补画一次(与整幅重绘同一套颜色与线宽)。
|
||||
PaintLaneDividers(g);
|
||||
// 派生预览曲线:基底不含它们(拖参数滑块时基底不变、可直接复用像素)。
|
||||
for (std::size_t i = 0; i < m_lanes.size(); ++i)
|
||||
{
|
||||
PaintLaneDerivedOverlays(g, static_cast<int>(i), m_lanes[i]);
|
||||
}
|
||||
// 派生预览曲线:基底不含它们(走像素缓存层 + 平移滚动,见 `PaintDerivedOverlayLayer`)。
|
||||
PaintDerivedOverlayLayer(g);
|
||||
// 装饰(Y 轴刻度 + GX 编号 + 图例):基底不含(否则平移会重影)。
|
||||
// 内容指纹一致 → 直接按 `AlphaBlend` 贴回缓存(省掉文字绘制);
|
||||
// 否则先把装饰单独绘制进透明缓存位图,再贴回。
|
||||
@@ -1167,9 +1168,27 @@ namespace jdi
|
||||
// 只有"外观样式"变化(典型场景:拖动自定义颜色滑块实时调色):网格结构、视图几何、
|
||||
// 波形采样都没变,只是颜色/不透明度/线宽/模式不同 —— 此时只重画受影响的网格,
|
||||
// 再写回缓存。(VERY_BIG_DATA 实测:整幅重绘 30ms+,单网格重画约 4ms。)
|
||||
//
|
||||
// ⚠️⚠️ **开启了派生预览曲线时不再走这条补丁路径**(用户 2026-11-02 反馈“改颜色极卡”):
|
||||
// 派生曲线已经改由独立的像素缓存层绘制(`PaintDerivedOverlayLayer`),而补丁是
|
||||
// “贴回场景缓存 → 就地重画受影响网格 → 写回缓存”,它会把缓存里**已经烘进去的曲线像素**
|
||||
// 覆盖掉(曲线随即消失),并且它内部 `PaintLane` 也不再画曲线。实测 15 信号 × 3 功能时
|
||||
// 这条路径每帧还要同步重建一个网格图层(宽基底 = 2 倍宽 × 2 倍高,AA 全质量)
|
||||
// ⇒ **单帧 2.2 秒**;而直接走整幅重绘只要 ~0.4 秒(其中约 0.06–0.09 秒是曲线重画)。
|
||||
// 因此这里要求“没有任何派生功能开启”才允许用补丁路径。
|
||||
bool derivedActive = false;
|
||||
for (const Lane& lane : Tab().Lanes())
|
||||
{
|
||||
if (lane.edgeCenter || lane.meanFilter || lane.weakPeak)
|
||||
{
|
||||
derivedActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::vector<int> patchLanes;
|
||||
if (!m_forceFullRepaint && FindStyleOnlyLanes(patchLanes) && BlitSceneCache(g))
|
||||
if (!derivedActive && !m_forceFullRepaint && FindStyleOnlyLanes(patchLanes) &&
|
||||
BlitSceneCache(g))
|
||||
{
|
||||
// ⚠️ 样式**正在连续变化**(拖透明度 / 粗细 / 拾色滑块):这本身就是一种"交互中"。
|
||||
// 若不切换快速帧,本帧会用**抗锯齿**重画波形包络带 —— 实测单条网格的包络带
|
||||
@@ -1179,17 +1198,16 @@ namespace jdi
|
||||
m_fastFrame = true;
|
||||
RepaintSceneCachePatch(g, patchLanes);
|
||||
m_sceneDegraded = true;
|
||||
// ⚠️ **同步重建受影响的宽基底图层**:补丁只更新了“静态场景缓存 + 后备缓冲区”,
|
||||
// 而每网格图层(宽基底)里仍是**旧样式**的像素。若不在这里重建,紧接着命中
|
||||
// “宽基底快路径”的那一帧会把刚画好的新样式**覆盖回旧像素** ——
|
||||
// 用户报告:拖动线宽滑块期间信号一直按旧的线宽绘制、松手后刷新,
|
||||
// 再移动鼠标又变回旧线宽(正是这个覆盖造成的)。
|
||||
// 按既有预算机制重建(每帧至多 1 个网格,不会在交互帧里做整批重建)。
|
||||
// ⚠️ 网格图层(宽基底)里仍是**旧样式**的像素,必须让它失效。
|
||||
// 但**不要在本帧同步重建**:宽基底是“2 倍宽 × 2 倍高”的整块重绘
|
||||
//(15 条稠密信号实测 2.2 秒),同步重建等于把整帧拖到几秒
|
||||
//(用户 2026-11-02 报告“改颜色时卡到无法操作”的直接原因)。
|
||||
// 只登记待重建:图层指纹(`LaneLayerHash` 含颜色/线宽/粗细…)会让宽基底快路径
|
||||
// 自动跳过它(用底板兜底),等视界真正需要时再由既有预算机制补齐。
|
||||
for (const int laneIndex : patchLanes)
|
||||
{
|
||||
QueueLaneRebuild(laneIndex);
|
||||
}
|
||||
RenderPendingLaneRebuilds(1);
|
||||
paintOverlays();
|
||||
return;
|
||||
}
|
||||
@@ -1225,6 +1243,10 @@ namespace jdi
|
||||
|
||||
PaintTimeAxis(g, timeTicks, timeStep);
|
||||
|
||||
// 派生预览曲线(像素缓存层):必须在装饰层**之前** —— 曲线要垫在
|
||||
// Y 轴刻度胶囊 / 图例下面(与改造前的层序一致)。
|
||||
PaintDerivedOverlayLayer(g);
|
||||
|
||||
// 装饰层:缓存与当前内容同源则直接贴回;否则先把装饰绘制进透明缓存位图再贴回。
|
||||
if (!m_decorValid || m_decorKey != SceneDecorHash())
|
||||
{
|
||||
@@ -1957,17 +1979,14 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍:把所有**派生预览曲线**统一绘制在**本网格全部原信号之上**。
|
||||
// 渲染「宽缓冲区」时跳过:宽缓冲只存"基底"(网格/波形,不含派生曲线与装饰),
|
||||
// 派生曲线改由前台叠加层绘制(见 `PaintLaneDerivedOverlays`),
|
||||
// 这样拖派生参数滑块时基底不变、可复用宽缓冲像素。
|
||||
// ⚠️ `m_renderDecorOnly`(把装饰单独画进透明缓存位图)时**也必须跳过**:
|
||||
// 派生曲线是"随时间视界变化"的内容,若烘进装饰位图,贴回时会把旧视界的
|
||||
// 预览曲线永久留在画面上。
|
||||
if (!m_renderWide && !m_renderDecorOnly)
|
||||
{
|
||||
PaintLaneDerivedOverlays(g, laneIndex, lane);
|
||||
}
|
||||
// 第二遍:派生预览曲线**不再**在这里绘制。
|
||||
//
|
||||
// ⚠️ 旧实现把派生曲线画进网格绘制流程(因此也烘进了静态场景缓存与宽缓冲图层),
|
||||
// 代价是“每帧重画全部曲线”(15 信号 × 3 功能 = 45 条,每条按像素列包络带填充
|
||||
// ≈2ms,单帧 90ms)。现在统一由**派生曲线像素缓存层**绘制:
|
||||
// `PaintDerivedOverlayLayer` 在两条快路径里贴图,平移时只滚动像素 + 补一条竖带
|
||||
//(用户 2026-11-02 要求“视界移动 / 光标动画 / 样式修改都要跟手”)。
|
||||
// 因此这里(以及 `m_renderWide` 的图层渲染)都不再画曲线。
|
||||
|
||||
// g.Restore 也会还原绘制状态,此处直接恢复图形状态即可。
|
||||
g.Restore(waveSave);
|
||||
@@ -2111,7 +2130,28 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::PaintLaneDerivedOverlays(gd::Graphics& g, int laneIndex, const LaneVisual& lane) const
|
||||
void PlotView::PaintLaneDerivedOverlays(gd::Graphics& g, int laneIndex,
|
||||
const LaneVisual& lane) const
|
||||
{
|
||||
PaintDerivedCurvesFor(g, laneIndex, lane, false);
|
||||
}
|
||||
|
||||
void PlotView::PaintDerivedHoverHighlight(gd::Graphics& g) const
|
||||
{
|
||||
if (m_loadingBusy || m_meanRowHoverLane < 0 || !m_meanRowHoverRef.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::size_t laneIndex = static_cast<std::size_t>(m_meanRowHoverLane);
|
||||
if (laneIndex >= m_lanes.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
PaintDerivedCurvesFor(g, m_meanRowHoverLane, m_lanes[laneIndex], true);
|
||||
}
|
||||
|
||||
void PlotView::PaintDerivedCurvesFor(gd::Graphics& g, int laneIndex, const LaneVisual& lane,
|
||||
bool hoverOnly) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 ||
|
||||
laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
@@ -2126,6 +2166,10 @@ namespace jdi
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (hoverOnly && !(centerOn || meanFilterOn || weakPeakOn))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(lane.rect, gd::CombineModeIntersect);
|
||||
// 面板条目栏的悬停高亮(用户 2026-10-30 要求):鼠标停在哪一行,就把那条**预览曲线**
|
||||
@@ -2133,11 +2177,21 @@ namespace jdi
|
||||
//
|
||||
// ⚠️⚠️ **必须按面板种类分别判定**(用户 2026-11-02 报告):三个面板(均值滤波 /
|
||||
// 中心轴边沿计数 / 弱峰调整)都有“信号条目栏”,同一条信号可能同时在三个面板里各有一行。
|
||||
// 旧实现只给 MEAN 分支传 `highlight` ⇒ 只开弱峰 + 中心轴时悬停毫无反应;三个全开时
|
||||
// 悬停任何一个面板都只高亮均值滤波那条。现在只高亮“鼠标所在面板”对应的曲线。
|
||||
// 只给 MEAN 分支传 `highlight` ⇒ 只开弱峰 + 中心轴时悬停毫无反应;三个全开时
|
||||
// 悬停任何一个面板都只高亮均值滤波那条。
|
||||
//
|
||||
// ⚠️⚠️⚠️ **高亮改由前景单独绘制**(用户 2026-11-02 报告“悬停高亮丢失”):
|
||||
// 悬停状态不参与 `SceneContentHash`,命中静态场景缓存时本函数**根本不会被调用**,
|
||||
// 因此把高亮画在这里等于“只有整幅重绘的那一帧能看到”。现在:
|
||||
// * `hoverOnly == false`(基底 / 场景缓存路径)→ 只画普通曲线(进入缓存,永远可见);
|
||||
// * `hoverOnly == true`(前景叠加)→ 只画悬停那一条并带光晕。
|
||||
const bool hoverActive = (m_meanRowHoverLane == laneIndex) && m_meanRowHoverRef.IsValid();
|
||||
for (const SignalRef& ref : tabLane.signals)
|
||||
{
|
||||
if (hoverOnly && !(hoverActive && (ref == m_meanRowHoverRef)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
|
||||
{
|
||||
@@ -2150,7 +2204,7 @@ namespace jdi
|
||||
if (centerItem != nullptr)
|
||||
{
|
||||
const bool highlight =
|
||||
hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::CENTER);
|
||||
hoverOnly && hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::CENTER);
|
||||
PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations,
|
||||
highlight);
|
||||
}
|
||||
@@ -2161,7 +2215,7 @@ namespace jdi
|
||||
if (item != nullptr)
|
||||
{
|
||||
const bool highlight =
|
||||
hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::MEAN);
|
||||
hoverOnly && hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::MEAN);
|
||||
PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations, highlight);
|
||||
}
|
||||
}
|
||||
@@ -2177,7 +2231,7 @@ namespace jdi
|
||||
kWeakPeakIterationsMin,
|
||||
std::min(kWeakPeakIterationsMaxLimit, item->iterations));
|
||||
const bool highlight =
|
||||
hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::PEAK);
|
||||
hoverOnly && hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::PEAK);
|
||||
PaintWeakPeakCurve(g, resolved, lane, amountPercent, boostIterations, highlight);
|
||||
}
|
||||
}
|
||||
@@ -2930,7 +2984,7 @@ namespace jdi
|
||||
std::size_t spanBegin = 0;
|
||||
const std::vector<float>* curvePtr =
|
||||
CurveSpanFor(kind, *resolved.data, points, iterations, amountPercent, begin,
|
||||
end, spanBegin, true);
|
||||
end, spanBegin, true, false);
|
||||
if (curvePtr == nullptr)
|
||||
{
|
||||
return;
|
||||
@@ -3288,6 +3342,168 @@ namespace jdi
|
||||
}
|
||||
const std::size_t stride = std::max<std::size_t>(1, visibleCount / std::max<std::size_t>(1, budget));
|
||||
|
||||
// ⭐⭐ **稠密区间改用「逐像素列包络带」**(2026-11-02 性能修复):
|
||||
// 旧实现按 `stride` 抽稀成折线后 `DrawLines`。视界覆盖百万级采样时,相邻抽稀点的
|
||||
// y 会**整幅跳变**(几乎每个线段都是竖直满幅),而 GDI+ 描边的代价 ∝ **覆盖的像素数**:
|
||||
// 实测 3200 个顶点的满幅锯齿折线要 **128ms**(抗锯齿)/ **31ms**(无抗锯齿),
|
||||
// 同样画面的「逐列 min/max 包络带」一次 FillPath 只要 **1.9ms**(约 60 倍),
|
||||
// 而且**不会漏掉尖峰**(等间隔抽样会整段跳过)。
|
||||
// 稀疏区间(可见采样数 ≤ 像素列数)继续走逐点折线:点数本来就少,深放大下也更圆滑。
|
||||
// 列密度:中等密度用「每 2 像素一列」,极密(> 8 采样/像素)时降到 4 像素一列。
|
||||
// 列数直接决定包络带的边数与填充面积 —— 稠密视界下这是唯一有效的加速手段。
|
||||
const float columnDivisor =
|
||||
(visibleCount >
|
||||
static_cast<std::size_t>(std::max(1.0f, m_plotRect.Width)) *
|
||||
kDerivedCurveVeryDenseSamplesPerPixel)
|
||||
? static_cast<float>(kDerivedCurveColumnDivisor)
|
||||
: 2.0f;
|
||||
const int columnCount = static_cast<int>(
|
||||
std::max(1.0f, std::floor(m_plotRect.Width / columnDivisor)));
|
||||
const float columnWidth = m_plotRect.Width / static_cast<float>(columnCount);
|
||||
const bool denseBand = resolved.axis->monotonic && (columnWidth > 0.0f) &&
|
||||
(visibleCount > static_cast<std::size_t>(columnCount));
|
||||
if (denseBand)
|
||||
{
|
||||
// 每列取「等间隔抽样」的若干个采样记录上下包络:抽样密度只影响包络的饱满程度,
|
||||
// 却把逐点扫描压到「像素列数 × 少量采样」量级。
|
||||
const std::size_t columnStride = std::max<std::size_t>(
|
||||
1, visibleCount /
|
||||
(static_cast<std::size_t>(columnCount) * kDerivedCurveSamplesPerColumn));
|
||||
std::vector<BandColumn> columnsData;
|
||||
std::vector<std::size_t> segmentBreaks;
|
||||
columnsData.reserve(static_cast<std::size_t>(columnCount) + 1);
|
||||
bool inSegment = false;
|
||||
int lastColumn = -1;
|
||||
for (std::size_t index = begin; index < end; index += columnStride)
|
||||
{
|
||||
// 水平镜像 = 数据反序:遍历下标映射到源下标后再取曲线值。
|
||||
const std::size_t sourceIndex = SourceIndexFor(*resolved.style, count, index);
|
||||
if (sourceIndex < curveBegin || (sourceIndex - curveBegin) >= curve.size())
|
||||
{
|
||||
inSegment = false;
|
||||
continue;
|
||||
}
|
||||
const float sampled = curve[sourceIndex - curveBegin];
|
||||
if (!std::isfinite(sampled))
|
||||
{
|
||||
inSegment = false;
|
||||
continue;
|
||||
}
|
||||
float y =
|
||||
ValueToY(ApplyDrawTransform(*resolved.style, static_cast<double>(sampled)), lane);
|
||||
y = static_cast<float>(ClampDouble(y, limitTop, limitBottom));
|
||||
// 时间偏移:该采样画在 `t + offsetTime` 处。
|
||||
const float x = TimeToX(time[index] + resolved.style->offsetTime);
|
||||
int column = static_cast<int>(std::floor((x - m_plotRect.X) / columnWidth));
|
||||
column = std::max(0, std::min(columnCount - 1, column));
|
||||
if (!inSegment)
|
||||
{
|
||||
segmentBreaks.push_back(columnsData.size());
|
||||
BandColumn fresh;
|
||||
fresh.x = x;
|
||||
fresh.top = y;
|
||||
fresh.bottom = y;
|
||||
columnsData.push_back(fresh);
|
||||
lastColumn = column;
|
||||
inSegment = true;
|
||||
continue;
|
||||
}
|
||||
if (column == lastColumn)
|
||||
{
|
||||
BandColumn& back = columnsData.back();
|
||||
back.top = std::min(back.top, y);
|
||||
back.bottom = std::max(back.bottom, y);
|
||||
continue;
|
||||
}
|
||||
BandColumn next;
|
||||
next.x = x;
|
||||
next.top = y;
|
||||
next.bottom = y;
|
||||
columnsData.push_back(next);
|
||||
lastColumn = column;
|
||||
}
|
||||
if (columnsData.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 线宽补偿与波形基底同一套(`EmitEnvelopeBand` 做纵向外扩 + 横向膨胀),
|
||||
// 因此“把线宽拖大”时预览曲线的竖直沿同样会变粗。
|
||||
const float strokeHalf = std::max(1.0f, resolved.style->lineWidth) * m_scale * 0.5f;
|
||||
const auto buildBand = [&](float halfWidth, std::vector<gd::PointF>& outBand,
|
||||
std::vector<std::size_t>& outBreaks, bool& outSpiky)
|
||||
{
|
||||
outBand.clear();
|
||||
outBreaks.clear();
|
||||
outSpiky = false;
|
||||
for (std::size_t s = 0; s < segmentBreaks.size(); ++s)
|
||||
{
|
||||
const std::size_t start = segmentBreaks[s];
|
||||
const std::size_t stop =
|
||||
(s + 1 < segmentBreaks.size()) ? segmentBreaks[s + 1] : columnsData.size();
|
||||
if (stop < start + 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::vector<BandColumn> part(columnsData.begin() + static_cast<std::ptrdiff_t>(start),
|
||||
columnsData.begin() + static_cast<std::ptrdiff_t>(stop));
|
||||
EmitEnvelopeBand(part, outBand, outBreaks, outSpiky, lane.rect.Height, halfWidth);
|
||||
}
|
||||
};
|
||||
const auto fillBand = [&](const std::vector<gd::PointF>& bandPoints,
|
||||
const std::vector<std::size_t>& bandBreaks, std::uint32_t color)
|
||||
{
|
||||
gd::GraphicsPath path;
|
||||
// 包络带会自交(相邻列的上下包络交叉),必须用缠绕规则,否则交叠处被挖空。
|
||||
path.SetFillMode(gd::FillModeWinding);
|
||||
for (std::size_t k = 0; k < bandBreaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = bandBreaks[k];
|
||||
const std::size_t stop =
|
||||
(k + 1 < bandBreaks.size()) ? bandBreaks[k + 1] : bandPoints.size();
|
||||
const std::size_t total = stop - start;
|
||||
if (total < 4 || (total % 2) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t half = total / 2;
|
||||
path.StartFigure();
|
||||
path.AddLines(bandPoints.data() + start, static_cast<INT>(half));
|
||||
path.AddLines(bandPoints.data() + start + half, static_cast<INT>(half));
|
||||
path.CloseFigure();
|
||||
}
|
||||
if (path.GetPointCount() > 0)
|
||||
{
|
||||
gd::SolidBrush brush(ToGdiColor(color));
|
||||
g.FillPath(&brush, &path);
|
||||
}
|
||||
};
|
||||
std::vector<gd::PointF> bandPoints;
|
||||
std::vector<std::size_t> bandBreaks;
|
||||
bool spiky = false;
|
||||
buildBand(strokeHalf, bandPoints, bandBreaks, spiky);
|
||||
if (bandBreaks.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int savedSmoothing = g.GetSmoothingMode();
|
||||
if (spiky || m_fastFrame)
|
||||
{
|
||||
g.SetSmoothingMode(gd::SmoothingModeNone);
|
||||
}
|
||||
// 悬停高亮:先用**更粗的同一几何**铺一层半透明光晕(垫在曲线下面),再画曲线本身。
|
||||
if (highlight)
|
||||
{
|
||||
std::vector<gd::PointF> haloSpan;
|
||||
std::vector<std::size_t> haloBreaks;
|
||||
bool haloSpiky = false;
|
||||
buildBand(strokeHalf + 2.5f * m_scale, haloSpan, haloBreaks, haloSpiky);
|
||||
fillBand(haloSpan, haloBreaks, WithAlpha(derived, 0x55u));
|
||||
}
|
||||
fillBand(bandPoints, bandBreaks, derived);
|
||||
g.SetSmoothingMode(static_cast<gd::SmoothingMode>(savedSmoothing));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<gd::PointF> points;
|
||||
std::vector<std::size_t> breaks;
|
||||
points.reserve(std::min<std::size_t>(visibleCount, budget) + 1);
|
||||
|
||||
@@ -744,6 +744,306 @@ namespace jdi
|
||||
return any;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 「派生预览曲线」像素缓存
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::uint64_t PlotView::DerivedOverlayKey() const
|
||||
{
|
||||
// 内容指纹 = 场景内容(含外观样式 + 派生参数 + 网格幅值范围 + 网格滚动量 + 绘图区几何)。
|
||||
// ⚠️ 视界**不在这里**:平移时要靠它判断“能否只滚动像素”,所以视界单独比对。
|
||||
std::uint64_t hash = SceneContentHash(true, true, true, true);
|
||||
// 后台算好的派生曲线到达时,曲线像素必须更新(`m_curveSerial` 随每次发布自增)。
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
hash ^= m_curveSerial * 1099511628211ull;
|
||||
}
|
||||
// 高亮状态**不参与**:高亮由前景单独绘制(见 `PaintDerivedHoverHighlight`)。
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool PlotView::EnsureDerivedCache(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_derivedDc != nullptr && m_derivedBits != nullptr && m_derivedW == width &&
|
||||
m_derivedH == height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
ReleaseDerivedCache();
|
||||
HDC screen = GetDC(nullptr);
|
||||
if (screen == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 与装饰层同理:必须是 32bpp DIB 段(带真 alpha + 像素可直接 memmove 滚动)。
|
||||
BITMAPINFO info = {};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = width;
|
||||
info.bmiHeader.biHeight = -height; // 自上而下
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
void* bits = nullptr;
|
||||
HBITMAP dib = CreateDIBSection(screen, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
ReleaseDC(nullptr, screen);
|
||||
if (dib == nullptr || bits == nullptr)
|
||||
{
|
||||
if (dib != nullptr)
|
||||
{
|
||||
DeleteObject(dib);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
HDC dc = CreateCompatibleDC(nullptr);
|
||||
if (dc == nullptr)
|
||||
{
|
||||
DeleteObject(dib);
|
||||
return false;
|
||||
}
|
||||
m_derivedOldBitmap = SelectObject(dc, dib);
|
||||
m_derivedDc = dc;
|
||||
m_derivedBitmap = dib;
|
||||
m_derivedBits = bits;
|
||||
m_derivedW = width;
|
||||
m_derivedH = height;
|
||||
m_derivedValid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::ReleaseDerivedCache()
|
||||
{
|
||||
m_derivedValid = false;
|
||||
if (m_derivedDc != nullptr)
|
||||
{
|
||||
if (m_derivedOldBitmap != nullptr)
|
||||
{
|
||||
SelectObject(m_derivedDc, m_derivedOldBitmap);
|
||||
m_derivedOldBitmap = nullptr;
|
||||
}
|
||||
DeleteDC(m_derivedDc);
|
||||
m_derivedDc = nullptr;
|
||||
}
|
||||
if (m_derivedBitmap != nullptr)
|
||||
{
|
||||
DeleteObject(m_derivedBitmap);
|
||||
m_derivedBitmap = nullptr;
|
||||
}
|
||||
m_derivedBits = nullptr;
|
||||
m_derivedW = 0;
|
||||
m_derivedH = 0;
|
||||
}
|
||||
|
||||
bool PlotView::RenderDerivedCache()
|
||||
{
|
||||
m_derivedValid = false;
|
||||
if (m_theme == nullptr || m_fonts == nullptr || m_lanes.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int width = static_cast<int>(std::ceil(m_area.X + m_area.Width));
|
||||
const int height = static_cast<int>(std::ceil(m_area.Y + m_area.Height));
|
||||
if (!EnsureDerivedCache(width, height))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t bytes = static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4u;
|
||||
if (!m_derivedStripActive)
|
||||
{
|
||||
// 整块重画:先把全部像素清成透明。
|
||||
std::memset(m_derivedBits, 0, bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 补条带:只把这条竖带清成透明(其余像素是滚动过来的有效内容)。
|
||||
const int x0 = std::max(0, static_cast<int>(std::floor(m_derivedStripX0)));
|
||||
const int x1 = std::min(width, static_cast<int>(std::ceil(m_derivedStripX1)));
|
||||
for (int y = 0; y < height; ++y)
|
||||
{
|
||||
unsigned char* row = static_cast<unsigned char*>(m_derivedBits) +
|
||||
(static_cast<std::size_t>(y) * static_cast<std::size_t>(width)) * 4u;
|
||||
if (x1 > x0)
|
||||
{
|
||||
std::memset(row + static_cast<std::size_t>(x0) * 4u, 0,
|
||||
static_cast<std::size_t>(x1 - x0) * 4u);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
gd::Graphics g(m_derivedDc);
|
||||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
if (m_derivedStripActive)
|
||||
{
|
||||
// ⚠️ 绘制期的横向裁剪(`PaintDerivedCurvesFor` 只做 `intersect`,不会覆盖这一层)。
|
||||
g.SetClip(gd::RectF(m_derivedStripX0, 0.0f, std::max(0.0f, m_derivedStripX1 - m_derivedStripX0),
|
||||
static_cast<float>(height)),
|
||||
gd::CombineModeIntersect);
|
||||
}
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
PaintDerivedCurvesFor(g, static_cast<int>(index), m_lanes[index], false);
|
||||
}
|
||||
}
|
||||
m_derivedContentKey = DerivedOverlayKey();
|
||||
m_derivedViewMin = Tab().ViewMin();
|
||||
m_derivedViewMax = Tab().ViewMax();
|
||||
m_derivedValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::BlitDerivedCache(gd::Graphics& g)
|
||||
{
|
||||
if (!m_derivedValid || m_derivedDc == nullptr || m_plotRect.Width < 1.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void* proc = ResolveAlphaBlend();
|
||||
if (proc == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using AlphaBlendFn = BOOL(WINAPI*)(HDC, int, int, int, int, HDC, int, int, int, int, BLENDFUNCTION);
|
||||
AlphaBlendFn blend = reinterpret_cast<AlphaBlendFn>(proc);
|
||||
HDC target = g.GetHDC();
|
||||
if (target == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 缓存位图原点 = 客户区 (0,0):源与目标坐标相同。只贴绘图区(曲线被夹在网格内)。
|
||||
const int x = std::max(0, static_cast<int>(std::floor(m_plotRect.X)));
|
||||
const int y = std::max(0, static_cast<int>(std::floor(m_plotRect.Y)));
|
||||
const int w = std::min(m_derivedW - x, static_cast<int>(std::ceil(m_plotRect.Width)));
|
||||
const int h = std::min(m_derivedH - y, static_cast<int>(std::ceil(m_plotRect.Height)));
|
||||
if (w <= 0 || h <= 0)
|
||||
{
|
||||
g.ReleaseHDC(target);
|
||||
return false;
|
||||
}
|
||||
BLENDFUNCTION fn = {};
|
||||
fn.BlendOp = AC_SRC_OVER;
|
||||
fn.SourceConstantAlpha = 255;
|
||||
fn.AlphaFormat = AC_SRC_ALPHA;
|
||||
const BOOL done = blend(target, x, y, w, h, m_derivedDc, x, y, w, h, fn);
|
||||
g.ReleaseHDC(target);
|
||||
return done != FALSE;
|
||||
}
|
||||
|
||||
void PlotView::PaintDerivedOverlayLayer(gd::Graphics& g)
|
||||
{
|
||||
if (m_loadingBusy || m_lanes.empty())
|
||||
{
|
||||
return; // 加载遮罩期间不花时间在派生曲线上(本来也看不见)
|
||||
}
|
||||
// 没有任何派生功能开启时直接跳过(并释放缓存,避免白占内存)。
|
||||
bool anyFeature = false;
|
||||
for (const Lane& lane : Tab().Lanes())
|
||||
{
|
||||
if (lane.edgeCenter || lane.meanFilter || lane.weakPeak)
|
||||
{
|
||||
anyFeature = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyFeature)
|
||||
{
|
||||
m_derivedValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::uint64_t contentKey = DerivedOverlayKey();
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
const double span = viewMax - viewMin;
|
||||
bool needRender = true;
|
||||
bool rolled = false;
|
||||
m_derivedStripActive = false;
|
||||
if (m_derivedValid && m_derivedContentKey == contentKey && span > 0.0)
|
||||
{
|
||||
const double cachedSpan = m_derivedViewMax - m_derivedViewMin;
|
||||
const double sameSpanTolerance = std::max(1.0e-12, std::fabs(span) * 1.0e-9);
|
||||
if (std::fabs(cachedSpan - span) <= sameSpanTolerance &&
|
||||
m_derivedViewMin == viewMin && m_derivedViewMax == viewMax)
|
||||
{
|
||||
needRender = false; // 完全同源:只贴图
|
||||
}
|
||||
else if (std::fabs(cachedSpan - span) <= sameSpanTolerance && m_plotRect.Width > 1.0f)
|
||||
{
|
||||
// 纯平移:像素整体横移,只重画新露出的竖带(与宽缓冲区的滚动同构)。
|
||||
const double pxPerTime = static_cast<double>(m_plotRect.Width) / span;
|
||||
const int shift = static_cast<int>(
|
||||
std::lround((m_derivedViewMin - viewMin) * pxPerTime));
|
||||
const int width = m_derivedW;
|
||||
if (shift != 0 && std::abs(shift) < width)
|
||||
{
|
||||
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4u;
|
||||
const std::size_t moveBytes =
|
||||
rowBytes - static_cast<std::size_t>(std::abs(shift)) * 4u;
|
||||
unsigned char* bits = static_cast<unsigned char*>(m_derivedBits);
|
||||
if (bits != nullptr && moveBytes > 0)
|
||||
{
|
||||
for (int y = 0; y < m_derivedH; ++y)
|
||||
{
|
||||
unsigned char* row = bits + static_cast<std::size_t>(y) * rowBytes;
|
||||
if (shift > 0)
|
||||
{
|
||||
// 内容右移:从本行右端往前搬(`memmove` 自带重叠安全)。
|
||||
std::memmove(row + static_cast<std::size_t>(shift) * 4u, row, moveBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 内容左移:源在本行右侧 —— 目标**不能**越过行首(否则越界)。
|
||||
std::memmove(row, row + static_cast<std::size_t>(-shift) * 4u, moveBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
const float plotLeft = m_plotRect.X;
|
||||
const float plotRight = m_plotRect.X + m_plotRect.Width;
|
||||
if (shift > 0)
|
||||
{
|
||||
m_derivedStripX0 = plotLeft;
|
||||
m_derivedStripX1 = std::min(plotRight, plotLeft + static_cast<float>(shift) + 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_derivedStripX0 = std::max(plotLeft, plotRight + static_cast<float>(shift) - 1.0f);
|
||||
m_derivedStripX1 = plotRight;
|
||||
}
|
||||
if (m_derivedStripX1 > m_derivedStripX0 + 0.5f)
|
||||
{
|
||||
m_derivedStripActive = true;
|
||||
rolled = true;
|
||||
}
|
||||
}
|
||||
needRender = true;
|
||||
}
|
||||
}
|
||||
if (needRender)
|
||||
{
|
||||
if (!RenderDerivedCache())
|
||||
{
|
||||
m_derivedStripActive = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_derivedStripActive = false;
|
||||
if (!BlitDerivedCache(g))
|
||||
{
|
||||
// 无法 AlphaBlend:退回逐条曲线直接绘制(正确性优先,代价高但极少发生)。
|
||||
if (!rolled)
|
||||
{
|
||||
m_derivedValid = false;
|
||||
}
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
PaintLaneDerivedOverlays(g, static_cast<int>(index), m_lanes[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PlotView::EnsureLaneLayer(LaneLayer& layer, int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
|
||||
Reference in new issue
Block a user