平移不再阻塞前台刷新;弱峰增强逐信号参数;浮动面板加入层序

一、平移:前台刷新线程永不阻塞(解决"按住键拖拽时质量下降/断线/周期性卡顿")
- RollWideBuffer 不再整块重画:逐网格整数像素平移 + 只补细条;内容指纹过期或锚点离视界
  过远的网格只登记到 m_layerRebuildPending 并仍按现有像素平移(位置照样跟随视界)。
- 新增 RenderPendingLaneRebuilds(budget):Paint 每帧最多重画 1 个网格,其余留到后续帧;
  队列非空时 PostMessage(kMessagePlotDerivedReady) 请宿主再送一帧,直到补齐。
- BlitWideBuffer 改为局部贴图:把 BitBlt 夹到图层可用区间,图层没覆盖到的部分留空由底色
  填充(旧实现直接跳过整块,导致网格瞬间变空 = "断线")。
- "结算"(整幅全质量精确帧)触发条件收紧为三者同时成立:内容与视界同上一条快路径帧、
  m_dragMode == NONE、全部图层就绪。拖拽期间不再结算,消除"宽基底快路径 / 整幅重绘(关AA)"
  每两帧振荡造成的质量下降与周期性卡顿。
- 新增 LaneLayerReady(i) / LanesAllReadyForView();原 LayersReadyForView() 更名
  LayersStructurallyReady()(只看结构,用于"跨度/布局变化必须整批重建一次")。
- 实测(.preflight/panperf,VERY_BIG_DATA 5 网格深放大,每帧平移 60px):平移 40 帧
  中位 6.2ms / p90 7.2ms / max 7.6ms;按住键且视界停在边界 12 帧中位 0.29ms(无整幅重绘)。

二、弱峰增强:改为逐信号参数(对齐均值滤波)
- Lane::WeakPeakItem 由 using = MeanFilterItem 改为独立结构体
  { ref, levelPercent(0-200, 默认 150), iterations(1-10, 默认 1) };删除网格级
  Lane::weakPeakLevel / weakPeakIterations。
- 面板改为与均值滤波完全同构:每个被勾选信号一行"名称 + 复选框",第二排两个输入框
  (增强等级 % / 迭代次数 N),底部"构建为新信号";删除网格级两个滑块及其常量、
  WeakPeakSliderHit、PlotDragMode::PEAK_LEVEL/PEAK_ITERATIONS、m_peakDragLane、
  EdgePanelLayout::slider/sliderIterations。
- 新增 PlotEditTarget::PEAK_LEVEL_PERCENT / PEAK_ITERATIONS,EndEdit 中分别夹到 0-200 / 1-10。
- 指纹/快照/预览曲线/光标胶囊/构建新信号全部改为逐信号参数(同一网格内不同信号可用不同强度,
  且"预览"与"构建"逐位一致);WeakPeakItem 不再是 MeanFilterItem,purgeRefs 改为泛型 lambda。

三、浮动面板:层序(z-order)与命中修复
- 新增 m_panelOrder(队尾 = 最上层):SyncPanelOrder 保留已有序、新面板追加到最上层;
  TopPanelAtPoint 从最上层往下取第一个命中;BringPanelToFront 在点击面板时把它置顶。
- 绘制改为按 m_panelOrder 自下而上;PanelGlobalKey 混合层序(层序变化即整层重画);
  QueryCursor / OnMouseMove 的"是否在面板上"也走 TopPanelAtPoint。
- 修复"上层面板的 ✕ / 标题栏被下层参数输入框抢走,导致上层面板关不掉/拖不动"。

四、测试
- uitest 新增 [AG]:层预算(每帧只重画 1 个网格、剩余排队、最终补齐并全部就绪)、
  局部贴图(只覆盖视界一半时左侧被贴、右侧保持哨兵色)、面板层序(上层 ✕ 命中上层、
  下层不受影响、点击下层置顶、TopPanelAtPoint 随之改变、新开面板在最上层)。
- [X] 改为逐信号参数(含两个输入框就地编辑:写入该信号自己的条目、越界夹取),
  移除已不存在的滑块断言。
- 新增离屏探针 .preflight/panperf(平移帧耗时 + 按住键静止帧耗时)。
- make all 零警告;uitest 388/388 通过;panperf PASS;layercheck 保持像素一致。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-12 12:06:42 +08:00
1 parent da83dc2987
commit 1ddbc37b78
5 files changed
+684 -522

No files matched your search

