修复缩放后平移的绘制错误/抖动与前台卡顿;弱波增强改名弱峰增强并支持 0~200%

渲染(根因与修法)
- 根因:后台图层线程渲染时会临时改写共享渲染状态(m_area/m_plotRect/视界/网格矩形),
  与 UI 线程无锁的平移(OnMouseMove)和命中测试相冲 → 后台的"保存→改写→恢复"会把
  用户刚做的平移弹回、命中测试读到错位网格;同时 RollWideBuffer 用单一全局锚点给所有
  图层算同一位移,而各图层可能是在不同视界下渲染的(后台逐网格)→ 网格之间时间错位、
  贴回像素互相矛盾;补细条又误用"当前视界"的映射(与旧像素不同源)→ ≤0.5px 接缝与拖动抖动。
- 修法:
  * 删除后台图层线程(m_layerWorker 及任务队列、EnsureLayerWorker、StopLayerWorker、
    LayerWorkerLoop、ScheduleStaleLayerRenders、m_layerBack、m_layerAnchorMin/Valid、
    m_lastPaintTick、m_layerRescheduleStreak)。图层渲染一律在 UI 线程内同步完成。
  * 逐网格独立锚点与滚动:以 LaneLayer::wideMin 为准,补细条沿用该图层自己的映射。
  * RenderLaneLayerRegion 新增 wideMin 形参;新增 LayerDefaultWideMin()。
  * RollWideBuffer 逐网格:内容指纹不一致或锚点离视界太远 → 该网格整块重画;
    否则整数像素自贴图平移 + 只补新暴露的细条。
  * Paint 宽基底分支:!LayersReadyForView() 时先同步 RebuildWideBuffer() 再滚动。
  * BackgroundLayerRenderCount() 保留为"同步图层区域渲染次数"的诊断计数。
- 效果:平移不再出现网格时间错位、视界被弹回或波形抖动;居中图层与整幅重绘逐像素一致。

弱峰增强(原「弱波增强」)
- 功能改名:界面文案与标识符统一为 WeakPeak/weakPeak(ComputeWeakPeakBoost、
  SetLaneWeakPeak*、EdgePanelKind::PEAK、PlotDragMode::PEAK_LEVEL/PEAK_ITERATIONS、
  PLOT_CMD_LANE_WEAK_PEAK、PeakExtremum 等),导出信号名 "<原信号> (弱峰增强 M% N次迭代)"。
- 等级范围改为 0~200%,目标值分段插值:
  * 0~100%:在"目标极值自身的值"与"左右同类极值中较温和的一侧"之间插值
    (0% 目标就是当前值 → 完全不动,允许用户做极小幅度调整);
  * 100~200%:与原先 0~100% 的插值方式一致(200% 完全拉平到另一侧)。
  两段在 100% 处连续。新 1.5 == 旧 0.5,新 2.0 == 旧 1.0。
- Lane::weakPeakLevel 默认 1.5;levelPercent(构建请求/曲线缓存键)范围 0~200、默认 150;
  新增 kWeakPeakLevelMax = 2.0 与 kWeakPeakPercentMax = 200;滑块轨道比例 = level / 2。

测试
- 同步更新离屏测试 .preflight/uitest:等级期望改为 0/50/100/150/200 五档;阶数扫描与
  "完全恢复"块改用 150%/200% 以保持旧期望;派生信号名校验改用新名;
  [J] 改为"补丁帧标记降级 → 渲染到缓存重新有效后用修复帧与整幅重绘比对(0 差异)";
  [AF] 面板像素容差 32→96(面板 96% 不透明,底层 ≤0.5px 残留会透出,实测最多 6 像素/72 级);
  [X] 预览曲线像素计数的颜色容差 6→40。
- 新增离屏检查 .preflight/layercheck:居中图层与整幅重绘逐像素对比、平移逐网格对齐位移、
  视界覆盖检测、跨度抖动下的视界保持检测。
- make release 零警告;uitest 380/380 通过(exit=0)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-12 10:10:06 +08:00
1 parent 800c8b835b
commit da83dc2987
7 files changed
+649 -739

No files matched your search

