装饰层改用带 alpha 的独立图层(消除 GX/图例"直角白边")+ 图例圆角托底 + 预览曲线内存释放
一、GX 编号 / 图例「圆角矩形外多出一圈直角白边」的根因与修复
- 根因:装饰层原本是"从后备缓冲区**抓取一块不透明矩形**"(CaptureDecorCache + BitBlt),
圆角之外(属于波形)的像素被一起烘进缓存;为了让这块矩形像素能跨视界复用,
又在圆角矩形外铺了一层**直角**的不透明底色(OpaqueBaseRect)。两者叠加 ⇒ 圆角之外
显示的是网格底色而不是波形 ⇒ 在密集波形上就是一圈直角白边(Y 轴刻度胶囊同理)。
- 修法:把装饰层改成**带真 alpha 的独立图层**:
* m_decorDc 改为 32bpp CreateDIBSection(自上而下),像素可直接读写 alpha;
* 新增 RenderDecorCache():把装饰**单独绘制**进一张全透明的位图(不再从后备缓冲区抓取),
圆角之外保持全透明;位图覆盖范围为「客户区原点 → 绘图区右下角」,绘制坐标与正常绘制
完全一致(GDI 文本不认 GDI+ 变换,绝不能做坐标平移);
* BlitDecorCache() 改用 AlphaBlend(AC_SRC_ALPHA,GDI+ 写入 DIB 段即预乘 alpha)逐块合成
⇒ 圆角之外透出的是**当前**波形,既没有白边也不会残留旧波形像素;
* AlphaBlend 经 GetProcAddress(gdi32!GdiAlphaBlend)(回退 msimg32!AlphaBlend)解析,
不新增链接依赖(两台协作设备工具链不同,避免改 Makefile);
* alpha 修补:GDI 文本(DrawTextW/ExtTextOutW)会把写入像素的 alpha 清成 0,
若不修补文字处会变成"透明洞",因此在装饰矩形内把「A==0 且有颜色」的像素还原为不透明;
* 删除全部外扩的直角不透明底色:Y 轴胶囊与图例托底都只画圆角形状本身;
* 补齐「+N」(图例空间不足)与「(无可见信号)」提示的装饰矩形(否则画进缓存却不被贴回)。
- 装饰缓存的每帧开销因此更低:BlitDecorCache 约 0.19ms/帧(原 BitBlt 方案约 0.3ms),
RenderDecorCache 仅在装饰内容变化时执行(约 2.4ms / 5 网格)。
二、图例样式:与 GX 编号徽标同款圆角托底 + 悬停/选中配色统一
- 每个图例条目 = 一个圆角矩形托底(kLegendPlateRadius=4,cardBg 底 + border 描边),
内容为色块 + 信号名 —— "近似于一个 GX 标签,只是内容换成了图例信息"。
- 选中态与悬停态同配色(accentSoft 底 + accent 描边 + textStrong 文字),与 GX 徽标悬停一致;
悬停高亮画在贴图之后,并**在不透明托底上原样重绘色块与文字**(否则内容会被盖住),
同时保留对应信号的曲线光晕;LegendHit 新增 radius 字段。
- 选中态与左侧信号列表同源(DataSet::Selected()):面板选中 → 图例高亮;图例点选 → 面板行选中。
三、预览曲线内存:thread_local 工作缓冲封顶 + 关闭功能即释放
- 根因:DataModel 里三处 thread_local 工作缓冲(弱峰调整的 current/next/极值表、
ApplyWeakPeakStage 的位移表、迭代均值滤波的中转表)。整段信号(2580 万采样)计算时
每条后台线程常驻数百 MB,而后台曲线线程池最多 4 条线程 ⇒ 2GB 以上;thread_local
只在线程退出时才析构 ⇒ 关掉预览不释放,只有 PauseDerivedAsync()(关闭文件分组会调用)
join 线程后才归还(用户实测 1.42GB → 4.21GB 且关不掉)。
- 修法:① 保留容量封顶(kReuseBufferMaxElements=4M + TrimReuseBuffer + ReuseBufferScope
作用域守卫,所有退出路径都会裁剪):超大计算改为一次性分配、用完即释放,内存不再随
线程数放大;中/小规模仍保留容量以维持"多阶弱峰调整 991ms → 577ms"的收益。
② 新增 AnyDerivedPreviewEnabled() / TrimDerivedCurveDataIfIdle(),在
SetLaneWeakPeak / SetLaneMeanFilter / SetLaneEdgeCenterCount(..., false) 里调用 ——
关闭最后一个预览功能时立即 PauseDerivedAsync()(join 线程 ⇒ thread_local 析构 ⇒ 归还内存)
并清空均值/弱峰曲线缓存。
- 实测(.preflight/perf/stylecheck mode 2,VERY_BIG_DATA):
baseline 1202MB → 弱峰(10 迭代,1 条) 1399MB → 5 条曲线 1792MB → **关闭面板后 1203MB**。
⚠️ 计算期间的瞬时峰值仍可达约 2.2GB(整段 O(n) 计算的固有代价),但计算结束立刻归还。
四、验证(全部离屏,不占用鼠标/键盘/屏幕)
- .preflight/perf/verify_main.cpp(离屏 Application harness)**49/49 通过**,新增/强化:
装饰位图圆角外 alpha≈0、中心 alpha=255(直接读 m_decorBits,证明没有直角白边);
选中图例=悬停配色(accentSoft 像素 未选中 0 / 选中 1525);平移(宽基底)帧的装饰与整幅重绘
逐像素一致(只比较装饰不透明像素:worst=1 / 29554px);图例溢出的「+N」提示已登记且可见。
- .preflight/perf/stylecheck_main.cpp:mode 0(装饰重建/贴回耗时)、mode 1(密集波形下导出
GX+图例角落 PNG 目视)、mode 2(预览曲线内存增长/释放)、mode 4(装饰位图 alpha 形状检查)。
- zoomcheck mode 13:收敛帧 vs 整幅重绘 **逐像素 0 差异**(此前为 3503)。
- make clean && make all 零警告。
This commit is contained in:
1 parent
7c32eca004
commit
1b3ab4ac09
4 files changed
+461
-153
No files matched your search
@@ -403,6 +403,24 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void PauseDerivedAsync();
|
void PauseDerivedAsync();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 当前标签页是否还有任何「带预览曲线」的实验性功能处于开启状态。
|
||||||
|
* @return 至少一个网格开启了弱峰调整 / 均值滤波 / 边沿计数(中心轴)返回 true。
|
||||||
|
*/
|
||||||
|
bool AnyDerivedPreviewEnabled() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 若已无任何预览曲线功能,立即释放派生曲线的全部数据。
|
||||||
|
*
|
||||||
|
* 释放内容包括:后台线程池(**join 之后各线程的 `thread_local` 工作缓冲才析构、
|
||||||
|
* 内存才归还**)、待算队列、已算好的曲线条目与均值/弱峰曲线缓存。
|
||||||
|
*
|
||||||
|
* ⚠️ 这是"关掉预览后内存不下降"的修复:一条整段(千万级采样)曲线的中间缓冲
|
||||||
|
* 可达数百 MB,而 `thread_local` 只在线程退出时释放 —— 过去只有"
|
||||||
|
* 关闭整个文件分组"(会 `PauseDerivedAsync`)才会真正还内存。
|
||||||
|
*/
|
||||||
|
void TrimDerivedCurveDataIfIdle();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 三态循环切换时间光标:无 → 单光标 → 双光标 → 无。
|
* @brief 三态循环切换时间光标:无 → 单光标 → 双光标 → 无。
|
||||||
* @return 状态发生变化返回 true。
|
* @return 状态发生变化返回 true。
|
||||||
@@ -1878,6 +1896,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
SignalRef ref; /**< 对应信号。 */
|
SignalRef ref; /**< 对应信号。 */
|
||||||
gd::RectF rect; /**< 条目托底矩形(同时是命中区域)。 */
|
gd::RectF rect; /**< 条目托底矩形(同时是命中区域)。 */
|
||||||
|
float radius = 0.0f; /**< 托底圆角半径(悬停高亮重绘时使用)。 */
|
||||||
gd::RectF swatch; /**< 色块矩形。 */
|
gd::RectF swatch; /**< 色块矩形。 */
|
||||||
gd::RectF text; /**< 文本矩形。 */
|
gd::RectF text; /**< 文本矩形。 */
|
||||||
std::wstring label; /**< 已经过省略处理的显示文本。 */
|
std::wstring label; /**< 已经过省略处理的显示文本。 */
|
||||||
@@ -2295,18 +2314,21 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool m_sceneDegraded = false;
|
bool m_sceneDegraded = false;
|
||||||
|
|
||||||
// ---- 「装饰层」像素缓存(Y 轴刻度胶囊 + 图例)----
|
// ---- 「装饰层」像素缓存(Y 轴刻度胶囊 + GX 编号 + 图例)----
|
||||||
// 装饰(Y 轴刻度文字与其圆角底板、图例)只取决于网格布局 / 幅值范围 / 信号外观与
|
// 装饰只取决于网格布局 / 幅值范围 / 信号外观与图例文案,与时间视界无关;但它
|
||||||
// 图例文案,与时间视界无关;但它**不适合**放进「宽基底」:平移是从基底里截取子矩形,
|
// **不适合**放进「宽基底」:平移是从基底里截取子矩形,烘进基底的装饰会跟着一起
|
||||||
// 烘进基底的装饰会跟着一起移动 → 在网格左缘之外留下一排旧图例/刻度(重影)。
|
// 移动 → 在网格左缘之外留下一排旧图例/刻度(重影)。因此单独缓存一层的像素。
|
||||||
// 因此单独缓存一帧**按当前视界绘制**的装饰像素,并在内容指纹完全一致时整块贴回。
|
|
||||||
//
|
//
|
||||||
// ⚠️ 关键不变量:**每一帧“真正绘制了装饰”都要重新捕获**(无论是否快速帧)。
|
// ⚠️ **本缓存带真 Alpha**(32bpp DIB + `AlphaBlend`),而不是"从后备缓冲区抓一块不透明
|
||||||
// 只要保证“缓存像素”与“缓存指纹”永远同源,贴回就不可能过期;
|
// 矩形":装饰是**圆角**图形,抓矩形会把圆角之外(属于波形)的像素也一并烘进缓存,
|
||||||
// 反之(例如只在全质量帧捕获)会在几何/外观变化后贴回旧像素,
|
// 平移贴回时露出"圆角矩形外围多出一圈直角白边 / 旧波形像素残留"(用户报告)。
|
||||||
// 表现为残留 / 黑块 / 错位等错误元素。
|
// 现在改为:**把装饰单独绘制进一张透明 DIB**(圆角之外保持全透明),贴回时用
|
||||||
|
// `AlphaBlend` 合成 ⇒ 圆角之外透出的是**当前**波形,既干净又可跨视界复用。
|
||||||
|
//
|
||||||
|
// ⚠️ 关键不变量:**每一帧要显示装饰时,都用 `m_decorKey == SceneDecorHash()` 判定缓存
|
||||||
|
// 是否同源**;不同源就必须先 `RenderDecorCache()` 再贴回。
|
||||||
/**
|
/**
|
||||||
* @brief 确保装饰缓存位图尺寸正确(不足则重建)。
|
* @brief 确保装饰缓存位图尺寸正确(不足则重建为 32bpp 的 DIB 段)。
|
||||||
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
|
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
|
||||||
* @return 可用返回 true。
|
* @return 可用返回 true。
|
||||||
*/
|
*/
|
||||||
@@ -2314,42 +2336,47 @@ namespace jdi
|
|||||||
/** @brief 释放装饰缓存位图并作废缓存。 */
|
/** @brief 释放装饰缓存位图并作废缓存。 */
|
||||||
void ReleaseDecorCache();
|
void ReleaseDecorCache();
|
||||||
/**
|
/**
|
||||||
* @brief 把本帧记录下来的装饰矩形从后备缓冲区捕获进装饰缓存。
|
* @brief 把装饰(Y 轴胶囊 + GX 编号 + 图例)**单独绘制**进透明的装饰缓存位图。
|
||||||
* @param [IN,OUT] g 绘图上下文。 @return 捕获成功返回 true。
|
*
|
||||||
|
* 位图覆盖范围 = 客户区原点到绘图区右下角(`m_area` 的右下角),因此绘制坐标与
|
||||||
|
* 正常绘制**完全一致**(GDI 文本不认 GDI+ 变换,必须避免任何坐标平移)。
|
||||||
|
* 结束后做一次 alpha 修补:GDI 文本会把写入像素的 alpha 清成 0,需要还原为不透明。
|
||||||
|
* @return 成功返回 true。
|
||||||
*/
|
*/
|
||||||
bool CaptureDecorCache(gd::Graphics& g);
|
bool RenderDecorCache();
|
||||||
/**
|
/**
|
||||||
* @brief 把装饰缓存里的装饰贴回后备缓冲区(本帧跳过了装饰绘制时使用)。
|
* @brief 把装饰缓存里的装饰按 `AlphaBlend` 贴回后备缓冲区。
|
||||||
* @param [IN,OUT] g 绘图上下文。 @return 贴图成功返回 true。
|
* @param [IN,OUT] g 绘图上下文。 @return 贴图成功返回 true。
|
||||||
*/
|
*/
|
||||||
bool BlitDecorCache(gd::Graphics& g);
|
bool BlitDecorCache(gd::Graphics& g);
|
||||||
|
/**
|
||||||
|
* @brief 取 `AlphaBlend` 函数指针(`gdi32!GdiAlphaBlend`,必要时回退到 `msimg32!AlphaBlend`)。
|
||||||
|
*
|
||||||
|
* 走 `GetProcAddress` 而不是链接库:本项目的 Makefile 只链接 gdi32 等固定几个库,
|
||||||
|
* 且两台协作设备的工具链不同,动态解析可避免改动构建脚本、也避免引入新的链接依赖。
|
||||||
|
* @return 可用返回函数指针,否则返回 nullptr。
|
||||||
|
*/
|
||||||
|
static void* ResolveAlphaBlend();
|
||||||
|
|
||||||
mutable HDC m_decorDc = nullptr; /**< 装饰缓存设备上下文。 */
|
mutable HDC m_decorDc = nullptr; /**< 装饰缓存设备上下文。 */
|
||||||
mutable HBITMAP m_decorBitmap = nullptr; /**< 装饰缓存位图。 */
|
mutable HBITMAP m_decorBitmap = nullptr; /**< 装饰缓存位图。 */
|
||||||
mutable HGDIOBJ m_decorOldBitmap = nullptr; /**< 装饰 DC 原选位图。 */
|
mutable HGDIOBJ m_decorOldBitmap = nullptr; /**< 装饰 DC 原选位图。 */
|
||||||
|
mutable void* m_decorBits = nullptr; /**< 位图像素(32bpp DIB 段,可写 alpha)。 */
|
||||||
mutable int m_decorW = 0; /**< 装饰缓存宽度。 */
|
mutable int m_decorW = 0; /**< 装饰缓存宽度。 */
|
||||||
mutable int m_decorH = 0; /**< 装饰缓存高度。 */
|
mutable int m_decorH = 0; /**< 装饰缓存高度。 */
|
||||||
mutable bool m_decorValid = false; /**< 装饰缓存内容是否有效。 */
|
mutable bool m_decorValid = false; /**< 装饰缓存内容是否有效。 */
|
||||||
mutable std::uint64_t m_decorKey = 0; /**< 装饰缓存对应的内容指纹。 */
|
mutable std::uint64_t m_decorKey = 0; /**< 装饰缓存对应的内容指纹。 */
|
||||||
mutable bool m_useDecorCache = false; /**< 本帧是否跳过绘制装饰、改用缓存贴回。 */
|
mutable bool m_useDecorCache = false; /**< 本帧是否跳过绘制装饰、改用缓存贴回。 */
|
||||||
/** 本帧需要捕获的装饰矩形(各网格的 Y 轴胶囊与图例带)。 */
|
/** 本帧需要捕获的装饰矩形(各网格的 Y 轴胶囊 + GX 编号 + 图例托底)。 */
|
||||||
mutable std::vector<gd::RectF> m_decorRects;
|
mutable std::vector<gd::RectF> m_decorRects;
|
||||||
/**
|
/**
|
||||||
* @brief 缓存里那批像素对应的装饰矩形(捕获时与像素同时保存)。
|
* @brief 缓存里那批像素对应的装饰矩形(与像素同时生成)。
|
||||||
*
|
*
|
||||||
* ⚠️ 贴回必须用这一份,**不能**用当前的 `m_decorRects`:
|
* ⚠️ 贴回必须用这一份,**不能**用当前的 `m_decorRects`:
|
||||||
* 命中缓存时各网格会跳过绘制、根本不会重建矩形列表(为空),
|
* 命中缓存时各网格会跳过绘制、根本不会重建矩形列表(为空),
|
||||||
* 若用当前列表就会贴错位置或一个都不贴(表现为装饰消失 / 黑块 / 错位)。
|
* 若用当前列表就会贴错位置或一个都不贴(表现为装饰消失 / 黑块 / 错位)。
|
||||||
*/
|
*/
|
||||||
mutable std::vector<gd::RectF> m_decorCapturedRects;
|
mutable std::vector<gd::RectF> m_decorCapturedRects;
|
||||||
/**
|
|
||||||
* @brief 捕获缓存像素时使用的绘图区原点(`m_area` 左上角)。
|
|
||||||
*
|
|
||||||
* ⚠️ 贴回时**必须用这一份**推算源偏移:宽缓冲区渲染会临时改写 `m_area`,
|
|
||||||
* 若捕获与贴回两帧的 `m_area` 不同,直接用当前 `m_area` 作原点会让整块装饰错位。
|
|
||||||
*/
|
|
||||||
mutable int m_decorOriginX = 0;
|
|
||||||
mutable int m_decorOriginY = 0;
|
|
||||||
|
|
||||||
// ---- 每网格「信号图层」(前台同步维护;交互帧只做贴图 / 坐标偏移) ----
|
// ---- 每网格「信号图层」(前台同步维护;交互帧只做贴图 / 坐标偏移) ----
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1328,6 +1328,35 @@ namespace jdi
|
|||||||
m_curveSceneDirty.store(false, std::memory_order_relaxed);
|
m_curveSceneDirty.store(false, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PlotView::AnyDerivedPreviewEnabled() const
|
||||||
|
{
|
||||||
|
if (m_tab == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const Lane& lane : m_tab->Lanes())
|
||||||
|
{
|
||||||
|
if (lane.weakPeak || lane.meanFilter || lane.edgeCenter)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotView::TrimDerivedCurveDataIfIdle()
|
||||||
|
{
|
||||||
|
if (AnyDerivedPreviewEnabled())
|
||||||
|
{
|
||||||
|
return; // 还有网格在用预览曲线:缓存仍然有用,不能清
|
||||||
|
}
|
||||||
|
// 停掉后台线程池:这会让各线程的 `thread_local` 工作缓冲**析构并归还内存** ——
|
||||||
|
// 这是"关掉预览后内存不下降"的根因(thread_local 只在线程退出时释放)。
|
||||||
|
PauseDerivedAsync();
|
||||||
|
m_meanCurveCache.clear();
|
||||||
|
m_weakPeakCurveCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool PlotView::FitTimeToLane(int laneIndex)
|
bool PlotView::FitTimeToLane(int laneIndex)
|
||||||
{
|
{
|
||||||
if (m_dataSet == nullptr || m_tab == nullptr)
|
if (m_dataSet == nullptr || m_tab == nullptr)
|
||||||
@@ -1896,6 +1925,12 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
lane.edgeCenterPanelOpen = false;
|
lane.edgeCenterPanelOpen = false;
|
||||||
}
|
}
|
||||||
|
// 关闭后若本标签页**已无任何预览曲线功能**,立即释放派生曲线数据(缓存 + 后台线程的
|
||||||
|
// thread_local 工作缓冲)—— 否则关掉面板后内存不会下降(用户报告)。
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
TrimDerivedCurveDataIfIdle();
|
||||||
|
}
|
||||||
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
|
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
|
||||||
RebuildWideBuffer();
|
RebuildWideBuffer();
|
||||||
return true;
|
return true;
|
||||||
@@ -1984,6 +2019,12 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
lane.weakPeakPanelOpen = false;
|
lane.weakPeakPanelOpen = false;
|
||||||
}
|
}
|
||||||
|
// 关闭后若本标签页**已无任何预览曲线功能**,立即释放派生曲线数据(缓存 + 后台线程的
|
||||||
|
// thread_local 工作缓冲)—— 否则关掉面板后内存不会下降(用户报告)。
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
TrimDerivedCurveDataIfIdle();
|
||||||
|
}
|
||||||
// 预建「宽缓冲基底」(不含派生曲线),使用户首次拖动该滑块时无需从零重建(避免一次性尖峰)。
|
// 预建「宽缓冲基底」(不含派生曲线),使用户首次拖动该滑块时无需从零重建(避免一次性尖峰)。
|
||||||
RebuildWideBuffer();
|
RebuildWideBuffer();
|
||||||
return true;
|
return true;
|
||||||
@@ -2067,6 +2108,12 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
lane.meanFilterPanelOpen = false;
|
lane.meanFilterPanelOpen = false;
|
||||||
}
|
}
|
||||||
|
// 关闭后若本标签页**已无任何预览曲线功能**,立即释放派生曲线数据(缓存 + 后台线程的
|
||||||
|
// thread_local 工作缓冲)—— 否则关掉面板后内存不会下降(用户报告)。
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
TrimDerivedCurveDataIfIdle();
|
||||||
|
}
|
||||||
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
|
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
|
||||||
RebuildWideBuffer();
|
RebuildWideBuffer();
|
||||||
return true;
|
return true;
|
||||||
@@ -6053,28 +6100,31 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
PaintLaneDerivedOverlays(g, static_cast<int>(i), m_lanes[i]);
|
PaintLaneDerivedOverlays(g, static_cast<int>(i), m_lanes[i]);
|
||||||
}
|
}
|
||||||
// 装饰(Y 轴刻度 + 图例):基底不含(否则平移会重影)。
|
// 装饰(Y 轴刻度 + GX 编号 + 图例):基底不含(否则平移会重影)。
|
||||||
// 内容指纹一致 → 直接整块贴回缓存(省掉占整幅渲染一半以上的文字绘制);
|
// 内容指纹一致 → 直接按 `AlphaBlend` 贴回缓存(省掉文字绘制);
|
||||||
// 否则用 `m_renderDecorOnly` 重画一遍并**立即重新捕获**。
|
// 否则先把装饰单独绘制进透明缓存位图,再贴回。
|
||||||
|
// ⚠️ 贴回的是**带 alpha 的圆角图形**:圆角之外是全透明,
|
||||||
|
// 因此透出的是**当前**波形 —— 既没有"直角白边",也不会残留旧波形像素。
|
||||||
m_useDecorCache = m_decorValid && (m_decorKey == SceneDecorHash());
|
m_useDecorCache = m_decorValid && (m_decorKey == SceneDecorHash());
|
||||||
|
if (!m_useDecorCache && RenderDecorCache())
|
||||||
|
{
|
||||||
|
m_decorKey = SceneDecorHash();
|
||||||
|
m_useDecorCache = true;
|
||||||
|
}
|
||||||
if (m_useDecorCache)
|
if (m_useDecorCache)
|
||||||
{
|
{
|
||||||
// 贴回用**捕获时**的矩形(命中缓存时各网格不会重建矩形列表)。
|
|
||||||
BlitDecorCache(g);
|
BlitDecorCache(g);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_decorRects.clear();
|
// 退化路径(无法建立透明缓存,例如系统不提供 AlphaBlend):
|
||||||
|
// 直接把装饰画进后备缓冲区(就地绘制,不做缓存)。
|
||||||
m_renderDecorOnly = true;
|
m_renderDecorOnly = true;
|
||||||
for (std::size_t i = 0; i < m_lanes.size(); ++i)
|
for (std::size_t i = 0; i < m_lanes.size(); ++i)
|
||||||
{
|
{
|
||||||
PaintLane(g, static_cast<int>(i), m_lanes[i], wideTimeTicks);
|
PaintLane(g, static_cast<int>(i), m_lanes[i], wideTimeTicks);
|
||||||
}
|
}
|
||||||
m_renderDecorOnly = false;
|
m_renderDecorOnly = false;
|
||||||
if (CaptureDecorCache(g))
|
|
||||||
{
|
|
||||||
m_decorKey = SceneDecorHash();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
paintOverlays();
|
paintOverlays();
|
||||||
if (morePending)
|
if (morePending)
|
||||||
@@ -6127,10 +6177,10 @@ namespace jdi
|
|||||||
// 图例命中矩形每帧重建,避免残留已消失的信号。
|
// 图例命中矩形每帧重建,避免残留已消失的信号。
|
||||||
m_legendHits.clear();
|
m_legendHits.clear();
|
||||||
|
|
||||||
// 「装饰层」缓存决策:快速帧(拖拽/平移)期间、且装饰内容与上次捕获完全一致时,
|
// 「装饰层」:装饰**不在网格循环里就地绘制**,而是统一由带 alpha 的装饰层贴回
|
||||||
// 跳过 Y 轴胶囊与图例的绘制(约占整幅重绘的一半),改用上次捕获的像素贴回。
|
// (这样圆角之外透出的是当前波形,不会出现"直角白边")。
|
||||||
m_decorRects.clear();
|
m_decorRects.clear();
|
||||||
m_useDecorCache = m_decorValid && (m_decorKey == SceneDecorHash());
|
m_useDecorCache = true;
|
||||||
|
|
||||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||||
{
|
{
|
||||||
@@ -6139,16 +6189,15 @@ namespace jdi
|
|||||||
|
|
||||||
PaintTimeAxis(g, timeTicks, timeStep);
|
PaintTimeAxis(g, timeTicks, timeStep);
|
||||||
|
|
||||||
// 装饰层:命中缓存则上一轮网格循环已经跳过绘制,这里整块贴回;
|
// 装饰层:缓存与当前内容同源则直接贴回;否则先把装饰绘制进透明缓存位图再贴回。
|
||||||
// 否则本轮已经真正绘制了装饰,立即捕获(保证像素与指纹同源)。
|
if (!m_decorValid || m_decorKey != SceneDecorHash())
|
||||||
if (m_useDecorCache)
|
|
||||||
{
|
{
|
||||||
BlitDecorCache(g);
|
if (RenderDecorCache())
|
||||||
}
|
{
|
||||||
else if (CaptureDecorCache(g))
|
m_decorKey = SceneDecorHash();
|
||||||
{
|
}
|
||||||
m_decorKey = SceneDecorHash();
|
|
||||||
}
|
}
|
||||||
|
BlitDecorCache(g);
|
||||||
|
|
||||||
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
|
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
|
||||||
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
||||||
@@ -6274,11 +6323,8 @@ namespace jdi
|
|||||||
if (badge.Width > 0.0f)
|
if (badge.Width > 0.0f)
|
||||||
{
|
{
|
||||||
const float radius = kLaneBadgeHeight * m_scale * 0.5f;
|
const float radius = kLaneBadgeHeight * m_scale * 0.5f;
|
||||||
// 不透明底色铺满外接矩形(与缓存里的画法一致,避免圆角之外残留旧像素)。
|
// 只重画**圆角形状本身**(不透明):圆角之外保持"贴图层"的像素,
|
||||||
const gd::RectF base(std::floor(badge.X) - 1.0f, std::floor(badge.Y) - 1.0f,
|
// 因此不会出现直角白边。
|
||||||
std::ceil(badge.X + badge.Width) - std::floor(badge.X) + 2.0f,
|
|
||||||
std::ceil(badge.Y + badge.Height) - std::floor(badge.Y) + 2.0f);
|
|
||||||
UiFillRect(g, base, m_theme->cardBg | 0xFF000000u);
|
|
||||||
UiFillRoundRect(g, badge, radius, m_theme->accentSoft);
|
UiFillRoundRect(g, badge, radius, m_theme->accentSoft);
|
||||||
UiDrawRoundRect(g, badge, radius, m_theme->accent, 1.0f);
|
UiDrawRoundRect(g, badge, radius, m_theme->accent, 1.0f);
|
||||||
UiText(g, L"G" + std::to_wstring(m_hoverBadge + 1), m_fonts->monoSmall, badge,
|
UiText(g, L"G" + std::to_wstring(m_hoverBadge + 1), m_fonts->monoSmall, badge,
|
||||||
@@ -6320,17 +6366,18 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 条目托底高亮:**不透明**圆角矩形(避免半透明叠加出杂色),并在其上**原样重绘
|
// 条目悬停高亮:**圆角托底 + 与选中态相同的高亮**(用户明确要求"悬停效果与 GX 标签
|
||||||
// 色块与文字** —— 否则不透明托底会把图例内容整块盖住(只剩一个空色块)。
|
// 相同、选中状态与悬停提示效果相同"),并在其上**原样重绘色块与文字** ——
|
||||||
|
// 不透明托底若只画底色会把图例内容整块盖住(只剩一个空色块)。
|
||||||
for (const LegendHit& hit : m_legendHits)
|
for (const LegendHit& hit : m_legendHits)
|
||||||
{
|
{
|
||||||
if (hit.ref == m_hoverLegend)
|
if (hit.ref == m_hoverLegend)
|
||||||
{
|
{
|
||||||
const float radius = kLegendPlateRadius * m_scale;
|
const float radius = (hit.radius > 0.0f) ? hit.radius : kLegendPlateRadius * m_scale;
|
||||||
UiFillRoundRect(g, hit.rect, radius, m_theme->accentSoft);
|
UiFillRoundRect(g, hit.rect, radius, m_theme->accentSoft);
|
||||||
UiDrawRoundRect(g, hit.rect, radius, m_theme->accent, 1.0f);
|
UiDrawRoundRect(g, hit.rect, radius, m_theme->accent, 1.0f);
|
||||||
UiDrawSwatch(g, hit.swatch, hit.swatchColor, hit.swatchOpacity, 1.5f * m_scale, m_scale);
|
UiDrawSwatch(g, hit.swatch, hit.swatchColor, hit.swatchOpacity, 1.5f * m_scale, m_scale);
|
||||||
UiText(g, hit.label, m_fonts->uiSmall, hit.text, hit.textColor, TextAlign::LEFT);
|
UiText(g, hit.label, m_fonts->uiSmall, hit.text, m_theme->textStrong, TextAlign::LEFT);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6771,6 +6818,11 @@ namespace jdi
|
|||||||
std::vector<double> timeTicks;
|
std::vector<double> timeTicks;
|
||||||
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
|
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
|
||||||
|
|
||||||
|
// ⚠️ **补丁必须就地绘制装饰**(而不是"跳过装饰"):补丁范围覆盖整个受影响网格
|
||||||
|
// (含网格左缘的 Y 轴胶囊与图例),若跳过绘制,这部分像素会被网格底色/波形覆盖,
|
||||||
|
// 而补丁结果还会写回场景缓存 —— 表现为"改样式时图例/Y 轴读数闪掉一下"。
|
||||||
|
const bool savedUseDecorCache = m_useDecorCache;
|
||||||
|
m_useDecorCache = false;
|
||||||
for (const int laneIndex : repaint)
|
for (const int laneIndex : repaint)
|
||||||
{
|
{
|
||||||
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
|
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
|
||||||
@@ -6778,6 +6830,7 @@ namespace jdi
|
|||||||
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
|
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m_useDecorCache = savedUseDecorCache;
|
||||||
g.Restore(saveState);
|
g.Restore(saveState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7176,7 +7229,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (m_decorDc != nullptr && m_decorW == width && m_decorH == height)
|
if (m_decorDc != nullptr && m_decorBits != nullptr && m_decorW == width && m_decorH == height)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -7186,15 +7239,37 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m_decorDc = CreateCompatibleDC(screen);
|
// ⚠️ **必须用 32bpp DIB 段**(而不是 `CreateCompatibleBitmap`):
|
||||||
m_decorBitmap = CreateCompatibleBitmap(screen, width, height);
|
// 装饰要带真 alpha(圆角之外全透明),才能用 `AlphaBlend` 合成到波形之上;
|
||||||
|
// 同时 DIB 段的像素可直接写(用于 alpha 修补与清空)。
|
||||||
|
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);
|
ReleaseDC(nullptr, screen);
|
||||||
if (m_decorDc == nullptr || m_decorBitmap == nullptr)
|
if (dib == nullptr || bits == nullptr)
|
||||||
{
|
{
|
||||||
ReleaseDecorCache();
|
if (dib != nullptr)
|
||||||
|
{
|
||||||
|
DeleteObject(dib);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m_decorOldBitmap = SelectObject(m_decorDc, m_decorBitmap);
|
HDC dc = CreateCompatibleDC(nullptr);
|
||||||
|
if (dc == nullptr)
|
||||||
|
{
|
||||||
|
DeleteObject(dib);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_decorOldBitmap = SelectObject(dc, dib);
|
||||||
|
m_decorDc = dc;
|
||||||
|
m_decorBitmap = dib;
|
||||||
|
m_decorBits = bits;
|
||||||
m_decorW = width;
|
m_decorW = width;
|
||||||
m_decorH = height;
|
m_decorH = height;
|
||||||
return true;
|
return true;
|
||||||
@@ -7219,57 +7294,103 @@ namespace jdi
|
|||||||
DeleteObject(m_decorBitmap);
|
DeleteObject(m_decorBitmap);
|
||||||
m_decorBitmap = nullptr;
|
m_decorBitmap = nullptr;
|
||||||
}
|
}
|
||||||
|
m_decorBits = nullptr;
|
||||||
m_decorW = 0;
|
m_decorW = 0;
|
||||||
m_decorH = 0;
|
m_decorH = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PlotView::CaptureDecorCache(gd::Graphics& g)
|
void* PlotView::ResolveAlphaBlend()
|
||||||
{
|
{
|
||||||
if (m_decorRects.empty())
|
// `AlphaBlend` 由 msimg32 导出、并由 gdi32 的 `GdiAlphaBlend` 实现;
|
||||||
|
// 两个来源都试一遍,取到即缓存(静态局部变量,仅解析一次)。
|
||||||
|
static void* cached = nullptr;
|
||||||
|
static bool resolved = false;
|
||||||
|
if (resolved)
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
resolved = true;
|
||||||
|
HMODULE gdi = GetModuleHandleW(L"gdi32.dll");
|
||||||
|
if (gdi != nullptr)
|
||||||
|
{
|
||||||
|
cached = reinterpret_cast<void*>(GetProcAddress(gdi, "GdiAlphaBlend"));
|
||||||
|
}
|
||||||
|
if (cached == nullptr)
|
||||||
|
{
|
||||||
|
HMODULE msimg = LoadLibraryW(L"msimg32.dll");
|
||||||
|
if (msimg != nullptr)
|
||||||
|
{
|
||||||
|
cached = reinterpret_cast<void*>(GetProcAddress(msimg, "AlphaBlend"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlotView::RenderDecorCache()
|
||||||
|
{
|
||||||
|
m_decorValid = false;
|
||||||
|
if (m_theme == nullptr || m_fonts == nullptr || m_lanes.empty())
|
||||||
{
|
{
|
||||||
m_decorValid = false;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 用绘图区矩形作为缓存位图尺寸(装饰都在绘图区内)。
|
// 位图覆盖「客户区原点 → 绘图区右下角」:绘制坐标与正常绘制完全一致,
|
||||||
const int width = static_cast<int>(std::ceil(m_area.Width));
|
// 不需要任何平移(GDI 文本不认 GDI+ 变换,平移会让文字错位)。
|
||||||
const int height = static_cast<int>(std::ceil(m_area.Height));
|
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 (!EnsureDecorCache(width, height))
|
if (!EnsureDecorCache(width, height))
|
||||||
{
|
{
|
||||||
m_decorValid = false;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
HDC source = g.GetHDC();
|
// 清空为全透明(圆角之外必须保持透明,贴回时才不会盖住波形)。
|
||||||
if (source == nullptr)
|
std::memset(m_decorBits, 0, static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4u);
|
||||||
|
|
||||||
|
m_decorRects.clear();
|
||||||
{
|
{
|
||||||
m_decorValid = false;
|
gd::Graphics g(m_decorDc);
|
||||||
return false;
|
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||||
|
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||||
|
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||||
|
|
||||||
|
const bool savedUse = m_useDecorCache;
|
||||||
|
m_useDecorCache = false; // 本次是"真正绘制":不允许走"跳过绘制"的分支
|
||||||
|
m_renderDecorOnly = true; // 只画装饰(跳过网格线与波形)
|
||||||
|
const std::vector<double> noTicks;
|
||||||
|
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||||
|
{
|
||||||
|
PaintLane(g, static_cast<int>(index), m_lanes[index], noTicks);
|
||||||
|
}
|
||||||
|
m_renderDecorOnly = false;
|
||||||
|
m_useDecorCache = savedUse;
|
||||||
}
|
}
|
||||||
const int originX = static_cast<int>(std::floor(m_area.X));
|
|
||||||
const int originY = static_cast<int>(std::floor(m_area.Y));
|
// ⚠️ **alpha 修补**:GDI 的文本绘制(`DrawTextW`)会把它写入的像素 alpha 清成 0
|
||||||
bool any = false;
|
// (GDI 不维护 alpha),若原样保留,贴上后文字位置会变成"透明洞"(透出波形)。
|
||||||
|
// 在这里把装饰矩形内"有颜色但 alpha 为 0"的像素还原为不透明即可 —— 装饰本身
|
||||||
|
// 是画在不透明底板上的,这些像素本来就该不透明。
|
||||||
for (const gd::RectF& rect : m_decorRects)
|
for (const gd::RectF& rect : m_decorRects)
|
||||||
{
|
{
|
||||||
const int x0 = static_cast<int>(std::floor(rect.X)) - originX;
|
const int x0 = std::max(0, static_cast<int>(std::floor(rect.X)));
|
||||||
const int y0 = static_cast<int>(std::floor(rect.Y)) - originY;
|
const int y0 = std::max(0, static_cast<int>(std::floor(rect.Y)));
|
||||||
const int x1 = static_cast<int>(std::ceil(rect.X + rect.Width)) - originX;
|
const int x1 = std::min(width, static_cast<int>(std::ceil(rect.X + rect.Width)));
|
||||||
const int y1 = static_cast<int>(std::ceil(rect.Y + rect.Height)) - originY;
|
const int y1 = std::min(height, static_cast<int>(std::ceil(rect.Y + rect.Height)));
|
||||||
if (x1 <= x0 || y1 <= y0)
|
for (int y = y0; y < y1; ++y)
|
||||||
{
|
{
|
||||||
continue;
|
unsigned char* row = static_cast<unsigned char*>(m_decorBits) +
|
||||||
|
(static_cast<std::size_t>(y) * static_cast<std::size_t>(width)) * 4u;
|
||||||
|
for (int x = x0; x < x1; ++x)
|
||||||
|
{
|
||||||
|
unsigned char* p = row + static_cast<std::size_t>(x) * 4u;
|
||||||
|
if (p[3] == 0 && (p[0] | p[1] | p[2]) != 0)
|
||||||
|
{
|
||||||
|
p[3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BitBlt(m_decorDc, x0, y0, x1 - x0, y1 - y0, source, x0 + originX, y0 + originY, SRCCOPY);
|
|
||||||
any = true;
|
|
||||||
}
|
}
|
||||||
g.ReleaseHDC(source);
|
|
||||||
m_decorValid = any;
|
m_decorCapturedRects = m_decorRects;
|
||||||
if (any)
|
m_decorValid = true;
|
||||||
{
|
return true;
|
||||||
// 与像素同时保存矩形列表与**坐标原点**:贴回时必须用这一份(见成员注释)。
|
|
||||||
m_decorCapturedRects = m_decorRects;
|
|
||||||
m_decorOriginX = originX;
|
|
||||||
m_decorOriginY = originY;
|
|
||||||
}
|
|
||||||
return any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PlotView::BlitDecorCache(gd::Graphics& g)
|
bool PlotView::BlitDecorCache(gd::Graphics& g)
|
||||||
@@ -7278,29 +7399,41 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
void* proc = ResolveAlphaBlend();
|
||||||
|
if (proc == nullptr)
|
||||||
|
{
|
||||||
|
return false; // 极小概率:系统不提供 AlphaBlend(贴回会带透明洞,故直接放弃)
|
||||||
|
}
|
||||||
|
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();
|
HDC target = g.GetHDC();
|
||||||
if (target == nullptr)
|
if (target == nullptr)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 用**捕获时**的原点推算源偏移,但按**绝对坐标**写入:
|
// 装饰位图原点 = 客户区 (0,0):源坐标与目标坐标**相同**。
|
||||||
// 即使两帧的 `m_area` 不同(宽缓冲区渲染会临时改写它),装饰也不会错位。
|
BLENDFUNCTION fn = {};
|
||||||
|
fn.BlendOp = AC_SRC_OVER;
|
||||||
|
fn.SourceConstantAlpha = 255;
|
||||||
|
fn.AlphaFormat = AC_SRC_ALPHA; // 源为**预乘** alpha(GDI+ 写入 DIB 段即预乘)
|
||||||
|
bool any = false;
|
||||||
for (const gd::RectF& rect : m_decorCapturedRects)
|
for (const gd::RectF& rect : m_decorCapturedRects)
|
||||||
{
|
{
|
||||||
const int destX = static_cast<int>(std::floor(rect.X));
|
const int x = static_cast<int>(std::floor(rect.X));
|
||||||
const int destY = static_cast<int>(std::floor(rect.Y));
|
const int y = static_cast<int>(std::floor(rect.Y));
|
||||||
const int srcX = destX - m_decorOriginX;
|
const int w = static_cast<int>(std::ceil(rect.X + rect.Width)) - x;
|
||||||
const int srcY = destY - m_decorOriginY;
|
const int h = static_cast<int>(std::ceil(rect.Y + rect.Height)) - y;
|
||||||
const int w = static_cast<int>(std::ceil(rect.X + rect.Width)) - destX;
|
|
||||||
const int h = static_cast<int>(std::ceil(rect.Y + rect.Height)) - destY;
|
|
||||||
if (w <= 0 || h <= 0)
|
if (w <= 0 || h <= 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
BitBlt(target, destX, destY, w, h, m_decorDc, srcX, srcY, SRCCOPY);
|
if (blend(target, x, y, w, h, m_decorDc, x, y, w, h, fn) != FALSE)
|
||||||
|
{
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
g.ReleaseHDC(target);
|
g.ReleaseHDC(target);
|
||||||
return true;
|
return any;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PlotView::EnsureLaneLayer(LaneLayer& layer, int width, int height)
|
bool PlotView::EnsureLaneLayer(LaneLayer& layer, int width, int height)
|
||||||
@@ -8788,7 +8921,10 @@ namespace jdi
|
|||||||
// 渲染「宽缓冲区」时跳过:宽缓冲只存"基底"(网格/波形,不含派生曲线与装饰),
|
// 渲染「宽缓冲区」时跳过:宽缓冲只存"基底"(网格/波形,不含派生曲线与装饰),
|
||||||
// 派生曲线改由前台叠加层绘制(见 `PaintLaneDerivedOverlays`),
|
// 派生曲线改由前台叠加层绘制(见 `PaintLaneDerivedOverlays`),
|
||||||
// 这样拖派生参数滑块时基底不变、可复用宽缓冲像素。
|
// 这样拖派生参数滑块时基底不变、可复用宽缓冲像素。
|
||||||
if (!m_renderWide)
|
// ⚠️ `m_renderDecorOnly`(把装饰单独画进透明缓存位图)时**也必须跳过**:
|
||||||
|
// 派生曲线是"随时间视界变化"的内容,若烘进装饰位图,贴回时会把旧视界的
|
||||||
|
// 预览曲线永久留在画面上。
|
||||||
|
if (!m_renderWide && !m_renderDecorOnly)
|
||||||
{
|
{
|
||||||
PaintLaneDerivedOverlays(g, laneIndex, lane);
|
PaintLaneDerivedOverlays(g, laneIndex, lane);
|
||||||
}
|
}
|
||||||
@@ -8877,35 +9013,18 @@ namespace jdi
|
|||||||
pillZero.push_back(tick == 0.0);
|
pillZero.push_back(tick == 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录胶囊矩形(供装饰层捕获/贴回;略微外扩以容纳圆角与文字边缘)。
|
// 记录胶囊矩形(供装饰层绘制/贴回;略微外扩以容纳圆角与文字边缘)。
|
||||||
for (const gd::RectF& pill : pills)
|
for (const gd::RectF& pill : pills)
|
||||||
{
|
{
|
||||||
m_decorRects.push_back(gd::RectF(pill.X - 2.0f * m_scale, pill.Y - 1.0f * m_scale,
|
m_decorRects.push_back(gd::RectF(pill.X - 2.0f * m_scale, pill.Y - 1.0f * m_scale,
|
||||||
pill.Width + 4.0f * m_scale, pill.Height + 2.0f * m_scale));
|
pill.Width + 4.0f * m_scale, pill.Height + 2.0f * m_scale));
|
||||||
}
|
}
|
||||||
// 命中装饰缓存:跳过重绘(文字 + 圆角底板),由上层整块贴回。
|
// 命中装饰缓存:跳过重绘(文字 + 圆角底板),由上层整块贴回。
|
||||||
|
// ⚠️ 底板是**圆角且不透明**:装饰位图带 alpha,圆角之外必须保持全透明,
|
||||||
|
// 贴回时才能透出当前波形(避免"圆角外多出一圈直角白边")。
|
||||||
if (!m_useDecorCache)
|
if (!m_useDecorCache)
|
||||||
{
|
{
|
||||||
if (!pills.empty())
|
UiFillRoundRects(g, pills, 3.0f * m_scale, m_theme->cardBg | 0xFF000000u);
|
||||||
{
|
|
||||||
// 胶囊底板**不透明**:装饰像素因此与时间视界无关,可以跨视界整块复用;
|
|
||||||
// 半透明底板会把旧视界的波形混进缓存像素,平移贴回时在底板内部留下残影。
|
|
||||||
// ⚠️ 先用不透明底色**铺满每个胶囊的外接矩形**(含圆角之外的几个像素),
|
|
||||||
// 再画圆角胶囊:否则圆角之外的波形像素会被装饰缓存烘进去,
|
|
||||||
// 平移贴回时在胶囊边缘留下一圈"旧波形残留"。
|
|
||||||
const std::uint32_t pillBase = m_theme->cardBg | 0xFF000000u;
|
|
||||||
for (const gd::RectF& pill : pills)
|
|
||||||
{
|
|
||||||
const gd::RectF outer(pill.X - 2.0f * m_scale, pill.Y - 1.0f * m_scale,
|
|
||||||
pill.Width + 4.0f * m_scale, pill.Height + 2.0f * m_scale);
|
|
||||||
UiFillRect(g,
|
|
||||||
gd::RectF(std::floor(outer.X) - 1.0f, std::floor(outer.Y) - 1.0f,
|
|
||||||
std::ceil(outer.X + outer.Width) - std::floor(outer.X) + 2.0f,
|
|
||||||
std::ceil(outer.Y + outer.Height) - std::floor(outer.Y) + 2.0f),
|
|
||||||
pillBase);
|
|
||||||
}
|
|
||||||
UiFillRoundRects(g, pills, 3.0f * m_scale, pillBase);
|
|
||||||
}
|
|
||||||
for (std::size_t index = 0; index < pills.size(); ++index)
|
for (std::size_t index = 0; index < pills.size(); ++index)
|
||||||
{
|
{
|
||||||
const gd::RectF& pill = pills[index];
|
const gd::RectF& pill = pills[index];
|
||||||
@@ -9115,16 +9234,17 @@ namespace jdi
|
|||||||
return gd::RectF(item.left - platePadX, top - platePadY, item.width + platePadX * 2.0f,
|
return gd::RectF(item.left - platePadX, top - platePadY, item.width + platePadX * 2.0f,
|
||||||
height + platePadY * 2.0f);
|
height + platePadY * 2.0f);
|
||||||
};
|
};
|
||||||
// 不透明底色矩形:把托底外接的**整数像素**再外扩 1px。装饰缓存的捕获边界由
|
// 条目托底 = 与 GX 编号徽标同一套风格的**圆角矩形**(用户明确要求:
|
||||||
// `floor/ceil` 得出,必须完全落在这块不透明填充之内,否则边缘会残留旧波形像素。
|
// "相当于一个 GX 标签,只是内容换成了图例信息")。
|
||||||
const auto OpaqueBaseRect = [](const gd::RectF& rect)
|
const float plateRadius = kLegendPlateRadius * m_scale;
|
||||||
{
|
// 「+N」(图例空间不足)与「(无可见信号)」提示的矩形。
|
||||||
const float x0 = std::floor(rect.X) - 1.0f;
|
// ⚠️ 它们同样属于装饰层、必须登记进 `m_decorRects`:否则会被画进装饰位图
|
||||||
const float y0 = std::floor(rect.Y) - 1.0f;
|
// 却因不在贴回列表里而永远看不见。
|
||||||
const float x1 = std::ceil(rect.X + rect.Width) + 1.0f;
|
const bool hasMoreText = (visibleCount > static_cast<int>(items.size())) &&
|
||||||
const float y1 = std::ceil(rect.Y + rect.Height) + 1.0f;
|
(contentRight + 20.0f * m_scale <= maxX);
|
||||||
return gd::RectF(x0, y0, x1 - x0, y1 - y0);
|
const bool hasEmptyText = (visibleCount == 0);
|
||||||
};
|
const gd::RectF moreTextRect(contentRight, top, 24.0f * m_scale, height);
|
||||||
|
const gd::RectF emptyTextRect(legendBand.X, top, lane.rect.Width, height);
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 2) 命中矩形:**无论是否命中装饰缓存都必须重建**。
|
// 2) 命中矩形:**无论是否命中装饰缓存都必须重建**。
|
||||||
@@ -9149,11 +9269,13 @@ namespace jdi
|
|||||||
LegendHit hit;
|
LegendHit hit;
|
||||||
hit.ref = item.ref;
|
hit.ref = item.ref;
|
||||||
hit.rect = PlateRect(item);
|
hit.rect = PlateRect(item);
|
||||||
|
hit.radius = plateRadius;
|
||||||
hit.swatch =
|
hit.swatch =
|
||||||
gd::RectF(item.left, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize);
|
gd::RectF(item.left, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize);
|
||||||
hit.text = gd::RectF(item.left + swatchSize + 5.0f * m_scale, top,
|
hit.text = gd::RectF(item.left + swatchSize + 5.0f * m_scale, top,
|
||||||
std::max(8.0f, item.textWidth + 2.0f), height);
|
std::max(8.0f, item.textWidth + 2.0f), height);
|
||||||
hit.label = item.label;
|
hit.label = item.label;
|
||||||
|
// 「选中」与「悬停」用同一套配色(用户明确要求),因此文字色在这里也按选中态取。
|
||||||
hit.textColor = item.selected ? m_theme->textStrong : m_theme->text;
|
hit.textColor = item.selected ? m_theme->textStrong : m_theme->text;
|
||||||
if (resolved.style != nullptr)
|
if (resolved.style != nullptr)
|
||||||
{
|
{
|
||||||
@@ -9167,13 +9289,21 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 装饰矩形:**逐块托底**(徽标一块 + 每个条目一块),而不是覆盖整条图例带 ——
|
// 装饰矩形:**逐块**(徽标一块 + 每个条目一块),而不是覆盖整条图例带 ——
|
||||||
// 覆盖整条带会把中间的波形像素也烘进缓存,平移贴回时露出"旧波形的像素残留"。
|
// 覆盖整条带没有必要,且逐块能减少装饰层贴图面积。
|
||||||
m_decorRects.push_back(badgeRect);
|
m_decorRects.push_back(badgeRect);
|
||||||
for (const LegendItem& item : items)
|
for (const LegendItem& item : items)
|
||||||
{
|
{
|
||||||
m_decorRects.push_back(PlateRect(item));
|
m_decorRects.push_back(PlateRect(item));
|
||||||
}
|
}
|
||||||
|
if (hasEmptyText)
|
||||||
|
{
|
||||||
|
m_decorRects.push_back(emptyTextRect);
|
||||||
|
}
|
||||||
|
if (hasMoreText)
|
||||||
|
{
|
||||||
|
m_decorRects.push_back(moreTextRect);
|
||||||
|
}
|
||||||
|
|
||||||
// 命中装饰缓存:徽标与图例的像素改由装饰层贴回,跳过重绘(命中矩形已在上方重建)。
|
// 命中装饰缓存:徽标与图例的像素改由装饰层贴回,跳过重绘(命中矩形已在上方重建)。
|
||||||
if (m_useDecorCache)
|
if (m_useDecorCache)
|
||||||
@@ -9182,14 +9312,14 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 3) 绘制。⚠️ 每块托底都先用**不透明底色铺满外接矩形**再画圆角:
|
// 3) 绘制。托底是**圆角矩形**(与 GX 编号徽标同风格):
|
||||||
// 圆角之外的那几个像素若仍是波形,会被装饰缓存烘进去,平移贴回时表现为
|
// "一个 GX 标签,只是内容换成了图例信息"(用户明确要求)。
|
||||||
// "徽标 / 图例边缘残留旧波形"。底色 = 网格底色,铺满外接矩形在视觉上不可见。
|
// ⚠️ **不再铺任何外扩的不透明直角底色**:装饰位图带 alpha,圆角之外必须保持
|
||||||
|
// 全透明,贴回时才能透出**当前**波形 —— 否则圆角外会多出一圈"直角白边"。
|
||||||
// ⚠️ 徽标与条目的**悬停 / 激活高亮不在这里**(随鼠标变化,画在贴图之后,
|
// ⚠️ 徽标与条目的**悬停 / 激活高亮不在这里**(随鼠标变化,画在贴图之后,
|
||||||
// 见 PaintLegendHighlight)—— 否则会被缓存冻结,悬停时看不到任何反馈。
|
// 见 PaintLegendHighlight)—— 否则会被缓存冻结,悬停时看不到任何反馈。
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
UiFillRect(g, OpaqueBaseRect(badgeRect), m_theme->cardBg | 0xFF000000u);
|
|
||||||
UiFillRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->cardBg);
|
UiFillRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->cardBg);
|
||||||
UiDrawRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->border, 1.0f);
|
UiDrawRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->border, 1.0f);
|
||||||
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
|
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
|
||||||
@@ -9203,10 +9333,11 @@ namespace jdi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const gd::RectF plate = PlateRect(item);
|
const gd::RectF plate = PlateRect(item);
|
||||||
UiFillRect(g, OpaqueBaseRect(plate), m_theme->cardBg | 0xFF000000u);
|
// 选中态 = 悬停态配色(`accentSoft` 底 + `accent` 描边),
|
||||||
UiFillRoundRect(g, plate, kLegendPlateRadius * m_scale,
|
// 与 GX 编号徽标的鼠标悬停效果完全一致。
|
||||||
item.selected ? m_theme->selection : m_theme->cardBg);
|
UiFillRoundRect(g, plate, plateRadius,
|
||||||
UiDrawRoundRect(g, plate, kLegendPlateRadius * m_scale,
|
item.selected ? m_theme->accentSoft : m_theme->cardBg);
|
||||||
|
UiDrawRoundRect(g, plate, plateRadius,
|
||||||
item.selected ? m_theme->accent : m_theme->border, 1.0f);
|
item.selected ? m_theme->accent : m_theme->border, 1.0f);
|
||||||
|
|
||||||
// 改名编辑态:整块托底变成输入框(与左侧列表双击改名一致)。
|
// 改名编辑态:整块托底变成输入框(与左侧列表双击改名一致)。
|
||||||
@@ -9226,23 +9357,17 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visibleCount == 0)
|
if (hasEmptyText)
|
||||||
{
|
{
|
||||||
UiText(g, L"(无可见信号)", m_fonts->uiSmall,
|
UiText(g, L"(无可见信号)", m_fonts->uiSmall, emptyTextRect, m_theme->textMuted,
|
||||||
gd::RectF(legendBand.X, top, lane.rect.Width, height), m_theme->textMuted,
|
|
||||||
TextAlign::LEFT);
|
TextAlign::LEFT);
|
||||||
}
|
}
|
||||||
else if (static_cast<int>(items.size()) < visibleCount)
|
else if (hasMoreText)
|
||||||
{
|
{
|
||||||
// 图例空间不足时提示剩余信号数量,避免误认为信号丢失。
|
// 图例空间不足时提示剩余信号数量,避免误认为信号丢失。
|
||||||
const std::wstring moreText =
|
const std::wstring moreText =
|
||||||
L"+" + std::to_wstring(visibleCount - static_cast<int>(items.size()));
|
L"+" + std::to_wstring(visibleCount - static_cast<int>(items.size()));
|
||||||
if (contentRight + 20.0f * m_scale <= maxX)
|
UiText(g, moreText, m_fonts->uiSmall, moreTextRect, m_theme->textMuted, TextAlign::LEFT);
|
||||||
{
|
|
||||||
UiText(g, moreText, m_fonts->uiSmall,
|
|
||||||
gd::RectF(contentRight, top, 24.0f * m_scale, height), m_theme->textMuted,
|
|
||||||
TextAlign::LEFT);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -801,6 +801,102 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 9.20 装饰层改为「带 alpha 的独立图层」+ 图例圆角托底 + 预览曲线内存释放(本轮)
|
||||||
|
|
||||||
|
#### 一、GX 编号 / 图例「圆角外多出一圈直角白边」的根因与修复
|
||||||
|
|
||||||
|
- **症状**:把密集波形(如 `VERY_BIG_DATA` 的编码器信号)平移到 GX 编号与图例下方时,
|
||||||
|
明明是圆角矩形的 GX 标签 / 图例托底,**外边却多出一个直角长方形**(浅色底 = 网格底色),非常突兀。
|
||||||
|
- **根因**:装饰层原来是「**从后备缓冲区抓取一块不透明矩形**」——
|
||||||
|
`CaptureDecorCache()` 用 `BitBlt` 把 `m_decorRects`(矩形)里的像素搬进 `m_decorDc`。
|
||||||
|
要让这块**矩形**像素跨视界可复用(不残留旧波形),又必须在圆角矩形之外**铺一层不透明底色**
|
||||||
|
(`OpaqueBaseRect`,比元素大 1px 的直角矩形)。两者叠加 ⇒ 圆角之外是"网格底色"而不是波形
|
||||||
|
⇒ 在密集波形上就是**一圈直角白边**。Y 轴刻度胶囊同理。
|
||||||
|
- **修法:把装饰层改成带真 alpha 的独立图层**(不再"抓矩形"):
|
||||||
|
1. `m_decorDc` 改为 **32bpp `CreateDIBSection`**(自上而下),像素可直接读写 alpha;
|
||||||
|
2. 新增 **`RenderDecorCache()`**:把装饰**单独绘制**进一张全透明的 DIB(`memset` 清零后
|
||||||
|
`m_renderDecorOnly = true` 逐网格 `PaintLane`)—— 圆角之外保持全透明;
|
||||||
|
⚠️ 位图覆盖范围 = **客户区原点 → 绘图区右下角**,绘制坐标与正常绘制**完全一致**
|
||||||
|
(GDI 文本不认 GDI+ 变换,绝不能做坐标平移);
|
||||||
|
3. `BlitDecorCache()` 改用 **`AlphaBlend`**(`AC_SRC_ALPHA`,GDI+ 写入 DIB 段即**预乘** alpha)
|
||||||
|
逐块合成 ⇒ 圆角之外透出的是**当前**波形;
|
||||||
|
4. **`AlphaBlend` 用 `GetProcAddress` 解析**(`gdi32!GdiAlphaBlend`,回退 `msimg32!AlphaBlend`),
|
||||||
|
**不新增链接依赖**(两台协作设备工具链不同,避免改 Makefile);
|
||||||
|
5. ⚠️ **alpha 修补**:GDI 文本(`DrawTextW` / `ExtTextOutW`)会把写入像素的 alpha **清成 0**
|
||||||
|
(GDI 不维护 alpha),若不修补,文字位置会变成"透明洞"(透出波形)。
|
||||||
|
因此在装饰矩形内把「`A == 0` 且有颜色(`RGB != 0`)」的像素还原为不透明 ——
|
||||||
|
装饰本就画在不透明底板上,这些像素本来就该不透明;
|
||||||
|
6. 删除全部 `OpaqueBaseRect`(外扩的直角不透明底色)。Y 轴胶囊与图例托底都只画**圆角形状本身**。
|
||||||
|
- **顺带修好的一处遗漏**:图例的「`+N`」(空间不足提示)与「(无可见信号)」也是装饰层内容,
|
||||||
|
必须**登记进 `m_decorRects`**,否则会被画进装饰位图却因不在贴回列表里而永远看不见。
|
||||||
|
|
||||||
|
#### 二、图例样式:与 GX 编号徽标同款圆角托底 + 悬停/选中配色统一
|
||||||
|
|
||||||
|
- 每个图例条目 = **一个圆角矩形托底**(`kLegendPlateRadius = 4`,与 GX 徽标同风格的
|
||||||
|
`cardBg` 底 + `border` 描边),内容 = 色块 + 信号名 —— 用户要求"近似于一个 GX 标签,
|
||||||
|
只是内容换成了图例信息"。
|
||||||
|
- **选中态与悬停态同配色**(用户明确要求"选中状态与鼠标悬停时的提示效果相同"):
|
||||||
|
`accentSoft` 底 + `accent` 描边 + `textStrong` 文字 —— 与 GX 徽标的悬停效果完全一致。
|
||||||
|
- 悬停高亮画在**贴图之后**(叠加层):先铺不透明 `accentSoft` 圆角矩形 + `accent` 描边,
|
||||||
|
再在其上**原样重绘色块与文字**(否则不透明托底会把内容盖住),并保持**对应信号的曲线光晕**
|
||||||
|
(`PaintSignalHaloFor`)。`LegendHit` 新增 `radius` 字段供悬停重绘使用。
|
||||||
|
- 选中态与左侧信号列表**同源**(`DataSet::Selected()`):面板选中 → 图例高亮;图例点选 →
|
||||||
|
面板行选中。左右一致由同一条数据保证。
|
||||||
|
- ⚠️ 沿用上一轮的硬约束:`m_legendHits` **必须**在**任何**绘制路径(含装饰缓存命中)先重建。
|
||||||
|
|
||||||
|
#### 三、预览曲线内存:thread_local 工作缓冲封顶 + 关闭功能即释放
|
||||||
|
|
||||||
|
- **症状**(用户报告):`VERY_BIG_DATA` 加载并显示全部信号 ≈1.42GB;对某个网格开启弱峰调整
|
||||||
|
(带预览曲线)后立刻 ≈2.17GB;迭代次数拉到 10 后 ≈4.21GB;**点浮动面板的 ✕ 关掉预览,内存仍然
|
||||||
|
不下降**;只有"关闭整个文件分组"才释放。
|
||||||
|
- **根因**:`Utilities/Src/DataModel.cpp` 里三处 `thread_local` **工作缓冲**(弱峰调整的
|
||||||
|
`currentBuffer`/`nextBuffer`/`extremesBuffer`、`ApplyWeakPeakStage` 的 `shiftBuffer`、
|
||||||
|
迭代均值滤波的 `bufferBuffer`)。整段信号(2580 万采样)计算时,每条后台线程会常驻
|
||||||
|
**数百 MB**;后台曲线线程池有 **4 条线程** ⇒ 2GB 以上。而 `thread_local` **只在线程退出时**
|
||||||
|
才析构 ⇒ 关掉预览不释放,只有 `PauseDerivedAsync()`(关文件分组会调用)join 线程后才归还。
|
||||||
|
- **修法(两处)**:
|
||||||
|
1. **保留容量封顶**:新增 `kReuseBufferMaxElements = 4M` + `TrimReuseBuffer` +
|
||||||
|
`ReuseBufferScope<T>`(作用域守卫,**所有退出路径**都会裁剪)。容量超上限的"超大计算"
|
||||||
|
改为**一次性分配、用完即释放** —— 这类整段计算本身是秒级的一次性操作(结果会被曲线缓存复用),
|
||||||
|
内存不再随线程数放大;中/小规模仍保留容量以维持"多阶弱峰调整 991ms → 577ms"的收益。
|
||||||
|
2. **关闭功能即释放**:新增 `AnyDerivedPreviewEnabled()`(本标签页是否还有网格开启
|
||||||
|
弱峰调整 / 均值滤波 / 边沿计数)与 **`TrimDerivedCurveDataIfIdle()`**;在
|
||||||
|
`SetLaneWeakPeak/SetLaneMeanFilter/SetLaneEdgeCenterCount(..., false)` 里调用 ——
|
||||||
|
**关闭最后一个预览功能时立即 `PauseDerivedAsync()`**(join 线程 ⇒ thread_local 析构 ⇒
|
||||||
|
内存归还)并清空均值/弱峰曲线缓存。
|
||||||
|
- **实测**(新增 `.preflight/perf/stylecheck` mode 2,`VERY_BIG_DATA`):
|
||||||
|
`baseline 1202MB → 弱峰(10 迭代, 1 条) 1399MB → 5 条曲线 1792MB → **关闭面板后 1203MB**`
|
||||||
|
(修复前该值不下降)。5 条整段曲线的驻留仍受曲线缓存预算(`kCurveSpanCacheMaxBytes` 768MB)约束。
|
||||||
|
- ⚠️ **诚实说明:计算期间的瞬时峰值仍可达 ≈2.2GB**(同一进程重复测量为 1.4~2.2GB)——
|
||||||
|
一次整段弱峰调整需要 `out` + 2 个 float 工作缓冲(各 ≈103MB / 2580 万采样),
|
||||||
|
多线程/多次重排时会有 2 份同时在算。这是"整段 O(n) 计算"的固有代价
|
||||||
|
(要彻底消除需把算法分块或限制大任务的并发度),**但它在计算结束后立刻归还**,
|
||||||
|
不再像修复前那样永久驻留。
|
||||||
|
|
||||||
|
#### 四、性能与保真度实测(`stylecheck` / `zoomcheck`)
|
||||||
|
|
||||||
|
| 指标 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| `RenderDecorCache`(重建装饰位图,仅装饰内容变化时执行) | **2.40ms**(5 网格) |
|
||||||
|
| `BlitDecorCache`(**每帧**贴回,`AlphaBlend`) | **0.19ms**(原 `BitBlt` 方案约 0.3ms) |
|
||||||
|
| `zoomcheck` mode 13(收敛帧 vs 整幅重绘) | **逐像素 0 差异**(此前为 3503 像素) |
|
||||||
|
|
||||||
|
#### 五、验证(全部离屏,不占用鼠标/键盘/屏幕)
|
||||||
|
|
||||||
|
- `.preflight/perf/verify_main.cpp`(`build.ps1 -Main verify_main.cpp -Out verify.exe`)
|
||||||
|
**49/49 通过**,本轮新增/强化:
|
||||||
|
* **装饰位图圆角外 alpha ≈ 0、中心 alpha = 255**(直接读 `m_decorBits`)—— 直接证明"没有直角白边";
|
||||||
|
* **选中图例 = 悬停配色**(在托底矩形内统计 `accentSoft` 像素:未选中 0、选中 1525);
|
||||||
|
* **平移(宽基底)帧的装饰与整幅重绘逐像素一致**(只比较装饰**不透明**像素:`worst=1 / 29554 px`;
|
||||||
|
圆角外的波形属于已知的 ≤0.5px 整数像素平移残差,不参与比较);
|
||||||
|
* **图例溢出的「+N」提示**:在窄布局 + 合并网格下断言提示矩形已登记且可见(墨迹 570px);
|
||||||
|
* 图例悬停可见高亮(2011px)、GX 徽标悬停可见高亮(1122px)、装饰矩形内 0 像素残留。
|
||||||
|
- `.preflight/perf/stylecheck_main.cpp`:mode 0(装饰重建/贴回耗时)、mode 1(密集波形下导出
|
||||||
|
GX+图例角落 PNG 供目视)、mode 2(预览曲线内存增长/释放)、mode 4(装饰位图 alpha 形状检查)。
|
||||||
|
- `make release` 零警告。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 10. 变更日志(本文件维护记录)
|
## 10. 变更日志(本文件维护记录)
|
||||||
|
|
||||||
| 日期 | 设备/模型 | 摘要 |
|
| 日期 | 设备/模型 | 摘要 |
|
||||||
@@ -832,6 +928,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
|||||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**另一台设备**) | **修复「垂直中键拖拽后再平移,网格内容纵向跳变 / 绘制错误」**(详见 §9.16):根因是 `RenderLaneLayerRegion` 结束时**无条件**把 `layer.anchorYMax` 重置为"当前 `yMax`"—— 对整块重画 / 纵向补条成立,但**横向补细条(只改一条竖带、其余像素仍在原锚点)**不成立。纵向拖动以 `kLayerRollQuantum=16px` 量化,停下时图层会残留 `<16px` 的纵向偏移且 `anchorYMax != 当前 yMax`;第一次水平微移就把该偏移**静默清零**(`vOffset→0`)而像素未动 → 整个网格波形纵向错位(最多 15px)。修法:`RenderLaneLayerRegion` 新增形参 `anchorYMax`,内部以它作幅值映射基准(`yMin = anchorYMax - laneSpan`)渲染并写回 `layer.anchorYMax`;横向补条传 `layer.anchorYMax`(保持不变),整块重画/纵向补条传当前(或滚动后)`yMax`。`vOffset==0` 时新旧逻辑完全等价。验证:新增离屏探针 `.preflight/perf/pancheck`(窗口 `SW_HIDE` + 注入消息 + 读图层内部 `anchorYMax`/`vOffset` + 持按钮帧 vs 释放后整幅帧的纵向互相关):修复前 A 场景 `dy=+15px`(lane0 差异 39245),修复后 **`dy=0`**(差异 1220,仅已知列相位残差);对照/中间网格/长距水平/斜向/松开重按共 7 个场景在缩放 0/8/26 步下全部 `dy=0`(中间网格 `+1px` 为整像素量化残差);`make clean && make all` 零警告。 |
|
| 2026-10-12 | DeepSeek-V4.1-Flash(**另一台设备**) | **修复「垂直中键拖拽后再平移,网格内容纵向跳变 / 绘制错误」**(详见 §9.16):根因是 `RenderLaneLayerRegion` 结束时**无条件**把 `layer.anchorYMax` 重置为"当前 `yMax`"—— 对整块重画 / 纵向补条成立,但**横向补细条(只改一条竖带、其余像素仍在原锚点)**不成立。纵向拖动以 `kLayerRollQuantum=16px` 量化,停下时图层会残留 `<16px` 的纵向偏移且 `anchorYMax != 当前 yMax`;第一次水平微移就把该偏移**静默清零**(`vOffset→0`)而像素未动 → 整个网格波形纵向错位(最多 15px)。修法:`RenderLaneLayerRegion` 新增形参 `anchorYMax`,内部以它作幅值映射基准(`yMin = anchorYMax - laneSpan`)渲染并写回 `layer.anchorYMax`;横向补条传 `layer.anchorYMax`(保持不变),整块重画/纵向补条传当前(或滚动后)`yMax`。`vOffset==0` 时新旧逻辑完全等价。验证:新增离屏探针 `.preflight/perf/pancheck`(窗口 `SW_HIDE` + 注入消息 + 读图层内部 `anchorYMax`/`vOffset` + 持按钮帧 vs 释放后整幅帧的纵向互相关):修复前 A 场景 `dy=+15px`(lane0 差异 39245),修复后 **`dy=0`**(差异 1220,仅已知列相位残差);对照/中间网格/长距水平/斜向/松开重按共 7 个场景在缩放 0/8/26 步下全部 `dy=0`(中间网格 `+1px` 为整像素量化残差);`make clean && make all` 零警告。 |
|
||||||
| 2026-10-13 | DeepSeek-V4.1-Flash(**另一台设备**) | **交互流畅性三项修复**(详见 §9.17):① **光标不再跟随视界**——删除 `ClampCursorsAfterPan()` 及其在平移路径(中键/左键拖拽、Shift+滚轮、方向键)的三处调用,视图变化时光标时间位置**完全不变**(可滑出视口),`ClampCursorToView()` 只在拖拽光标时使用(mode 14:平移 20 帧后 `CursorTime` 不变且 `offViewport=1`)。② **缩放首帧不再卡顿**——根因是跨度变化时 `Paint` 会**同步重建全部 5 个网格图层**(实测 70~80ms)+ 随后整幅结算(40ms)=单次缩放 ~110ms 卡顿;修法:`BlitWideBuffer` 对跨度过期的图层改用**旧像素 `StretchBlt` 拉伸占位**(不再跳过导致空白)、`Paint` 只在"图层数量不符"时同步重建(新增 `LayersAllocated()`)、`RenderPendingLaneRebuilds` 允许按新跨度重建、且**只在视界稳定后**才花预算精细重绘(`viewStopped` 门控);实测首帧 **70~80ms → ~10ms**。保留"结算"帧(实测宽基底路径与整幅重绘有 17 万+ 像素列相位差,非同源)。③ **超大信号拖预览滑块**——量化发现开销**不在绘制**(`PaintLaneDerivedOverlays` 仅 0.375ms),而在"参数每帧变化"触发的**整条信号**(2576 万采样)预览重算调度;新增 `kHugeSignalPreviewSamples=4000000`,超大信号拖拽期间不再每帧排队重算(15.0→13.7ms,**剩余 ~8ms 根因未完全定位**)。验证:`pancheck` 7 场景全 `dy=0`、`zoomcheck` mode 0/13/14、`make clean && make all` 零警告。 |
|
| 2026-10-13 | DeepSeek-V4.1-Flash(**另一台设备**) | **交互流畅性三项修复**(详见 §9.17):① **光标不再跟随视界**——删除 `ClampCursorsAfterPan()` 及其在平移路径(中键/左键拖拽、Shift+滚轮、方向键)的三处调用,视图变化时光标时间位置**完全不变**(可滑出视口),`ClampCursorToView()` 只在拖拽光标时使用(mode 14:平移 20 帧后 `CursorTime` 不变且 `offViewport=1`)。② **缩放首帧不再卡顿**——根因是跨度变化时 `Paint` 会**同步重建全部 5 个网格图层**(实测 70~80ms)+ 随后整幅结算(40ms)=单次缩放 ~110ms 卡顿;修法:`BlitWideBuffer` 对跨度过期的图层改用**旧像素 `StretchBlt` 拉伸占位**(不再跳过导致空白)、`Paint` 只在"图层数量不符"时同步重建(新增 `LayersAllocated()`)、`RenderPendingLaneRebuilds` 允许按新跨度重建、且**只在视界稳定后**才花预算精细重绘(`viewStopped` 门控);实测首帧 **70~80ms → ~10ms**。保留"结算"帧(实测宽基底路径与整幅重绘有 17 万+ 像素列相位差,非同源)。③ **超大信号拖预览滑块**——量化发现开销**不在绘制**(`PaintLaneDerivedOverlays` 仅 0.375ms),而在"参数每帧变化"触发的**整条信号**(2576 万采样)预览重算调度;新增 `kHugeSignalPreviewSamples=4000000`,超大信号拖拽期间不再每帧排队重算(15.0→13.7ms,**剩余 ~8ms 根因未完全定位**)。验证:`pancheck` 7 场景全 `dy=0`、`zoomcheck` mode 0/13/14、`make clean && make all` 零警告。 |
|
||||||
| 2026-10-13 | DeepSeek-V4.1-Flash(**另一台设备**) | **深放大中键拖拽卡顿(根因=GDI+ 抗锯齿"满幅锯齿"病态)+ 横向补条裁剪 + 并行网格图层(B/C,详见 §9.18)**。① **B 拆解**:该视界(`[10.2719,10.2831]s`)下 `RollWideBuffer` 补条重绘 **13.08ms**,其余 ≤0.8ms —— 瓶颈全在补条重绘。② **补条裁剪**:横向补条只覆盖窄竖带,却按整条宽窗口构建/描边整段折线;改为按条带 x 范围裁剪采样区间(旁路折线缓存),`RollWideBuffer` **13.08ms → 1.73ms**,拖拽整帧 13~24ms → ~3ms,且**逐像素 0 差异**。③ **病态根因**:拖拽超出预渲染余量时走整块重画,**整宽单网格渲染 655ms** —— 同 4481 点 `DrawLines` 实测 **AA 694ms / 无 AA 29ms(23 倍)**(编码器方波在 1~2 采样/像素形成每列整幅跳变的锯齿,原实现此时既不走包络也不关 AA);修法:**包络带阈值从 2 采样/像素降到 1**,并扩展"梳状(关 AA)"判据(多数列列内跨幅 >0.5×网格高 / 相邻列整幅跳变)——**几何一字未改**;整宽渲染 **655→47.7ms**、补条 6.9→2.4ms、拖拽 200 帧 **avg 4.5 / max 5.8ms / 0 帧 >8ms**。④ **C(用户解除多线程限制)**:实现 `RebuildLaneLayersParallel` —— 每线程一个**隔离 `PlotView`**(自有 `m_workerTab`/`m_area`/`m_plotRect`/`m_lanes`)渲染不同网格,主线程 join 后按真实下标重算 key;并行 vs 顺序**逐像素 0 差异**;⚠️ 踩坑:`BuildLaneVisuals` 在 `m_tab==nullptr` 时清空 `m_lanes`,故**必须给 worker 一个非空的自有 tab**。**诚实结论:并行仅 1.03x**(同机 4 线程 CPU 真并行 3.98x → GDI+ 光栅化被串行化),故**并行对光栅化无收益**,仍保持"视界停后每帧补 1 个网格"。验证:`dragperf` mode 0/1/3/5/6/8/9、`pancheck` 全 `dy=0`、`zoomcheck` 无回归、`make clean && make all` 零警告。 |
|
| 2026-10-13 | DeepSeek-V4.1-Flash(**另一台设备**) | **深放大中键拖拽卡顿(根因=GDI+ 抗锯齿"满幅锯齿"病态)+ 横向补条裁剪 + 并行网格图层(B/C,详见 §9.18)**。① **B 拆解**:该视界(`[10.2719,10.2831]s`)下 `RollWideBuffer` 补条重绘 **13.08ms**,其余 ≤0.8ms —— 瓶颈全在补条重绘。② **补条裁剪**:横向补条只覆盖窄竖带,却按整条宽窗口构建/描边整段折线;改为按条带 x 范围裁剪采样区间(旁路折线缓存),`RollWideBuffer` **13.08ms → 1.73ms**,拖拽整帧 13~24ms → ~3ms,且**逐像素 0 差异**。③ **病态根因**:拖拽超出预渲染余量时走整块重画,**整宽单网格渲染 655ms** —— 同 4481 点 `DrawLines` 实测 **AA 694ms / 无 AA 29ms(23 倍)**(编码器方波在 1~2 采样/像素形成每列整幅跳变的锯齿,原实现此时既不走包络也不关 AA);修法:**包络带阈值从 2 采样/像素降到 1**,并扩展"梳状(关 AA)"判据(多数列列内跨幅 >0.5×网格高 / 相邻列整幅跳变)——**几何一字未改**;整宽渲染 **655→47.7ms**、补条 6.9→2.4ms、拖拽 200 帧 **avg 4.5 / max 5.8ms / 0 帧 >8ms**。④ **C(用户解除多线程限制)**:实现 `RebuildLaneLayersParallel` —— 每线程一个**隔离 `PlotView`**(自有 `m_workerTab`/`m_area`/`m_plotRect`/`m_lanes`)渲染不同网格,主线程 join 后按真实下标重算 key;并行 vs 顺序**逐像素 0 差异**;⚠️ 踩坑:`BuildLaneVisuals` 在 `m_tab==nullptr` 时清空 `m_lanes`,故**必须给 worker 一个非空的自有 tab**。**诚实结论:并行仅 1.03x**(同机 4 线程 CPU 真并行 3.98x → GDI+ 光栅化被串行化),故**并行对光栅化无收益**,仍保持"视界停后每帧补 1 个网格"。验证:`dragperf` mode 0/1/3/5/6/8/9、`pancheck` 全 `dy=0`、`zoomcheck` 无回归、`make clean && make all` 零警告。 |
|
||||||
|
| 2026-10-14 | DeepSeek-V4.1-Flash | **装饰层改为带 alpha 的独立图层(消除 GX/图例外围"直角白边")+ 图例圆角托底与悬停/选中配色统一 + 预览曲线内存释放(详见 §9.20)**。① **白边根因**:装饰层原为"从后备缓冲区抓取一块**不透明矩形**",圆角之外(属于波形)的像素被一起烘进缓存,为让其跨视界可复用又在圆角外铺了直角不透明底色 ⇒ 密集波形上就是一圈直角白边。**修法**:`m_decorDc` 改 **32bpp DIBSection**、新增 `RenderDecorCache()` 把装饰**单独绘制**进全透明位图(不再抓取)、`BlitDecorCache()` 改用 **`AlphaBlend`**(`AC_SRC_ALPHA`,`GetProcAddress(gdi32!GdiAlphaBlend)` 解析、不新增链接依赖)⇒ 圆角外透出**当前**波形;并做 **alpha 修补**(GDI 文本会把写入像素的 alpha 清 0,不修补文字会变透明洞);删除全部外扩直角底色。② **图例**:逐条目圆角托底(与 GX 徽标同风格),**选中态 = 悬停态配色**(`accentSoft`+`accent`+`textStrong`),悬停在不透明托底上重绘色块/文字并保留曲线光晕;`LegendHit` 新增 `radius`;补齐「+N」/「(无可见信号)」提示的装饰矩形。③ **内存**:`thread_local` 工作缓冲**保留容量封顶**(`kReuseBufferMaxElements` + `ReuseBufferScope`,超大计算用完即释放)+ 关闭最后一个预览功能时 `TrimDerivedCurveDataIfIdle()`(`PauseDerivedAsync` 让线程退出、thread_local 析构归还内存)—— 实测 1792MB → **关闭后 1203MB**(修复前不下降)。④ 性能:`BlitDecorCache` **0.19ms/帧**、`RenderDecorCache` 2.4ms(仅内容变化时);`zoomcheck` mode 13 收敛帧与整幅重绘**逐像素 0 差异**(此前 3503)。验证:离屏 `verify.exe` **49/49 通过**、`make release` 零警告。 |
|
||||||
| 2026-10-14 | DeepSeek-V4.1-Flash | **修复远端「调整」功能引入的性能回归 + 根治首帧卡顿 + 图例交互/托底重做(详见 §9.19)**。① **8 倍回归**:远端把 `DataSet::Style(ref)` 写进了 `AutoRangeLaneToView` 的逐采样循环(亿级解析调用)→ 工具栏「Y 轴自适应」在全视图下 **1200ms**(旧提交 147ms);修法:样式指针提循环外 + **整段自适应取已缓存 min/max(O(1),结果逐位一致)** + 无变换走紧凑循环 → **0.02ms**。② **首帧卡顿根治**:加载后宽基底图层从未分配,用户第一次平移/缩放时 `Paint` 在那一帧**同步重建全部 5 个网格图层**(首帧 **64.3 → 13.0ms**);修法:新增 `WarmLaneLayersStep()`,在**命中静态场景缓存**(视界未变、非拖拽)时用预算机制**每帧至多补 1 个网格**,把整批重建摊到静止帧。③ **派生信号把「调整」烘进数据**(不再继承样式指纹),仅 `offsetTime` 因共用文件时间轴仍需继承。④ **GX 拖拽让位判据改为鼠标指针**(鼠标越过相邻网格中心水平线即让位),并给 GX 徽标加悬停高亮(叠加层 + `IDC_HAND`,与信号列表 GX 徽标一致)。⑤ **图例命中矩形在装饰缓存路径下必须重建**(原先命中缓存时提前 return → `m_legendHits` 被清空且不重建 → 中键平移后图例悬停/点选全失效)。⑥ **每个图例条目加圆角托底 + 装饰矩形改为逐块 + 托底不透明铺满外接矩形** → 消除「GX/图例周边旧波形像素残留」(逐像素验证装饰矩形内 **0 差异**);悬停高亮为不透明托底并**重绘色块与文字**(否则内容被盖住)。验证:离屏 Application harness **40/40 通过**,`zoomcheck` mode 20–26 量化,`make release` 零警告。 |
|
| 2026-10-14 | DeepSeek-V4.1-Flash | **修复远端「调整」功能引入的性能回归 + 根治首帧卡顿 + 图例交互/托底重做(详见 §9.19)**。① **8 倍回归**:远端把 `DataSet::Style(ref)` 写进了 `AutoRangeLaneToView` 的逐采样循环(亿级解析调用)→ 工具栏「Y 轴自适应」在全视图下 **1200ms**(旧提交 147ms);修法:样式指针提循环外 + **整段自适应取已缓存 min/max(O(1),结果逐位一致)** + 无变换走紧凑循环 → **0.02ms**。② **首帧卡顿根治**:加载后宽基底图层从未分配,用户第一次平移/缩放时 `Paint` 在那一帧**同步重建全部 5 个网格图层**(首帧 **64.3 → 13.0ms**);修法:新增 `WarmLaneLayersStep()`,在**命中静态场景缓存**(视界未变、非拖拽)时用预算机制**每帧至多补 1 个网格**,把整批重建摊到静止帧。③ **派生信号把「调整」烘进数据**(不再继承样式指纹),仅 `offsetTime` 因共用文件时间轴仍需继承。④ **GX 拖拽让位判据改为鼠标指针**(鼠标越过相邻网格中心水平线即让位),并给 GX 徽标加悬停高亮(叠加层 + `IDC_HAND`,与信号列表 GX 徽标一致)。⑤ **图例命中矩形在装饰缓存路径下必须重建**(原先命中缓存时提前 return → `m_legendHits` 被清空且不重建 → 中键平移后图例悬停/点选全失效)。⑥ **每个图例条目加圆角托底 + 装饰矩形改为逐块 + 托底不透明铺满外接矩形** → 消除「GX/图例周边旧波形像素残留」(逐像素验证装饰矩形内 **0 差异**);悬停高亮为不透明托底并**重绘色块与文字**(否则内容被盖住)。验证:离屏 Application harness **40/40 通过**,`zoomcheck` mode 20–26 量化,`make release` 零警告。 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1014,6 +1014,57 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief `thread_local` 复用缓冲的**保留容量上限**(元素个数,约 4M)。
|
||||||
|
*
|
||||||
|
* 这些缓冲用 `thread_local` 复用,是为了省掉"每次调用都分配 + 零初始化上千万元素"
|
||||||
|
* 的开销(多阶弱峰调整实测 991ms → 577ms)。但**保留容量必须封顶**:
|
||||||
|
* 后台派生曲线线程池有 4 条线程,每条各自持有一份 `thread_local` 缓冲;
|
||||||
|
* 一条"整段信号"(VERY_BIG_DATA ≈ 2580 万采样)的弱峰调整需要
|
||||||
|
* `current` / `next`(float,各 ≈103MB)、极值表与位移表(≈数百 MB),
|
||||||
|
* 于是**每条线程常驻数百 MB、4 条线程就是 2GB 以上**,而 `thread_local`
|
||||||
|
* 只在**线程退出**时才析构 → 用户报告"启用预览后内存 1.42GB→4.21GB,
|
||||||
|
* 关掉预览也不下降,直到关闭整个文件分组才释放"。
|
||||||
|
*
|
||||||
|
* 因此只对"中/小规模"(≤ 上限)保留容量;**超大计算**改为一次性分配、用完即释放 ——
|
||||||
|
* 这类整段计算本身是秒级的一次性操作(结果会被曲线缓存复用),多一次分配完全可接受,
|
||||||
|
* 换来的是内存不再随线程数放大。
|
||||||
|
*/
|
||||||
|
const std::size_t kReuseBufferMaxElements = 4u * 1024u * 1024u;
|
||||||
|
|
||||||
|
/** @brief 容量超过上限就释放缓冲内存(`std::vector` 的容量不会随 `clear()` 归还)。 */
|
||||||
|
template <typename T>
|
||||||
|
void TrimReuseBuffer(std::vector<T>& buffer)
|
||||||
|
{
|
||||||
|
if (buffer.capacity() > kReuseBufferMaxElements)
|
||||||
|
{
|
||||||
|
std::vector<T> empty;
|
||||||
|
buffer.swap(empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 作用域守卫:离开作用域时对 `thread_local` 复用缓冲做一次容量裁剪。
|
||||||
|
*
|
||||||
|
* 用它(而不是在每个 `return` 前手写一遍)可以保证**所有**退出路径都会裁剪,
|
||||||
|
* 不会因为新增提前返回而漏掉、导致内存再次失控。
|
||||||
|
*/
|
||||||
|
template <typename T>
|
||||||
|
class ReuseBufferScope
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ReuseBufferScope(std::vector<T>& buffer) : m_buffer(buffer) {}
|
||||||
|
~ReuseBufferScope() { TrimReuseBuffer(m_buffer); }
|
||||||
|
ReuseBufferScope(const ReuseBufferScope&) = delete;
|
||||||
|
ReuseBufferScope& operator=(const ReuseBufferScope&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<T>& m_buffer;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int iterations,
|
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int iterations,
|
||||||
std::vector<float>& out)
|
std::vector<float>& out)
|
||||||
{
|
{
|
||||||
@@ -1027,7 +1078,9 @@ namespace jdi
|
|||||||
|
|
||||||
// 后续各阶以上一阶的结果为输入(double 中转,避免 float→double 的精度损失累积)。
|
// 后续各阶以上一阶的结果为输入(double 中转,避免 float→double 的精度损失累积)。
|
||||||
// 中转缓冲复用容量(线程本地),避免每阶重新分配上千万元素的向量。
|
// 中转缓冲复用容量(线程本地),避免每阶重新分配上千万元素的向量。
|
||||||
|
// ⚠️ 用 `ReuseBufferScope` 保证离开作用域时对超大容量做裁剪(见其说明)。
|
||||||
thread_local std::vector<double> bufferBuffer;
|
thread_local std::vector<double> bufferBuffer;
|
||||||
|
ReuseBufferScope<double> reuseScope(bufferBuffer);
|
||||||
std::vector<double>& buffer = bufferBuffer;
|
std::vector<double>& buffer = bufferBuffer;
|
||||||
buffer.assign(values.size(), std::numeric_limits<double>::quiet_NaN());
|
buffer.assign(values.size(), std::numeric_limits<double>::quiet_NaN());
|
||||||
std::vector<float> stage;
|
std::vector<float> stage;
|
||||||
@@ -1159,6 +1212,7 @@ namespace jdi
|
|||||||
// 2) 逐个极值点判定它属于哪几类规则点,并算出它自己的目标偏移量。
|
// 2) 逐个极值点判定它属于哪几类规则点,并算出它自己的目标偏移量。
|
||||||
const std::size_t extremeCount = extremes.size();
|
const std::size_t extremeCount = extremes.size();
|
||||||
thread_local std::vector<double> shiftBuffer;
|
thread_local std::vector<double> shiftBuffer;
|
||||||
|
ReuseBufferScope<double> reuseScope(shiftBuffer);
|
||||||
std::vector<double>& shift = shiftBuffer;
|
std::vector<double>& shift = shiftBuffer;
|
||||||
shift.assign(extremeCount, 0.0);
|
shift.assign(extremeCount, 0.0);
|
||||||
for (std::size_t k = 2; k + 2 < extremeCount; ++k)
|
for (std::size_t k = 2; k + 2 < extremeCount; ++k)
|
||||||
@@ -1324,9 +1378,14 @@ namespace jdi
|
|||||||
// 第 1 阶:从原始采样值出发(非有限值保持 NaN)。
|
// 第 1 阶:从原始采样值出发(非有限值保持 NaN)。
|
||||||
// 工作缓冲用线程本地变量复用容量:单条曲线动辄上千万采样,若每次调用都重新分配
|
// 工作缓冲用线程本地变量复用容量:单条曲线动辄上千万采样,若每次调用都重新分配
|
||||||
// 并零初始化,单条「弱峰调整」的耗时会被明显放大(内存分配 + page fault)。
|
// 并零初始化,单条「弱峰调整」的耗时会被明显放大(内存分配 + page fault)。
|
||||||
|
// ⚠️ 用 `ReuseBufferScope` 保证**离开作用域时裁剪超大容量**:否则 4 条后台线程
|
||||||
|
// 会各自常驻数百 MB(thread_local 只在线程退出时才析构),内存随线程数放大。
|
||||||
thread_local std::vector<float> currentBuffer;
|
thread_local std::vector<float> currentBuffer;
|
||||||
thread_local std::vector<float> nextBuffer;
|
thread_local std::vector<float> nextBuffer;
|
||||||
thread_local std::vector<PeakExtremum> extremesBuffer;
|
thread_local std::vector<PeakExtremum> extremesBuffer;
|
||||||
|
ReuseBufferScope<float> currentScope(currentBuffer);
|
||||||
|
ReuseBufferScope<float> nextScope(nextBuffer);
|
||||||
|
ReuseBufferScope<PeakExtremum> extremesScope(extremesBuffer);
|
||||||
std::vector<float>& current = currentBuffer;
|
std::vector<float>& current = currentBuffer;
|
||||||
current.assign(count, std::numeric_limits<float>::quiet_NaN());
|
current.assign(count, std::numeric_limits<float>::quiet_NaN());
|
||||||
for (std::size_t index = 0; index < count; ++index)
|
for (std::size_t index = 0; index < count; ++index)
|
||||||
|
|||||||
Reference in new issue
Block a user