+77 -52
View File
@@ -43,8 +43,6 @@ namespace jdi
MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */
EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
PEAK_LEVEL, /**< 拖动「弱峰增强」面板中的等级滑块。 */
PEAK_ITERATIONS, /**< 拖动「弱峰增强」面板中的迭代次数 N 滑块。 */
LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
};
@@ -72,7 +70,9 @@ namespace jdi
EDGE_CENTER_POINTS, /**< 编辑「中心轴边沿计数」的均值点数 X。 */
EDGE_CENTER_ITERATIONS, /**< 编辑「中心轴边沿计数」的迭代次数 N。 */
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
MEAN_FILTER_ITERATIONS /**< 编辑「均值滤波」的迭代次数 N。 */
MEAN_FILTER_ITERATIONS, /**< 编辑「均值滤波」的迭代次数 N。 */
PEAK_LEVEL_PERCENT, /**< 编辑「弱峰增强」某信号的增强等级(0–200%)。 */
PEAK_ITERATIONS /**< 编辑「弱峰增强」某信号的迭代次数 N。 */
};
/**
@@ -570,7 +570,7 @@ namespace jdi
/**
* @brief 「实验性功能 → 弱峰增强」的「构建为新信号」单个请求
* (一个被勾选的信号 + 整个网格共用的增强等级)。
* (一个被勾选的信号 + **它自己的**增强等级与迭代次数,与均值滤波同构)。
*/
struct WeakPeakBuild
{
@@ -610,36 +610,6 @@ namespace jdi
*/
bool ToggleLaneWeakPeak(int laneIndex);
/**
* @brief 读取某网格的弱峰增强等级(0–2)。
* @param [IN] laneIndex 网格下标。
* @return 增强等级;网格不存在时返回 0.5。
*/
double LaneWeakPeakLevel(int laneIndex) const;
/**
* @brief 读取某网格的弱峰增强迭代次数 N(1–10)。
* @param [IN] laneIndex 网格下标。
* @return 迭代次数;网格不存在时返回 1。
*/
int LaneWeakPeakIterations(int laneIndex) const;
/**
* @brief 设置某网格的弱峰增强迭代次数 N(1–10,超出范围会被夹取)。
* @param [IN] laneIndex 网格下标。
* @param [IN] iterations 迭代次数。
* @return 迭代次数发生变化返回 true。
*/
bool SetLaneWeakPeakIterations(int laneIndex, int iterations);
/**
* @brief 设置某网格的弱峰增强等级(0–2,超出范围会被夹取)。
* @param [IN] laneIndex 网格下标。
* @param [IN] level 增强等级。
* @return 等级发生变化返回 true。
*/
bool SetLaneWeakPeakLevel(int laneIndex, double level);
/**
* @brief 获取弱峰增强曲线(按「信号 + 增强等级 + 迭代次数」缓存)。
* @param [IN] signal 目标信号。
@@ -1158,14 +1128,13 @@ namespace jdi
gd::RectF close; /**< ✕ 关闭按钮。 */
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
gd::RectF slider; /**< 「弱峰增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */
gd::RectF sliderIterations; /**< 「弱峰增强」迭代次数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
/**
* @brief 「逐条目」面板的矩形(MEAN 与 CENTER 面板共用同一套排布)。
* @brief 「逐条目」面板的矩形(MEAN / CENTER / PEAK 面板共用同一套排布)。
*
* 索引与 `rowRefs` 一一对应(即网格内可见信号的顺序):
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为第二排的两个输入框;
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为第二排的两个输入框
*(PEEK 信号分别为「增强等级 %」与「迭代次数 N」);
* `rowStats` 为第三排的统计结果(中心轴边沿计数专用)。
* 未勾选的条目对应的矩形均为空。
*/
@@ -1688,7 +1657,6 @@ namespace jdi
int m_pendingWeakPeakLane = -1; /**< 待构建的弱峰增强信号所属网格下标(-1 表示无请求)。 */
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
std::vector<WeakPeakBuild> m_pendingWeakPeakItems; /**< 待构建的弱峰增强信号及其参数。 */
int m_peakDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
/**
* @brief 本帧是否使用「快速绘制」(交互期间关闭波形与派生曲线的抗锯齿)。
@@ -1979,9 +1947,7 @@ namespace jdi
int edgeCenter = 0; /**< 中心轴边沿计数开关。 */
int meanFilter = 0; /**< 均值滤波开关。 */
int weakPeak = 0; /**< 弱峰增强开关。 */
int weakPeakLevelPercent = 0; /**< 弱峰增强等级(0–200)。 */
int weakPeakIterations = 1; /**< 弱峰增强迭代次数 N。 */
/** 各条目展开为 [文件, 信号, 均值点数 X, 迭代次数 N],按原顺序(稳定)。 */
/** 各条目展开为 [文件, 信号, 参数…],按原顺序(稳定)。 */
std::vector<int> edgeCenterItems;
std::vector<int> meanFilterItems;
std::vector<int> weakPeakItems;
@@ -2287,32 +2253,55 @@ namespace jdi
/**
* @brief 让各图层"滚动"以跟随视界:逐网格整数像素自贴图平移 + 只补画新暴露的细条。
*
* 每个网格按**自己的** `wideMin` 计算位移;位移过大(锚点离当前视界太远)或内容指纹
* 与当前状态不一致的网格直接整块重画。任一网格图层缺失(尺寸 / 跨度不符)返回 false,
* 由调用方先 `RebuildWideBuffer()`。
* @return 全部网格都能用现成像素跟随视界返回 true。
* ⚠️ **本函数绝不做"整块重画"**:内容指纹过期 / 锚点离视界太远的网格只被排进
* `m_layerRebuildPending`(随后由 `RenderPendingLaneRebuilds` 按预算补画),本帧仍按
* 现有像素做整数像素平移,因此**前台线程永远不会因为"图层没准备好"而卡顿**。
*/
bool RollWideBuffer();
void RollWideBuffer();
/**
* @brief 逐个网格截取图层子矩形贴图到后备缓冲区(O(1) 位图搬运)。
*
* 图层只覆盖"视界 ± 半屏":若当前视界有一部分落在图层之外,这里**只贴有像素的那一段**
* (其余留由底色填充,下一帧补齐),而不是跳过整块 —— 这正是"未绘制的部分先暂时不管"。
* @param [IN,OUT] g 绘图上下文。
* @return 至少贴了若干网格返回 true。
* @return 至少贴了若干像素返回 true。
*/
bool BlitWideBuffer(gd::Graphics& g) const;
/**
* @brief 全部图层是否已就绪且跨度与当前视界一致。
* @brief 全部图层是否**结构可用**(数量 / 位图尺寸 / 视界跨度都与当前状态一致)。
*
* 只检查"位图尺寸 / 网格数量 / 视界跨度"这类**结构性**条件(内容指纹由
* `RollWideBuffer()` 逐个网格处理);不满足时应先 `RebuildWideBuffer()`。
* 结构不可用意味着"像素的时间映射已经不对",只能整批重建(跨度变化 / 布局变化 / 首帧);
* 内容指纹是否最新由 `LaneLayerReady()` 逐个判断。
* @return 可以直接滚动 + 贴图层像素返回 true。
*/
bool LayersReadyForView() const;
bool LayersStructurallyReady() const;
/**
* @brief 某个网格的图层是否**完全就绪**(结构可用且内容指纹与当前状态一致)。
* @param [IN] laneIndex 网格下标。
* @return 就绪返回 true。
*/
bool LaneLayerReady(int laneIndex) const;
/** @brief 全部网格图层是否都完全就绪(用于判断能否做"结算"整幅精确帧)。 */
bool LanesAllReadyForView() const;
/** @brief 把某网格排进"待重画"队列(去重;视界/跨度后续变化时会在渲染时再次校验)。 */
void QueueLaneRebuild(int laneIndex);
/**
* @brief 按预算补画"待重画"的网格图层(**本帧最多 `budget` 个**)。
*
* 这是"前台不做整批重绘"的关键:即使 5 个网格都需要重画,也一帧只画一个,
* 其余留到后续帧(缺的部分留空 / 用旧像素)。调用方若发现队列非空应请求再次重绘。
* @param [IN] budget 本次最多重画的网格数量。
* @return 队列中仍有待重画网格返回 true。
*/
bool RenderPendingLaneRebuilds(int budget);
/** @brief 使全部图层失效。 */
void InvalidateWideBuffer();
/** @brief 释放全部图层位图。 */
void ReleaseWideBuffer();
std::vector<LaneLayer> m_layerFront; /**< 各网格的「信号图层」(与 `m_lanes` 同序)。 */
/** 待重画的网格下标(去重;`RollWideBuffer` 登记,`RenderPendingLaneRebuilds` 消费)。 */
std::vector<int> m_layerRebuildPending;
/**
* @brief 图层元数据的互斥(当前图层只在 UI 线程维护,故它实际不承担跨线程保护;
* 保留是为了给 `BlitWideBuffer()` 这类 `const` 读取路径一个明确的一致性边界)。
@@ -2364,6 +2353,42 @@ namespace jdi
std::uint64_t m_panelGlobalKey = 0; /**< 上一层对应的全局键。 */
std::vector<PanelSlot> m_panelSlots; /**< 上一层缓存的面板(本帧比对用)。 */
/**
* @brief 浮动面板的**层序**(从下到上)。
*
* 面板允许互相重叠,因此必须有明确的层序:
* * **绘制**按此顺序从下往上;**命中测试**从后往前(最上层优先)。
* * 点击某个面板会把它移到队尾(置顶),因此重叠时总能操作到"用户想要"的那一个
* (否则上层面板的 ✕ / 标题栏会被下层控件的命中判定抢走)。
* * 新出现的面板追加到队尾(在最上层),不会被已有面板盖住。
*/
struct PanelRef
{
int laneIndex = -1; /**< 所属网格。 */
int kind = 0; /**< `EdgePanelKind` 的整型值。 */
bool operator==(const PanelRef& other) const
{
return laneIndex == other.laneIndex && kind == other.kind;
}
};
/** 同步 `m_panelOrder` 与当前"可见的面板集合"(新增的追加到最上层;消失的移除)。 */
void SyncPanelOrder() const;
/** @brief 把某个面板移到最上层(点击面板时调用)。 */
void BringPanelToFront(int laneIndex, int kind);
/**
* @brief 从最上层往下找出包含该点的面板。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [OUT] laneIndex 命中面板所属网格。
* @param [OUT] kind 命中面板的种类(`EdgePanelKind`)。
* @return 命中返回 true。
*/
bool TopPanelAtPoint(float x, float y, int& laneIndex, int& kind) const;
mutable std::vector<PanelRef> m_panelOrder; /**< 浮动面板层序(队尾 = 最上层;mutable 供 const 光标查询同步层序)。 */
/**
* @brief 查询坐标命中的图例条目。
* @param [IN] x 客户区横坐标。
File diff suppressed because it is too large. Load diff
+95 -8
View File
@@ -510,6 +510,7 @@ Documents/ 项目文档(含本文件)
| 2026-10-12 | DeepSeek-V4.1-Flash | 修复用户报告的「缩放后平移出现拖影/残留/错误元素」与「2ms 一大格区间仍然卡顿」。根因:①网格纵向滚动量未参与任何内容指纹→滚动时把旧位置像素贴回且每两帧触发一次整幅重绘(≈8fps);②`FindStyleOnlyLanes` 未校验视界→视界变化后仍走局部补丁;③宽基底用含装饰的整幅画面初始化→装饰随平移重影;④装饰缓存的矩形列表与坐标原点两帧不同源→装饰消失/黑块/错位。修复后交互帧 9~19ms → **4.5~6ms**,纵向拖动残留 2 万+ 通道 → 72 通道(亚像素量化),静态/结算帧像素精确。新增 `perf.exe ... 6` 诊断模式(逐帧计时 + 逐像素对比 + 差异包围盒)。未实现:每网格双缓冲后台渲染(需先把 `PaintLane` 改造成无状态渲染器,已在文档 9.15 说明)。 |
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **跨设备记忆合流**:把起源设备(本机)的详细工程记忆**并入本文件**(同一记忆文件,不再另建 `agent_mem_origin.md` 等旁支文件),新增 §11(跨设备协作须知)与 §12(起源设备详细记忆原文)。远端 `gitea/main` 已快进合并到本机(`7608e85` → `f97c4cf`,**16** 个提交)。本机 MinGW-w64 GCC 15.2.0 (UCRT) 下 `make release` 通过。 |
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复「缩放后平移出现严重绘制错误/波形抖动/前台被拖慢」**:根因是后台图层线程会改写共享渲染状态(保存→改写→恢复`m_area`/`m_plotRect`/视界/网格矩形),与 UI 线程无锁的平移/命中测试相冲 → 视界被弹回、网格错位;叠加"全局锚点 + 逐网格内容"导致各网格时间错位。修法:**移除后台图层线程**(改为 UI 线程同步维护)、**逐网格独立锚点与滚动**(补细条用该图层自己的 `wideMin`)、跨度变化时同步 `RebuildWideBuffer()`。同时把「弱波增强」改名「**弱峰增强**」并改为 **0~200% 分段插值**(0% 完全不动 / 100% 到较温和一侧 / 200% 完全拉平;新 1.5 = 旧 0.5)。离屏验证:居中图层 vs 整幅重绘 **5 网格逐像素 0 差异**、平移帧对齐 ≤1px、24 次竞态检测 0 覆盖;测试套件 **380/380 通过**。 |
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"按住键拖拽平移时波形质量下降 / 断线 / 周期性卡顿"与"浮动面板互相覆盖时交互错位"**,并把弱峰增强改为**逐信号参数**。① 平移:`RollWideBuffer` 不再整块重画(只平移+补细条,内容过期/锚点过远只入队),新增 `RenderPendingLaneRebuilds(budget)`(**每帧最多重画 1 个网格**,其余下帧)+ `BlitWideBuffer` **局部贴图**(图层没覆盖的部分留空,不再整块跳过);"结算"整幅精确帧**仅限非拖拽且全部图层就绪**(消除拖拽中每两帧的整幅重绘/质量振荡)。实测 `VERY_BIG_DATA` 5 网格深放大平移:中位 **6.2ms**、max 7.6ms(无尖峰);按住键且视界停在边界时 **0.29ms/帧**。② 弱峰增强:`WeakPeakItem` 改为携带 `levelPercent(0–200)+iterations`,面板改为与均值滤波同构的**逐信号两个输入框**(删除网格级两滑块),哈希/快照/构建/预览全部改为逐信号。③ 浮动面板:新增 `m_panelOrder` 层序,**命中从最上层往下**、点击 `BringPanelToFront`、新面板置顶;修复"上层 ✕ / 标题栏被下层参数框抢走"。离屏测试 **388/388 通过**。 |
---
@@ -542,14 +543,15 @@ Documents/ 项目文档(含本文件)
5. ⚠️ **图层渲染一律在 UI 线程内同步完成**(`Paint` 全程持有 `m_renderMutex`)。**绝不要把图层渲染搬到后台线程**:它会临时改写 `m_area`/`m_plotRect`/`Tab().ViewMin()/ViewMax()`/各网格矩形,而这些状态被 UI 线程的消息处理**无锁读/写**(平移读视界、命中测试读网格矩形),后台的"保存→改写→恢复"会**覆盖用户正在进行的平移**(视界弹回)并让命中测试错位 —— 这就是 2026-10-12 移除后台图层线程的原因(详见 §12 本轮)。
6. 每个图层记录**自己的**时间映射 `LaneLayer::wideMin`;滚动与贴图必须**逐网格**按各自 `wideMin` 计算,**绝不共用一个全局锚点**(否则各网格被平移不同的真实距离 → 网格间时间错位)。补细条时必须传 `layer.wideMin`(不能用"当前视界"的映射,否则有 ≤0.5px 接缝与抖动)。
7. 加锁顺序**恒定** `m_renderMutex`(recursive)→ `m_layerMutex`(图层不再有跨线程访问,但顺序约定保留)。
6. **GDI 绘制不受 GDI+ `SetClip` 约束**:`UiText`(`DrawTextW` / `ETO_CLIPPED`)与 `UiFillRect` 的 GDI 快路径(不透明且面积 ≥ 20000px²)都会画出裁剪区;宽矩形 / 文本必须**自己夹取**,GDI 批绘必须显式传 clip。
7. `SceneContentHash` 必须覆盖 `Tab().LaneScroll()`(网格纵向滚动量)与 `SelectedRef()`;`FindStyleOnlyLanes()` 必须**逐项比较 `SceneCacheKey`**(`SceneGeometryHash()` 刻意不含视界,只比它不足以判定"视图没变")。
8. 随鼠标变化的绘制(悬停高亮 / 光晕 / 图例高亮 / 光标读数 / 选区 / 提示 / 面板悬停)**必须同时**加在"缓存贴图快路径"与"整幅重绘路径"两侧。
9. **不要为波形构建 / 绘制引入多线程**;已落地的多线程只有:每网格图层后台线程(`m_layerWorker`,`BELOW_NORMAL`)、派生曲线线程池(`m_curveWorkers`)。任何"移动 / 释放采样"的操作前必须 `PauseDerivedAsync()`。
10. MinGW 的 `swprintf` 按 C99:宽格式串里的 `%s` = **窄**字符串,宽字符串必须用 `%ls`(否则 "KB" 会被截成 "K")。
11. 源文件名必须**全局唯一**(Makefile 按 basename 去重 + vpath 查找);新增目录要同时登记进 `CXX_INCLUDES_PATHS` / `CXX_SOURCES_PATHS`。
12. `GdiplusStartup` 之后创建的 GDI+ 成员(如 `ColorPickerPopup` 的色域缓存 `gd::Bitmap`)必须在 `GdiplusShutdown` **之前**释放,否则退出时访问违例(实测退出码 `0xC0000005`)。
13. 主题化弹出层禁止 `CreatePopupMenu` / `TrackPopupMenu` / `AppendMenuW`;一律用自绘 `PopupMenu` / `ColorPickerPopup`,且 `Open()` 后必须 `InvalidateRect`。
8. ⚠️ **前台刷新线程绝不做“整批图层重绘”**:平移/拖动期间 `RollWideBuffer()` **只**做逐网格整数像素平移 + 补细条;内容过期 / 锚点过远的网格只登记到 `m_layerRebuildPending`,由 `RenderPendingLaneRebuilds(budget)` **本帧至多重画 1 个**,其余下帧继续(并 `PostMessage(kMessagePlotDerivedReady)` 请求重绘)。图层没覆盖到的部分 **`BlitWideBuffer` 只贴有像素的那一段**(其余留给底色,下帧补齐),**不得整块跳过**(跳过 = 网格变空 / “断线”)。
9. ⚠️ **“结算”(整幅全质量精确帧)只能在 `m_dragMode == NONE` 且全部图层就绪时发生**:否则按住键拖拽时会在“宽基底快路径”与“整幅重绘(拖拽时关 AA)”之间每两帧振荡 → **波形质量轮流变差 / 抖动 / 周期性卡顿**(用户报告的“按住拖拽时显示效果下降、出现断线,松手才恢复”)。
10. **GDI 绘制不受 GDI+ `SetClip` 约束**:`UiText`(`DrawTextW` / `ETO_CLIPPED`)与 `UiFillRect` 的 GDI 快路径(不透明且面积 ≥ 20000px²)都会画出裁剪区;宽矩形 / 文本必须**自己夹取**,GDI 批绘必须显式传 clip。
11. `SceneContentHash` 必须覆盖 `Tab().LaneScroll()`(网格纵向滚动量)与 `SelectedRef()`;`FindStyleOnlyLanes()` 必须**逐项比较 `SceneCacheKey`**(`SceneGeometryHash()` 刻意不含视界,只比它不足以判定"视图没变")。
12. 随鼠标变化的绘制(悬停高亮 / 光晕 / 图例高亮 / 光标读数 / 选区 / 提示 / 面板悬停)**必须同时**加在"缓存贴图快路径"与"整幅重绘路径"两侧。
13. 浮动面板必须按**层序**(`m_panelOrder`,队尾 = 最上层)绘制与命中:命中从后往前取第一个命中者,点击面板 `BringPanelToFront()`,新开面板追加到队尾。**不要再按 `kEdgePanelKinds` 固定顺序命中再 break**(否则上层 ✕ / 标题栏会被下层参数输入框抢走 → “上层面板关不掉 / 拖不动”)。
14. **不要为波形构建 / 绘制引入多线程**;已落地的多线程只有派生曲线线程池(`m_curveWorkers`)。任何"移动 / 释放采样"的操作前必须 `PauseDerivedAsync()`。
15. MinGW 的 `swprintf` 按 C99:宽格式串里的 `%s` = **窄**字符串,宽字符串必须用 `%ls`(否则 "KB" 会被截成 "K")。
16. 源文件名必须**全局唯一**(Makefile 按 basename 去重 + vpath 查找);新增目录要同时登记进 `CXX_INCLUDES_PATHS` / `CXX_SOURCES_PATHS`。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
@@ -2397,3 +2399,88 @@ Documents/ 项目文档(含本文件)
→ 容差由 32 提到 **96**(真正“面板内容过期”的差异是百级)。
- `[X]` 预览曲线像素计数的颜色容差 6 → **40**(真实信号上曲线多与波形重合,纯色像素本就少)。
- **验证**:`make release` 零警告;离屏测试套件 **380/380 通过**(exit=0)。
---
## 本轮(2026-10-12,起源设备):平移不再阻塞前台 + 弱峰增强逐信号参数 + 浮动面板层序
### A. 平移:前台刷新线程**永不阻塞**(用户猜测的评估与落地)
**用户的猜测与评估**:用户推测“网格自己的前后台缓冲区没准备好时,前台会等待它完成,于是退化到
未优化之前”—— **机制描述与当前代码不完全一致**(`f97c4cf` 的后台图层线程已在上一轮被删除,
因为后台改写共享渲染状态会覆盖用户的平移),但**问题类别与补救方向完全正确**:
平移时前台确实会因为“某个网格的图层没准备好(内容指纹过期 / 锚点离视界太远)”而**同步重画整块图层**
(旧代码里 `RollWideBuffer` 直接 `RenderLaneLayerRegion(..., 0, 0, defaultWideMin)`),
5 个网格就会在一帧里做完整批重画 → 卡顿;而“结算”帧在**拖拽期间**也会触发,
于是按住鼠标不动时在“宽基底快路径”与“整幅重绘(拖拽 → 关抗锯齿)”之间**每两帧振荡**,
表现为“波形质量轮流变差 / 断线 / 周期性卡顿,松手才恢复”。
**本轮改动**:
1. `RollWideBuffer()` **不再做任何整块重画**:逐网格整数像素平移 + 补细条;
内容过期 / 锚点过远(`|shift| >= bitmapWidth-4`)的网格只 `QueueLaneRebuild()`,
本帧仍按现有像素平移(位置照样跟随)。
2. 新增 `RenderPendingLaneRebuilds(budget)`:**每帧最多重画 `budget` 个网格**(`Paint` 传 1)。
队列非空时 `PostMessageW(kMessagePlotDerivedReady)` 请宿主再送一帧,直到补齐。
3. `BlitWideBuffer()` 改为**局部贴图**:图层只覆盖“视界 ± 半屏”,视界落在图层之外时
**只贴有像素的那一段**(`srcX/destOffset/blitWidth` 三个量把 BlitBlt 夹到可用区间),
其余留由底色填充 —— 即“未绘制的部分先暂时不管,下个循环就绪后再用新缓冲”。
旧行为是 `subX < -0.5 || subX + width > bitmapWidth + 0.5` 就 `continue` → **整个网格瞬间变空**。
4. **“结算”(整幅全质量精确帧)的触发条件收紧为三者同时成立**:
内容+视界与上一条快路径帧相同(`viewStopped`)**且** `m_dragMode == NONE` **且**
`LanesAllReadyForView()`。拖拽期间绝不结算 → 不再有质量振荡与周期性整幅重绘。
新增 `LaneLayerReady(i)` / `LanesAllReadyForView()`(结构 + 内容指纹 + 跨度全一致);
原 `LayersReadyForView()` 改名 `LayersStructurallyReady()`(只看结构,用于“跨度/布局变了
必须整批重建一次”这一种情形)。
5. 跨度 / 尺寸变化(缩放、布局变化、首帧)仍然走**同步整批** `RebuildWideBuffer()`:
这是“一次性”代价(不是平移路径),不做预算分摊,否则会出现整屏空白的中间帧。
**实测(离屏 `.preflight/panperf`,`VERY_BIG_DATA` 5 网格,深放大 ≈2ms/大格,每帧平移 60px)**:
- 平移 40 帧:**中位 6.2ms / p90 7.2ms / max 7.6ms**(旧实现会周期性出现数十~上百毫秒的尖峰)。
- **按住键且视界停在数据边界(视界不再变化)12 帧:中位 0.29ms / max 1.29ms**
—— 证明拖拽期间不再有整幅重绘(旧实现这里会每两帧穿插一次整幅重绘 + 关抗锯齿)。
### B. 弱峰增强:改为**逐信号参数**(对齐均值滤波)
- 数据层:`Lane::WeakPeakItem` 从 `using WeakPeakItem = MeanFilterItem`(只有 `ref` 有效)
改为**独立结构体** `{ ref, levelPercent = 150, iterations = 1 }`;
删除 `Lane::weakPeakLevel` / `Lane::weakPeakIterations`(网格级)。
- 因 `WeakPeakItem` 不再是 `MeanFilterItem`,`TabConfig::RemapAfterSignalRemoval()` 里原本
绑定到 `std::vector<Lane::MeanFilterItem>&` 的 `purgeRefs` lambda 改为**泛型 lambda + `std::decay_t`**。
- `PlotView`:删除 `LaneWeakPeakLevel/LaneWeakPeakIterations/SetLaneWeakPeakLevel/SetLaneWeakPeakIterations`、
`PlotDragMode::PEAK_LEVEL/PEAK_ITERATIONS`、`m_peakDragLane`、`EdgePanelLayout::slider/sliderIterations`、
`WeakPeakSliderHit()` 与三个滑块常量。
- 面板:弱峰增强**与均值滤波完全同构**(逐条目「名称 + 复选框」+ 第二排两个输入框
「增强等级 %」/「迭代次数 N」+ 底部「构建为新信号」);`EdgePanelLayout.itemHasParams`
统一了 MEAN/CENTER/PEAK 的参数行占位。新增 `PlotEditTarget::PEAK_LEVEL_PERCENT / PEAK_ITERATIONS`,
在 `EndEdit` 里分别夹到 0–200 / 1–10 并清 `m_weakPeakCurveCache`。
- 指纹与快照:`SceneContentHash` 混合每条 `WeakPeakItem` 的 `levelPercent + iterations`;
`CollectSceneDerived` 的 `flatten` 增加 `valueOf` 参数(MEAN/CENTER 取 `points`,PEAK 取 `levelPercent`);
`PanelSlotKey` 混合每条目的等级/迭代(面板数字随输入实时刷新)。
- 预览曲线 / 光标胶囊 / 「构建为新信号」全部改为**读取该信号自己的条目参数**,
因此「预览」与「构建」逐位一致,且同一网格内不同信号可用不同强度。
### C. 浮动面板:层序(z-order)与命中修复
**问题**(用户报告):两个面板重叠时,对**上层**面板的 ✕ / 标题栏操作会落到**下层**面板
(因为命中按固定的 `kEdgePanelKinds` 顺序遍历、先命中先 break),于是“上层 ✕ 关不掉 / 标题栏拖不动”。
**修法**:
- 新增层序 `m_panelOrder`(`std::vector<PanelRef{laneIndex, kind}>`,**队尾 = 最上层**,`mutable`)。
- `SyncPanelOrder()`:按现有顺序保留仍可见的面板,把**新出现**的面板**追加到队尾**(新面板永不被盖住)。
- `TopPanelAtPoint(x, y, lane, kind)`:**从队尾往前**找第一个命中的面板。
- `BringPanelToFront(lane, kind)`:点击面板(任意部位,含 ✕/标题/输入框)时把它移到队尾 = 置顶。
- 绘制循环改为**按 `m_panelOrder` 自下而上**绘制(与命中顺序、像素缓存级联一致);
`PanelGlobalKey()` 混合层序 → 层序变化时整层面板缓存作废并按新层序重画。
- `QueryCursor` 与 `OnMouseMove` 的“是否在面板上”判断也改用 `TopPanelAtPoint`(与点击一致)。
### D. 测试与脚本
- `uitest` 新增 `[AG]`:层预算(`RenderPendingLaneRebuilds(1)` 一帧只重画 1 个网格、剩余留在队列、
最终补齐且全部就绪)、**局部贴图**(把某层 `wideMin` 推到只覆盖视界左半 → 断言左侧被贴、
右侧保持哨兵色 = “未被绘制就不管”)、面板层序(上层面板 ✕ 命中上层、下层面板不受影响、
点击下层把它置顶、`TopPanelAtPoint` 随之改变、新开面板在最上层)。
- `uitest` 的 `[X]` 改为逐信号参数(含两个输入框的就地编辑:按下进入编辑、输入 175 回车写入
**该信号自己的条目**、999 被夹到 200、迭代次数写入、上下限夹取),并删除已不存在的滑块断言。
- 新增离屏探针 `.preflight/panperf`(平移帧耗时 + 按住键静止帧耗时)。
- **验证**:`make all` 零警告;`uitest` **388/388 通过**;`panperf` PASS;
`.preflight/layercheck` 保持“居中图层 vs 整幅重绘逐像素 0 差异 / 平移对齐 ≤1px / 0 次视界覆盖”。
+15 -23
View File
@@ -374,13 +374,17 @@ namespace jdi
using EdgeCenterItem = MeanFilterItem;
/**
* @brief 「弱峰增强」中某个信号自己的条目。
* @brief 「弱峰增强」中某个信号自己的参数条目。
*
* 目前只用到 `ref`(增强等级属于整个网格,统一保存在 `weakPeakLevel` 中);
* 复用 `MeanFilterItem` 结构是为了与均值滤波/中心轴共享
* 「条目查找 + 信号插入/删除时的引用重映射」等既有实现。
* 与「均值滤波」完全一致:**每个被勾选的信号各自持有参数**
*(增强等级 0–200% + 迭代次数 N),因此同一网格内不同信号可以使用不同强度。
*/
using WeakPeakItem = MeanFilterItem;
struct WeakPeakItem
{
SignalRef ref; /**< 目标信号。 */
int levelPercent = 150; /**< 增强等级(0–200;100% = 到达较温和的一侧,200% = 完全拉平)。 */
int iterations = 1; /**< 迭代次数 N(1–10)。 */
};
std::wstring name; /**< 网格名称。 */
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
@@ -505,35 +509,23 @@ namespace jdi
*
* 开启后为本网格内**被勾选**的信号额外绘制一条「弱峰增强」曲线:
* 识别纹波的波峰/波谷,把“比左右同类极值更弱”的波峰上拉、波谷下拉,
* 并按「弱峰增强等级」在左右两个同类极值之间取目标值,使其恢复与周边一致的振幅。
* 并按各信号自己的「增强等级」在左右两个同类极值之间取目标值,使其恢复与周边振幅。
* 这条曲线只是叠加显示,不会作为信号加入左侧列表。
*/
bool weakPeak = false;
/**
* @brief 弱峰增强等级(0–2,面板上显示为 0%–200%)。
* @brief 参与弱峰增强预览的信号及其**各自的参数**(只包含被勾选的信号)。
*
* 0 = 目标就是当前值(完全不动);1 = 拉到较温和的一侧(波峰取较低的左/右波峰,
* 波谷取较高的左/右波谷);2 = 拉到另一侧(完全拉平)。默认 1.5,等价于旧版的 50%。
* 参数(等级 0–200% + 迭代次数 N)按信号保存,与「均值滤波」的实现方式一致:
* 同一网格内不同信号可以使用不同的增强强度,也可以分别「构建为新信号」。
*/
double weakPeakLevel = 1.5;
/**
* @brief 弱峰增强的迭代次数 N(1–10)。
*
* 与「均值滤波」的迭代次数 N 一样表示“把同一处理重复应用 N 次”(每一阶以上一阶为输入):
* 一阶时改定量就已严格等于「等级」对应的目标值;多阶会把相邻弱峰互相抬高
* 彼此的基准,从而把较宽的弱峰区逐步恢复。
*/
int weakPeakIterations = 1;
/** 参与弱峰增强预览的信号条目(只包含被勾选的信号)。 */
std::vector<WeakPeakItem> weakPeakItems;
/** 取某个信号的弱峰增强条目(未勾选时返回空指针)。 */
/** 取某个信号的弱峰增强参数(未勾选时返回空指针)。 */
const WeakPeakItem* FindWeakPeakItem(const SignalRef& ref) const;
/** 取得或创建某信号的弱峰增强条目(用于勾选)。 */
/** 取得或创建某信号的弱峰增强参数(用于勾选)。 */
WeakPeakItem& EnsureWeakPeakItem(const SignalRef& ref);
/** 取消某信号的弱峰增强勾选。 */
+7 -12
View File
@@ -136,8 +136,6 @@ namespace jdi
double meanFilterPanelY = -1.0;
bool weakPeak = false;
std::vector<Lane::WeakPeakItem> weakPeakItems;
double weakPeakLevel = 1.5;
int weakPeakIterations = 1;
bool weakPeakPanelOpen = true;
double weakPeakPanelX = -1.0;
double weakPeakPanelY = -1.0;
@@ -168,8 +166,6 @@ namespace jdi
carry.meanFilterPanelY = lane.meanFilterPanelY;
carry.weakPeak = lane.weakPeak;
carry.weakPeakItems = lane.weakPeakItems;
carry.weakPeakLevel = lane.weakPeakLevel;
carry.weakPeakIterations = lane.weakPeakIterations;
carry.weakPeakPanelOpen = lane.weakPeakPanelOpen;
carry.weakPeakPanelX = lane.weakPeakPanelX;
carry.weakPeakPanelY = lane.weakPeakPanelY;
@@ -211,8 +207,6 @@ namespace jdi
lane.meanFilterPanelY = carry.meanFilterPanelY;
lane.weakPeak = carry.weakPeak;
lane.weakPeakItems = carry.weakPeakItems;
lane.weakPeakLevel = carry.weakPeakLevel;
lane.weakPeakIterations = carry.weakPeakIterations;
lane.weakPeakPanelOpen = carry.weakPeakPanelOpen;
lane.weakPeakPanelX = carry.weakPeakPanelX;
lane.weakPeakPanelY = carry.weakPeakPanelY;
@@ -1618,17 +1612,20 @@ namespace jdi
}
}
// 均值滤波 / 中心轴边沿计数的条目同样持有信号引用,必须一起删除/前移。
const auto purgeRefs = [fileIndex, signalIndex](std::vector<Lane::MeanFilterItem>& items)
// 均值滤波 / 中心轴边沿计数 / 弱峰增强的条目同样持有信号引用,必须一起删除/前移。
// 三类条目都是「带 `ref` 的结构体」,因此用泛型 lambda 统一处理
//(弱峰增强的参数与均值滤波不同,不能再共用同一个具体类型)。
const auto purgeRefs = [fileIndex, signalIndex](auto& items)
{
using ItemType = typename std::decay_t<decltype(items)>::value_type;
items.erase(std::remove_if(items.begin(), items.end(),
[fileIndex, signalIndex](const Lane::MeanFilterItem& item)
[fileIndex, signalIndex](const ItemType& item)
{
return item.ref.fileIndex == fileIndex &&
item.ref.signalIndex == signalIndex;
}),
items.end());
for (Lane::MeanFilterItem& item : items)
for (ItemType& item : items)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex > signalIndex)
{
@@ -1931,8 +1928,6 @@ namespace jdi
lane.edgeCenterPanelX = source.edgeCenterPanelX;
lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.weakPeak = source.weakPeak;
lane.weakPeakLevel = source.weakPeakLevel;
lane.weakPeakIterations = source.weakPeakIterations;
lane.weakPeakPanelOpen = source.weakPeakPanelOpen;
lane.weakPeakPanelX = source.weakPeakPanelX;
lane.weakPeakPanelY = source.weakPeakPanelY;