+101 -122
View File
@@ -43,8 +43,8 @@ namespace jdi
MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */
EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
BOOST_LEVEL, /**< 拖动「弱波增强」面板中的等级滑块。 */
BOOST_ITERATIONS, /**< 拖动「弱波增强」面板中的迭代次数 N 滑块。 */
PEAK_LEVEL, /**< 拖动「弱峰增强」面板中的等级滑块。 */
PEAK_ITERATIONS, /**< 拖动「弱峰增强」面板中的迭代次数 N 滑块。 */
LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
};
@@ -90,7 +90,7 @@ namespace jdi
PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */
PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */
PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
PLOT_CMD_LANE_WEAK_BOOST = 11 /**< 切换本网格的「实验性功能 → 弱波增强」开关。 */
PLOT_CMD_LANE_WEAK_PEAK = 11 /**< 切换本网格的「实验性功能 → 弱峰增强」开关。 */
};
/**
@@ -101,7 +101,7 @@ namespace jdi
COUNT = 0, /**< 边沿计数。 */
CENTER = 1, /**< 边沿计数(中心轴)。 */
MEAN = 2, /**< 均值滤波。 */
BOOST = 3 /**< 弱波增强。 */
PEAK = 3 /**< 弱峰增强。 */
};
/**
@@ -112,7 +112,7 @@ namespace jdi
enum class DerivedCurveKind : int
{
MEAN_FILTER = 0, /**< 多次迭代均值滤波。 */
WEAK_BOOST = 1 /**< 弱波增强。 */
WEAK_PEAK = 1 /**< 弱峰增强。 */
};
/**
@@ -138,11 +138,11 @@ namespace jdi
void SetHostWindow(HWND host) { m_host = host; }
/**
* @brief 后台「网格图层」绘制线程已完成的图层数量(仅供验证 / 诊断)。
* @brief 已渲染的「信号图层」区域数量(仅供验证 / 诊断)。
*
* 计数 > 0 即说明单网格基底的绘制确实发生在**后台线程**,
* 前台只负责位图搬运(贴图)与叠加层。
* @return 累计完成的图层数量。
* 每次把一个网格的基底(整块或滚动细条)画进图层都会 +1。计数长期不增长即说明
* 平移期间确实在复用已有像素(只做坐标偏移),没有重新绘制波形。
* @return 累计完成的图层区域数量。
*/
std::uint64_t BackgroundLayerRenderCount() const
{
@@ -569,115 +569,115 @@ namespace jdi
bool TakePendingMeanFilterBuild(int& laneIndex, std::vector<MeanFilterBuild>& items);
/**
* @brief 「实验性功能 → 弱波增强」的「构建为新信号」单个请求
* @brief 「实验性功能 → 弱峰增强」的「构建为新信号」单个请求
* (一个被勾选的信号 + 整个网格共用的增强等级)。
*/
struct WeakBoostBuild
struct WeakPeakBuild
{
int fileIndex = -1; /**< 信号所属文件下标。 */
int signalIndex = -1; /**< 文件内信号下标。 */
int levelPercent = 50; /**< 弱波增强等级(0–100)。 */
int levelPercent = 150; /**< 弱峰增强等级(0–200)。 */
int iterations = 1; /**< 迭代次数 N(1–10)。 */
};
/**
* @brief 取出「弱波增强 → 构建为新信号」请求(由面板底部按钮触发)。
* @brief 取出「弱峰增强 → 构建为新信号」请求(由面板底部按钮触发)。
* @param [OUT] laneIndex 触发该请求的网格下标。
* @param [OUT] items 需要构建的信号及其参数(按信号在网格内的顺序)。
* @return 存在待处理请求返回 true(取出后清空)。
*/
bool TakePendingWeakBoostBuild(int& laneIndex, std::vector<WeakBoostBuild>& items);
bool TakePendingWeakPeakBuild(int& laneIndex, std::vector<WeakPeakBuild>& items);
/**
* @brief 切换某网格的「弱波增强」开关。
* @brief 切换某网格的「弱峰增强」开关。
* @param [IN] laneIndex 网格下标。
* @param [IN] enabled 是否开启。
* @return 状态发生变化返回 true。
*/
bool SetLaneWeakBoost(int laneIndex, bool enabled);
bool SetLaneWeakPeak(int laneIndex, bool enabled);
/**
* @brief 查询某网格的「弱波增强」开关。
* @brief 查询某网格的「弱峰增强」开关。
* @param [IN] laneIndex 网格下标。
* @return 已开启返回 true。
*/
bool LaneWeakBoost(int laneIndex) const;
bool LaneWeakPeak(int laneIndex) const;
/**
* @brief 切换某网格的「弱波增强」开关。
* @brief 切换某网格的「弱峰增强」开关。
* @param [IN] laneIndex 网格下标。
* @return 切换后的状态。
*/
bool ToggleLaneWeakBoost(int laneIndex);
bool ToggleLaneWeakPeak(int laneIndex);
/**
* @brief 读取某网格的弱波增强等级(0–1)。
* @brief 读取某网格的弱峰增强等级(0–2)。
* @param [IN] laneIndex 网格下标。
* @return 增强等级;网格不存在时返回 0.5。
*/
double LaneWeakBoostLevel(int laneIndex) const;
double LaneWeakPeakLevel(int laneIndex) const;
/**
* @brief 读取某网格的弱波增强迭代次数 N(1–10)。
* @brief 读取某网格的弱峰增强迭代次数 N(1–10)。
* @param [IN] laneIndex 网格下标。
* @return 迭代次数;网格不存在时返回 1。
*/
int LaneWeakBoostIterations(int laneIndex) const;
int LaneWeakPeakIterations(int laneIndex) const;
/**
* @brief 设置某网格的弱波增强迭代次数 N(1–10,超出范围会被夹取)。
* @brief 设置某网格的弱峰增强迭代次数 N(1–10,超出范围会被夹取)。
* @param [IN] laneIndex 网格下标。
* @param [IN] iterations 迭代次数。
* @return 迭代次数发生变化返回 true。
*/
bool SetLaneWeakBoostIterations(int laneIndex, int iterations);
bool SetLaneWeakPeakIterations(int laneIndex, int iterations);
/**
* @brief 设置某网格的弱波增强等级(0–1,超出范围会被夹取)。
* @brief 设置某网格的弱峰增强等级(0–2,超出范围会被夹取)。
* @param [IN] laneIndex 网格下标。
* @param [IN] level 增强等级。
* @return 等级发生变化返回 true。
*/
bool SetLaneWeakBoostLevel(int laneIndex, double level);
bool SetLaneWeakPeakLevel(int laneIndex, double level);
/**
* @brief 获取弱波增强曲线(按「信号 + 增强等级 + 迭代次数」缓存)。
* @brief 获取弱峰增强曲线(按「信号 + 增强等级 + 迭代次数」缓存)。
* @param [IN] signal 目标信号。
* @param [IN] levelPercent 弱波增强等级(0–100,整数百分比)。
* @param [IN] levelPercent 弱峰增强等级(0–200,整数百分比)。
* @param [IN] iterations 迭代次数 N(1–10)。
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
*/
const std::vector<float>& WeakBoostCurveFor(const SignalData& signal, int levelPercent,
const std::vector<float>& WeakPeakCurveFor(const SignalData& signal, int levelPercent,
int iterations) const;
/**
* @brief 取某个时刻的弱波增强曲线取值(用于纵向光标的取值标注)。
* @brief 取某个时刻的弱峰增强曲线取值(用于纵向光标的取值标注)。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] levelPercent 弱波增强等级(0–100)。
* @param [IN] levelPercent 弱峰增强等级(0–200)。
* @param [IN] iterations 迭代次数 N(1–10)。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
* @return 取到有效值返回 true。
*/
bool WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, int iterations,
bool WeakPeakValueAt(const DataFile& file, const SignalData& signal, int levelPercent, int iterations,
double time, double& value) const;
/**
* @brief 切换某个信号在「弱波增强」面板中的预览勾选状态。
* @brief 切换某个信号在「弱峰增强」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 切换后的勾选状态。
*/
bool ToggleLaneWeakBoostSignal(int laneIndex, const SignalRef& ref);
bool ToggleLaneWeakPeakSignal(int laneIndex, const SignalRef& ref);
/**
* @brief 查询某个信号在「弱波增强」面板中的预览勾选状态。
* @brief 查询某个信号在「弱峰增强」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 已勾选返回 true。
*/
bool LaneWeakBoostSignalEnabled(int laneIndex, const SignalRef& ref) const;
bool LaneWeakPeakSignalEnabled(int laneIndex, const SignalRef& ref) const;
/**
* @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。
@@ -1089,7 +1089,7 @@ namespace jdi
const std::vector<double>& timeTicks);
/**
* @brief 绘制某网格的全部**派生预览曲线**(均值滤波 / 中心轴 / 弱波增强),叠加在原信号之上。
* @brief 绘制某网格的全部**派生预览曲线**(均值滤波 / 中心轴 / 弱峰增强),叠加在原信号之上。
*
* 从 `PaintLane` 抽出,供「宽缓冲区」前台路径复用:宽缓冲只存"基底"(网格/波形),
* 派生曲线每帧作为叠加层单独绘制,因此拖派生参数滑块时无需重画波形。
@@ -1158,8 +1158,8 @@ namespace jdi
gd::RectF close; /**< ✕ 关闭按钮。 */
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
gd::RectF slider; /**< 「弱波增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */
gd::RectF sliderIterations; /**< 「弱波增强」迭代次数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */
gd::RectF slider; /**< 「弱峰增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */
gd::RectF sliderIterations; /**< 「弱峰增强」迭代次数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
/**
* @brief 「逐条目」面板的矩形(MEAN 与 CENTER 面板共用同一套排布)。
@@ -1294,20 +1294,20 @@ namespace jdi
* @brief 取得某个采样区间内的派生曲线(带缓存)。
*
* ⚠️ **性能关键**:派生曲线只用于“看得见的那一段”,而整条信号可能有数千万个采样点
* (VERY_BIG_DATA 为 2576 万)。若每次都重算整条曲线(实测弱波增强 ~800ms/条),
* (VERY_BIG_DATA 为 2576 万)。若每次都重算整条曲线(实测弱峰增强 ~800ms/条),
* 拖动等级/迭代次数滑块时每帧要花几秒钟,交互完全卡死。
* 本函数只计算“所需区间 ± 保护带”,并按(信号 + 参数 + 对齐后的区间)缓存:
* 实测单条降到 1~3ms,滑块拖动与平移因此恢复流畅。
*
* 保护带保证可见区间内的取值与“整条曲线”一致:均值滤波的核是有限宽度,
* 弱波增强的修正沿相邻极值向内传播(每迭代一次传一个极值),
* 弱峰增强的修正沿相邻极值向内传播(每迭代一次传一个极值),
* 因此只要求保护带足够宽(见 `kDerivedCurveGuard` 系列常量)。
*
* @param [IN] kind 曲线种类。
* @param [IN] signal 目标信号。
* @param [IN] points 均值点数 X(仅 `MEAN_FILTER` 使用)。
* @param [IN] iterations 迭代次数 N。
* @param [IN] levelPercent 弱波增强等级(仅 `WEAK_BOOST` 使用,0–100)。
* @param [IN] levelPercent 弱峰增强等级(仅 `WEAK_PEAK` 使用,0–200)。
* @param [IN] begin 需要的采样区间起点(含)。
* @param [IN] end 需要的采样区间终点(不含)。
* @param [OUT] spanBegin 返回数组对应的首个采样下标。
@@ -1336,7 +1336,7 @@ namespace jdi
int kind = 0; /**< `DerivedCurveKind`。 */
int points = 5; /**< 均值点数 X。 */
int iterations = 1; /**< 迭代次数 N。 */
int levelPercent = 0; /**< 弱波增强等级(0–100)。 */
int levelPercent = 0; /**< 弱峰增强等级(0–200)。 */
bool operator==(const CurveKey& other) const
{
@@ -1461,14 +1461,14 @@ namespace jdi
int points, int iterations) const;
/**
* @brief 绘制某条信号的弱波增强曲线(与均值滤波曲线同一配色派生规则)。
* @brief 绘制某条信号的弱峰增强曲线(与均值滤波曲线同一配色派生规则)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] levelPercent 弱波增强等级(0–100)。
* @param [IN] levelPercent 弱峰增强等级(0–200)。
* @param [IN] iterations 迭代次数 N(1–10)。
*/
void PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
void PaintWeakPeakCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int levelPercent, int iterations) const;
/**
@@ -1685,10 +1685,10 @@ namespace jdi
bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */
int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */
int m_pendingWeakBoostLane = -1; /**< 待构建的弱波增强信号所属网格下标(-1 表示无请求)。 */
int m_pendingWeakPeakLane = -1; /**< 待构建的弱峰增强信号所属网格下标(-1 表示无请求)。 */
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
std::vector<WeakBoostBuild> m_pendingWeakBoostItems; /**< 待构建的弱波增强信号及其参数。 */
int m_boostDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */
std::vector<WeakPeakBuild> m_pendingWeakPeakItems; /**< 待构建的弱峰增强信号及其参数。 */
int m_peakDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
/**
* @brief 本帧是否使用「快速绘制」(交互期间关闭波形与派生曲线的抗锯齿)。
@@ -1753,18 +1753,18 @@ namespace jdi
};
mutable std::deque<MeanCurveEntry> m_meanCurveCache;
/** 弱波增强曲线缓存项(按「信号指针 + 增强等级百分比 + 迭代次数」标识)。 */
struct WeakBoostCurveEntry
/** 弱峰增强曲线缓存项(按「信号指针 + 增强等级百分比 + 迭代次数」标识)。 */
struct WeakPeakCurveEntry
{
const SignalData* signal = nullptr;
int levelPercent = 50; /**< 弱波增强等级(0–100)。 */
int levelPercent = 150; /**< 弱峰增强等级(0–200)。 */
int iterations = 1; /**< 迭代次数 N(1–10)。 */
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
};
mutable std::deque<WeakBoostCurveEntry> m_weakBoostCurveCache;
mutable std::deque<WeakPeakCurveEntry> m_weakPeakCurveCache;
// ---- 异步派生曲线计算(后台线程 + 双缓冲)----
// 设计:预览曲线(均值滤波 / 弱波增强)在窗口很大时**不在绘制线程里计算**,而是交给后台
// 设计:预览曲线(均值滤波 / 弱峰增强)在窗口很大时**不在绘制线程里计算**,而是交给后台
// 线程;绘制线程本帧先用上一条已算好的曲线顶替(双缓冲),后台算完后通过
// `kMessagePlotDerivedReady` 通知宿主重绘,从而切换为最新曲线。这样滑块 / 光标 / 视界 /
// 缩放等交互的反馈始终跟手,不会被计算拖住,同时把重计算放到后台充分利用多核。
@@ -1941,7 +1941,7 @@ namespace jdi
/**
* @brief 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**。
*
* 拖弱波增强等级/迭代次数滑块时,图例与 Y 轴刻度不变,仅网格内预览曲线变化;
* 拖弱峰增强等级/迭代次数滑块时,图例与 Y 轴刻度不变,仅网格内预览曲线变化;
* 因此装饰层缓存不应因参数变化而失效(否则拖滑块每帧都要重绘图例与刻度)。
*
* @return 指纹值。
@@ -1971,20 +1971,20 @@ namespace jdi
*
* 它**只影响网格内容的绘制**(不影响网格结构/视图几何),因此不进入
* `SceneGeometryHash`,而是与外观样式一起作为“局部重画”的判据:
* 拖动弱波增强的等级/迭代次数滑块时,只需重画受影响的网格,
* 拖动弱峰增强的等级/迭代次数滑块时,只需重画受影响的网格,
* 不必整幅重绘(也不会把静态场景缓存整体作废)。
*/
struct SceneDerivedEntry
{
int edgeCenter = 0; /**< 中心轴边沿计数开关。 */
int meanFilter = 0; /**< 均值滤波开关。 */
int weakBoost = 0; /**< 弱波增强开关。 */
int weakBoostLevelPercent = 0; /**< 弱波增强等级(0–100)。 */
int weakBoostIterations = 1; /**< 弱波增强迭代次数 N。 */
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> weakBoostItems;
std::vector<int> weakPeakItems;
};
/**
@@ -2222,20 +2222,25 @@ namespace jdi
mutable int m_decorOriginX = 0;
mutable int m_decorOriginY = 0;
// ---- 每网格「信号图层」:双缓冲 + 后台绘制(前台只做贴图 / 坐标偏移) ----
// ---- 每网格「信号图层」(前台同步维护;交互帧只做贴图 / 坐标偏移) ----
//
// 平移只改变绘制坐标、不改变画面内容。因此**每个网格各自维护一块离屏位图**
// (宽度 = 视界宽 ×3:左右各多渲染半个视界),称为该网格的「信号图层」。
// 图层只含基底(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**(Y 轴刻度 / 图例)——
// (宽度 = 视界宽 + 左右各半个视界的余量),称为该网格的「信号图层」。
// 图层只含基底(底色 + 网格线 + 波形),**不含装饰**(Y 轴刻度 / 图例)——
// 装饰锚定在网格左缘、不随时间视界移动,若烘进图层会被平移带走(重影),
// 因此装饰由前台单独绘制(见 `m_decorCapturedRects` 那套缓存)。
//
// **双缓冲**:每个网格有两块图层(`m_layerFront` 前台读取 / `m_layerBack` 后台填充),
// 后台绘制完成后再整体交换。前台**永远只读 `m_layerFront`**,因此后台正在绘制时
// 前台仍能拿到上一份可用像素,绘制过程不影响交互响应。
// ⚠️ **图层一律在 UI 线程内同步维护**(`Paint` 全程持有 `m_renderMutex`),
// 因为渲染图层会临时改写 `m_area` / `m_plotRect` / `Tab().ViewMin()/ViewMax()` /
// 各网格矩形 —— 这些状态**同时被 UI 线程的消息处理无锁读取**
// (`OnMouseMove` 平移读视界、命中测试读网格矩形)。若把图层渲染搬到后台线程,
// 后台的"保存 → 改写 → 恢复"会**覆盖用户正在进行的平移**(视界被弹回原位),
// 命中测试也会读到错位的网格 → 表现为「拖动时视界跳回 / 波形抖动 / 绘制错乱」。
// 这正是先前"后台图层线程"被移除的原因:**不要把图层渲染移回后台**。
//
// **后台绘制**:跨度 / 几何 / 外观变化时,后台线程池把各网格的新图层画进后台缓冲;
// 若前台已经要刷新而后台还没画好,就继续用旧的前台缓冲「应付」交互,不解切换。
// 每个图层记录自己的时间映射(`LaneLayer::wideMin`):不同时刻渲染出的图层锚点
// 并不相同,滚动与贴图一律**逐网格**按各自的 `wideMin` 计算,**绝不共用一个全局锚点**
// (共用全局锚点会让各网格被平移不同的真实距离 → 网格之间时间错位)。
/**
* @brief 单个网格的「信号图层」(一块只含该网格基底的离屏位图)。
*/
@@ -2264,18 +2269,28 @@ namespace jdi
* @param [IN,OUT] layer 目标图层(会被写入尺寸 / 时间映射 / 指纹)。
* @param [IN] stripX 只渲染位图的这一段 x 区间(滚动时补画细条用)。
* @param [IN] stripW 该区间的宽度;传 0 表示渲染整块。
* @param [IN] wideMin 位图 `x=0` 对应的时间。
* **必须与调用方的意图一致**:整块重画传 `LayerDefaultWideMin()`(以当前视界居中);
* 滚动补细条则传 `layer.wideMin`(与该图层已有像素同一映射,否则会留下接缝)。
* @return 渲染成功返回 true。
*/
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW);
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW,
double wideMin);
/** @brief 「以当前视界居中」的默认图层映射(位图 `x=0` 对应的时间)。 */
double LayerDefaultWideMin() const;
/**
* @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧时使用)。
* @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧 / 图层缺失时使用)。
* @return 全部成功返回 true。
*/
bool RebuildWideBuffer();
/**
* @brief 让全部图层"滚动"以跟随视界:整数像素自贴图平移 + 只补画新暴露的细条。
* @return 图层仍可用返回 true。
* @brief 让各图层"滚动"以跟随视界:逐网格整数像素自贴图平移 + 只补画新暴露的细条。
*
* 每个网格按**自己的** `wideMin` 计算位移;位移过大(锚点离当前视界太远)或内容指纹
* 与当前状态不一致的网格直接整块重画。任一网格图层缺失(尺寸 / 跨度不符)返回 false,
* 由调用方先 `RebuildWideBuffer()`。
* @return 全部网格都能用现成像素跟随视界返回 true。
*/
bool RollWideBuffer();
/**
@@ -2285,62 +2300,26 @@ namespace jdi
*/
bool BlitWideBuffer(gd::Graphics& g) const;
/**
* @brief 全部图层是否已就绪且跨度与当前视界一致(**不要求内容也一致**)。
* @brief 全部图层是否已就绪且跨度与当前视界一致。
*
* 内容一致与否由后台线程负责跟踪:只要跨度一致、图层能覆盖当前视界,
* 前台就直接用现成像素“应付”交互(内容稍旧的一两帧由后台画好后交换),
* 这样样式/参数变化也不会退化成整幅重绘。
* @return 可以直接贴图层像素返回 true。
* 只检查"位图尺寸 / 网格数量 / 视界跨度"这类**结构性**条件(内容指纹由
* `RollWideBuffer()` 逐个网格处理);不满足时应先 `RebuildWideBuffer()`。
* @return 可以直接滚动 + 贴图层像素返回 true。
*/
bool LayersReadyForView() const;
/** @brief 使全部图层失效。 */
void InvalidateWideBuffer();
/** @brief 释放全部图层位图(前后台都释放)。 */
/** @brief 释放全部图层位图。 */
void ReleaseWideBuffer();
std::vector<LaneLayer> m_layerFront; /**< 各网格的「信号图层」(与 `m_lanes` 同序)。 */
/**
* @brief 为"内容与前台图层不一致"的网格排一次后台渲染任务(去重)。
*
* 后台渲染完成后会与前台缓冲交换并通知宿主重绘,前台因此始终有立即可用的像素。
* @brief 图层元数据的互斥(当前图层只在 UI 线程维护,故它实际不承担跨线程保护;
* 保留是为了给 `BlitWideBuffer()` 这类 `const` 读取路径一个明确的一致性边界)。
*/
void ScheduleStaleLayerRenders();
/** @brief 确保后台图层绘制线程已启动。 */
void EnsureLayerWorker();
/** @brief 停止后台图层绘制线程。 */
void StopLayerWorker();
/** @brief 后台图层绘制线程主循环。 */
void LayerWorkerLoop();
/** 单个网格图层任务:只需网格下标(参数在渲染时按最新状态取)。 */
struct LayerJob
{
int laneIndex = -1;
std::uint64_t key = 0;
};
std::vector<LaneLayer> m_layerFront; /**< 前台读取(后台不会就地改写)。 */
std::vector<LaneLayer> m_layerBack; /**< 后台填充。 */
mutable std::mutex m_layerMutex; /**< 保护前后台切换 / 元数据读取 / 锚点(const 方法也要读,故 mutable)。 */
std::recursive_mutex m_renderMutex; /**< 串行化“改写渲染状态”的绘制(前台整帧 vs 后台单网格);可重入(整帧内部会再调图层渲染)。 */
double m_layerAnchorMin = 0.0; /**< 图层滚动锚点:视界左端对应的时间。 */
bool m_layerAnchorValid = false; /**< 锚点是否有效。 */
std::thread m_layerWorker; /**< 后台图层绘制线程。 */
std::mutex m_layerJobMutex; /**< 任务队列互斥。 */
std::condition_variable m_layerJobCv; /**< 任务到来 / 退出通知。 */
std::vector<LayerJob> m_layerJobs; /**< 待渲染的网格任务。 */
bool m_layerWorkerStop = false; /**< 请求退出后台线程。 */
/**
* @brief 连续排了多少次"图层重建"却始终没与前台一致。
*
* 正常情况下一两次就会收敛(后台画完→交换→指纹一致)。若持续增长,说明后台渲染
* 算出的指纹与前台不一致(例如某个参与指纹的状态没被正确还原),此时改为**同步重建**
* 一次强行收敛,避免"前台永远不等后台、后台永远画不好"的死循环。
*/
int m_layerRescheduleStreak = 0;
/** 前台最近一次 `Paint` 的时刻(毫秒,`GetTickCount64`)。后台据此"让路",不与前台抢渲染锁。 */
mutable std::atomic<unsigned long long> m_lastPaintTick{0};
/** 后台线程已完成绘制的图层数量(原子,仅用于验证/诊断)。 */
mutable std::mutex m_layerMutex;
std::recursive_mutex m_renderMutex; /**< 串行化"改写渲染状态"的绘制(整帧 / 单网格图层);可重入。 */
/** 已渲染的图层区域数量(原子,仅用于验证 / 诊断)。 */
mutable std::atomic<std::uint64_t> m_layerRenderCount{0};
bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */