恢复显示效果并消除「实验性功能」交互卡顿;浮动面板改为像素图层

显示效果(回退本轮为提速引入的两处观感变化)
- 包络带恢复精确几何(去掉 ±1 列极值扩展:它把梳状细节并成实心块,用户反馈"显示效果没之前好")
- 派生曲线光晕恢复折线描边画法(不再用"带外扩填充")

交互流畅度
- 派生曲线只计算「可见区间 ± 保护带」窗口(CurveSpanFor + 区间窗口缓存),
  拖动参数滑块不再重算整条曲线;均值滤波的保护带按传播半径 X*N 计算,保证窗口内取值与整条曲线一致
- 超大窗口(中心轴未限定光标区间时的"整条信号"统计,千万级 ≈100MB)改写进一次性缓冲、不进缓存:
  消除 1141ms 卡顿尖峰,缓存内存 518MB → 2.6MB
- EdgeCenterCountsFor 先查计数缓存再取曲线,且只请求差集所需下标区间(该函数面板每帧都调用)
- 浮动面板改为像素图层(PanelGlobalKey / PanelSlotKey + BitBlt 贴图):只重画变脏的面板,
  其余直接贴图;重叠面板用 dirtyRects 级联保证不会把旧像素盖回去
- 快速帧(交互期间)统一关闭抗锯齿(几何逐点不变),松手后强制补一帧全质量画面

其他
- 弱波增强面板内「迭代次数 N」与「弱波增强等级」两个滑块标签左对齐到同一列

验证
- .preflight/uitest 新增 [AF]:贴图帧与重画帧绘图区逐像素一致(0 差异)、面板像素一致、
  连续重画确定性、PaintEdgeCursor 62~70ms → 0.3ms;全套 374 项检查通过
- .preflight/perf4 实测(VERY_BIG_DATA,5 网格全开实验性功能):
  弱波增强滑块拖拽中位 58.2 → 23.6ms;平移交互帧 61 → 23ms;最大卡顿尖峰 1141 → 47ms
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 21:35:31 +08:00
1 parent b960b03cb5
commit 7608e85cf0
2 files changed
+1109 -345

No files matched your search

