Compare commits
2
Commits
940b8e67e6
...
1b3ab4ac09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b3ab4ac09 | ||
|
|
7c32eca004 |
No files matched your search
@@ -1528,23 +1528,26 @@ namespace jdi
|
||||
// 0. 先把源信号的「调整」变换快照下来(下面 `InsertDerivedSignal` 会移动信号存储,
|
||||
// 之后再取 `source` 会悬垂)。
|
||||
//
|
||||
// ⚠️ **新信号必须继承这套变换**,否则会出现“预览是调整后的波形、构建出来的却按原始状态绘制”
|
||||
//(用户报告的不一致):网格里的预览曲线绘制的是 `变换(滤波结果)`,而构建出的信号只存了
|
||||
// `滤波结果`;把变换一并继承后,两者绘制完全相同。
|
||||
// 另外**时间轴偏移只能靠继承**:派生信号与源信号共用同一个文件时间轴,无法把时间平移烘进数据。
|
||||
SignalStyle inheritedTransform;
|
||||
// ⚠️ **变换要"烘进数据",而不是让新信号继承这套指纹**(用户明确要求):
|
||||
// 网格里的预览曲线画的是 `变换(滤波结果)`。若新信号只存 `滤波结果` 再**继承变换**,
|
||||
// 它的「调整」面板里就会莫名其妙带着一整套系数 / 镜像 / 偏移 —— 用户看到的是
|
||||
// "新信号只是继承了原信号的指纹,数据本身没变"。现在改为:
|
||||
// * 数值域变换(系数 → 绝对值 → 垂直镜像 → Y 偏移)**与**水平镜像(数据反序)
|
||||
// 全部作用在派生采样值上;
|
||||
// * 于是新信号**用默认外观**绘制出来就与预览曲线逐位一致。
|
||||
// * ⚠️ **时间轴偏移是唯一的例外**:它改变的是"绘制在哪个时刻",而派生信号与源信号
|
||||
// 共用同一个文件时间轴,无法把时间平移烘进采样数据,因此仍由新信号的样式携带
|
||||
// (否则新信号会相对预览整体横向错位)。
|
||||
SignalStyle sourceTransform;
|
||||
bool hasSourceStyle = false;
|
||||
{
|
||||
SignalRef sourceRef;
|
||||
sourceRef.fileIndex = request.fileIndex;
|
||||
sourceRef.signalIndex = request.signalIndex;
|
||||
if (const SignalStyle* sourceStyle = m_dataSet.Style(sourceRef))
|
||||
{
|
||||
inheritedTransform.gain = sourceStyle->gain;
|
||||
inheritedTransform.absolute = sourceStyle->absolute;
|
||||
inheritedTransform.mirrorVertical = sourceStyle->mirrorVertical;
|
||||
inheritedTransform.mirrorHorizontal = sourceStyle->mirrorHorizontal;
|
||||
inheritedTransform.offsetY = sourceStyle->offsetY;
|
||||
inheritedTransform.offsetTime = sourceStyle->offsetTime;
|
||||
sourceTransform = *sourceStyle;
|
||||
hasSourceStyle = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1567,11 +1570,33 @@ namespace jdi
|
||||
std::to_wstring(request.iterations) + L"次迭代)";
|
||||
}
|
||||
|
||||
// 2. 组装新信号(外观先沿用默认值,插入后再继承源信号的「调整」变换)。
|
||||
// 2. 组装新信号(外观一律用默认值:变换已经烘进采样数据,不再是"继承的指纹")。
|
||||
derived.rawHeader = derived.name;
|
||||
derived.unit = source.unit;
|
||||
derived.values.assign(derived_values.begin(), derived_values.end());
|
||||
|
||||
// 2b. 把源信号的「调整」变换**烘进派生采样值**:
|
||||
// * 数值域(系数 → 绝对值 → 垂直镜像 → Y 偏移)逐点套用 `ApplyDrawTransform`;
|
||||
// * 水平镜像 = 数据反序,等价于把采样数组整体反转。
|
||||
// ⚠️ `ApplyDrawTransform` 是逐点函数,因此"先变换再反转"与"先反转再变换"等价。
|
||||
if (hasSourceStyle)
|
||||
{
|
||||
const bool valueTransform =
|
||||
(sourceTransform.gain != 1.0f || sourceTransform.absolute ||
|
||||
sourceTransform.mirrorVertical || sourceTransform.offsetY != 0.0f);
|
||||
if (valueTransform)
|
||||
{
|
||||
for (double& value : derived.values)
|
||||
{
|
||||
value = ApplyDrawTransform(sourceTransform, value);
|
||||
}
|
||||
}
|
||||
if (sourceTransform.mirrorHorizontal)
|
||||
{
|
||||
std::reverse(derived.values.begin(), derived.values.end());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 插入到原信号的下方(数据层 + 各标签页引用重映射)。
|
||||
int newIndex = -1;
|
||||
// 插入派生信号会移动该文件的信号存储:先暂停后台计算,避免其读取已移动的数据。
|
||||
@@ -1586,16 +1611,15 @@ namespace jdi
|
||||
tab.RemapAfterSignalInsert(request.fileIndex, newIndex);
|
||||
}
|
||||
|
||||
// 3b. 新信号继承源信号的「调整」变换(系数 / 绝对值 / 镜像 / 偏移),
|
||||
// 使其绘制结果与网格里的预览曲线**完全一致**。
|
||||
// 3b. 仅把**时间轴偏移**带到新信号上(唯一无法烘进数据的一项,见上面第 0 步说明)。
|
||||
// 其余「调整」项(系数 / 绝对值 / 镜像 / Y 偏移)保持在默认值 —— 它们已经
|
||||
// 烘进了采样数据,因此新信号不经过任何"调整"就与预览曲线完全一致。
|
||||
if (hasSourceStyle && sourceTransform.offsetTime != 0.0)
|
||||
{
|
||||
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{request.fileIndex, newIndex}))
|
||||
{
|
||||
derivedStyle->gain = inheritedTransform.gain;
|
||||
derivedStyle->absolute = inheritedTransform.absolute;
|
||||
derivedStyle->mirrorVertical = inheritedTransform.mirrorVertical;
|
||||
derivedStyle->mirrorHorizontal = inheritedTransform.mirrorHorizontal;
|
||||
derivedStyle->offsetY = inheritedTransform.offsetY;
|
||||
derivedStyle->offsetTime = inheritedTransform.offsetTime;
|
||||
derivedStyle->offsetTime = sourceTransform.offsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 在当前标签页中勾选新信号,并把它所在的网格移到原信号网格的下方。
|
||||
|
||||
@@ -403,6 +403,24 @@ namespace jdi
|
||||
*/
|
||||
void PauseDerivedAsync();
|
||||
|
||||
/**
|
||||
* @brief 当前标签页是否还有任何「带预览曲线」的实验性功能处于开启状态。
|
||||
* @return 至少一个网格开启了弱峰调整 / 均值滤波 / 边沿计数(中心轴)返回 true。
|
||||
*/
|
||||
bool AnyDerivedPreviewEnabled() const;
|
||||
|
||||
/**
|
||||
* @brief 若已无任何预览曲线功能,立即释放派生曲线的全部数据。
|
||||
*
|
||||
* 释放内容包括:后台线程池(**join 之后各线程的 `thread_local` 工作缓冲才析构、
|
||||
* 内存才归还**)、待算队列、已算好的曲线条目与均值/弱峰曲线缓存。
|
||||
*
|
||||
* ⚠️ 这是"关掉预览后内存不下降"的修复:一条整段(千万级采样)曲线的中间缓冲
|
||||
* 可达数百 MB,而 `thread_local` 只在线程退出时释放 —— 过去只有"
|
||||
* 关闭整个文件分组"(会 `PauseDerivedAsync`)才会真正还内存。
|
||||
*/
|
||||
void TrimDerivedCurveDataIfIdle();
|
||||
|
||||
/**
|
||||
* @brief 三态循环切换时间光标:无 → 单光标 → 双光标 → 无。
|
||||
* @return 状态发生变化返回 true。
|
||||
@@ -1767,6 +1785,14 @@ namespace jdi
|
||||
EdgePanelKind m_pendingPanelCloseKind = EdgePanelKind::COUNT; /**< 待关闭面板的功能类型。 */
|
||||
|
||||
int m_hoverLane = -1; /**< 当前悬停网格下标。 */
|
||||
/**
|
||||
* @brief 上一帧鼠标是否悬停在某个网格的“GX”编号徽标上(是则记录网格下标,否则 -1)。
|
||||
*
|
||||
* 徽标本体绘制在静态场景 / 装饰缓存里(不随鼠标变化),悬停高亮是**贴图之后的叠加层**
|
||||
*(见 PaintLegendHighlight)。鼠标在同一个网格的徽标范围内移动时 `m_hoverLane` 不变,
|
||||
* 因此必须单独记住这个状态才能触发重绘与取消高亮。
|
||||
*/
|
||||
int m_hoverBadge = -1;
|
||||
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
|
||||
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
|
||||
bool m_spanHover = false; /**< 鼠标是否悬停在双光标差值标签上。 */
|
||||
@@ -1859,12 +1885,24 @@ namespace jdi
|
||||
gd::PointF m_edgePanelDragOffset; /**< 抓取面板时的相对偏移(保持不跳变)。 */
|
||||
|
||||
/**
|
||||
* @brief 图例条目的命中信息(用于点选与双击改名)。
|
||||
* @brief 图例条目的命中信息(用于点选与双击改名),同时记录"悬停高亮"重绘内容所需
|
||||
* 的全部几何与颜色。
|
||||
*
|
||||
* 悬停高亮画在**装饰缓存贴图之后**(随鼠标变化,不能进缓存),且托底必须**不透明**
|
||||
*(半透明叠加会让底下的旧像素透出来)。为了在"不透明托底"上仍能看到色块与文字,
|
||||
* 这里把绘制时用到的色块矩形 / 文本矩形 / 颜色也一并记下来,供悬停时原样重绘。
|
||||
*/
|
||||
struct LegendHit
|
||||
{
|
||||
SignalRef ref; /**< 对应信号。 */
|
||||
gd::RectF rect; /**< 条目矩形(已含色块与文本)。 */
|
||||
gd::RectF rect; /**< 条目托底矩形(同时是命中区域)。 */
|
||||
float radius = 0.0f; /**< 托底圆角半径(悬停高亮重绘时使用)。 */
|
||||
gd::RectF swatch; /**< 色块矩形。 */
|
||||
gd::RectF text; /**< 文本矩形。 */
|
||||
std::wstring label; /**< 已经过省略处理的显示文本。 */
|
||||
std::uint32_t swatchColor = 0; /**< 色块颜色。 */
|
||||
float swatchOpacity = 1.0f; /**< 色块不透明度。 */
|
||||
std::uint32_t textColor = 0; /**< 文本颜色。 */
|
||||
};
|
||||
|
||||
std::vector<LegendHit> m_legendHits; /**< 最近一次绘制时记录的图例条目矩形。 */
|
||||
@@ -2276,18 +2314,21 @@ namespace jdi
|
||||
*/
|
||||
bool m_sceneDegraded = false;
|
||||
|
||||
// ---- 「装饰层」像素缓存(Y 轴刻度胶囊 + 图例)----
|
||||
// 装饰(Y 轴刻度文字与其圆角底板、图例)只取决于网格布局 / 幅值范围 / 信号外观与
|
||||
// 图例文案,与时间视界无关;但它**不适合**放进「宽基底」:平移是从基底里截取子矩形,
|
||||
// 烘进基底的装饰会跟着一起移动 → 在网格左缘之外留下一排旧图例/刻度(重影)。
|
||||
// 因此单独缓存一帧**按当前视界绘制**的装饰像素,并在内容指纹完全一致时整块贴回。
|
||||
// ---- 「装饰层」像素缓存(Y 轴刻度胶囊 + GX 编号 + 图例)----
|
||||
// 装饰只取决于网格布局 / 幅值范围 / 信号外观与图例文案,与时间视界无关;但它
|
||||
// **不适合**放进「宽基底」:平移是从基底里截取子矩形,烘进基底的装饰会跟着一起
|
||||
// 移动 → 在网格左缘之外留下一排旧图例/刻度(重影)。因此单独缓存一层的像素。
|
||||
//
|
||||
// ⚠️ 关键不变量:**每一帧“真正绘制了装饰”都要重新捕获**(无论是否快速帧)。
|
||||
// 只要保证“缓存像素”与“缓存指纹”永远同源,贴回就不可能过期;
|
||||
// 反之(例如只在全质量帧捕获)会在几何/外观变化后贴回旧像素,
|
||||
// 表现为残留 / 黑块 / 错位等错误元素。
|
||||
// ⚠️ **本缓存带真 Alpha**(32bpp DIB + `AlphaBlend`),而不是"从后备缓冲区抓一块不透明
|
||||
// 矩形":装饰是**圆角**图形,抓矩形会把圆角之外(属于波形)的像素也一并烘进缓存,
|
||||
// 平移贴回时露出"圆角矩形外围多出一圈直角白边 / 旧波形像素残留"(用户报告)。
|
||||
// 现在改为:**把装饰单独绘制进一张透明 DIB**(圆角之外保持全透明),贴回时用
|
||||
// `AlphaBlend` 合成 ⇒ 圆角之外透出的是**当前**波形,既干净又可跨视界复用。
|
||||
//
|
||||
// ⚠️ 关键不变量:**每一帧要显示装饰时,都用 `m_decorKey == SceneDecorHash()` 判定缓存
|
||||
// 是否同源**;不同源就必须先 `RenderDecorCache()` 再贴回。
|
||||
/**
|
||||
* @brief 确保装饰缓存位图尺寸正确(不足则重建)。
|
||||
* @brief 确保装饰缓存位图尺寸正确(不足则重建为 32bpp 的 DIB 段)。
|
||||
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
|
||||
* @return 可用返回 true。
|
||||
*/
|
||||
@@ -2295,42 +2336,47 @@ namespace jdi
|
||||
/** @brief 释放装饰缓存位图并作废缓存。 */
|
||||
void ReleaseDecorCache();
|
||||
/**
|
||||
* @brief 把本帧记录下来的装饰矩形从后备缓冲区捕获进装饰缓存。
|
||||
* @param [IN,OUT] g 绘图上下文。 @return 捕获成功返回 true。
|
||||
* @brief 把装饰(Y 轴胶囊 + GX 编号 + 图例)**单独绘制**进透明的装饰缓存位图。
|
||||
*
|
||||
* 位图覆盖范围 = 客户区原点到绘图区右下角(`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。
|
||||
*/
|
||||
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 HBITMAP m_decorBitmap = nullptr; /**< 装饰缓存位图。 */
|
||||
mutable HGDIOBJ m_decorOldBitmap = nullptr; /**< 装饰 DC 原选位图。 */
|
||||
mutable void* m_decorBits = nullptr; /**< 位图像素(32bpp DIB 段,可写 alpha)。 */
|
||||
mutable int m_decorW = 0; /**< 装饰缓存宽度。 */
|
||||
mutable int m_decorH = 0; /**< 装饰缓存高度。 */
|
||||
mutable bool m_decorValid = false; /**< 装饰缓存内容是否有效。 */
|
||||
mutable std::uint64_t m_decorKey = 0; /**< 装饰缓存对应的内容指纹。 */
|
||||
mutable bool m_useDecorCache = false; /**< 本帧是否跳过绘制装饰、改用缓存贴回。 */
|
||||
/** 本帧需要捕获的装饰矩形(各网格的 Y 轴胶囊与图例带)。 */
|
||||
/** 本帧需要捕获的装饰矩形(各网格的 Y 轴胶囊 + GX 编号 + 图例托底)。 */
|
||||
mutable std::vector<gd::RectF> m_decorRects;
|
||||
/**
|
||||
* @brief 缓存里那批像素对应的装饰矩形(捕获时与像素同时保存)。
|
||||
* @brief 缓存里那批像素对应的装饰矩形(与像素同时生成)。
|
||||
*
|
||||
* ⚠️ 贴回必须用这一份,**不能**用当前的 `m_decorRects`:
|
||||
* 命中缓存时各网格会跳过绘制、根本不会重建矩形列表(为空),
|
||||
* 若用当前列表就会贴错位置或一个都不贴(表现为装饰消失 / 黑块 / 错位)。
|
||||
*/
|
||||
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;
|
||||
|
||||
// ---- 每网格「信号图层」(前台同步维护;交互帧只做贴图 / 坐标偏移) ----
|
||||
//
|
||||
@@ -2484,6 +2530,18 @@ namespace jdi
|
||||
* @return 队列中仍有待重画网格返回 true。
|
||||
*/
|
||||
bool RenderPendingLaneRebuilds(int budget);
|
||||
/**
|
||||
* @brief **空闲预热**:静置期间用既有预算机制**每帧至多补画 1 个网格图层**。
|
||||
*
|
||||
* 图层不存在(刚加载 / 网格数量变化)或还是旧跨度时,若等到用户**第一次**
|
||||
* 平移 / 选区放大才去建,`Paint` 会在那一帧里**同步重建全部网格图层**
|
||||
*(5 个网格实测 30~80ms)—— 这就是"选区放大的第一帧特别卡"的根因
|
||||
*(该帧其余工作只有约 16ms 的占位拉伸)。
|
||||
*
|
||||
* 这里只在**视界没有变化**(命中静态场景缓存 ⇒ 本帧视图与缓存同源)且**不在拖拽中**时
|
||||
* 调用一次,把整批重建的代价摊到若干静止帧上;任何交互帧都不再承担它。
|
||||
*/
|
||||
void WarmLaneLayersStep();
|
||||
/** @brief 使全部图层失效。 */
|
||||
void InvalidateWideBuffer(); /** @brief 释放全部图层位图。 */
|
||||
void ReleaseWideBuffer();
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -681,6 +681,222 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
||||
|
||||
---
|
||||
|
||||
### 9.19 远端「调整」功能带来的性能回归 + 首帧卡顿根治 + 图例交互/托底重做(本轮)
|
||||
|
||||
> 接手背景:远端(`c334390`…`940b8e6`)新增了样式区「调整」(系数/绝对值/镜像/偏移)与
|
||||
> 标题栏内存占用等。本轮先**逐项量化**再修,全部离屏验证(不占用鼠标/键盘/屏幕)。
|
||||
|
||||
#### 一、性能回归:`AutoRangeLaneToView` 把 `Style(ref)` 放进了逐采样循环(**8 倍**)
|
||||
|
||||
- **症状**:加载 `VERY_BIG_DATA.CSV` 并显示全部信号后,**点一下工具栏「Y 轴自适应」开关**就卡顿
|
||||
很久;从局部放大状态 `FitAll` 回全局同样卡。
|
||||
- **量化**(新增 `.preflight/perf/zoomcheck` mode 21/23):
|
||||
- `SetAllLanesAutoFitToSelection(true,true)`(全视图、5 个网格):**修复前 1200ms → 修复后 0.02ms**;
|
||||
旧提交(`20b9084`)为 **147ms** —— 即远端把这一步劣化了约 8 倍。
|
||||
- `AutoRangeLaneToView(0)`(单网格、全视图):**修复前 ≈240ms → 修复后 0.012ms**,且结果与
|
||||
「逐采样扫描」的参考值**逐位一致**(`match=1`)。
|
||||
- **根因**:远端为支持「调整」变换,把 `m_dataSet->Style(ref)` **写在了逐采样循环体里**(还写了两次)——
|
||||
`Style()` 每次都要解析文件/信号下标;2580 万 × 5 次 × 2 = 亿级解析调用。
|
||||
- **修法(显示效果零变化)**:
|
||||
1. **把样式指针提到循环外**(`resolvedStyle`),并预计算 `mirrored` / `transform` 两个布尔。
|
||||
2. **整段自适应走 O(1)**:`timeMonotonic && begin==0 && end==count && !mirrored && !transform &&
|
||||
finiteCount>0` 时直接取信号**已缓存的 `minValue/maxValue`**(与 `ComputeStats` 同源,
|
||||
因此结果与逐点扫描完全一致)。
|
||||
3. 无变换时走**紧凑循环**(无逐点分支,便于向量化);有变换/水平镜像时逐点套用与绘制相同的变换。
|
||||
- ⚠️ 有「调整」变换时仍必须逐点扫描(158ms/单网格,全视图)——这是变换语义的固有代价。
|
||||
|
||||
#### 二、首帧卡顿根治:**空闲预热「每网格信号图层」**
|
||||
|
||||
- **复现与量化**(`zoomcheck` mode 22,`VERY_BIG_DATA` 5 网格,同一负载下 A/B):
|
||||
加载 → 静止 → 第一次「选区放大」的**首帧**:旧提交 **64.3ms**、本轮修复前 **54ms**。
|
||||
- **根因**:加载后画面走的是「静态场景缓存贴图」,**宽基底图层从未被分配**;用户第一次平移/缩放时
|
||||
`Paint` 里 `if (!LayersAllocated()) RebuildWideBuffer();` 会在**那一帧同步重建全部 5 个网格图层**
|
||||
(实测整帧 = 5 个图层同步重建 + 占位拉伸)。逐项拆解(mode 25/26):
|
||||
占位 `StretchBlt`(HALFTONE)×5 ≈16ms、装饰层命中缓存仅 0.4ms、**其余全是整批图层重建**。
|
||||
- **修法**:新增 `PlotView::WarmLaneLayersStep()`,在**命中静态场景缓存**的那条快路径里调用
|
||||
(⇒ 本帧视图与缓存同源、**视界没有变化**、且**不在拖拽中**):
|
||||
- 图层数组与网格数量不符时先对齐(多余图层 `ReleaseLaneLayer` 后 `resize`);
|
||||
- 用既有预算机制 `RollWideBuffer()` + `RenderPendingLaneRebuilds(1)` **每帧至多补 1 个网格**,
|
||||
队列非空时 `PostMessage(kMessagePlotDerivedReady)` 让宿主再送一帧,直到补齐。
|
||||
- ⇒ 整批重建的代价被**摊到若干静止帧**上,**任何交互帧都不再承担它**。
|
||||
- **效果**(同一负载,mode 22):首帧 **64.3ms → 13.0ms**(≈5 倍);`zoomcheck` mode 0 逐步缩放
|
||||
f1 也同步下降。图层数一旦就绪,后续交互全程走"占位拉伸 + 每帧补 1 个网格"。
|
||||
- ⚠️ 预热只在**视界不变**时推进,因此不会在用户拖动时做无用功;视界一变,宽基底路径自己按预算补。
|
||||
|
||||
#### 三、派生信号:把「调整」**烘进数据**,不再继承指纹
|
||||
|
||||
- **用户要求原文**:"重建出来的新信号原本不经过调整就已经是预览状态的样子,而不是继承指纹"。
|
||||
- **旧行为**(`fc77b02`):把源信号的 `gain/absolute/mirrorVideo/mirrorHorizontal/offsetY/offsetTime`
|
||||
复制给新信号的 `SignalStyle` —— 新信号只是"套了一层同样的调整",数据本身没变,
|
||||
「调整」面板里还莫名带着一整套参数。
|
||||
- **新行为**(`Application::BuildDerivedSignal`):
|
||||
1. 快照源信号样式;
|
||||
2. 滤波结果算完后,**逐点套用 `ApplyDrawTransform`**(系数 → 绝对值 → 垂直镜像 → Y 偏移)写回采样值;
|
||||
3. **水平镜像 = 数据反序**:把派生采样数组整体反转(`ApplyDrawTransform` 是逐点函数,
|
||||
"先变换再反转"与"先反转再变换"等价);
|
||||
4. 新信号样式一律**保持默认**(`gain=1 / absolute=false / 两个镜像=false / offsetY=0`)。
|
||||
- ⚠️ **唯一例外:时间轴偏移 `offsetTime` 仍由样式携带**——它改变的是"绘制在哪个时刻",
|
||||
而派生信号与源信号**共用同一个文件时间轴**,无法把时间平移烘进采样数据;
|
||||
若不继承,新信号会相对预览整体横向错位。已在该函数注释中写明。
|
||||
- 验证(本轮 `verify` harness):`derived.values[i] == ApplyDrawTransform(tf, float(source[count-1-i]))`
|
||||
逐点一致;新样式各项均为默认;`offsetTime` 保留。
|
||||
|
||||
#### 四、网格 GX 拖拽让位阈值 + 徽标悬停
|
||||
|
||||
- **让位判据改为「鼠标指针」**(`LaneDragTargetAt`):`index = floor((mouseY - firstTop)/laneHeight + 0.5)`。
|
||||
- 旧判据用"**被拖网格的中心**",等价于要求把整个网格拖过去一格才吸附(用户报告"要拖到另一个网格
|
||||
的位置附近才有吸附提示")。新判据下**只有两个网格时,鼠标越过 G1 的中心水平线,G1 立刻让位并
|
||||
出现吸附提示**;任意网格数下都成立(进入相邻网格的上半即让位)。
|
||||
- **GX 徽标悬停提示**:徽标本体画在静态场景 / 装饰缓存里(被冻结),因此悬停高亮必须是**叠加层**
|
||||
——新增状态 `m_hoverBadge`(`OnMouseMove` 里更新、`OnMouseLeave` 里清零,因为鼠标在同一网格的
|
||||
徽标范围内移动时 `m_hoverLane` 不变),`PaintLegendHighlight` 在贴图之后**重画徽标**
|
||||
(不透明底色铺满外接矩形 + `accentSoft` 圆角底 + `accent` 描边 + `textStrong` 文字),
|
||||
与左侧信号列表里同一个 GX 徽标的悬停效果完全一致;`QueryCursor` 在徽标上返回 `IDC_HAND`。
|
||||
|
||||
#### 五、图例:命中矩形在装饰缓存路径下**必须重建**(悬停/点选失效的真因)
|
||||
|
||||
- **症状**:一旦用中键平移过视界,网格图例的**悬停高亮 / 点击选中 / 双击改名全部失效**。
|
||||
- **根因**:`m_legendHits` 只在**整幅重绘**路径开头被 `clear()`,随后靠 `PaintLaneLegend` 重新填充;
|
||||
而装饰缓存命中时 `PaintLaneLegend` **提前 return**(跳过绘制)→ 命中矩形列表**被清空且永不重建**。
|
||||
中键平移后必然出现一次"装饰缓存有效的整幅重绘"(结算帧),于是图例从此点不动。
|
||||
- **修法(结构性)**:把 `PaintLaneLegend` 拆成「**几何 → 命中/装饰矩形 → 绘制**」三段:
|
||||
- 先用**唯一来源**算出"放得下的条目"及其托底矩形(命中矩形 == 托底矩形,看到的与点得到的严格一致);
|
||||
- **无论是否命中装饰缓存都重建 `m_legendHits` 与 `m_decorRects`**,之后才 `if (m_useDecorCache) return;`;
|
||||
- 绘制段只负责画像素。
|
||||
|
||||
#### 六、图例托底 + 消除「旧波形像素残留」
|
||||
|
||||
- **症状**:中键把密集波形区域推过左上角 GX 编号/图例区域后再移开,**GX/图例周边残留旧波形像素**。
|
||||
- **根因**:远端 `c334390` 去掉了图例的整块不透明托底后,装饰缓存捕获的那**一条大矩形**
|
||||
里包含了**它下面的波形像素**(波形会透出),而装饰指纹 `SceneDecorHash()` 不含视界 ⇒
|
||||
平移贴回时把旧视界的波形一起贴了回来。
|
||||
- **修法**:
|
||||
1. **每个图例条目各自一块圆角托底**(`kLegendPlateRadius = 4`),风格与 GX 徽标一致
|
||||
(`cardBg` 底 + `border` 描边;选中态用 `selection` 底 + `accent` 描边);
|
||||
2. **装饰矩形改为逐块**(徽标一块 + 每个条目一块 + 每个 Y 轴胶囊一块),
|
||||
**不再覆盖整条图例带** —— 中间的波形像素因此完全不进缓存;
|
||||
3. 每块托底先用**不透明底色铺满其"外接整数像素再外扩 1px"的矩形**再画圆角:
|
||||
否则圆角之外那几个像素仍是波形,会被烘进缓存(`CaptureDecorCache` 的边界由 `floor/ceil` 得出,
|
||||
必须完全落在不透明填充内)。Y 轴胶囊同理。
|
||||
4. 悬停高亮(随鼠标变化)画在**贴图之后**:不透明 `accentSoft` 圆角矩形 + `accent` 描边,
|
||||
并**在原托底上重绘色块与文字**(`LegendHit` 现在记录色块矩形/文本矩形/颜色/已省略文本)——
|
||||
不透明托底若只画底色会把图例内容整块盖住(只剩一个空色块)。
|
||||
悬停时**同时**画对应信号的曲线光晕(`PaintSignalHaloFor`)。
|
||||
- **实测**(`verify` harness):把"宽基底快路径的实时帧"与"同一视界从零重绘的装饰"逐像素比较,
|
||||
**全部装饰矩形内差异 = 0**(`decor ghost check: total=0 worst=0 rects=23`)——
|
||||
直接证明缓存像素与时间视界无关、不会残留旧波形。
|
||||
|
||||
#### 七、验证工具(本轮新增,均在 `.preflight/` 下,不入库)
|
||||
|
||||
- `.preflight/perf/verify_main.cpp`(`build.ps1 -Main verify_main.cpp -Out verify.exe`):
|
||||
**离屏 Application harness**(`Initialize(GetModuleHandleW(nullptr), SW_HIDE)` + 直接注入鼠标消息 +
|
||||
从 `m_backDc` 截图),**40 项检查全部通过**,覆盖:自适应正确性(恒等/变换/镜像、与逐点扫描逐位一致)、
|
||||
派生信号烘值与样式默认、拖拽让位阈值(鼠标越过中线即让位)、平移后图例命中与可点选、
|
||||
装饰矩形不覆盖整条图例带、图例悬停有可见高亮、GX 徽标悬停有可见高亮且离开后清除、
|
||||
**装饰矩形内 0 像素残留**。还会导出 `verify_legend.png` / `verify_legend_hover.png` 供目视核对。
|
||||
- `.preflight/perf/zoomcheck_main.cpp` 新增 mode:20(FitAll from deep zoom)、21(全局 Y 轴自适应开关)、
|
||||
22(选区放大首帧 + 图层的空闲预热进度)、23(自适应**正确性**与耗时)、24(带着色自适应 FitAll)、
|
||||
25(首帧拆解)、26(占位拉伸 HALFTONE vs COLORONCOLOR 与装饰重绘耗时)。
|
||||
- 旧版本 A/B 基线用 `git worktree add --detach ..\jdi_old 20b9084` 建立。
|
||||
|
||||
---
|
||||
|
||||
### 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. 变更日志(本文件维护记录)
|
||||
|
||||
| 日期 | 设备/模型 | 摘要 |
|
||||
@@ -712,6 +928,8 @@ 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-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-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` 零警告。 |
|
||||
|
||||
---
|
||||
|
||||
@@ -756,6 +974,22 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
||||
14. ⚠️ **多线程限制已解禁(2026-10-13),但必须"隔离状态"**:波形渲染可以多线程 —— 前提是每个线程用**自有的渲染上下文**(自有 `m_tab`/`m_area`/`m_plotRect`/`m_lanes`/DC),绝不共享可变渲染状态(见第 5 条的安全姿势)。已落地的并行只有:派生曲线线程池(`m_curveWorkers`)与并行网格图层渲染(`m_layerWorkers`)。⚠️ 实测**光栅化并行无收益**(GDI+ 串行化),所以并行只在"整批重建"这类本来就要一次画完的地方用。任何"移动 / 释放采样"的操作前必须 `PauseDerivedAsync()`。
|
||||
15. MinGW 的 `swprintf` 按 C99:宽格式串里的 `%s` = **窄**字符串,宽字符串必须用 `%ls`(否则 "KB" 会被截成 "K")。
|
||||
16. 源文件名必须**全局唯一**(Makefile 按 basename 去重 + vpath 查找);新增目录要同时登记进 `CXX_INCLUDES_PATHS` / `CXX_SOURCES_PATHS`。
|
||||
17. ⚠️ **`m_legendHits` 只在"整幅重绘"路径开头被清空**,因此 `PaintLaneLegend` 在**任何**绘制路径
|
||||
(**包括装饰缓存命中、提前 return 的那条**)都必须**先重建命中矩形**再决定是否跳过绘制 ——
|
||||
否则清空后不再填充,图例的悬停高亮 / 点击选中 / 双击改名会**永久失效**
|
||||
(用户报告"用中键平移过视界后图例点不动")。同理 `m_editFieldRect` 也必须在几何段设置。
|
||||
18. ⚠️ **装饰缓存的像素必须与时间视界无关**:`CaptureDecorCache` 的捕获边界由 `floor/ceil(rect)` 得出,
|
||||
因此每块装饰(GX 徽标 / 图例托底 / Y 轴胶囊)都必须**先用不透明底色铺满"外接整数像素再外扩 1px"的矩形**
|
||||
再画圆角。否则圆角之外的那几个像素仍是波形 → 平移贴回时表现为"GX/图例周边旧波形像素残留"。
|
||||
装饰矩形也必须**逐块**(禁止覆盖整条图例带)。验证方法:宽基底实时帧 vs 同视界从零重绘,
|
||||
**装饰矩形内必须 0 差异**。
|
||||
19. ⚠️ **逐采样循环里禁止调用 `DataSet::Style(ref)` / `Resolve()` 等按下标解析的函数**:
|
||||
`AutoRangeLaneToView` 曾因此把全段自适应从 147ms 劣化到 **1200ms(8 倍)**。
|
||||
一律把指针提到循环外。整段(`begin==0 && end==count`)且无变换/无水平镜像时,
|
||||
自适应应直接取 `SignalData::minValue/maxValue`(O(1),与原逐点扫描逐位一致)。
|
||||
20. ⚠️ **"整批图层重建"绝不允许发生在交互帧里**:新增 `WarmLaneLayersStep()`,
|
||||
在**命中静态场景缓存**(视界未变、非拖拽)时用既有预算机制**每帧至多补 1 个网格**并自请求下一帧。
|
||||
否则"加载后的第一次平移 / 选区放大"那一帧会同步重建全部网格图层(首帧 64ms → 预热后 13ms)。
|
||||
|
||||
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
|
||||
|
||||
|
||||
@@ -1014,6 +1014,57 @@ namespace jdi
|
||||
}
|
||||
} // 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,
|
||||
std::vector<float>& out)
|
||||
{
|
||||
@@ -1027,7 +1078,9 @@ namespace jdi
|
||||
|
||||
// 后续各阶以上一阶的结果为输入(double 中转,避免 float→double 的精度损失累积)。
|
||||
// 中转缓冲复用容量(线程本地),避免每阶重新分配上千万元素的向量。
|
||||
// ⚠️ 用 `ReuseBufferScope` 保证离开作用域时对超大容量做裁剪(见其说明)。
|
||||
thread_local std::vector<double> bufferBuffer;
|
||||
ReuseBufferScope<double> reuseScope(bufferBuffer);
|
||||
std::vector<double>& buffer = bufferBuffer;
|
||||
buffer.assign(values.size(), std::numeric_limits<double>::quiet_NaN());
|
||||
std::vector<float> stage;
|
||||
@@ -1159,6 +1212,7 @@ namespace jdi
|
||||
// 2) 逐个极值点判定它属于哪几类规则点,并算出它自己的目标偏移量。
|
||||
const std::size_t extremeCount = extremes.size();
|
||||
thread_local std::vector<double> shiftBuffer;
|
||||
ReuseBufferScope<double> reuseScope(shiftBuffer);
|
||||
std::vector<double>& shift = shiftBuffer;
|
||||
shift.assign(extremeCount, 0.0);
|
||||
for (std::size_t k = 2; k + 2 < extremeCount; ++k)
|
||||
@@ -1324,9 +1378,14 @@ namespace jdi
|
||||
// 第 1 阶:从原始采样值出发(非有限值保持 NaN)。
|
||||
// 工作缓冲用线程本地变量复用容量:单条曲线动辄上千万采样,若每次调用都重新分配
|
||||
// 并零初始化,单条「弱峰调整」的耗时会被明显放大(内存分配 + page fault)。
|
||||
// ⚠️ 用 `ReuseBufferScope` 保证**离开作用域时裁剪超大容量**:否则 4 条后台线程
|
||||
// 会各自常驻数百 MB(thread_local 只在线程退出时才析构),内存随线程数放大。
|
||||
thread_local std::vector<float> currentBuffer;
|
||||
thread_local std::vector<float> nextBuffer;
|
||||
thread_local std::vector<PeakExtremum> extremesBuffer;
|
||||
ReuseBufferScope<float> currentScope(currentBuffer);
|
||||
ReuseBufferScope<float> nextScope(nextBuffer);
|
||||
ReuseBufferScope<PeakExtremum> extremesScope(extremesBuffer);
|
||||
std::vector<float>& current = currentBuffer;
|
||||
current.assign(count, std::numeric_limits<float>::quiet_NaN());
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
|
||||
Reference in new issue
Block a user