深放大中键拖拽卡顿根因修复(GDI+ 抗锯齿"满幅锯齿")+ 横向补条裁剪 + 并行网格图层(B/C)
用户报告:VERY_BIG_DATA.CSV 只显示编码器A相、选区放大到 [10.2719,10.2831]s(≈1.12ms 一大格)
后,中键拖拽视界非常卡顿。要求做 B(拆分宽基底路径定位瓶颈)与 C(解除"不允许多线程"约束
并实现并行网格渲染)。
B —— 宽基底路径耗时拆解(.preflight/perf/dragperf mode 1):
该视界下 RollWideBuffer(补条重绘)13.08ms,BlitWideBuffer 0.50 / PaintTimeAxis 0.78 /
PaintLaneDividers 0.04 / PaintLaneDerivedOverlays 0.00 / CaptureDecorCache 0.03ms
—— 瓶颈全在"补条重绘"。
横向补条裁剪(RollWideBuffer 加速):
补条只覆盖图层位图一条窄竖带,但折线原本按整条宽窗口(2x 视界)构建/描边整段顶点。
新增 m_stripRenderActive/m_stripX0/m_stripX1:横向补条时把折线采样区间裁剪到条带对应 x 范围
(两侧留 max(4, lineWidth+3) 像素余量),并旁路折线缓存(补条只构建片段,写缓存会污染整段复用)。
裁剪只对"逐点描边"路径生效,且必须在"包络/描边"判定之后(否则 visibleCount 变化会改路径判定)。
实测 RollWideBuffer 13.08ms -> 1.73ms;拖拽整帧 13~24ms -> ~3ms;补条 vs 整块逐像素 0 差异。
真正的病态根因:GDI+ 抗锯齿描边"满幅锯齿":
拖拽超出预渲染余量时走整块重画,整宽单网格渲染实测 655ms。逐层定位:BuildPolyline 仅 0.03ms;
同样 4481 点交给 Graphics::DrawLines —— AA round 694ms / 无 AA 29ms(23 倍)。即编码器方波在
1~2 采样/像素时形成每像素列一次整幅跳变的锯齿,GDI+ 抗锯齿逐段扫描近竖直满幅边,代价爆炸
(原实现只在 >2 采样/像素或"梳状"时关 AA,这一区间两个条件都不满足)。
修法(几何完全不变,只改"何时走包络/何时关 AA"):
1) 包络带阈值从"每像素 2 个采样(budget)"降到"每像素 1 个采样"(visibleCount > plotWidth);
2) EmitEnvelopeBand 新增 laneHeight 形参,扩展"梳状(关 AA)"判据:多数列列内跨幅 > 0.5x网格高,
或相邻列上下包络整幅跳变(> 0.25x网格高)-> 本段关闭抗锯齿。
实测:整宽单网格渲染 655ms -> 47.7ms;补条 6.9 -> 2.4ms;中键拖拽 200 帧 avg 4.5ms /
max 5.8ms / 0 帧 >8ms(修复前 13~24ms 且含 700ms 尖峰)。目视核对(mode 9 导出 PNG)正常。
C —— 并行网格图层渲染(用户明确解除多线程限制):
文档中"图层渲染一律在 UI 线程内同步完成 / 绝不要搬到后台线程"的约束已按用户要求解除,但根因
(共享可变渲染状态)依然成立,故并行的唯一安全姿势是"每线程持有隔离渲染上下文"。
实现 PlotView::RebuildLaneLayersParallel:每线程一个隔离 PlotView(m_tab 指向自有 m_workerTab
副本,m_area/m_plotRect/m_lanes 都是它自己的),各自渲染不同 laneIndex 到不同的 m_layerFront 元素;
主线程 join 后按真实网格下标重算 LaneLayerKey。
踩坑:BuildLaneVisuals() 在 m_tab==nullptr 时会清空 m_lanes -> 不能靠置空 m_tab 隔离,必须给它一个
非空的、自有的 TabConfig(m_workerTab)。
正确性:并行 vs 顺序逐网格比较 0 差异(5 网格全 0)。
诚实结论:并行仅 1.03x —— 同机自检证明 4 线程 CPU 真并行(3.98x,hw=20),故瓶颈是 GDI+ 光栅化
本身的串行化,不是调度。因此并行对光栅化无收益,仍保持"视界停后每帧补 1 个网格"(一次补齐只会把
一帧从 ~10ms 抬到 ~31ms,更易掉帧);并行只用于本来就要一次画完的 RebuildWideBuffer()。
验证:dragperf mode 0/1/3/5/6/8/9;pancheck 7 场景全 dy=0;zoomcheck mode 0/13/14 无回归
(f1 8~9ms、缩放收敛 3649 差异=预存在残差);make clean && make all 零警告。
文档同步:§9.18 新增;§11.2 第 5、14 条改为"解禁但必须隔离状态"的新表述。
This commit is contained in:
1 parent
065b948297
commit
20b90848fe
3 files changed
+467
-27
No files matched your search
@@ -1880,6 +1880,23 @@ namespace jdi
|
||||
|
||||
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
|
||||
|
||||
/**
|
||||
* @brief 图层**横向补条**渲染期间的折线裁剪范围(省掉整段折线的重建与描边)。
|
||||
*
|
||||
* 横向补条只覆盖图层位图的**一条窄竖带**(`m_area` 的 x 区间),但折线原本会按
|
||||
* `m_plotRect`(整条宽窗口 = 2× 视界)构建整段顶点并交给 GDI+ 描边 —— 即使只画
|
||||
* 一条窄带,`DrawLines`/`FillPath` 仍要逐段提交,实测在"逐点描边"区间单帧高达
|
||||
* **13ms**(这正是"深放大下中键拖拽非常卡顿"的根因)。
|
||||
* 因此补条渲染时把折线的采样区间按条带的 x 范围裁剪(两侧各留少量像素余量以避免
|
||||
* 接缝),只构建并绘制落在条带内的那一段顶点。条带之外的像素由已有图层像素提供,
|
||||
* 因此结果仍与整块重画**逐像素一致**。
|
||||
*/
|
||||
bool m_stripRenderActive = false;
|
||||
float m_stripX0 = 0.0f; /**< 条带左边界(与 `m_plotRect` 同坐标系)。 */
|
||||
float m_stripX1 = 0.0f; /**< 条带右边界。 */
|
||||
/** 条带渲染专用的折线结果(**不进缓存** —— 它只覆盖条带区间,不能当作整段折线复用)。 */
|
||||
mutable PolylineCacheEntry m_stripPolylineEntry;
|
||||
|
||||
/**
|
||||
* @brief 静态场景缓存的几何/内容键。
|
||||
*
|
||||
@@ -2400,10 +2417,38 @@ namespace jdi
|
||||
*/
|
||||
bool RenderPendingLaneRebuilds(int budget);
|
||||
/** @brief 使全部图层失效。 */
|
||||
void InvalidateWideBuffer();
|
||||
/** @brief 释放全部图层位图。 */
|
||||
void InvalidateWideBuffer(); /** @brief 释放全部图层位图。 */
|
||||
void ReleaseWideBuffer();
|
||||
|
||||
/**
|
||||
* @brief **并行**重建多个网格的信号图层(多核;用于"视界已停"时一次性补齐所有过期网格)。
|
||||
*
|
||||
* ⚠️ 并行化方式:为每条工作线程准备一个**隔离的 `PlotView`**(它把 `m_tab` 置空、
|
||||
* 用自带的 `m_localTab` 副本),因此 `RenderLaneLayerRegion` 改写 `m_area` / `m_plotRect` /
|
||||
* 视界 / 网格矩形时**只作用于该线程自己的副本**,不会与 UI 线程或其它工作线程互相覆盖
|
||||
*(这正是 2026-10-12 移除"后台图层线程"的根因;用隔离上下文即可安全并行)。
|
||||
* 合成只需各线程把自己那块位图交给主线程(`std::swap` 交换 `m_layerFront`)。
|
||||
* @param [IN] laneIndices 要重建的网格下标(各自独立)。
|
||||
* @param [IN] wideMin 图层位图 `x=0` 对应的时间(各网格一致,即"以当前视界居中")。
|
||||
*/
|
||||
void RebuildLaneLayersParallel(const std::vector<int>& laneIndices, double wideMin);
|
||||
/** @brief 创建(或按需扩容)并行图层渲染的工作线程上下文。 */
|
||||
void EnsureLayerWorkers();
|
||||
/** @brief 释放并行图层渲染的工作线程上下文与暂存图层。 */
|
||||
void ReleaseLayerWorkers();
|
||||
|
||||
/** 并行图层渲染的工作上下文(每线程一个,惰性创建;每个都有自己的 `m_workerTab`)。 */
|
||||
std::vector<std::unique_ptr<PlotView>> m_layerWorkers;
|
||||
int m_layerWorkerCount = 0;
|
||||
/**
|
||||
* @brief 工作线程实例**自有的**标签页配置(`m_tab` 指向它)。
|
||||
*
|
||||
* 工作线程的 `RenderLaneLayerRegion` 会改写视界 / 网格矩形;让它改在自己这份副本上,
|
||||
* 就不会碰到 UI 线程共享的 `TabConfig`(安全并行的前提)。
|
||||
* ⚠️ `m_tab` **必须非空** —— `BuildLaneVisuals()` 在 `m_tab == nullptr` 时会清空 `m_lanes`。
|
||||
*/
|
||||
TabConfig m_workerTab;
|
||||
|
||||
std::vector<LaneLayer> m_layerFront; /**< 各网格的「信号图层」(与 `m_lanes` 同序)。 */
|
||||
/** 待重画的网格下标(去重;`RollWideBuffer` 登记,`RenderPendingLaneRebuilds` 消费)。 */
|
||||
std::vector<int> m_layerRebuildPending;
|
||||
|
||||
Reference in new issue
Block a user