+201 -17
View File
@@ -91,6 +91,17 @@ namespace jdi
BOOST = 3 /**< 弱波增强。 */
};
/**
* @brief 派生(叠加预览)曲线的种类。
*
* 「中心轴边沿计数」与「均值滤波」共用 `MEAN_FILTER`(同一套参数与同一条曲线)。
*/
enum class DerivedCurveKind : int
{
MEAN_FILTER = 0, /**< 多次迭代均值滤波。 */
WEAK_BOOST = 1 /**< 弱波增强。 */
};
/**
* @brief 波形绘图区:网格、时间轴、光标、缩放、自适应与信号拖拽。
*
@@ -1030,7 +1041,7 @@ namespace jdi
void BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks,
std::vector<gd::PointF>& band, std::vector<std::size_t>& bandBreaks,
bool& bandMode) const;
bool& bandMode, bool& bandSpiky) const;
/**
* @brief 绘制单个网格。
@@ -1064,6 +1075,23 @@ namespace jdi
*/
void PaintEdgeCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes);
/**
* @brief 确保面板图层缓存位图可用(尺寸不符时重建)。
* @return 可用返回 true。
*/
bool EnsurePanelCache();
/** @brief 释放面板图层缓存位图。 */
void ReleasePanelCache();
/**
* @brief 计算「全局」面板缓存键。
*
* 它涵盖所有面板共同依赖的状态(几何、缩放、主题、光标、选区、悬停、统计区间等);
* 一旦变化,所有面板缓存一律作废(整层重画)。
*/
std::uint64_t PanelGlobalKey() const;
/**
* @brief 判断坐标是否命中某网格的「边沿计数」横向光标线。
* @param [IN] x 客户区横坐标。
@@ -1109,6 +1137,23 @@ namespace jdi
*/
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const;
/**
* @brief 计算单个面板的缓存键(全局键之外,面板自身的内容)。
* @param [IN] laneIndex 面板所属网格。
* @param [IN] kind 面板种类。
* @param [IN] layout 面板布局。
* @return 缓存键。
*/
std::uint64_t PanelSlotKey(int laneIndex, EdgePanelKind kind,
const EdgePanelLayout& layout) const;
/**
* @brief 判断某个矩形是否完全落在面板图层位图之内。
* @param [IN] rect 目标矩形(客户区坐标)。
* @return 完全落在图层内返回 true。
*/
bool PanelRectInsideLayer(const gd::RectF& rect) const;
/**
* @brief 获取多阶(迭代)均值滤波曲线,结果带缓存。
*
@@ -1192,10 +1237,49 @@ namespace jdi
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] curve 曲线数值数组(与信号采样等长,缺失处为 NaN)。
* @param [IN] curve 曲线数值数组(缺失处为 NaN)。
* @param [IN] curveBegin `curve[0]` 对应的采样下标。
*/
void PaintDerivedCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
const std::vector<float>& curve) const;
const std::vector<float>& curve, std::size_t curveBegin) const;
/**
* @brief 取得某个采样区间内的派生曲线(带缓存)。
*
* ⚠️ **性能关键**:派生曲线只用于“看得见的那一段”,而整条信号可能有数千万个采样点
* (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] begin 需要的采样区间起点(含)。
* @param [IN] end 需要的采样区间终点(不含)。
* @param [OUT] spanBegin 返回数组对应的首个采样下标。
* @return 曲线数组指针(覆盖 `[spanBegin, spanBegin + size())`);无法计算时返回 nullptr。
*/
const std::vector<float>* CurveSpanFor(DerivedCurveKind kind, const SignalData& signal,
int points, int iterations, int levelPercent,
std::size_t begin, std::size_t end,
std::size_t& spanBegin) const;
/**
* @brief 计算当前视图对应的采样下标区间(时间单调时二分定位,否则为整段)。
* @param [IN] file 目标文件。
* @param [IN] count 可用采样点数。
* @param [OUT] begin 区间起点(含)。
* @param [OUT] end 区间终点(不含)。
*/
void VisibleIndexRange(const DataFile& file, std::size_t count, std::size_t& begin,
std::size_t& end) const;
/**
* @brief 绘制某条信号的多次迭代均值滤波曲线(与中心平均值曲线同一配色派生规则)。
@@ -1438,6 +1522,14 @@ namespace jdi
std::vector<WeakBoostBuild> m_pendingWeakBoostItems; /**< 待构建的弱波增强信号及其参数。 */
int m_boostDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
/**
* @brief 本帧是否使用「快速绘制」(交互期间关闭波形与派生曲线的抗锯齿)。
*
* 用户明确要求"先用低分辨率,绘制完成后再用高分辨率替换":抗锯齿是单帧最贵的
* 一项(同一条包络带实测 27.6ms → 3.2ms),而它的几何完全不变,
* 因此拖拽/拖动滑块期间关闭它,松手后由宿主整帧失效自动恢复全质量绘制。
*/
bool m_fastFrame = false;
/**
* @brief 实验性功能浮动面板的 ✕ 处于"已按下、等待抬起确认"状态。
@@ -1503,6 +1595,37 @@ namespace jdi
};
mutable std::deque<WeakBoostCurveEntry> m_weakBoostCurveCache;
/**
* @brief 派生曲线的「区间窗口」缓存项。
*
* `values[i]` 对应采样下标 `begin + i`;`begin/end` 是**实际计算**的区间
* (已向外扩了保护带),命中条件是「参数一致且窗口包含所需区间」。
*/
struct CurveSpanEntry
{
const SignalData* signal = nullptr; /**< 目标信号。 */
int kind = 0; /**< `DerivedCurveKind`。 */
int points = 5; /**< 均值点数 X。 */
int iterations = 1; /**< 迭代次数 N。 */
int levelPercent = 0; /**< 弱波增强等级(0–100)。 */
std::size_t begin = 0; /**< 已计算区间的起点(采样下标)。 */
std::size_t end = 0; /**< 已计算区间的终点(不含)。 */
std::vector<float> values; /**< 曲线数值。 */
};
mutable std::deque<CurveSpanEntry> m_curveSpanCache; /**< 区间窗口缓存(deque 保证引用稳定)。 */
/**
* @brief 「超大窗口」派生曲线的一次性缓冲。
*
* 中心轴边沿计数在**没有限定光标区间**时要统计整条信号,对应窗口与信号等长
*(亿级采样 ≈ 100MB/信号)。这类窗口绝不进 `m_curveSpanCache`:
* 一旦被淘汰,下一帧就会重算整条曲线(实测单次 ≈ 1s 的卡顿尖峰),
* 而且几个信号同时驻留会吃掉数百 MB。
* 改为写入本缓冲:调用方(`EdgeCenterCountsFor`)在其返回后立即用完,
* 因此生命周期安全。⚠️ 指针仅在下一次「超大窗口」请求之前有效。
*/
mutable std::vector<float> m_curveSpanScratch;
/** 中心轴边沿统计缓存项(增量统计机制与 `EdgeCountEntry` 完全一致)。 */
struct EdgeCenterCountEntry
{
@@ -1569,6 +1692,17 @@ namespace jdi
*/
bool bandMode = false;
/**
* @brief 该包络带是否为「梳状」(相邻列的上下包络几乎不相交)。
*
* 振荡周期与像素列宽相当时(例如编码器方波在 2ms/大格下约 3 个采样点一列),
* 逐列包络会在"全高"与"极窄"之间交替。这种形状下 GDI+ 的抗锯齿扫描线代价暴涨
* (实测同一条 2448 顶点的路径:27.6ms vs 无抗锯齿 3.2ms),
* 而抗锯齿对它的平滑作用几乎看不出来 —— 因此绘制时改用无抗锯齿填充。
* ⚠️ 几何本身仍然是**精确**的(不做任何加宽),否则波形会被"糊"成实心块。
*/
bool bandSpiky = false;
/** 包络带顶点:每段为「上包络正向 + 下包络反向」,闭合后即为该段的填充多边形。 */
std::vector<gd::PointF> band;
@@ -1661,12 +1795,39 @@ namespace jdi
int mode = 0; /**< 绘制模式。 */
};
/**
* @brief 单个网格内「叠加预览曲线」的状态(开关 + 每条被勾选信号的参数)。
*
* 它**只影响网格内容的绘制**(不影响网格结构/视图几何),因此不进入
* `SceneGeometryHash`,而是与外观样式一起作为“局部重画”的判据:
* 拖动弱波增强的等级/迭代次数滑块时,只需重画受影响的网格,
* 不必整幅重绘(也不会把静态场景缓存整体作废)。
*/
struct SceneDerivedEntry
{
int edgeCenter = 0; /**< 中心轴边沿计数开关。 */
int meanFilter = 0; /**< 均值滤波开关。 */
int weakBoost = 0; /**< 弱波增强开关。 */
int weakBoostLevelPercent = 0; /**< 弱波增强等级(0–100)。 */
int weakBoostIterations = 1; /**< 弱波增强迭代次数 N。 */
/** 各条目展开为 [文件, 信号, 均值点数 X, 迭代次数 N],按原顺序(稳定)。 */
std::vector<int> edgeCenterItems;
std::vector<int> meanFilterItems;
std::vector<int> weakBoostItems;
};
/**
* @brief 采集当前各网格内可见信号的外观样式快照。
* @param [OUT] out 采集结果(按网格分组)。
*/
void CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const;
/**
* @brief 采集当前各网格的「叠加预览曲线」状态快照。
* @param [OUT] out 采集结果(按网格分组)。
*/
void CollectSceneDerived(std::vector<SceneDerivedEntry>& out) const;
/**
* @brief 判断当前相对缓存是否"只有外观样式发生了变化"。
*
@@ -1731,20 +1892,6 @@ namespace jdi
void PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved,
const std::vector<gd::PointF>& points, const std::vector<std::size_t>& breaks);
/**
* @brief 稠密包络带的光晕:把带整体外扩 `extra` 像素后填充一次。
*
* 比用宽画笔描整条折线快一个数量级(含大量近垂直跳变的信号上尤其明显),
* 观感也更均匀。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] band 包络带顶点(上包络正向 + 下包络反向)。
* @param [IN] bandBreaks 各段起点。
* @param [IN] extra 纵向额外外扩量(像素)。
*/
void PaintBandHalo(gd::Graphics& g, const std::vector<gd::PointF>& band,
const std::vector<std::size_t>& bandBreaks, float extra);
/**
* @brief 计算某网格图例中 GX 编号徽标的矩形(绘制与命中测试共用,保证一致)。
* @param [IN] laneIndex 网格下标。
@@ -1837,6 +1984,43 @@ namespace jdi
/** 缓存捕获时各网格内信号的外观样式快照(按网格分组,与 m_sceneStyles 同序)。 */
std::vector<std::vector<SceneStyleEntry>> m_sceneStyles;
/** 缓存捕获时的“叠加预览曲线”状态快照(用于判断只有曲线参数变化)。 */
std::vector<SceneDerivedEntry> m_sceneDerived;
/**
* @brief 静态场景缓存里存的是否为「快速帧」(交互期间关掉了抗锯齿)。
*
* 为 true 时 `SceneCacheMatches()` 直接判不命中,强制重画一次全质量版本 ——
* 这就是用户要求的“先用低分辨率、绘制完成后再用高分辨率替换”。
*/
bool m_sceneDegraded = false;
/**
* @brief 实验性功能浮动面板的像素图层项。
*
* 面板被完整绘制进 `m_panelDc`;只要 `key` 不变,下一帧就直接按
* `[x0, y0, x1, y1)` 贴图,不再重复绘制(15 个面板全画约 33ms,贴图 <1ms)。
*/
struct PanelSlot
{
int laneIndex = -1; /**< 所属网格。 */
int kind = 0; /**< `EdgePanelKind`。 */
std::uint64_t key = 0; /**< 面板自身的缓存键。 */
int x0 = 0; /**< 已缓存矩形(含阴影外扩),左。 */
int y0 = 0; /**< 上。 */
int x1 = 0; /**< 右(不含)。 */
int y1 = 0; /**< 下(不含)。 */
};
HBITMAP m_panelBitmap = nullptr; /**< 面板图层位图(原点与客户区一致)。 */
HDC m_panelDc = nullptr; /**< 面板图层设备上下文。 */
HGDIOBJ m_panelOldBitmap = nullptr; /**< 面板图层 DC 原选位图。 */
int m_panelWidth = 0; /**< 面板图层位图宽度。 */
int m_panelHeight = 0; /**< 面板图层位图高度。 */
bool m_panelValid = false; /**< 图层内容是否有效。 */
std::uint64_t m_panelGlobalKey = 0; /**< 上一层对应的全局键。 */
std::vector<PanelSlot> m_panelSlots; /**< 上一层缓存的面板(本帧比对用)。 */
/**
* @brief 查询坐标命中的图例条目。
* @param [IN] x 客户区横坐标。
File diff suppressed because it is too large. Load diff