深放大中键拖拽卡顿根因修复(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;
|
||||
|
||||
@@ -421,10 +421,11 @@ namespace jdi
|
||||
* @param [IN] columns 逐列极值(同一段内按列顺序排列)。
|
||||
* @param [OUT] band 追加写入的包络顶点(上包络正向 + 下包络反向)。
|
||||
* @param [OUT] bandBreaks 追加写入的各段起点(相对 band 的下标)。
|
||||
* @param [OUT] spiky 该段是否为「梳状」(相邻列包络几乎不相交)。
|
||||
* @param [OUT] spiky 该段是否为「梳状 / 密集满幅」(建议关闭抗锯齿)。
|
||||
* @param [IN] laneHeight 网格的可见像素高度(用于「覆盖率」判据;≤0 时跳过)。
|
||||
*/
|
||||
void EmitEnvelopeBand(const std::vector<BandColumn>& columns, std::vector<gd::PointF>& band,
|
||||
std::vector<std::size_t>& bandBreaks, bool& spiky)
|
||||
std::vector<std::size_t>& bandBreaks, bool& spiky, float laneHeight)
|
||||
{
|
||||
if (columns.size() < 2)
|
||||
{
|
||||
@@ -450,6 +451,43 @@ namespace jdi
|
||||
spiky = true;
|
||||
}
|
||||
|
||||
// 「密集满幅」判定:包络带在**每一列内部**都横跨了网格的大部分高度
|
||||
//(编码器方波在 1~2 采样/像素时,多数像素列里都含一次整幅跳变)。
|
||||
// GDI+ **抗锯齿**的代价与这些近竖直、满幅的边成正比:实测同一 4832 顶点的几何
|
||||
// **抗锯齿 694ms、关抗锯齿 29ms(23 倍)**,而这么密的色块上抗锯齿的平滑作用肉眼不可辨。
|
||||
// ⚠️ 只影响「本段是否关抗锯齿」,几何(逐列 min/max)完全不变。
|
||||
if (laneHeight > 0.0f)
|
||||
{
|
||||
std::size_t tallColumns = 0;
|
||||
double covered = 0.0;
|
||||
for (const BandColumn& column : columns)
|
||||
{
|
||||
const float span = column.bottom - column.top;
|
||||
covered += static_cast<double>(span);
|
||||
if (span > 0.5f * laneHeight)
|
||||
{
|
||||
++tallColumns;
|
||||
}
|
||||
}
|
||||
// 「锯齿」判定(最能代表 GDI+ 抗锯齿代价):相邻列的上下包络**整幅跳变**。
|
||||
std::size_t jumps = 0;
|
||||
const float jumpThreshold = 0.25f * laneHeight;
|
||||
for (std::size_t index = 1; index < count; ++index)
|
||||
{
|
||||
const float dTop = std::fabs(columns[index].top - columns[index - 1].top);
|
||||
const float dBottom = std::fabs(columns[index].bottom - columns[index - 1].bottom);
|
||||
if (dTop > jumpThreshold || dBottom > jumpThreshold)
|
||||
{
|
||||
++jumps;
|
||||
}
|
||||
}
|
||||
if (tallColumns * 4 > count || jumps * 4 > count ||
|
||||
covered > 0.45 * static_cast<double>(count) * static_cast<double>(laneHeight))
|
||||
{
|
||||
spiky = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
band.push_back(gd::PointF(columns[index].x, columns[index].top));
|
||||
@@ -741,6 +779,8 @@ namespace jdi
|
||||
{
|
||||
// 先停掉后台计算线程:它可能正在读取采样数据,必须在释放其它资源前回收。
|
||||
StopCurveWorker();
|
||||
// 并行图层渲染的工作上下文(各自持有图层位图,须在 GDI+ 关闭前释放)。
|
||||
ReleaseLayerWorkers();
|
||||
ReleaseSceneCache();
|
||||
ReleaseDecorCache();
|
||||
ReleaseWideBuffer();
|
||||
@@ -5273,6 +5313,17 @@ namespace jdi
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ⚠️ **横向补条渲染:不进缓存**。补条只构建"条带区间"的折线顶点,若写进缓存,
|
||||
// 后续整块渲染命中同键时会拿到这段**不完整**的折线(只画了条带那一段)。
|
||||
// 因此补条专用一个独立条目,避免污染按"宽窗口几何"键控的缓存。
|
||||
if (m_stripRenderActive)
|
||||
{
|
||||
BuildPolyline(resolved, lane, m_stripPolylineEntry.points, m_stripPolylineEntry.breaks,
|
||||
m_stripPolylineEntry.band, m_stripPolylineEntry.bandBreaks,
|
||||
m_stripPolylineEntry.bandMode, m_stripPolylineEntry.bandSpiky);
|
||||
return &m_stripPolylineEntry;
|
||||
}
|
||||
|
||||
// 键 = 构建折线时实际读取的全部输入:
|
||||
// 信号(引用 + 文件内容 + 采样点数)、时间轴显示范围、绘图区横向几何、网格纵向几何。
|
||||
// 这些值全部相同时,BuildPolyline 的输出必然完全一致,可直接复用。
|
||||
@@ -5370,6 +5421,12 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚠️ **横向补条裁剪**(只对本函数后半段的"逐点描边"路径生效,见 `budget` 之后):
|
||||
// 图层横向补条只覆盖位图里的一条窄竖带(`m_area` 的 x 区间),其余像素由已有图层像素提供,
|
||||
// 因此折线只需构建并绘制落在条带 x 范围内的那一段顶点。放在"包络/描边"分支判定**之后**
|
||||
// 是必须的 —— 若先裁剪,`visibleCount` 会变成条带内的采样数,可能改变"走包络还是走描边"
|
||||
// 的判定,使条带与整块渲染的像素不一致(出现接缝)。
|
||||
|
||||
const float limitTop = lane.rect.Y - lane.rect.Height * 4.0f;
|
||||
const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f;
|
||||
|
||||
@@ -5383,10 +5440,62 @@ namespace jdi
|
||||
budget = std::max(budget, kMinPolylinePoints);
|
||||
budget = std::min(budget, kMaxPolylinePoints);
|
||||
|
||||
// 采样点密度超过像素分辨率时改用“像素列上下包络”抽稀:
|
||||
// 每个像素列只保留最小/最大两个顶点(按出现先后排序),
|
||||
// 既不会漏掉尖峰(等间隔抽样会整段跳过),又把顶点数压到像素量级。
|
||||
//
|
||||
// ⚠️ **阈值必须是「每像素列 1 个采样」而不是 `budget`(每列 2 个)**:
|
||||
// 逐点描边(`DrawLines`)的代价与「竖向线段数 × 覆盖行数」成正比 ——
|
||||
// 方波/编码器这类信号在 1~2 采样/像素时会形成**满幅高的锯齿**,GDI+ 抗锯齿
|
||||
// 逐段扫描这些竖直段,实测 **4481 个顶点要 712ms**(关抗锯齿也无用,因为瓶颈是
|
||||
// 每段的扫描线而非边缘平滑),而同样画面的「像素列包络」只要 ~1.7ms。
|
||||
// 把阈值降到 1 采样/像素后,1~2 采样/像素区间走包络带(逐列 min/max 与折线在该密度下
|
||||
// 几乎等价,且**保留抗锯齿**),彻底消除这一区间的病态卡顿。
|
||||
const bool useEnvelopeBand = (visibleCount > static_cast<std::size_t>(m_plotRect.Width)) &&
|
||||
resolved.file->timeMonotonic && (m_plotRect.Width > 1.0f);
|
||||
|
||||
// 逐点描边路径若处于"横向补条"渲染:把采样区间裁剪到条带对应的 x 范围
|
||||
//(两侧各留少量像素余量以避免接缝与抗锯齿溢出)。逐点描边路径必然 `visibleCount <= budget`
|
||||
//(否则走包络),此时 `stride == 1`,裁剪后条带内的顶点序列与整块渲染完全一致 → 像素不变。
|
||||
if (!useEnvelopeBand && m_stripRenderActive && resolved.file->timeMonotonic &&
|
||||
m_stripX1 > m_stripX0)
|
||||
{
|
||||
const float marginPx =
|
||||
std::max(4.0f, std::max(1.0f, resolved.style->lineWidth) * m_scale + 3.0f);
|
||||
const double ta = XToTime(m_stripX0 - marginPx);
|
||||
const double tb = XToTime(m_stripX1 + marginPx);
|
||||
const double clipLo = std::min(ta, tb);
|
||||
const double clipHi = std::max(ta, tb);
|
||||
std::size_t sBegin = static_cast<std::size_t>(
|
||||
std::lower_bound(time.begin(), time.end(), clipLo) - time.begin());
|
||||
if (sBegin > 0)
|
||||
{
|
||||
--sBegin; // 多含一个采样点:让跨越条带左界的线段完整绘出。
|
||||
}
|
||||
std::size_t sEnd = static_cast<std::size_t>(
|
||||
std::lower_bound(time.begin(), time.end(), clipHi) - time.begin());
|
||||
if (sEnd < count)
|
||||
{
|
||||
++sEnd;
|
||||
}
|
||||
if (sBegin > begin)
|
||||
{
|
||||
begin = sBegin;
|
||||
}
|
||||
if (sEnd < end)
|
||||
{
|
||||
end = sEnd;
|
||||
}
|
||||
if (end <= begin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 采样点密度远超像素分辨率时改用“像素列上下包络”抽稀:
|
||||
// 每个像素列只保留最小/最大两个顶点(按出现先后排序),
|
||||
// 既不会漏掉尖峰(等间隔抽样会整段跳过),又把顶点数压到像素量级。
|
||||
if (visibleCount > budget && resolved.file->timeMonotonic && m_plotRect.Width > 1.0f)
|
||||
if (useEnvelopeBand)
|
||||
{
|
||||
const double viewSpan = viewMax - viewMin;
|
||||
const float plotLeft = m_plotRect.X;
|
||||
@@ -5415,10 +5524,10 @@ namespace jdi
|
||||
const float strokeHalf =
|
||||
std::max(1.0f, resolved.style->lineWidth) * m_scale * 0.5f;
|
||||
std::vector<BandColumn> segmentColumns;
|
||||
const auto flushBandSegment = [&band, &bandBreaks, &segmentColumns, &bandSpiky]()
|
||||
const auto flushBandSegment = [&band, &bandBreaks, &segmentColumns, &bandSpiky, &lane]()
|
||||
{
|
||||
bool spiky = false;
|
||||
EmitEnvelopeBand(segmentColumns, band, bandBreaks, spiky);
|
||||
EmitEnvelopeBand(segmentColumns, band, bandBreaks, spiky, lane.rect.Height);
|
||||
bandSpiky = bandSpiky || spiky;
|
||||
segmentColumns.clear();
|
||||
};
|
||||
@@ -5829,10 +5938,12 @@ namespace jdi
|
||||
// ⚠️ **必须在贴图之前**:否则本帧画好的网格要到下一帧才显示,
|
||||
// 拖拽时表现为"波形比鼠标慢半拍"、快速拖动时出现"断线"(图层覆盖不到视界)。
|
||||
//
|
||||
// ⚠️ **只在视界稳定后才花预算做精细重绘**:视界仍在变化(缩放 / 平移的每一帧)
|
||||
// 时,本帧只贴"旧跨度像素拉伸"的占位图(O(1)),把重建推迟到视界停下来之后
|
||||
// —— 这正是用户要求的"先画了再说,别卡住前台"。否则缩放序列的每一帧都会
|
||||
// 夹带一个网格的整块重绘(宽跨度下约 20ms)→ 缩放过程持续掉帧。
|
||||
// ⚠️ **只在视界稳定后才花预算做精细重绘**(视界仍在变化的每一帧只贴"旧跨度像素拉伸"
|
||||
// 的占位图,O(1),把重建推迟到视界停下来之后 —— 用户要求"先画了再说,别卡住前台"),
|
||||
// 且**仍按预算逐个补**(不用一次把全部过期网格塞进同一帧):实测同一台机器上 4 线程
|
||||
// 真并行(3.98x),但**图层光栅化(GDI+)本身会被串行化**,并行重建 5 个网格 ≈ 逐个重建
|
||||
//(1.03x)→ 一次性补齐只会把一帧从 ~10ms 抬到 ~31ms(更容易掉帧)。
|
||||
// 因此保持"视界停后**每帧补一个网格**"(`RenderPendingLaneRebuilds` 在 `budget>1` 时才走并行)。
|
||||
const bool morePending =
|
||||
viewStopped ? RenderPendingLaneRebuilds(1) : !m_layerRebuildPending.empty();
|
||||
// 绘图区底色(覆盖时间轴与侧边留白区域)。
|
||||
@@ -7323,15 +7434,30 @@ namespace jdi
|
||||
return !m_layerRebuildPending.empty();
|
||||
}
|
||||
const double defaultWideMin = LayerDefaultWideMin();
|
||||
int done = 0;
|
||||
while (!m_layerRebuildPending.empty() && done < budget)
|
||||
// 本批要重画的网格:`budget` 足够大时一次取完(视界已停时调用方会传很大的预算)。
|
||||
const int available = static_cast<int>(m_layerRebuildPending.size());
|
||||
const int take = std::min(budget, available);
|
||||
std::vector<int> batch;
|
||||
batch.reserve(static_cast<std::size_t>(take));
|
||||
for (int index = 0; index < take; ++index)
|
||||
{
|
||||
const int laneIndex = m_layerRebuildPending.front();
|
||||
m_layerRebuildPending.erase(m_layerRebuildPending.begin());
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
const int laneIndex = m_layerRebuildPending[static_cast<std::size_t>(index)];
|
||||
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
continue;
|
||||
batch.push_back(laneIndex);
|
||||
}
|
||||
}
|
||||
m_layerRebuildPending.erase(m_layerRebuildPending.begin(),
|
||||
m_layerRebuildPending.begin() + take);
|
||||
|
||||
if (batch.size() > 1)
|
||||
{
|
||||
// **多网格一次补齐**:并行渲染(各网格用隔离上下文),比"一帧只补 1 个"收敛快 N 倍。
|
||||
RebuildLaneLayersParallel(batch, defaultWideMin);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const int laneIndex : batch)
|
||||
{
|
||||
// 加锁顺序与前台一致:先 `m_renderMutex` 再 `m_layerMutex`。
|
||||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||||
@@ -7352,11 +7478,178 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
}
|
||||
++done;
|
||||
}
|
||||
return !m_layerRebuildPending.empty();
|
||||
}
|
||||
|
||||
void PlotView::EnsureLayerWorkers()
|
||||
{
|
||||
unsigned int hardware = std::thread::hardware_concurrency();
|
||||
const int want = std::max(1, std::min(static_cast<int>(hardware == 0 ? 2u : hardware), 4));
|
||||
if (m_layerWorkerCount == want && static_cast<int>(m_layerWorkers.size()) == want)
|
||||
{
|
||||
// 字体 / 主题 / 缩放 / 数据源可能已变化:刷新一次绑定(不重建线程上下文)。
|
||||
for (int index = 0; index < want; ++index)
|
||||
{
|
||||
m_layerWorkers[static_cast<std::size_t>(index)]->Setup(m_fonts, m_theme);
|
||||
m_layerWorkers[static_cast<std::size_t>(index)]->SetScale(m_scale);
|
||||
m_layerWorkers[static_cast<std::size_t>(index)]->m_dataSet = m_dataSet;
|
||||
m_layerWorkers[static_cast<std::size_t>(index)]->m_dataSetWritable = m_dataSetWritable;
|
||||
}
|
||||
return;
|
||||
}
|
||||
ReleaseLayerWorkers();
|
||||
m_layerWorkers.reserve(static_cast<std::size_t>(want));
|
||||
for (int index = 0; index < want; ++index)
|
||||
{
|
||||
auto worker = std::make_unique<PlotView>();
|
||||
worker->Setup(m_fonts, m_theme);
|
||||
worker->SetScale(m_scale);
|
||||
// ⚠️ `m_tab` 置空 → worker 的 `Tab()` 返回它**自带的** `m_localTab` 副本,
|
||||
// 于是它改写视界 / 网格矩形时不会碰到共享的标签页配置(安全并行的前提)。
|
||||
worker->m_dataSet = m_dataSet;
|
||||
worker->m_dataSetWritable = m_dataSetWritable;
|
||||
// ⚠️ `m_tab` **必须非空**(`BuildLaneVisuals()` 在空时会清空 `m_lanes`);
|
||||
// 指向 worker **自有**的 `m_workerTab`,这样它改写视界 / 网格矩形时不会碰到共享标签页。
|
||||
worker->m_tab = &worker->m_workerTab;
|
||||
worker->RefreshLanes();
|
||||
m_layerWorkers.push_back(std::move(worker));
|
||||
}
|
||||
m_layerWorkerCount = want;
|
||||
}
|
||||
|
||||
void PlotView::ReleaseLayerWorkers()
|
||||
{
|
||||
m_layerWorkers.clear();
|
||||
m_layerWorkerCount = 0;
|
||||
}
|
||||
|
||||
void PlotView::RebuildLaneLayersParallel(const std::vector<int>& laneIndices, double wideMin)
|
||||
{
|
||||
if (laneIndices.empty() || m_tab == nullptr || m_dataSet == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 单网格 / 单核:直接顺序走既有路径(避免线程开销)。
|
||||
unsigned int hardware = std::thread::hardware_concurrency();
|
||||
if (laneIndices.size() <= 1 || (hardware == 0 ? 2u : hardware) <= 1)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||||
if (m_layerFront.size() != m_lanes.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (const int laneIndex : laneIndices)
|
||||
{
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
RenderLaneLayerRegion(laneIndex, m_layerFront[static_cast<std::size_t>(laneIndex)], 0, 0, 0,
|
||||
0, wideMin, m_lanes[static_cast<std::size_t>(laneIndex)].yMax);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||||
if (m_layerFront.size() != m_lanes.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
EnsureLayerWorkers();
|
||||
if (m_layerWorkerCount <= 1)
|
||||
{
|
||||
return; // 保守回退:让调用方走顺序路径
|
||||
}
|
||||
|
||||
// 主线程**先快照**工作线程需要的一切(之后它们只读,不再触碰共享状态)。
|
||||
const TabConfig tabSnapshot = *m_tab;
|
||||
const gd::RectF plotRectSnapshot = m_plotRect;
|
||||
const gd::RectF areaSnapshot = m_area;
|
||||
const float scaleSnapshot = m_scale;
|
||||
const std::vector<LaneVisual> laneSnapshot = m_lanes;
|
||||
// 目标图层位图数组(已确认尺寸与网格数一致,线程内不会扩容)。
|
||||
std::vector<LaneLayer>* const front = &m_layerFront;
|
||||
|
||||
const int workerCount = m_layerWorkerCount;
|
||||
std::atomic<std::size_t> next{0};
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(static_cast<std::size_t>(workerCount));
|
||||
for (int workerIndex = 0; workerIndex < workerCount; ++workerIndex)
|
||||
{
|
||||
threads.emplace_back([&, workerIndex]()
|
||||
{
|
||||
PlotView* worker = m_layerWorkers[static_cast<std::size_t>(workerIndex)].get();
|
||||
for (;;)
|
||||
{
|
||||
const std::size_t slot = next.fetch_add(1, std::memory_order_relaxed);
|
||||
if (slot >= laneIndices.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
const int laneIndex = laneIndices[slot];
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(laneSnapshot.size()) ||
|
||||
laneIndex >= static_cast<int>(tabSnapshot.Lanes().size()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 隔离上下文:自带标签页副本 + 只含该网格 + 只读的数据源 / 字体 / 主题。
|
||||
worker->m_workerTab = tabSnapshot;
|
||||
worker->m_workerTab.Lanes() = {tabSnapshot.Lanes()[static_cast<std::size_t>(laneIndex)]};
|
||||
worker->m_tab = &worker->m_workerTab;
|
||||
worker->m_fonts = m_fonts;
|
||||
worker->m_theme = m_theme;
|
||||
worker->m_scale = scaleSnapshot;
|
||||
worker->m_dataSet = m_dataSet;
|
||||
worker->m_dataSetWritable = m_dataSetWritable;
|
||||
worker->m_area = areaSnapshot;
|
||||
worker->m_plotRect = plotRectSnapshot;
|
||||
worker->m_lanes = {laneSnapshot[static_cast<std::size_t>(laneIndex)]};
|
||||
// ⚠️ 工作线程用**自己的** `m_workerTab` / `m_area` / `m_plotRect` / `m_lanes` 渲染,
|
||||
// 因此不会与 UI 线程或其它工作线程互相覆盖渲染状态(这正是 2026-10-12 移除
|
||||
// "后台图层线程"的根因)。目标图层位图按 `laneIndex` 各自不同,可并发写入。
|
||||
worker->RenderLaneLayerRegion(0, (*front)[static_cast<std::size_t>(laneIndex)], 0, 0, 0, 0,
|
||||
wideMin,
|
||||
laneSnapshot[static_cast<std::size_t>(laneIndex)].yMax);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (std::thread& thread : threads)
|
||||
{
|
||||
if (thread.joinable())
|
||||
{
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
// 合并(主线程):工作线程内部以 `laneIndex = 0` 渲染,因此图层内容指纹要按**真实网格下标**重算。
|
||||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||||
if (m_layerFront.size() != m_lanes.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::uint64_t rendered = 0;
|
||||
for (const int laneIndex : laneIndices)
|
||||
{
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_layerFront.size()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
LaneLayer& target = m_layerFront[static_cast<std::size_t>(laneIndex)];
|
||||
if (!target.valid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
target.key = LaneLayerKey(laneIndex);
|
||||
++rendered;
|
||||
}
|
||||
m_layerRenderCount.fetch_add(rendered, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool PlotView::RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW,
|
||||
int stripY, int stripH, double wideMin, double anchorYMax)
|
||||
{
|
||||
@@ -7421,6 +7714,15 @@ namespace jdi
|
||||
// x 坐标仍用**整条宽窗口**的映射(`m_plotRect` 为完整宽图),因此网格线位置与整块图层一致。
|
||||
m_area = gd::RectF(static_cast<float>(stripX), static_cast<float>(stripY),
|
||||
static_cast<float>(stripW), static_cast<float>(stripH));
|
||||
// 横向补条(窄竖带):把折线的构建与描边裁剪到条带的 x 范围(见 `m_stripRenderActive`)。
|
||||
// 纵向补条(整宽 × 窄 y)不裁剪 x —— 那一方向本来就需要整条折线的顶点。
|
||||
const bool stripIsHorizontalBand = (static_cast<float>(stripW) < static_cast<float>(bitmapWidth) - 0.5f);
|
||||
const bool savedStripActive = m_stripRenderActive;
|
||||
const float savedStripX0 = m_stripX0;
|
||||
const float savedStripX1 = m_stripX1;
|
||||
m_stripRenderActive = stripIsHorizontalBand;
|
||||
m_stripX0 = static_cast<float>(stripX);
|
||||
m_stripX1 = static_cast<float>(stripX + stripW);
|
||||
Tab().ViewMin() = wideMin;
|
||||
Tab().ViewMax() = wideMin + static_cast<double>(bitmapWidth) / scale;
|
||||
|
||||
@@ -7482,6 +7784,9 @@ namespace jdi
|
||||
m_renderWide = savedRenderWide;
|
||||
m_wideTickSpan = savedWideTickSpan;
|
||||
m_wideTickHeight = savedWideTickHeight;
|
||||
m_stripRenderActive = savedStripActive;
|
||||
m_stripX0 = savedStripX0;
|
||||
m_stripX1 = savedStripX1;
|
||||
|
||||
// 恢复布局 / 视图 / 网格。
|
||||
// ⚠️ **必须调用 `BuildLaneVisuals()`**(而不是只把 `m_lanes` 拷回去):它会按正常布局
|
||||
@@ -7530,19 +7835,31 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||||
m_layerFront.resize(m_lanes.size());
|
||||
m_layerRebuildPending.clear();
|
||||
// 只在**扩容 + 清空待重画队列**这一段持有 `m_layerMutex`;
|
||||
// 随后释放,`RebuildLaneLayersParallel` 会各自重新获取(它内部还会取 `m_renderMutex`)。
|
||||
// ⚠️ 如果这里一直持有 `m_layerMutex`,而并行重建内部再取一次 → `std::mutex` 不可重入 → 死锁。
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||||
m_layerFront.resize(m_lanes.size());
|
||||
m_layerRebuildPending.clear();
|
||||
}
|
||||
|
||||
// 全部网格统一用"当前视界居中"的映射(与 `BlitWideBuffer` 的逐网格 `wideMin` 相容)。
|
||||
const double wideMin = LayerDefaultWideMin();
|
||||
std::vector<int> allLanes;
|
||||
allLanes.reserve(m_lanes.size());
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
allLanes.push_back(static_cast<int>(index));
|
||||
}
|
||||
// **并行**重建全部网格(各网格用隔离上下文,多核时约等于一块网格的耗时)。
|
||||
RebuildLaneLayersParallel(allLanes, wideMin);
|
||||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||||
bool ok = true;
|
||||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||||
{
|
||||
// 整块重画:每个图层都以"当前 `yMax`"为纵向锚点。
|
||||
if (!RenderLaneLayerRegion(static_cast<int>(index), m_layerFront[index], 0, 0, 0, 0, wideMin,
|
||||
m_lanes[index].yMax))
|
||||
if (!m_layerFront[index].valid)
|
||||
{
|
||||
ok = false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user