一、悬停高亮回归修复 - 根因:高亮画在预览曲线绘制路径里,而该路径在命中静态场景缓存时完全不会被调用 (悬停态又不参与 SceneContentHash),因此缓存命中帧永远看不到高亮。 - 修法:拆出 PaintDerivedCurvesFor(hoverOnly) 两态,新增 PaintDerivedHoverHighlight, 在 paintOverlays() 中紧跟 PaintHoverHighlight 以前景方式绘制(面板之下、光标之上)。 高亮仍按面板种类(均值滤波/中心轴/弱峰调整)分别判定。 二、预览曲线绘制重写(单条 15ms → 1.7ms) - 稠密区间改用与波形基底同一套「逐像素列 min/max 包络带」+ 一次 FillPath; 旧抽稀折线在稠密视界下每段都是竖直满幅,GDI+ 描边代价正比于覆盖像素数 (3200 顶点满幅锯齿抗锯齿 128ms / 无抗锯齿 31ms,包络带 1.9ms)。 - 列密度自适应:中等密度 2px/列,极密(>8 采样/像素)4px/列。 - 稀疏区间保持逐点折线;附带修复「等间隔抽样漏掉尖峰」。 三、预览曲线像素缓存层(Picture cache + pan roll) - 45 条曲线画进带 alpha 的 32bpp DIB,正常帧只做一次 AlphaBlend(1~2ms)。 - 纯平移时整行 memmove 一个 dx 并只重画新露出的竖带(与宽缓冲区滚动同构), 因此拖动视界的每帧代价与信号数无关。 - 内容指纹 = SceneContentHash(含样式/派生参数/网格几何) ⊕ 曲线发布序号;高亮不进指纹。 四、其他热点 - 光标取值胶囊不再逐帧重算并发布 45 条曲线(CurveSpanFor 新增 computeIfMissing), 只取已覆盖的缓存曲线,未命中退到最新曲线或本帧不标注。 - 样式补丁路径不再同步重建网格图层(宽基底 2 倍宽 × 2 倍高,实测单次 2.2 秒), 且开启派生功能时直接走整幅重绘(补丁会覆盖缓存中已烘入的曲线像素)。 实测(15 信号合并一条网格 + 全部实验性功能,1600x900 离屏探针): 拖动视界 264→5.9ms,双光标拖动 187→20.9ms,弱峰滑块 555→89ms, 改信号颜色 2661→200ms,悬停面板行 225→38ms,深放大拖动 207→23.8ms。 全屏 1920x1080:拖动视界 9.3ms。 验证:新增离屏探针 p27 8/8(悬停高亮在缓存命中帧可见、平移/缩放/改颜色后的叠加层 与「完全重画」参考帧逐像素一致 diff=0);p20 9/9、p21 9/9、p22 14/14、p23 8/8、p25 12/12; make release / make debug 零 warning;Release 814,080 B。规则已同步到 Documents/agent_mem.md。
2284 lines
86 KiB
C++
2284 lines
86 KiB
C++
// PlotView 实现分片 —— 光栅化像素缓存与每帧预算("怎么画才不卡")。
|
||
//
|
||
// 本分片只包含 `PlotView` 的**成员函数定义**(从 `PlotViewPaint.cpp` 机械搬移,行为与搬移前逐字节一致)。
|
||
// 职责:
|
||
// * 信号光晕的离屏位图层(整条曲线一次性渲染 + 每帧贴图);
|
||
// * 静态场景缓存(底色 + 网格 + 时间轴)的建立 / 抓取 / 贴回 / 作废;
|
||
// * 装饰层(带真 alpha 的独立图层,AlphaBlend 贴回);
|
||
// * 每网格"宽基底"图层:分配 / 内容指纹 / 就绪判定 / 并行重建 / 区域重画 / 滚动补条 / 贴图 / 预热预算。
|
||
//
|
||
// ⚠️ 与绘制(`PlotViewPaint.cpp`)、指纹(`PlotViewCacheKey.cpp`)的分工:
|
||
// * 本文件**只管像素与预算**,不做业务判据;
|
||
// * 内容指纹由 `PlotViewCacheKey.cpp` 提供(`LaneLayerKey/LaneLayerHash` 里用到的哈希函数在那里);
|
||
// * 共用的常量与工具函数在 `PlotViewInternal.hpp`(命名空间 `pvdetail`,全部 `inline`),因此跨分片安全。
|
||
|
||
#include "PlotViewInternal.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
void PlotView::PaintSignalHaloFor(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||
const PolylineCacheEntry& cached)
|
||
{
|
||
if (resolved.style == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
// ------------------------------------------------------------------
|
||
// 稠密信号(走"像素列包络带"):光晕 = **沿包络带向外扩一圈的填充**。
|
||
//
|
||
// ⚠️ 绝不能用「加粗折线描边」画稠密信号的光晕:包络带的顶点序列是
|
||
// 「每列上/下包络交替」的满幅竖锯齿,GDI+ 描边的代价 = 线段数 × 覆盖行数,
|
||
// 实测 2503 个顶点要 **210ms**。而 `FillPath` 同一条几何只要 ~14ms。
|
||
// ⚠️⚠️ **必须向外扩 `grow`(= 描边笔宽的一半)**:折线描边的笔宽是
|
||
// `lineWidth + 4`,即沿曲线向两侧各扩 2~3px —— 那正是用户看到的"外延高亮描边"。
|
||
// 曾经直接按包络带原始几何填充,结果光晕与曲线**逐像素同形**:当信号颜色恰好等于
|
||
// 主题强调色时(调色板第 1 色就是强调色),曲线上的那层半透明强调色完全看不出变化,
|
||
// 只剩下带宽内部的一点点底色变化 ⇒ 用户报告"高亮几乎看不到、只有曲线换了颜色"
|
||
//(2026-10-18 第三轮)。扩一圈后与旧版描边的观感一致。
|
||
// ⚠️ 光晕**必须覆盖整条曲线**:只画鼠标附近一段时,在满幅方波上就是一个长方形色块。
|
||
// 代价问题由 `PaintSignalHaloCached`(渲染一次 + 每帧贴图)解决。
|
||
// ------------------------------------------------------------------
|
||
if (resolved.style->mode != DrawMode::SCATTER && cached.bandMode && !cached.bandBreaks.empty())
|
||
{
|
||
// 与旧版 `PaintSignalHalo` 的笔宽同源:`(lineWidth + 4) * scale`,向外取一半。
|
||
const float grow = (std::max(1.0f, resolved.style->lineWidth) + 4.0f) * m_scale * 0.5f;
|
||
const int savedSmoothing = g.GetSmoothingMode();
|
||
g.SetSmoothingMode(gd::SmoothingModeNone); // 半透明填充:抗锯齿没有观感收益
|
||
gd::GraphicsPath bandPath;
|
||
bandPath.SetFillMode(gd::FillModeWinding);
|
||
gd::PointF quad[4];
|
||
for (std::size_t k = 0; k < cached.bandBreaks.size(); ++k)
|
||
{
|
||
const std::size_t start = cached.bandBreaks[k];
|
||
const std::size_t stop =
|
||
(k + 1 < cached.bandBreaks.size()) ? cached.bandBreaks[k + 1] : cached.band.size();
|
||
const std::size_t total = stop - start;
|
||
if (total < 4 || (total % 2) != 0)
|
||
{
|
||
continue;
|
||
}
|
||
// 段内布局 = 「上包络正向(`half` 个)+ 下包络反向(`half` 个)」,因此
|
||
// 列 `i` 的上/下包络分别是 `start+i` 与 `start+half+(half-1-i)`。
|
||
//
|
||
// ⚠️⚠️ **逐列成对填四边形,而不是把整条包络拼成一个多边形**:满幅方波的
|
||
// 包络相邻列区间**互相交叉**,拼成单个多边形后缠绕填充规则会在交叠处
|
||
// 互相抵消 ⇒ 填充结果变成一条条竖缝(实测光晕只覆盖约一半带宽,用户看到
|
||
// "只有曲线换了个颜色")。逐列四边形永远是简单多边形,不会被挖空。
|
||
// ⚠️ 横向/纵向都外扩 `grow`:竖向跃变处旧版 5px 笔会涂出一条整高色带,
|
||
// 只填 1px 宽的列区间根本看不出来。相邻四边形同向、且在同一次 `FillPath`
|
||
// 内,重叠区域只混合一次(不会叠出更深的竖条)。
|
||
const std::size_t half = total / 2;
|
||
for (std::size_t column = 0; column + 1 < half; ++column)
|
||
{
|
||
const gd::PointF& topA = cached.band[start + column];
|
||
const gd::PointF& bottomA = cached.band[start + half + (half - 1 - column)];
|
||
const gd::PointF& topB = cached.band[start + column + 1];
|
||
const gd::PointF& bottomB =
|
||
cached.band[start + half + (half - 1 - (column + 1))];
|
||
const float highY = std::min(std::min(topA.Y, bottomA.Y), std::min(topB.Y, bottomB.Y));
|
||
const float lowY = std::max(std::max(topA.Y, bottomA.Y), std::max(topB.Y, bottomB.Y));
|
||
quad[0] = gd::PointF(topA.X - grow, highY - grow);
|
||
quad[1] = gd::PointF(topB.X + grow, highY - grow);
|
||
quad[2] = gd::PointF(topB.X + grow, lowY + grow);
|
||
quad[3] = gd::PointF(topA.X - grow, lowY + grow);
|
||
bandPath.AddPolygon(quad, 4);
|
||
}
|
||
}
|
||
if (bandPath.GetPointCount() > 0)
|
||
{
|
||
// alpha 与旧版描边笔一致(0x66),保证任何信号颜色下观感相同。
|
||
gd::SolidBrush brush(ToGdiColor(WithAlpha(m_theme->accent, 0x66)));
|
||
g.FillPath(&brush, &bandPath);
|
||
}
|
||
g.SetSmoothingMode(static_cast<gd::SmoothingMode>(savedSmoothing));
|
||
return;
|
||
}
|
||
// 其余情形一律用折线描边(与"之前"的画法一致)。
|
||
PaintSignalHalo(g, resolved, lane, cached.points, cached.breaks);
|
||
}
|
||
|
||
void PlotView::PaintSignalHaloCached(gd::Graphics& g, const ResolvedSignal& resolved,
|
||
const LaneVisual& lane, const PolylineCacheEntry& cached)
|
||
{
|
||
if (resolved.style == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 光晕只可能画在该网格内:位图 = 网格矩形 ∩ 绘图区(外加少量余量容纳加粗线宽)。
|
||
const float pad = (resolved.style->lineWidth + 8.0f) * m_scale;
|
||
const float left = std::max(lane.rect.X, m_plotRect.X) - pad;
|
||
const float top = std::max(lane.rect.Y, m_plotRect.Y) - pad;
|
||
const float right =
|
||
std::min(lane.rect.X + lane.rect.Width, m_plotRect.X + m_plotRect.Width) + pad;
|
||
const float bottom =
|
||
std::min(lane.rect.Y + lane.rect.Height, m_plotRect.Y + m_plotRect.Height) + pad;
|
||
if (!(right > left + 1.0f) || !(bottom > top + 1.0f))
|
||
{
|
||
return;
|
||
}
|
||
const int targetX = static_cast<int>(std::floor(left));
|
||
const int targetY = static_cast<int>(std::floor(top));
|
||
const int width = static_cast<int>(std::ceil(right)) - targetX;
|
||
const int height = static_cast<int>(std::ceil(bottom)) - targetY;
|
||
if (width <= 0 || height <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
HaloLayerCache& cache = m_haloLayer;
|
||
// 键 = 决定光晕几何的全部输入(与 `AcquirePolyline` 的键同源)。
|
||
// ⚠️ **不含网格的屏幕位置**:滚动网格时几何不变,只需按新位置贴同一张位图。
|
||
const bool sameSize = cache.bitmap && static_cast<int>(cache.bitmap->GetWidth()) == width &&
|
||
static_cast<int>(cache.bitmap->GetHeight()) == height;
|
||
const bool sameKey =
|
||
cache.valid && sameSize && cache.ref == cached.ref && cache.group == cached.group &&
|
||
cache.sampleCount == cached.sampleCount && cache.viewMin == cached.viewMin &&
|
||
cache.viewMax == cached.viewMax && cache.plotX == cached.plotX &&
|
||
cache.plotWidth == cached.plotWidth && cache.laneHeight == cached.laneHeight &&
|
||
cache.laneYMin == cached.laneYMin && cache.laneYMax == cached.laneYMax &&
|
||
cache.lineWidth == cached.lineWidth && cache.mode == resolved.style->mode &&
|
||
cache.gain == resolved.style->gain && cache.absolute == resolved.style->absolute &&
|
||
cache.mirrorVertical == resolved.style->mirrorVertical &&
|
||
cache.mirrorHorizontal == resolved.style->mirrorHorizontal &&
|
||
cache.offsetY == resolved.style->offsetY &&
|
||
cache.offsetTime == resolved.style->offsetTime;
|
||
|
||
if (!sameKey)
|
||
{
|
||
if (!sameSize)
|
||
{
|
||
cache.bitmap = std::make_unique<gd::Bitmap>(width, height, PixelFormat32bppARGB);
|
||
}
|
||
{
|
||
gd::Graphics layerGraphics(cache.bitmap.get());
|
||
layerGraphics.Clear(gd::Color(0, 0, 0, 0));
|
||
layerGraphics.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||
layerGraphics.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||
// 位图坐标 = 屏幕坐标 - 目标原点,光晕几何可直接沿用屏幕坐标。
|
||
layerGraphics.TranslateTransform(static_cast<gd::REAL>(-targetX),
|
||
static_cast<gd::REAL>(-targetY));
|
||
if (cached.points.size() > kHaloNoAaMinPoints)
|
||
{
|
||
// 顶点很多的区间(例如深放大后的方波:每段都跨满网格高):
|
||
// 抗锯齿描边的扫描线代价 ≈ 线段数 × 覆盖行数(实测可差 20 倍),
|
||
// 而这么密的半透明光晕上平滑作用肉眼不可辨。几何完全不变。
|
||
layerGraphics.SetSmoothingMode(gd::SmoothingModeNone);
|
||
}
|
||
PaintSignalHaloFor(layerGraphics, resolved, lane, cached);
|
||
}
|
||
cache.ref = cached.ref;
|
||
cache.group = cached.group;
|
||
cache.sampleCount = cached.sampleCount;
|
||
cache.viewMin = cached.viewMin;
|
||
cache.viewMax = cached.viewMax;
|
||
cache.plotX = cached.plotX;
|
||
cache.plotWidth = cached.plotWidth;
|
||
cache.laneHeight = cached.laneHeight;
|
||
cache.laneYMin = cached.laneYMin;
|
||
cache.laneYMax = cached.laneYMax;
|
||
cache.lineWidth = cached.lineWidth;
|
||
cache.mode = resolved.style->mode;
|
||
cache.gain = resolved.style->gain;
|
||
cache.absolute = resolved.style->absolute;
|
||
cache.mirrorVertical = resolved.style->mirrorVertical;
|
||
cache.mirrorHorizontal = resolved.style->mirrorHorizontal;
|
||
cache.offsetY = resolved.style->offsetY;
|
||
cache.offsetTime = resolved.style->offsetTime;
|
||
cache.originX = targetX;
|
||
cache.originY = targetY;
|
||
cache.valid = true;
|
||
if (!UploadHaloLayer(width, height))
|
||
{
|
||
cache.valid = false;
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 贴图:`AlphaBlend` + `AC_SRC_ALPHA`(源位图已预乘)。命中缓存时只做这一次位块搬运。
|
||
if (cache.dc == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
const HDC targetDc = g.GetHDC();
|
||
if (targetDc != nullptr)
|
||
{
|
||
BLENDFUNCTION blend = {};
|
||
blend.BlendOp = AC_SRC_OVER;
|
||
blend.SourceConstantAlpha = 0xFF;
|
||
blend.AlphaFormat = AC_SRC_ALPHA;
|
||
::AlphaBlend(targetDc, targetX, targetY, width, height, cache.dc, 0, 0, width, height,
|
||
blend);
|
||
g.ReleaseHDC(targetDc);
|
||
}
|
||
}
|
||
|
||
bool PlotView::UploadHaloLayer(int width, int height)
|
||
{
|
||
HaloLayerCache& cache = m_haloLayer;
|
||
if (!cache.bitmap)
|
||
{
|
||
return false;
|
||
}
|
||
if (cache.dib == nullptr || cache.dibWidth != width || cache.dibHeight != height)
|
||
{
|
||
ReleaseHaloLayer();
|
||
BITMAPINFO info = {};
|
||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||
info.bmiHeader.biWidth = width;
|
||
info.bmiHeader.biHeight = -height; // 自上而下(与 GDI+ 的扫描行顺序一致)
|
||
info.bmiHeader.biPlanes = 1;
|
||
info.bmiHeader.biBitCount = 32;
|
||
info.bmiHeader.biCompression = BI_RGB;
|
||
HDC memoryDc = CreateCompatibleDC(nullptr);
|
||
if (memoryDc == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
void* bits = nullptr;
|
||
HBITMAP dib = CreateDIBSection(memoryDc, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||
if (dib == nullptr || bits == nullptr)
|
||
{
|
||
DeleteDC(memoryDc);
|
||
if (dib != nullptr)
|
||
{
|
||
DeleteObject(dib);
|
||
}
|
||
return false;
|
||
}
|
||
SelectObject(memoryDc, dib);
|
||
cache.dc = memoryDc;
|
||
cache.dib = dib;
|
||
cache.bits = bits;
|
||
cache.dibWidth = width;
|
||
cache.dibHeight = height;
|
||
}
|
||
|
||
// 预乘 alpha:`AlphaBlend` 的 `AC_SRC_ALPHA` 要求源 RGB 已乘过 alpha。
|
||
// ⚠️ 用 `(v * a + 128) * 257 >> 16` 代替 `(v * a + 127) / 255`(等价、无除法),
|
||
// **不要为此建 64KB 查表**:那张表会让 Release 产物凭空多出 64KB(见 §二进制体积),
|
||
// 而这里的乘法-移位在 1.6M 像素上也只要几毫秒。
|
||
gd::Rect rect(0, 0, width, height);
|
||
gd::BitmapData data = {};
|
||
if (cache.bitmap->LockBits(&rect, gd::ImageLockModeRead, PixelFormat32bppARGB, &data) !=
|
||
gd::Ok)
|
||
{
|
||
return false;
|
||
}
|
||
unsigned char* destination = static_cast<unsigned char*>(cache.bits);
|
||
for (int y = 0; y < height; ++y)
|
||
{
|
||
const unsigned char* source =
|
||
static_cast<const unsigned char*>(data.Scan0) + static_cast<std::size_t>(y) * data.Stride;
|
||
unsigned char* row = destination + static_cast<std::size_t>(y) * width * 4u;
|
||
for (int x = 0; x < width; ++x)
|
||
{
|
||
const unsigned alpha = source[x * 4 + 3];
|
||
if (alpha == 0)
|
||
{
|
||
row[x * 4 + 0] = 0;
|
||
row[x * 4 + 1] = 0;
|
||
row[x * 4 + 2] = 0;
|
||
row[x * 4 + 3] = 0;
|
||
continue;
|
||
}
|
||
if (alpha == 0xFF)
|
||
{
|
||
row[x * 4 + 0] = source[x * 4 + 0];
|
||
row[x * 4 + 1] = source[x * 4 + 1];
|
||
row[x * 4 + 2] = source[x * 4 + 2];
|
||
row[x * 4 + 3] = 0xFF;
|
||
continue;
|
||
}
|
||
row[x * 4 + 0] = static_cast<unsigned char>(
|
||
(static_cast<unsigned>(source[x * 4 + 0]) * alpha + 128u) * 257u >> 16);
|
||
row[x * 4 + 1] = static_cast<unsigned char>(
|
||
(static_cast<unsigned>(source[x * 4 + 1]) * alpha + 128u) * 257u >> 16);
|
||
row[x * 4 + 2] = static_cast<unsigned char>(
|
||
(static_cast<unsigned>(source[x * 4 + 2]) * alpha + 128u) * 257u >> 16);
|
||
row[x * 4 + 3] = static_cast<unsigned char>(alpha);
|
||
}
|
||
}
|
||
cache.bitmap->UnlockBits(&data);
|
||
return true;
|
||
}
|
||
|
||
void PlotView::ReleaseHaloLayer()
|
||
{
|
||
HaloLayerCache& cache = m_haloLayer;
|
||
if (cache.dc != nullptr)
|
||
{
|
||
DeleteDC(cache.dc);
|
||
cache.dc = nullptr;
|
||
}
|
||
if (cache.dib != nullptr)
|
||
{
|
||
DeleteObject(cache.dib);
|
||
cache.dib = nullptr;
|
||
}
|
||
cache.bits = nullptr;
|
||
cache.dibWidth = 0;
|
||
cache.dibHeight = 0;
|
||
cache.valid = false;
|
||
}
|
||
|
||
void PlotView::PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||
const std::vector<gd::PointF>& points,
|
||
const std::vector<std::size_t>& breaks)
|
||
{
|
||
// ⚠️ **只有单个数据点**的信号(默认样式是「点线」,那个点本身是一个粗圆点):
|
||
// 折线光晕无从下手(不足两个顶点),改为在数据点外包一圈强调色圆环。
|
||
// 否则悬停时完全没有“可拖拽”的视觉反馈(用户 2026-10-15 报告)。
|
||
if (resolved.data != nullptr && resolved.data->finiteCount == 1)
|
||
{
|
||
gd::PointF dot;
|
||
if (SinglePointScreenPos(resolved, lane, dot))
|
||
{
|
||
const float radius = SinglePointDotRadius(resolved) + 3.0f * m_scale;
|
||
gd::Pen ring(ToGdiColor(WithAlpha(m_theme->accent, 0x88)), 2.0f * m_scale);
|
||
g.DrawEllipse(&ring, dot.X - radius, dot.Y - radius, radius * 2.0f, radius * 2.0f);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const bool drawLine = (resolved.style->mode != DrawMode::SCATTER);
|
||
if (!drawLine)
|
||
{
|
||
// ⚠️ **散点模式的高亮 = 只高亮数据点本身(不画连线)**(用户 2026-10-17 要求):
|
||
// 旧实现直接 `return`,于是散点信号悬停时**只有拖拽提示、没有任何高亮**。
|
||
// 这里给每个参与绘制的散点套一圈强调色圆环 —— 与「线段/点线」的折线光晕相比,
|
||
// 正好是“去掉数据点之间连线的部分”(用户原话)。
|
||
// ⚠️ 步长必须与 `PaintLane` 的散点绘制**同源**(`kMaxScatterPoints`),
|
||
// 否则高亮圈会出现在没画出来的点上(或漏掉可见的点)。
|
||
if (points.empty() || resolved.style == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
const float radius = static_cast<float>(
|
||
ClampDouble(static_cast<double>(resolved.style->lineWidth) * kScatterRadiusScale,
|
||
static_cast<double>(kMinScatterRadius), static_cast<double>(kMaxScatterRadius)) *
|
||
m_scale);
|
||
const float ringRadius = radius + 2.5f * m_scale;
|
||
const std::size_t scatterStride =
|
||
std::max<std::size_t>(1, points.size() / kMaxScatterPoints);
|
||
// 散点数量本身有上限(`kMaxScatterPoints`),逐点画圈是 O(数千) 的小代价。
|
||
gd::Pen ring(ToGdiColor(WithAlpha(m_theme->accent, 0x88)), 1.6f * m_scale);
|
||
for (std::size_t index = 0; index < points.size(); index += scatterStride)
|
||
{
|
||
const gd::PointF& point = points[index];
|
||
g.DrawEllipse(&ring, point.X - ringRadius, point.Y - ringRadius, ringRadius * 2.0f,
|
||
ringRadius * 2.0f);
|
||
}
|
||
return;
|
||
}
|
||
if (breaks.empty())
|
||
{
|
||
return;
|
||
}
|
||
|
||
gd::Pen halo(ToGdiColor(WithAlpha(m_theme->accent, 0x66)),
|
||
(resolved.style->lineWidth + 4.0f) * m_scale);
|
||
halo.SetLineJoin(gd::LineJoinRound);
|
||
halo.SetStartCap(gd::LineCapRound);
|
||
halo.SetEndCap(gd::LineCapRound);
|
||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||
{
|
||
const std::size_t start = breaks[k];
|
||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||
if (stop <= start + 1)
|
||
{
|
||
continue;
|
||
}
|
||
g.DrawLines(&halo, points.data() + start, static_cast<INT>(stop - start));
|
||
}
|
||
}
|
||
|
||
bool PlotView::EnsureSceneCache(int width, int height)
|
||
{
|
||
if (width <= 0 || height <= 0)
|
||
{
|
||
return false;
|
||
}
|
||
// 位图尺寸必须与当前缓存区域**严格一致**:贴图时按位图尺寸整块拷贝,
|
||
// 若沿用更大的旧位图,缩小窗口后会把旧像素画到绘图区之外。
|
||
if (m_sceneDc != nullptr && m_sceneWidth == width && m_sceneHeight == height)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
ReleaseSceneCache();
|
||
// 场景位图重建 → 面板图层里缓存的"面板背后的场景"随之失效。
|
||
m_panelValid = false;
|
||
|
||
HDC screen = GetDC(nullptr);
|
||
if (screen == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
m_sceneDc = CreateCompatibleDC(screen);
|
||
m_sceneBitmap = CreateCompatibleBitmap(screen, width, height);
|
||
ReleaseDC(nullptr, screen);
|
||
|
||
if (m_sceneDc == nullptr || m_sceneBitmap == nullptr)
|
||
{
|
||
ReleaseSceneCache();
|
||
return false;
|
||
}
|
||
m_sceneOldBitmap = SelectObject(m_sceneDc, m_sceneBitmap);
|
||
m_sceneWidth = width;
|
||
m_sceneHeight = height;
|
||
return true;
|
||
}
|
||
|
||
bool PlotView::CaptureSceneCache(gd::Graphics& g)
|
||
{
|
||
// 缓存区域严格限定为绘图区本身(m_area):工具栏/标签栏/面板由宿主每帧绘制,
|
||
// 因为它们各自带有悬停态;绘图区的波形才是真正昂贵的部分(大文件约 80ms/帧)。
|
||
const int x = static_cast<int>(std::floor(m_area.X));
|
||
const int y = static_cast<int>(std::floor(m_area.Y));
|
||
const int right = static_cast<int>(std::ceil(m_area.X + m_area.Width));
|
||
const int bottom = static_cast<int>(std::ceil(m_area.Y + m_area.Height));
|
||
const int width = right - x;
|
||
const int height = bottom - y;
|
||
if (width <= 0 || height <= 0)
|
||
{
|
||
m_sceneValid = false;
|
||
return false;
|
||
}
|
||
|
||
m_sceneX = x;
|
||
m_sceneY = y;
|
||
|
||
if (!EnsureSceneCache(width, height))
|
||
{
|
||
m_sceneValid = false;
|
||
return false;
|
||
}
|
||
|
||
HDC source = g.GetHDC();
|
||
if (source == nullptr)
|
||
{
|
||
m_sceneValid = false;
|
||
return false;
|
||
}
|
||
const BOOL ok = BitBlt(m_sceneDc, 0, 0, width, height, source, m_sceneX, m_sceneY, SRCCOPY);
|
||
g.ReleaseHDC(source);
|
||
|
||
if (ok == FALSE)
|
||
{
|
||
m_sceneValid = false;
|
||
return false;
|
||
}
|
||
|
||
m_sceneKey = MakeSceneCacheKey();
|
||
m_sceneGeometryHash = SceneGeometryHash();
|
||
CollectSceneStyles(m_sceneStyles);
|
||
CollectSceneDerived(m_sceneDerived);
|
||
m_sceneDegraded = false;
|
||
m_sceneValid = true;
|
||
return true;
|
||
}
|
||
|
||
bool PlotView::BlitSceneCache(gd::Graphics& g)
|
||
{
|
||
if (!m_sceneValid || m_sceneDc == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
HDC target = g.GetHDC();
|
||
if (target == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
const BOOL ok = BitBlt(target, m_sceneX, m_sceneY, m_sceneWidth, m_sceneHeight, m_sceneDc, 0, 0,
|
||
SRCCOPY);
|
||
g.ReleaseHDC(target);
|
||
return ok != FALSE;
|
||
}
|
||
|
||
void PlotView::InvalidateSceneCache()
|
||
{
|
||
m_sceneValid = false;
|
||
m_sceneStyles.clear();
|
||
m_sceneDerived.clear();
|
||
m_sceneDegraded = false;
|
||
m_sceneGeometryHash = 0;
|
||
// 面板像素图层与场景缓存同源(面板像素里含"面板背后的场景"):
|
||
// 信号增删、重命名、参数变化等一律连带作废,避免面板里残留旧波形。
|
||
m_panelValid = false;
|
||
}
|
||
|
||
void PlotView::ReleaseSceneCache()
|
||
{
|
||
m_sceneValid = false;
|
||
if (m_sceneDc != nullptr)
|
||
{
|
||
if (m_sceneOldBitmap != nullptr)
|
||
{
|
||
SelectObject(m_sceneDc, m_sceneOldBitmap);
|
||
m_sceneOldBitmap = nullptr;
|
||
}
|
||
DeleteDC(m_sceneDc);
|
||
m_sceneDc = nullptr;
|
||
}
|
||
if (m_sceneBitmap != nullptr)
|
||
{
|
||
DeleteObject(m_sceneBitmap);
|
||
m_sceneBitmap = nullptr;
|
||
}
|
||
m_sceneWidth = 0;
|
||
m_sceneHeight = 0;
|
||
}
|
||
|
||
bool PlotView::EnsureDecorCache(int width, int height)
|
||
{
|
||
if (width <= 0 || height <= 0)
|
||
{
|
||
return false;
|
||
}
|
||
if (m_decorDc != nullptr && m_decorBits != nullptr && m_decorW == width && m_decorH == height)
|
||
{
|
||
return true;
|
||
}
|
||
ReleaseDecorCache();
|
||
HDC screen = GetDC(nullptr);
|
||
if (screen == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
// ⚠️ **必须用 32bpp DIB 段**(而不是 `CreateCompatibleBitmap`):
|
||
// 装饰要带真 alpha(圆角之外全透明),才能用 `AlphaBlend` 合成到波形之上;
|
||
// 同时 DIB 段的像素可直接写(用于 alpha 修补与清空)。
|
||
BITMAPINFO info = {};
|
||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||
info.bmiHeader.biWidth = width;
|
||
info.bmiHeader.biHeight = -height; // 自上而下
|
||
info.bmiHeader.biPlanes = 1;
|
||
info.bmiHeader.biBitCount = 32;
|
||
info.bmiHeader.biCompression = BI_RGB;
|
||
void* bits = nullptr;
|
||
HBITMAP dib = CreateDIBSection(screen, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||
ReleaseDC(nullptr, screen);
|
||
if (dib == nullptr || bits == nullptr)
|
||
{
|
||
if (dib != nullptr)
|
||
{
|
||
DeleteObject(dib);
|
||
}
|
||
return false;
|
||
}
|
||
HDC dc = CreateCompatibleDC(nullptr);
|
||
if (dc == nullptr)
|
||
{
|
||
DeleteObject(dib);
|
||
return false;
|
||
}
|
||
m_decorOldBitmap = SelectObject(dc, dib);
|
||
m_decorDc = dc;
|
||
m_decorBitmap = dib;
|
||
m_decorBits = bits;
|
||
m_decorW = width;
|
||
m_decorH = height;
|
||
return true;
|
||
}
|
||
|
||
void PlotView::ReleaseDecorCache()
|
||
{
|
||
m_decorValid = false;
|
||
m_decorCapturedRects.clear();
|
||
if (m_decorDc != nullptr)
|
||
{
|
||
if (m_decorOldBitmap != nullptr)
|
||
{
|
||
SelectObject(m_decorDc, m_decorOldBitmap);
|
||
m_decorOldBitmap = nullptr;
|
||
}
|
||
DeleteDC(m_decorDc);
|
||
m_decorDc = nullptr;
|
||
}
|
||
if (m_decorBitmap != nullptr)
|
||
{
|
||
DeleteObject(m_decorBitmap);
|
||
m_decorBitmap = nullptr;
|
||
}
|
||
m_decorBits = nullptr;
|
||
m_decorW = 0;
|
||
m_decorH = 0;
|
||
}
|
||
|
||
void* PlotView::ResolveAlphaBlend()
|
||
{
|
||
// `AlphaBlend` 由 msimg32 导出、并由 gdi32 的 `GdiAlphaBlend` 实现;
|
||
// 两个来源都试一遍,取到即缓存(静态局部变量,仅解析一次)。
|
||
static void* cached = nullptr;
|
||
static bool resolved = false;
|
||
if (resolved)
|
||
{
|
||
return cached;
|
||
}
|
||
resolved = true;
|
||
HMODULE gdi = GetModuleHandleW(L"gdi32.dll");
|
||
if (gdi != nullptr)
|
||
{
|
||
cached = reinterpret_cast<void*>(GetProcAddress(gdi, "GdiAlphaBlend"));
|
||
}
|
||
if (cached == nullptr)
|
||
{
|
||
HMODULE msimg = LoadLibraryW(L"msimg32.dll");
|
||
if (msimg != nullptr)
|
||
{
|
||
cached = reinterpret_cast<void*>(GetProcAddress(msimg, "AlphaBlend"));
|
||
}
|
||
}
|
||
return cached;
|
||
}
|
||
|
||
bool PlotView::RenderDecorCache()
|
||
{
|
||
m_decorValid = false;
|
||
if (m_theme == nullptr || m_fonts == nullptr || m_lanes.empty())
|
||
{
|
||
return false;
|
||
}
|
||
// 位图覆盖「客户区原点 → 绘图区右下角」:绘制坐标与正常绘制完全一致,
|
||
// 不需要任何平移(GDI 文本不认 GDI+ 变换,平移会让文字错位)。
|
||
const int width = static_cast<int>(std::ceil(m_area.X + m_area.Width));
|
||
const int height = static_cast<int>(std::ceil(m_area.Y + m_area.Height));
|
||
if (!EnsureDecorCache(width, height))
|
||
{
|
||
return false;
|
||
}
|
||
// 清空为全透明(圆角之外必须保持透明,贴回时才不会盖住波形)。
|
||
std::memset(m_decorBits, 0, static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4u);
|
||
|
||
m_decorRects.clear();
|
||
{
|
||
gd::Graphics g(m_decorDc);
|
||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||
|
||
const bool savedUse = m_useDecorCache;
|
||
m_useDecorCache = false; // 本次是"真正绘制":不允许走"跳过绘制"的分支
|
||
m_renderDecorOnly = true; // 只画装饰(跳过网格线与波形)
|
||
const std::vector<double> noTicks;
|
||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||
{
|
||
PaintLane(g, static_cast<int>(index), m_lanes[index], noTicks);
|
||
}
|
||
m_renderDecorOnly = false;
|
||
m_useDecorCache = savedUse;
|
||
}
|
||
|
||
// ⚠️ **alpha 修补**:GDI 的文本绘制(`DrawTextW`)会把它写入的像素 alpha 清成 0
|
||
// (GDI 不维护 alpha),若原样保留,贴上后文字位置会变成"透明洞"(透出波形)。
|
||
// 在这里把装饰矩形内"有颜色但 alpha 为 0"的像素还原为不透明即可 —— 装饰本身
|
||
// 是画在不透明底板上的,这些像素本来就该不透明。
|
||
for (const gd::RectF& rect : m_decorRects)
|
||
{
|
||
const int x0 = std::max(0, static_cast<int>(std::floor(rect.X)));
|
||
const int y0 = std::max(0, static_cast<int>(std::floor(rect.Y)));
|
||
const int x1 = std::min(width, static_cast<int>(std::ceil(rect.X + rect.Width)));
|
||
const int y1 = std::min(height, static_cast<int>(std::ceil(rect.Y + rect.Height)));
|
||
for (int y = y0; y < y1; ++y)
|
||
{
|
||
unsigned char* row = static_cast<unsigned char*>(m_decorBits) +
|
||
(static_cast<std::size_t>(y) * static_cast<std::size_t>(width)) * 4u;
|
||
for (int x = x0; x < x1; ++x)
|
||
{
|
||
unsigned char* p = row + static_cast<std::size_t>(x) * 4u;
|
||
if (p[3] == 0 && (p[0] | p[1] | p[2]) != 0)
|
||
{
|
||
p[3] = 255;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
m_decorCapturedRects = m_decorRects;
|
||
m_decorValid = true;
|
||
return true;
|
||
}
|
||
|
||
bool PlotView::BlitDecorCache(gd::Graphics& g)
|
||
{
|
||
if (!m_decorValid || m_decorDc == nullptr || m_decorCapturedRects.empty())
|
||
{
|
||
return false;
|
||
}
|
||
void* proc = ResolveAlphaBlend();
|
||
if (proc == nullptr)
|
||
{
|
||
return false; // 极小概率:系统不提供 AlphaBlend(贴回会带透明洞,故直接放弃)
|
||
}
|
||
using AlphaBlendFn = BOOL(WINAPI*)(HDC, int, int, int, int, HDC, int, int, int, int, BLENDFUNCTION);
|
||
AlphaBlendFn blend = reinterpret_cast<AlphaBlendFn>(proc);
|
||
HDC target = g.GetHDC();
|
||
if (target == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
// 装饰位图原点 = 客户区 (0,0):源坐标与目标坐标**相同**。
|
||
BLENDFUNCTION fn = {};
|
||
fn.BlendOp = AC_SRC_OVER;
|
||
fn.SourceConstantAlpha = 255;
|
||
fn.AlphaFormat = AC_SRC_ALPHA; // 源为**预乘** alpha(GDI+ 写入 DIB 段即预乘)
|
||
bool any = false;
|
||
for (const gd::RectF& rect : m_decorCapturedRects)
|
||
{
|
||
const int x = static_cast<int>(std::floor(rect.X));
|
||
const int y = static_cast<int>(std::floor(rect.Y));
|
||
const int w = static_cast<int>(std::ceil(rect.X + rect.Width)) - x;
|
||
const int h = static_cast<int>(std::ceil(rect.Y + rect.Height)) - y;
|
||
if (w <= 0 || h <= 0)
|
||
{
|
||
continue;
|
||
}
|
||
if (blend(target, x, y, w, h, m_decorDc, x, y, w, h, fn) != FALSE)
|
||
{
|
||
any = true;
|
||
}
|
||
}
|
||
g.ReleaseHDC(target);
|
||
return any;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 「派生预览曲线」像素缓存
|
||
// ---------------------------------------------------------------------------
|
||
|
||
std::uint64_t PlotView::DerivedOverlayKey() const
|
||
{
|
||
// 内容指纹 = 场景内容(含外观样式 + 派生参数 + 网格幅值范围 + 网格滚动量 + 绘图区几何)。
|
||
// ⚠️ 视界**不在这里**:平移时要靠它判断“能否只滚动像素”,所以视界单独比对。
|
||
std::uint64_t hash = SceneContentHash(true, true, true, true);
|
||
// 后台算好的派生曲线到达时,曲线像素必须更新(`m_curveSerial` 随每次发布自增)。
|
||
{
|
||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||
hash ^= m_curveSerial * 1099511628211ull;
|
||
}
|
||
// 高亮状态**不参与**:高亮由前景单独绘制(见 `PaintDerivedHoverHighlight`)。
|
||
return hash;
|
||
}
|
||
|
||
bool PlotView::EnsureDerivedCache(int width, int height)
|
||
{
|
||
if (width <= 0 || height <= 0)
|
||
{
|
||
return false;
|
||
}
|
||
if (m_derivedDc != nullptr && m_derivedBits != nullptr && m_derivedW == width &&
|
||
m_derivedH == height)
|
||
{
|
||
return true;
|
||
}
|
||
ReleaseDerivedCache();
|
||
HDC screen = GetDC(nullptr);
|
||
if (screen == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
// 与装饰层同理:必须是 32bpp DIB 段(带真 alpha + 像素可直接 memmove 滚动)。
|
||
BITMAPINFO info = {};
|
||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||
info.bmiHeader.biWidth = width;
|
||
info.bmiHeader.biHeight = -height; // 自上而下
|
||
info.bmiHeader.biPlanes = 1;
|
||
info.bmiHeader.biBitCount = 32;
|
||
info.bmiHeader.biCompression = BI_RGB;
|
||
void* bits = nullptr;
|
||
HBITMAP dib = CreateDIBSection(screen, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||
ReleaseDC(nullptr, screen);
|
||
if (dib == nullptr || bits == nullptr)
|
||
{
|
||
if (dib != nullptr)
|
||
{
|
||
DeleteObject(dib);
|
||
}
|
||
return false;
|
||
}
|
||
HDC dc = CreateCompatibleDC(nullptr);
|
||
if (dc == nullptr)
|
||
{
|
||
DeleteObject(dib);
|
||
return false;
|
||
}
|
||
m_derivedOldBitmap = SelectObject(dc, dib);
|
||
m_derivedDc = dc;
|
||
m_derivedBitmap = dib;
|
||
m_derivedBits = bits;
|
||
m_derivedW = width;
|
||
m_derivedH = height;
|
||
m_derivedValid = false;
|
||
return true;
|
||
}
|
||
|
||
void PlotView::ReleaseDerivedCache()
|
||
{
|
||
m_derivedValid = false;
|
||
if (m_derivedDc != nullptr)
|
||
{
|
||
if (m_derivedOldBitmap != nullptr)
|
||
{
|
||
SelectObject(m_derivedDc, m_derivedOldBitmap);
|
||
m_derivedOldBitmap = nullptr;
|
||
}
|
||
DeleteDC(m_derivedDc);
|
||
m_derivedDc = nullptr;
|
||
}
|
||
if (m_derivedBitmap != nullptr)
|
||
{
|
||
DeleteObject(m_derivedBitmap);
|
||
m_derivedBitmap = nullptr;
|
||
}
|
||
m_derivedBits = nullptr;
|
||
m_derivedW = 0;
|
||
m_derivedH = 0;
|
||
}
|
||
|
||
bool PlotView::RenderDerivedCache()
|
||
{
|
||
m_derivedValid = false;
|
||
if (m_theme == nullptr || m_fonts == nullptr || m_lanes.empty())
|
||
{
|
||
return false;
|
||
}
|
||
const int width = static_cast<int>(std::ceil(m_area.X + m_area.Width));
|
||
const int height = static_cast<int>(std::ceil(m_area.Y + m_area.Height));
|
||
if (!EnsureDerivedCache(width, height))
|
||
{
|
||
return false;
|
||
}
|
||
const std::size_t bytes = static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4u;
|
||
if (!m_derivedStripActive)
|
||
{
|
||
// 整块重画:先把全部像素清成透明。
|
||
std::memset(m_derivedBits, 0, bytes);
|
||
}
|
||
else
|
||
{
|
||
// 补条带:只把这条竖带清成透明(其余像素是滚动过来的有效内容)。
|
||
const int x0 = std::max(0, static_cast<int>(std::floor(m_derivedStripX0)));
|
||
const int x1 = std::min(width, static_cast<int>(std::ceil(m_derivedStripX1)));
|
||
for (int y = 0; y < height; ++y)
|
||
{
|
||
unsigned char* row = static_cast<unsigned char*>(m_derivedBits) +
|
||
(static_cast<std::size_t>(y) * static_cast<std::size_t>(width)) * 4u;
|
||
if (x1 > x0)
|
||
{
|
||
std::memset(row + static_cast<std::size_t>(x0) * 4u, 0,
|
||
static_cast<std::size_t>(x1 - x0) * 4u);
|
||
}
|
||
}
|
||
}
|
||
{
|
||
gd::Graphics g(m_derivedDc);
|
||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||
if (m_derivedStripActive)
|
||
{
|
||
// ⚠️ 绘制期的横向裁剪(`PaintDerivedCurvesFor` 只做 `intersect`,不会覆盖这一层)。
|
||
g.SetClip(gd::RectF(m_derivedStripX0, 0.0f, std::max(0.0f, m_derivedStripX1 - m_derivedStripX0),
|
||
static_cast<float>(height)),
|
||
gd::CombineModeIntersect);
|
||
}
|
||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||
{
|
||
PaintDerivedCurvesFor(g, static_cast<int>(index), m_lanes[index], false);
|
||
}
|
||
}
|
||
m_derivedContentKey = DerivedOverlayKey();
|
||
m_derivedViewMin = Tab().ViewMin();
|
||
m_derivedViewMax = Tab().ViewMax();
|
||
m_derivedValid = true;
|
||
return true;
|
||
}
|
||
|
||
bool PlotView::BlitDerivedCache(gd::Graphics& g)
|
||
{
|
||
if (!m_derivedValid || m_derivedDc == nullptr || m_plotRect.Width < 1.0f)
|
||
{
|
||
return false;
|
||
}
|
||
void* proc = ResolveAlphaBlend();
|
||
if (proc == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
using AlphaBlendFn = BOOL(WINAPI*)(HDC, int, int, int, int, HDC, int, int, int, int, BLENDFUNCTION);
|
||
AlphaBlendFn blend = reinterpret_cast<AlphaBlendFn>(proc);
|
||
HDC target = g.GetHDC();
|
||
if (target == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
// 缓存位图原点 = 客户区 (0,0):源与目标坐标相同。只贴绘图区(曲线被夹在网格内)。
|
||
const int x = std::max(0, static_cast<int>(std::floor(m_plotRect.X)));
|
||
const int y = std::max(0, static_cast<int>(std::floor(m_plotRect.Y)));
|
||
const int w = std::min(m_derivedW - x, static_cast<int>(std::ceil(m_plotRect.Width)));
|
||
const int h = std::min(m_derivedH - y, static_cast<int>(std::ceil(m_plotRect.Height)));
|
||
if (w <= 0 || h <= 0)
|
||
{
|
||
g.ReleaseHDC(target);
|
||
return false;
|
||
}
|
||
BLENDFUNCTION fn = {};
|
||
fn.BlendOp = AC_SRC_OVER;
|
||
fn.SourceConstantAlpha = 255;
|
||
fn.AlphaFormat = AC_SRC_ALPHA;
|
||
const BOOL done = blend(target, x, y, w, h, m_derivedDc, x, y, w, h, fn);
|
||
g.ReleaseHDC(target);
|
||
return done != FALSE;
|
||
}
|
||
|
||
void PlotView::PaintDerivedOverlayLayer(gd::Graphics& g)
|
||
{
|
||
if (m_loadingBusy || m_lanes.empty())
|
||
{
|
||
return; // 加载遮罩期间不花时间在派生曲线上(本来也看不见)
|
||
}
|
||
// 没有任何派生功能开启时直接跳过(并释放缓存,避免白占内存)。
|
||
bool anyFeature = false;
|
||
for (const Lane& lane : Tab().Lanes())
|
||
{
|
||
if (lane.edgeCenter || lane.meanFilter || lane.weakPeak)
|
||
{
|
||
anyFeature = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!anyFeature)
|
||
{
|
||
m_derivedValid = false;
|
||
return;
|
||
}
|
||
|
||
const std::uint64_t contentKey = DerivedOverlayKey();
|
||
const double viewMin = Tab().ViewMin();
|
||
const double viewMax = Tab().ViewMax();
|
||
const double span = viewMax - viewMin;
|
||
bool needRender = true;
|
||
bool rolled = false;
|
||
m_derivedStripActive = false;
|
||
if (m_derivedValid && m_derivedContentKey == contentKey && span > 0.0)
|
||
{
|
||
const double cachedSpan = m_derivedViewMax - m_derivedViewMin;
|
||
const double sameSpanTolerance = std::max(1.0e-12, std::fabs(span) * 1.0e-9);
|
||
if (std::fabs(cachedSpan - span) <= sameSpanTolerance &&
|
||
m_derivedViewMin == viewMin && m_derivedViewMax == viewMax)
|
||
{
|
||
needRender = false; // 完全同源:只贴图
|
||
}
|
||
else if (std::fabs(cachedSpan - span) <= sameSpanTolerance && m_plotRect.Width > 1.0f)
|
||
{
|
||
// 纯平移:像素整体横移,只重画新露出的竖带(与宽缓冲区的滚动同构)。
|
||
const double pxPerTime = static_cast<double>(m_plotRect.Width) / span;
|
||
const int shift = static_cast<int>(
|
||
std::lround((m_derivedViewMin - viewMin) * pxPerTime));
|
||
const int width = m_derivedW;
|
||
if (shift != 0 && std::abs(shift) < width)
|
||
{
|
||
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4u;
|
||
const std::size_t moveBytes =
|
||
rowBytes - static_cast<std::size_t>(std::abs(shift)) * 4u;
|
||
unsigned char* bits = static_cast<unsigned char*>(m_derivedBits);
|
||
if (bits != nullptr && moveBytes > 0)
|
||
{
|
||
for (int y = 0; y < m_derivedH; ++y)
|
||
{
|
||
unsigned char* row = bits + static_cast<std::size_t>(y) * rowBytes;
|
||
if (shift > 0)
|
||
{
|
||
// 内容右移:从本行右端往前搬(`memmove` 自带重叠安全)。
|
||
std::memmove(row + static_cast<std::size_t>(shift) * 4u, row, moveBytes);
|
||
}
|
||
else
|
||
{
|
||
// 内容左移:源在本行右侧 —— 目标**不能**越过行首(否则越界)。
|
||
std::memmove(row, row + static_cast<std::size_t>(-shift) * 4u, moveBytes);
|
||
}
|
||
}
|
||
}
|
||
const float plotLeft = m_plotRect.X;
|
||
const float plotRight = m_plotRect.X + m_plotRect.Width;
|
||
if (shift > 0)
|
||
{
|
||
m_derivedStripX0 = plotLeft;
|
||
m_derivedStripX1 = std::min(plotRight, plotLeft + static_cast<float>(shift) + 1.0f);
|
||
}
|
||
else
|
||
{
|
||
m_derivedStripX0 = std::max(plotLeft, plotRight + static_cast<float>(shift) - 1.0f);
|
||
m_derivedStripX1 = plotRight;
|
||
}
|
||
if (m_derivedStripX1 > m_derivedStripX0 + 0.5f)
|
||
{
|
||
m_derivedStripActive = true;
|
||
rolled = true;
|
||
}
|
||
}
|
||
needRender = true;
|
||
}
|
||
}
|
||
if (needRender)
|
||
{
|
||
if (!RenderDerivedCache())
|
||
{
|
||
m_derivedStripActive = false;
|
||
return;
|
||
}
|
||
}
|
||
m_derivedStripActive = false;
|
||
if (!BlitDerivedCache(g))
|
||
{
|
||
// 无法 AlphaBlend:退回逐条曲线直接绘制(正确性优先,代价高但极少发生)。
|
||
if (!rolled)
|
||
{
|
||
m_derivedValid = false;
|
||
}
|
||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||
{
|
||
PaintLaneDerivedOverlays(g, static_cast<int>(index), m_lanes[index]);
|
||
}
|
||
}
|
||
}
|
||
|
||
bool PlotView::EnsureLaneLayer(LaneLayer& layer, int width, int height)
|
||
{
|
||
if (width <= 0 || height <= 0)
|
||
{
|
||
return false;
|
||
}
|
||
if (layer.dc != nullptr && layer.width == width && layer.height == height)
|
||
{
|
||
return true;
|
||
}
|
||
ReleaseLaneLayer(layer);
|
||
HDC screen = GetDC(nullptr);
|
||
if (screen == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
layer.dc = CreateCompatibleDC(screen);
|
||
layer.bitmap = CreateCompatibleBitmap(screen, width, height);
|
||
ReleaseDC(nullptr, screen);
|
||
if (layer.dc == nullptr || layer.bitmap == nullptr)
|
||
{
|
||
ReleaseLaneLayer(layer);
|
||
return false;
|
||
}
|
||
layer.oldBitmap = SelectObject(layer.dc, layer.bitmap);
|
||
layer.width = width;
|
||
layer.height = height;
|
||
layer.valid = false;
|
||
return true;
|
||
}
|
||
|
||
void PlotView::ReleaseLaneLayer(LaneLayer& layer)
|
||
{
|
||
layer.valid = false;
|
||
if (layer.dc != nullptr)
|
||
{
|
||
if (layer.oldBitmap != nullptr)
|
||
{
|
||
SelectObject(layer.dc, layer.oldBitmap);
|
||
layer.oldBitmap = nullptr;
|
||
}
|
||
DeleteDC(layer.dc);
|
||
layer.dc = nullptr;
|
||
}
|
||
if (layer.bitmap != nullptr)
|
||
{
|
||
DeleteObject(layer.bitmap);
|
||
layer.bitmap = nullptr;
|
||
}
|
||
layer.width = 0;
|
||
layer.height = 0;
|
||
}
|
||
|
||
void PlotView::ReleaseWideBuffer()
|
||
{
|
||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||
for (LaneLayer& layer : m_layerFront)
|
||
{
|
||
ReleaseLaneLayer(layer);
|
||
}
|
||
m_layerFront.clear();
|
||
m_layerRebuildPending.clear();
|
||
}
|
||
|
||
void PlotView::InvalidateWideBuffer()
|
||
{
|
||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||
for (LaneLayer& layer : m_layerFront)
|
||
{
|
||
layer.valid = false;
|
||
}
|
||
m_layerRebuildPending.clear();
|
||
}
|
||
|
||
std::uint64_t PlotView::LaneLayerKey(int laneIndex) const
|
||
{
|
||
// 图层只含基底(不含派生曲线与装饰),因此指纹 = **该网格自己的内容**
|
||
// ⊕ 网格下标 ⊕ 视界跨度 ⊕ 绘图区宽度(决定像素/时间与位图尺寸)。
|
||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||
std::uint64_t key = LaneLayerHash(laneIndex);
|
||
key ^= static_cast<std::uint64_t>(laneIndex + 1) * 1315423911ull;
|
||
key *= 1099511628211ull;
|
||
key ^= static_cast<std::uint64_t>(std::llround(span * 1.0e6));
|
||
key *= 1099511628211ull;
|
||
key ^= static_cast<std::uint64_t>(std::llround(m_plotRect.Width));
|
||
return key;
|
||
}
|
||
|
||
std::uint64_t PlotView::LaneLayerHash(int laneIndex) const
|
||
{
|
||
// 单个网格图层的内容 = 位图高度(= 网格高度)+ 幅值**跨度** + 本网格各信号的
|
||
// 可见性与外观。
|
||
// ⚠️ 图例与 GX 徽标**不在图层里**(它们是装饰层,每帧单独绘制),因此也**不在**
|
||
// 本指纹里;改信号名不需要重建图层。
|
||
//
|
||
// ⚠️ **只计跨度、不计幅值的绝对范围**:图层的纵向锚点(`anchorYMax`)与 `wideMin`
|
||
// 一样是"贴图偏移"的来源,幅值整体平移不改变图层内容(只是贴图时纵向偏移若干像素),
|
||
// 因此纵向拖动不会让图层失效。真正改变内容的是**跨度**(决定像素/数值与刻度步长)。
|
||
std::uint64_t hash = 1469598103934665603ull;
|
||
const auto mix = [&hash](std::uint64_t value)
|
||
{
|
||
hash ^= value;
|
||
hash *= 1099511628211ull;
|
||
};
|
||
const auto mixFloat = [&mix](float value)
|
||
{
|
||
std::uint32_t bits = 0;
|
||
std::memcpy(&bits, &value, sizeof(bits));
|
||
mix(bits);
|
||
};
|
||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||
{
|
||
return hash;
|
||
}
|
||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||
mixFloat(visual.rect.Height);
|
||
mixFloat(static_cast<float>(visual.yMax - visual.yMin));
|
||
const std::vector<SignalRef>& signals = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].signals;
|
||
mix(signals.size());
|
||
for (const SignalRef& ref : signals)
|
||
{
|
||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||
mix(static_cast<std::uint64_t>(ref.groupIndex));
|
||
mix(static_cast<std::uint64_t>(ref.signalIndex));
|
||
mix(resolved.visible ? 1ull : 0ull);
|
||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||
{
|
||
continue;
|
||
}
|
||
mix(static_cast<std::uint64_t>(resolved.style->color));
|
||
mixFloat(resolved.style->opacity);
|
||
mixFloat(resolved.style->lineWidth);
|
||
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
||
// 「调整」变换会改变图层里的波形,也必须让图层失效
|
||
//(否则贴回的旧像素会与当前样式不一致)。
|
||
mixFloat(resolved.style->gain);
|
||
mix(resolved.style->absolute ? 1ull : 0ull);
|
||
mix(resolved.style->mirrorVertical ? 1ull : 0ull);
|
||
mix(resolved.style->mirrorHorizontal ? 1ull : 0ull);
|
||
mixFloat(resolved.style->offsetY);
|
||
mixFloat(static_cast<float>(resolved.style->offsetTime));
|
||
}
|
||
return hash;
|
||
}
|
||
|
||
int PlotView::LayerMarginX() const
|
||
{
|
||
const int width = static_cast<int>(std::lround(m_plotRect.Width));
|
||
return std::max(1, width / 2); // 左右各半屏
|
||
}
|
||
|
||
int PlotView::LayerMarginY(float laneHeight) const
|
||
{
|
||
// 上下各半个网格高:一整屏的额外内容已足够覆盖实际拖拽范围,
|
||
// 又不至于让每个横向补条 / 整块重画按三倍高度放大(横向平移因此不受影响)。
|
||
const int half = static_cast<int>(std::lround(static_cast<double>(laneHeight) * 0.5));
|
||
return std::max(1, half);
|
||
}
|
||
|
||
double PlotView::LayerDefaultWideMin() const
|
||
{
|
||
// 以当前视界为中心:位图左右各留半个视界宽,因此可向两侧平移 ±半屏。
|
||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||
const int width = static_cast<int>(std::lround(m_plotRect.Width));
|
||
if (!(span > 0.0) || width <= 0)
|
||
{
|
||
return Tab().ViewMin();
|
||
}
|
||
const double scale = static_cast<double>(width) / span;
|
||
return Tab().ViewMin() - static_cast<double>(LayerMarginX()) / scale;
|
||
}
|
||
|
||
bool PlotView::LayersStructurallyReady() const
|
||
{
|
||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||
if (m_layerFront.size() != m_lanes.size() || m_layerFront.empty())
|
||
{
|
||
return false;
|
||
}
|
||
const float plotWidth = m_plotRect.Width;
|
||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||
if (!(plotWidth > 8.0f) || !(span > 0.0))
|
||
{
|
||
return false;
|
||
}
|
||
const int width = static_cast<int>(std::lround(plotWidth));
|
||
const int bitmapWidth = width + 2 * LayerMarginX();
|
||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||
{
|
||
const LaneLayer& layer = m_layerFront[index];
|
||
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth)
|
||
{
|
||
return false;
|
||
}
|
||
// 跨度必须一致:跨度变了像素/时间的映射就不同,旧像素不能再当“坐标偏移”用。
|
||
if (std::fabs(span - layer.span) > 1.0e-12)
|
||
{
|
||
return false;
|
||
}
|
||
// 高度也要一致:网格高度变化会让贴图纵向错位(含上下预渲染余量)。
|
||
const float laneHeight = m_lanes[index].rect.Height;
|
||
const int bitmapHeight =
|
||
static_cast<int>(std::ceil(laneHeight)) + 2 * LayerMarginY(laneHeight);
|
||
if (layer.height != bitmapHeight)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool PlotView::LayersAllocated() const
|
||
{
|
||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||
return !m_layerFront.empty() && m_layerFront.size() == m_lanes.size();
|
||
}
|
||
|
||
bool PlotView::LaneLayerReady(int laneIndex) const
|
||
{
|
||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||
{
|
||
return false;
|
||
}
|
||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||
if (laneIndex >= static_cast<int>(m_layerFront.size()))
|
||
{
|
||
return false;
|
||
}
|
||
const LaneLayer& layer = m_layerFront[static_cast<std::size_t>(laneIndex)];
|
||
const float plotWidth = m_plotRect.Width;
|
||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||
if (!(plotWidth > 8.0f) || !(span > 0.0))
|
||
{
|
||
return false;
|
||
}
|
||
const int width = static_cast<int>(std::lround(plotWidth));
|
||
const int bitmapWidth = width + 2 * LayerMarginX();
|
||
const float laneHeight = m_lanes[static_cast<std::size_t>(laneIndex)].rect.Height;
|
||
const int bitmapHeight = static_cast<int>(std::ceil(laneHeight)) + 2 * LayerMarginY(laneHeight);
|
||
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
|
||
layer.height != bitmapHeight || std::fabs(layer.span - span) > 1.0e-12)
|
||
{
|
||
return false;
|
||
}
|
||
return layer.key == LaneLayerKey(laneIndex);
|
||
}
|
||
|
||
bool PlotView::LanesAllReadyForView() const
|
||
{
|
||
if (m_lanes.empty())
|
||
{
|
||
return false;
|
||
}
|
||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||
{
|
||
if (!LaneLayerReady(static_cast<int>(index)))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool PlotView::LaneVisuallyRelevant(int laneIndex) const
|
||
{
|
||
return LaneVisuallyRelevant(laneIndex, 0);
|
||
}
|
||
|
||
bool PlotView::LaneVisuallyRelevant(int laneIndex, int prefetchScreens) const
|
||
{
|
||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||
{
|
||
return false;
|
||
}
|
||
if (!(m_plotRect.Height > 0.0f))
|
||
{
|
||
return false;
|
||
}
|
||
const gd::RectF& rect = m_lanes[static_cast<std::size_t>(laneIndex)].rect;
|
||
// 可视判定本身留一个网格高的余量(即将滚入视区的网格也算相关);
|
||
// 预取带在其上再按屏高放大(见头文件说明)。
|
||
const float margin = std::max(rect.Height, 0.0f) +
|
||
static_cast<float>(std::max(0, prefetchScreens)) * m_plotRect.Height;
|
||
return (rect.Y + rect.Height + margin > m_plotRect.Y) &&
|
||
(rect.Y - margin < m_plotRect.Y + m_plotRect.Height);
|
||
}
|
||
|
||
bool PlotView::LanesVisiblyReadyForView() const
|
||
{
|
||
if (m_lanes.empty())
|
||
{
|
||
return false;
|
||
}
|
||
bool anyRelevant = false;
|
||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||
{
|
||
if (!LaneVisuallyRelevant(static_cast<int>(index)))
|
||
{
|
||
continue;
|
||
}
|
||
anyRelevant = true;
|
||
if (!LaneLayerReady(static_cast<int>(index)))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return anyRelevant;
|
||
}
|
||
|
||
bool PlotView::AnyVisibleLanePending() const
|
||
{
|
||
// ⚠️ 用 **±1 屏预取带**(而不是“严格可见”):网格滚动动画会在几百毫秒内扫过一整个屏高,
|
||
// 只预建严格可见的网格时,动画扫进去的每一帧都要靠底衬兜底重画十几份网格
|
||
// (实测 ~8ms/帧 ⇒ 动画明显卡顿)。预取带让静置期间就把附近网格的图层建好。
|
||
for (const int laneIndex : m_layerRebuildPending)
|
||
{
|
||
if (LaneVisuallyRelevant(laneIndex, 1))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void PlotView::QueueLaneRebuild(int laneIndex)
|
||
{
|
||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||
{
|
||
return;
|
||
}
|
||
for (const int existing : m_layerRebuildPending)
|
||
{
|
||
if (existing == laneIndex)
|
||
{
|
||
return; // 已经在队列里
|
||
}
|
||
}
|
||
m_layerRebuildPending.push_back(laneIndex);
|
||
}
|
||
|
||
bool PlotView::RenderPendingLaneRebuilds(int budget)
|
||
{
|
||
if (m_layerRebuildPending.empty() || budget <= 0)
|
||
{
|
||
return !m_layerRebuildPending.empty();
|
||
}
|
||
// ⚠️ **优先补画可视网格**:队列是按下标顺序登记的,若严格 FIFO,用户正在看的那几个
|
||
// 网格(往往位于列表下半部)可能要等上百帧才轮到,期间画面一直停留在兜底状态
|
||
//(用户报告:缩放 / 折起后“需要等一会才正确加载”)。
|
||
// 做一次**稳定划分**:可视网格排到队首,其余保持原有相对顺序。
|
||
std::stable_partition(m_layerRebuildPending.begin(), m_layerRebuildPending.end(),
|
||
[this](int laneIndex) { return LaneVisuallyRelevant(laneIndex); });
|
||
const double defaultWideMin = LayerDefaultWideMin();
|
||
// 本批要重画的网格:`budget` 足够大时一次取完(视界已停时调用方会传很大的预算)。
|
||
// ⚠️ **离屏网格一律丢弃**(不重画、也不留在队列里):它们的像素当前不可见,
|
||
// 而单个巨型信号的图层重建要 10~30ms。真正进入可视/预取带时 `RollWideBuffer`
|
||
// 会按新视界重新登记它们(见那里的说明)。历史实现会排空整个队列 ⇒ 勾选一个分组后
|
||
// 连续数秒每帧 10~30ms(用户 2026-10-18 报告的列表动画卡顿)。
|
||
m_layerRebuildPending.erase(
|
||
std::remove_if(m_layerRebuildPending.begin(), m_layerRebuildPending.end(),
|
||
[this](int laneIndex) { return !LaneVisuallyRelevant(laneIndex, 1); }),
|
||
m_layerRebuildPending.end());
|
||
if (m_layerRebuildPending.empty())
|
||
{
|
||
return false;
|
||
}
|
||
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[static_cast<std::size_t>(index)];
|
||
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
|
||
{
|
||
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);
|
||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||
if (m_layerFront.size() != m_lanes.size())
|
||
{
|
||
break;
|
||
}
|
||
LaneLayer& layer = m_layerFront[static_cast<std::size_t>(laneIndex)];
|
||
// ⚠️ **跨度过期(缩放瞬间)也在这里就地整块重画**(按**新跨度**)。
|
||
// 旧实现遇到跨度不符就 `continue`(把该网格推给"下一次整批重建"),
|
||
// 而"整批重建"正是那个 70~80ms 的同步卡顿源;现在改为一帧补一个网格,
|
||
// 其余网格由 `BlitWideBuffer` 的占位拉伸顶着 → 前台永不因图层未就绪而滞后。
|
||
// 整块重画:整个图层都对齐到"当前 `yMax`"(锚点 = 当前 yMax)。
|
||
if (!RenderLaneLayerRegion(laneIndex, layer, 0, 0, 0, 0, defaultWideMin,
|
||
m_lanes[static_cast<std::size_t>(laneIndex)].yMax))
|
||
{
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
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)
|
||
{
|
||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||
{
|
||
return false;
|
||
}
|
||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||
|
||
const float plotWidth = m_plotRect.Width;
|
||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||
if (!(plotWidth > 8.0f) || !(span > 0.0))
|
||
{
|
||
layer.valid = false;
|
||
return false;
|
||
}
|
||
|
||
const int width = static_cast<int>(std::lround(plotWidth));
|
||
const int margin = LayerMarginX(); // 左右各半屏 → 可平移 ±半屏
|
||
const int bitmapWidth = width + 2 * margin;
|
||
const float laneHeight = m_lanes[static_cast<std::size_t>(laneIndex)].rect.Height;
|
||
const int lanePixelHeight = static_cast<int>(std::ceil(laneHeight));
|
||
const int marginY = LayerMarginY(laneHeight); // 上下各半个网格高 → 幅值可平移 ±半个网格高
|
||
const int bitmapHeight = lanePixelHeight + 2 * marginY;
|
||
if (bitmapHeight <= 0 || !EnsureLaneLayer(layer, bitmapWidth, bitmapHeight))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const double scale = static_cast<double>(width) / span; // 像素 / 时间
|
||
const double laneSpan = m_lanes[static_cast<std::size_t>(laneIndex)].yMax -
|
||
m_lanes[static_cast<std::size_t>(laneIndex)].yMin;
|
||
const double pxPerValue = (laneSpan > 0.0) ? static_cast<double>(laneHeight) / laneSpan : 0.0;
|
||
|
||
if (stripW <= 0 || stripX < 0 || stripX + stripW > bitmapWidth)
|
||
{
|
||
stripX = 0;
|
||
stripW = bitmapWidth;
|
||
}
|
||
// 纵向同理:`stripH <= 0` 表示纵向整块(横向滚动的细条就是这种情形)。
|
||
if (stripH <= 0 || stripY < 0 || stripY + stripH > bitmapHeight)
|
||
{
|
||
stripY = 0;
|
||
stripH = bitmapHeight;
|
||
}
|
||
|
||
// 保存当前布局 / 视图 / 网格,临时切换到"宽窗口"映射后离屏渲染,再恢复。
|
||
const gd::RectF savedArea = m_area;
|
||
const gd::RectF savedPlot = m_plotRect;
|
||
const double savedViewMin = Tab().ViewMin();
|
||
const double savedViewMax = Tab().ViewMax();
|
||
const std::vector<LaneVisual> savedLanes = m_lanes;
|
||
const float savedLaneContentHeight = m_laneContentHeight;
|
||
const bool savedFastFrame = m_fastFrame;
|
||
const bool savedRenderWide = m_renderWide;
|
||
const float savedScroll = Tab().LaneScroll();
|
||
const double savedWideTickSpan = m_wideTickSpan;
|
||
const float savedWideTickHeight = m_wideTickHeight;
|
||
const double savedRenderTimeStep = m_renderTimeStep;
|
||
|
||
m_plotRect = gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth),
|
||
static_cast<float>(bitmapHeight));
|
||
// 只把 `m_area` 限为待渲染的那一段 x 区间,使 GDI/GDI+ 的裁剪只落在它上面;
|
||
// 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;
|
||
|
||
m_renderWide = true; // 图层只含基底:跳过派生曲线与装饰
|
||
// 图层必须与"同一视界的整幅渲染"**逐像素同源**:强制全质量抗锯齿(不受拖拽快速帧影响)。
|
||
m_fastFrame = false;
|
||
// 幅值刻度的参考跨度 / 参考高度取**前台可见网格**的值(而不是含上下余量的画布值):
|
||
// 这样图层里的横向网格线步长与前台一致,纵向平移才是纯粹的像素平移。
|
||
m_wideTickSpan = laneSpan;
|
||
m_wideTickHeight = laneHeight;
|
||
// ⚠️⚠️ **不要在这里再调 `BuildLaneVisuals()`**:它会对**全部网格**重算
|
||
// `ComputeLaneRange`(O(网格数)),而本函数只需要**被渲染的那一个网格**。
|
||
// 253 个网格、30 个可见网格时,每帧 30 次补条 × 2 次 BuildLaneVisuals
|
||
// = 单帧 **26.8ms**(用户 2026-10-17 报告:三明治加载后拖拽视界卡顿)。
|
||
// 该网格的矩形/幅值范围已在下方显式设置(`m_lanes` 其余成员保持原值不变)。
|
||
// ⚠️ **该网格的幅值映射必须以本次渲染的纵向锚点为准**(而不是"当前 `yMax`"):
|
||
// - 整块重画 / 纵向补条:锚点就是当前 `yMax`(纵向补条是滚动后的值),二者一致;
|
||
// - **横向补细条**:只重画一条竖带,其余像素仍在**原锚点**上;若这里用当前 `yMax`
|
||
// 渲染,竖带与其余像素的纵向映射就不同源,贴图后出现撕裂/错位。
|
||
// 详见 `RenderLaneLayerRegion` 的 `anchorYMax` 形参说明。
|
||
m_lanes[static_cast<std::size_t>(laneIndex)].yMax = anchorYMax;
|
||
m_lanes[static_cast<std::size_t>(laneIndex)].yMin = anchorYMax - laneSpan;
|
||
// 该网格在本图层中的位置:横向铺满整条宽图,纵向铺满整张画布。
|
||
// 画布 = 「可见网格 + 上下各 `marginY` 行余量」。**上下扩展量按"像素行"精确换算**
|
||
//(而不是上下各扩展固定的数值量):位图高度是整数(`ceil(laneHeight) + 2*marginY`),
|
||
// 只有当「扩展后总跨度 / 画布行数」严格等于前台的 px/数值 时,可见网格在画布里的映射
|
||
// 才与前台整幅渲染**逐像素同源**(否则会差一个 <1px 的纵向缩放误差,
|
||
// 让拖拽期间的像素残差明显变大)。
|
||
if (pxPerValue > 0.0)
|
||
{
|
||
const double topExtend = static_cast<double>(marginY) / pxPerValue;
|
||
const double bottomRows =
|
||
static_cast<double>(bitmapHeight - marginY) - static_cast<double>(laneHeight);
|
||
const double bottomExtend = std::max(0.0, bottomRows) / pxPerValue;
|
||
m_lanes[static_cast<std::size_t>(laneIndex)].yMin -= bottomExtend;
|
||
m_lanes[static_cast<std::size_t>(laneIndex)].yMax += topExtend;
|
||
}
|
||
m_lanes[static_cast<std::size_t>(laneIndex)].rect =
|
||
gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), static_cast<float>(bitmapHeight));
|
||
{
|
||
gd::Graphics g(layer.dc);
|
||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||
|
||
UiFillRect(g, m_area, m_theme->plotBg);
|
||
UiFillRect(g, m_area, ThemeLaneBackground(*m_theme));
|
||
|
||
// 竖向网格刻度沿用**前台视界跨度**(而非宽窗口的跨度),否则竖网格线会只有一半。
|
||
// ⚠️ 同时把它作为竖向网格的**主刻步长**传给 `PaintLane`(见 `m_renderTimeStep`):
|
||
// 不传就会让图层用宽窗口跨度重算步长 → 竖向网格线与前台错位、部分整条丢失。
|
||
const double fgSpan = savedViewMax - savedViewMin;
|
||
const double timeStep = NiceStep((fgSpan > 0.0) ? fgSpan : 1.0, kTimeTickTarget);
|
||
m_renderTimeStep = timeStep;
|
||
std::vector<double> timeTicks;
|
||
BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeStep, timeTicks);
|
||
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
|
||
m_renderTimeStep = 0.0;
|
||
}
|
||
m_fastFrame = savedFastFrame;
|
||
m_renderWide = savedRenderWide;
|
||
m_wideTickSpan = savedWideTickSpan;
|
||
m_wideTickHeight = savedWideTickHeight;
|
||
m_renderTimeStep = savedRenderTimeStep;
|
||
m_stripRenderActive = savedStripActive;
|
||
m_stripX0 = savedStripX0;
|
||
m_stripX1 = savedStripX1;
|
||
|
||
// 恢复布局 / 视图 / 网格。
|
||
// ⚠️ 这里**不调 `BuildLaneVisuals()`**(同上面的原因:O(网格数) 的 `ComputeLaneRange`
|
||
// 在每次补条时都跑一遍是拖拽卡顿的主因)。`m_lanes` 在进入本函数时已经被完整拷贝,
|
||
// 直接拷回即可 —— 它本来就是正常布局下的正确结果(矩形/幅值/内容高度都在里面)。
|
||
m_area = savedArea;
|
||
m_plotRect = savedPlot;
|
||
Tab().ViewMin() = savedViewMin;
|
||
Tab().ViewMax() = savedViewMax;
|
||
m_lanes = savedLanes;
|
||
m_laneContentHeight = savedLaneContentHeight;
|
||
Tab().LaneScroll() = savedScroll;
|
||
|
||
layer.wideMin = wideMin;
|
||
layer.span = span;
|
||
layer.marginY = marginY;
|
||
// 纵向锚点 = 本次渲染的像素所对应的 `yMax`(由调用方按"改动了哪部分像素"决定;
|
||
// 横向补细条必须沿用图层原锚点,否则残余的纵向偏移会被静默清零 —— 见函数说明)。
|
||
layer.anchorYMax = anchorYMax;
|
||
layer.pxPerValue = pxPerValue;
|
||
layer.key = LaneLayerKey(laneIndex);
|
||
layer.valid = true;
|
||
m_layerRenderCount.fetch_add(1, std::memory_order_relaxed);
|
||
return true;
|
||
}
|
||
|
||
bool PlotView::RebuildWideBuffer()
|
||
{
|
||
const float plotWidth = m_plotRect.Width;
|
||
const float areaHeight = m_area.Height;
|
||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||
if (!(plotWidth > 8.0f) || !(areaHeight > 8.0f) || !(span > 0.0) || m_lanes.empty())
|
||
{
|
||
InvalidateWideBuffer();
|
||
return false;
|
||
}
|
||
|
||
// 只在**扩容 + 清空待重画队列**这一段持有 `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)
|
||
{
|
||
if (!m_layerFront[index].valid)
|
||
{
|
||
ok = false;
|
||
}
|
||
}
|
||
return ok;
|
||
}
|
||
|
||
bool PlotView::PrefetchStepAllowed()
|
||
{
|
||
const std::uint64_t now = GetTickCount64();
|
||
if (now >= m_lastPrefetchRequestMs &&
|
||
(now - m_lastPrefetchRequestMs) < kPrefetchMinIntervalMs)
|
||
{
|
||
return false;
|
||
}
|
||
m_lastPrefetchRequestMs = now;
|
||
return true;
|
||
}
|
||
|
||
void PlotView::EnsureLaneLayersPrefetched()
|
||
{
|
||
if (m_lanes.empty() || !(m_plotRect.Width > 8.0f) || !(Tab().ViewMax() - Tab().ViewMin() > 0.0))
|
||
{
|
||
return;
|
||
}
|
||
if (!LayersAllocated())
|
||
{
|
||
// 图层数组与网格数量不符(首次绘制 / 增删网格):没有可复用的像素,只能整批建立。
|
||
RebuildWideBuffer();
|
||
return;
|
||
}
|
||
// ⚠️ 只登记「缺失 / 内容过期」的网格(`RollWideBuffer` 不做整块重画),
|
||
// 由 `RenderPendingLaneRebuilds` 的既有预算逐帧补齐。
|
||
RollWideBuffer();
|
||
}
|
||
|
||
void PlotView::WarmLaneLayersStep()
|
||
{
|
||
if (m_dragMode != PlotDragMode::NONE || m_lanes.empty() || m_fonts == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
// ⚠️ **UI 动画期间绝不预热**:单个巨型信号的图层重建要 10~30ms,塞进动画帧就会
|
||
// 让"只有文字的信号列表"看起来卡顿(用户 2026-10-18 报告)。队列保留,动画结束后继续。
|
||
if (m_animationBusy)
|
||
{
|
||
return;
|
||
}
|
||
if (!(m_plotRect.Width > 8.0f) || !(Tab().ViewMax() - Tab().ViewMin() > 0.0))
|
||
{
|
||
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())
|
||
{
|
||
// 网格变少:先把多出来的图层位图释放掉(`resize` 不会调用 `ReleaseLaneLayer`)。
|
||
for (std::size_t index = m_lanes.size(); index < m_layerFront.size(); ++index)
|
||
{
|
||
ReleaseLaneLayer(m_layerFront[index]);
|
||
}
|
||
}
|
||
if (m_layerFront.size() != m_lanes.size())
|
||
{
|
||
m_layerFront.resize(m_lanes.size());
|
||
}
|
||
}
|
||
// 登记"跨度 / 内容不一致"的网格。空闲时视界未变 ⇒ `RollWideBuffer` 只登记、不做滚动,
|
||
// 也不会走"位移超出画布 → 立刻整块重画"那条分支。
|
||
RollWideBuffer();
|
||
RenderPendingLaneRebuilds(1);
|
||
// ⚠️ 是否继续请求下一帧:
|
||
// * 还有**可视 / 预取带内**的网格待补 ⇒ 立刻继续(收敛快,见 `AnyVisibleLanePending`);
|
||
// * 只剩**离屏**网格 ⇒ 按 `kPrefetchMinIntervalMs` **限速**继续(慢速后台预热,
|
||
// 既不饿死动画定时器,又能把整批图层建好 —— 见 `PrefetchStepAllowed`)。
|
||
const bool morePending =
|
||
AnyVisibleLanePending() ||
|
||
(!m_layerRebuildPending.empty() && PrefetchStepAllowed());
|
||
if (morePending)
|
||
{
|
||
const HWND host = m_host;
|
||
if (host != nullptr)
|
||
{
|
||
PostMessageW(host, kMessagePlotDerivedReady, 0, 0);
|
||
}
|
||
}
|
||
}
|
||
|
||
void PlotView::RollWideBuffer()
|
||
{
|
||
const float plotWidth = m_plotRect.Width;
|
||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||
if (!(plotWidth > 8.0f) || !(span > 0.0))
|
||
{
|
||
return;
|
||
}
|
||
const int width = static_cast<int>(std::lround(plotWidth));
|
||
const int margin = LayerMarginX();
|
||
const int bitmapWidth = width + 2 * margin;
|
||
const double scale = static_cast<double>(width) / span;
|
||
|
||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||
if (m_layerFront.size() != m_lanes.size() || m_layerFront.empty())
|
||
{
|
||
return;
|
||
}
|
||
// 本帧还剩多少个「重新居中」额度(见 `kLayerRollsPerFrame`)。
|
||
int rollBudget = kLayerRollsPerFrame;
|
||
|
||
// ⚠️ **逐网格独立滚动**:每个图层有自己的时间映射(`layer.wideMin`),
|
||
// 因此位移必须按各自的锚点分别计算。曾用**一个全局锚点**给所有图层算同一个位移,
|
||
// 结果各网格被平移了不同的真实距离 → 网格之间时间错位、贴回的像素互相矛盾,
|
||
// 表现为严重的绘制错误与波形抖动。
|
||
//
|
||
// ⚠️ **本函数绝不重画整块图层**:内容过期 / 锚点太远的网格只登记到待重画队列
|
||
//(由 `RenderPendingLaneRebuilds` 按预算在后续帧补画),本帧仍尽量用现有像素跟随视界。
|
||
// 这样前台**永远不会因为"图层没准备好"而做整批重绘**(那正是"拖拽时前台卡顿"的根因)。
|
||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||
{
|
||
LaneLayer& layer = m_layerFront[index];
|
||
const int laneIndex = static_cast<int>(index);
|
||
|
||
// ⚠️⚠️ **不在可视区(含预取带)内的网格:整段跳过 —— 必须放在最前面**。
|
||
//
|
||
// 它们的像素当前根本不会被贴出来,滚了也看不见;而每次补条都要重跑
|
||
// `BuildLaneVisuals()`(O(网格数))—— 253 个网格时这是拖拽卡顿的主因。
|
||
//
|
||
// 而且离屏网格**既不登记待重画、也不算内容指纹**:
|
||
// * 登记待重画:一次"网格数变化"会把上百个离屏网格塞进队列,随后每帧补 1 个
|
||
// × 10~30ms ⇒ 数秒持续掉帧(用户 2026-10-18 报告);
|
||
// * 算 `LaneLayerKey()`:那是**逐网格内容哈希**,283 个网格全算一遍实测约
|
||
// 0.7ms/帧,而其中 270 个的结果根本用不到 —— 这就是"空闲帧比强制整帧还慢"
|
||
// 的根因(用户 2026-10-29 要求排查的既有性能问题)。实测把它提到前面之后
|
||
// 空闲帧从 2.4ms 降到约 1.2ms。
|
||
//
|
||
// 离屏网格真正进入可视/预取带时,下面的分支会按新视界登记并重建它。
|
||
if (!LaneVisuallyRelevant(laneIndex, 1))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 结构不可用(尺寸 / 跨度不符):像素的时间映射已经不对,既不能贴也不能滚。
|
||
// ⚠️ 只对**可视网格**登记待重画(离屏的由上面那段说明处理)。
|
||
const float laneHeight = m_lanes[index].rect.Height;
|
||
const int bitmapHeight =
|
||
static_cast<int>(std::ceil(laneHeight)) + 2 * LayerMarginY(laneHeight);
|
||
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
|
||
layer.height != bitmapHeight || std::fabs(layer.span - span) > 1.0e-12)
|
||
{
|
||
QueueLaneRebuild(laneIndex);
|
||
continue;
|
||
}
|
||
|
||
// 视界左端当前落在该图层的哪个 x 上;期望它等于 `margin`(左右余量对称)。
|
||
const double desired = (Tab().ViewMin() - layer.wideMin) * scale;
|
||
const int shift = static_cast<int>(std::llround(desired - static_cast<double>(margin)));
|
||
// 内容(外观 / 网格几何 / 幅值**跨度** / 可见性…)与当前状态不一致。
|
||
const bool contentStale = (layer.key != LaneLayerKey(laneIndex));
|
||
|
||
// 纵向:幅值**平移**不改内容,只改贴图偏移(与横向同构)。
|
||
int vOffset = 0;
|
||
if (layer.pxPerValue > 0.0)
|
||
{
|
||
vOffset = static_cast<int>(
|
||
std::llround((layer.anchorYMax - m_lanes[index].yMax) * layer.pxPerValue));
|
||
}
|
||
|
||
// ⚠️⚠️ **"位移太大"绝不允许【作废图层】**(用户 2026-10-18 报告:
|
||
// 快速拖动中键时**所有网格的信号一起消失**,因为一旦作废就要等拖拽结束才重建)。
|
||
// 关键认识:`BlitWideBuffer` 按 `(ViewMin - layer.wideMin) * scale` 算源偏移,
|
||
// 只要视界仍落在位图覆盖范围内(`|shift| <= margin`),**不滚动也画得完全正确**。
|
||
// 因此这里分三种情形:
|
||
// * `|shift| <= driftLimit`(位图仍完整覆盖视界)⇒ **什么都不做**(免费);
|
||
// * 超出 ⇒ 向中心滚,但**每帧最多滚 `kLayerRollMaxStepPixels`**(把补条代价摊开),
|
||
// 剩余位移继续由贴图偏移承担 ⇒ **永远不会空白**;
|
||
// * 真的跳到画布之外(`|shift| >= bitmapWidth`)⇒ 才作废 + 登记待重画
|
||
// (那是“点选信号跳转”这类整屏换地方的情形,本来就要重画)。
|
||
const int driftLimit = std::max(8, margin - 24);
|
||
const int vOffsetAbs = std::abs(vOffset);
|
||
if (vOffsetAbs >= layer.height)
|
||
{
|
||
// 纵向已超出画布(画布高约等于 2×网格高):像素映射不再可用。
|
||
layer.valid = false;
|
||
QueueLaneRebuild(laneIndex);
|
||
continue;
|
||
}
|
||
if (contentStale)
|
||
{
|
||
QueueLaneRebuild(laneIndex);
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// 纵向(幅值)滚动:与横向**完全同构**——内容整体上下移动若干行,
|
||
// 再只补画新暴露的那一条(整宽 × 行数)。
|
||
//
|
||
// ⚠️ **必须先做纵向再做横向**:横向补条由 `RenderLaneLayerRegion` 渲染,
|
||
// 而该函数按传入的**纵向锚点**渲染细条;若先补横向,那时锚点还没跟上,
|
||
// 补出来的细条会带着一个错的纵向偏移(贴图后表现为波形错位/撕裂)。
|
||
// ⚠️ 没有这一步时:幅值一变就超出余量 → 每帧都排队**整块重画**(画布比可见网格高
|
||
// 3 倍,代价很大)→ 拖动时"抽搐";而且在重画完成前,`BlitWideBuffer` 只能贴出部分
|
||
// 行(其余留空)→ 网格内出现**空白条带**(用户报告的"绘制异常")。
|
||
//
|
||
// ⚠️ **滚动量必须量化**(`kLayerRollQuantum`):不足一个量子的偏移**不滚**,
|
||
// 交给贴图时的纵向偏移(`srcY`)处理。这样图层的画布映射在拖动过程中**保持稳定**,
|
||
// 折线缓存(键含幅值映射)得以命中;否则每帧滚 1~30 行会让折线每帧重算
|
||
// (实测同样的拖动:量化前 3~4ms 的滚动 + 10~14ms 整帧,量化后回到 ~5ms 量级)。
|
||
// 余量足够大(上下各 `marginY`),因此"不滚只偏移"绝不会超出画布。
|
||
// 上面已保证 |vOffset| < 画布高,所以这里的 BitBlt 高度恒为正。
|
||
// ------------------------------------------------------------------
|
||
if (std::abs(vOffset) >= kLayerRollQuantum)
|
||
{
|
||
const int roll = vOffset; // 正 = 内容整体上移 roll 行(窗口向下移动)
|
||
int stripY = 0;
|
||
int stripH = 0;
|
||
if (roll > 0)
|
||
{
|
||
// 内容上移:new[y] = old[y + roll],底部暴露 roll 行。
|
||
BitBlt(layer.dc, 0, 0, bitmapWidth, layer.height - roll, layer.dc, 0, roll, SRCCOPY);
|
||
stripY = layer.height - roll;
|
||
stripH = roll;
|
||
}
|
||
else
|
||
{
|
||
const int amount = -roll;
|
||
// 内容下移:new[y] = old[y - amount],顶部暴露 amount 行。
|
||
BitBlt(layer.dc, 0, amount, bitmapWidth, layer.height - amount, layer.dc, 0, 0,
|
||
SRCCOPY);
|
||
stripY = 0;
|
||
stripH = amount;
|
||
}
|
||
// 锚点随之移动(与横向 `layer.wideMin += shift / scale` 同一套记账方式)。
|
||
layer.anchorYMax -= static_cast<double>(roll) / layer.pxPerValue;
|
||
// 补画暴露的那一条(整宽)。锚点用**滚动后**的值 —— 该条与已滚动的像素同一映射。
|
||
RenderLaneLayerRegion(laneIndex, layer, 0, bitmapWidth, stripY, stripH, layer.wideMin,
|
||
layer.anchorYMax);
|
||
}
|
||
|
||
// ⚠️ **横向重新居中只在「快要失去覆盖」时才做**(见上方 `driftLimit` 的说明):
|
||
// `BlitWideBuffer` 按 `(ViewMin - layer.wideMin)` 算源偏移,因此视界小幅漂移
|
||
// **完全不需要滚动**(等价且免费)。只有漂移接近 `margin`(位图覆盖边界)时才需要
|
||
// 向中心滚回去,否则网格右侧/左侧会开始用底衬(看起来像“信号被切成两半”)。
|
||
if (std::abs(shift) <= driftLimit)
|
||
{
|
||
continue;
|
||
}
|
||
// ⚠️ **每帧只重居中有限的几个网格**(见 `kLayerRollsPerFrame`),且单次滚动量有上限
|
||
// (`kLayerRollMaxStepPixels`):不重居中也不会画错,超出部分继续由贴图偏移承担,
|
||
// 因此可以安全地分帧完成。
|
||
if (rollBudget <= 0)
|
||
{
|
||
continue;
|
||
}
|
||
--rollBudget;
|
||
const int maxStep = std::min(kLayerRollMaxStepPixels, bitmapWidth / 2);
|
||
const int roll = std::max(-maxStep, std::min(maxStep, shift));
|
||
|
||
// 整数像素自贴图平移:内容整体移动 `roll` 像素。
|
||
if (roll > 0)
|
||
{
|
||
// 内容整体左移 roll:new[x] = old[x + roll],右侧暴露 roll 列。
|
||
BitBlt(layer.dc, 0, 0, bitmapWidth - roll, layer.height, layer.dc, roll, 0, SRCCOPY);
|
||
}
|
||
else if (roll < 0)
|
||
{
|
||
const int amount = -roll;
|
||
BitBlt(layer.dc, amount, 0, bitmapWidth - amount, layer.height, layer.dc, 0, 0, SRCCOPY);
|
||
}
|
||
else
|
||
{
|
||
continue;
|
||
}
|
||
layer.wideMin += static_cast<double>(roll) / scale;
|
||
|
||
// 只补画新暴露的细条(每帧代价恒为 O(细条))。
|
||
// ⚠️ 细条必须用**该图层更新后的映射**渲染(传 `layer.wideMin`);若改用"当前视界"
|
||
// 的映射,细条与已平移的旧像素之间会有 ≤0.5px 的错位接缝 → 拖动时看起来在抖动。
|
||
// ⚠️ **纵向锚点必须沿用 `layer.anchorYMax`**(不能传当前 `yMax`):横向补条只改一条
|
||
// 竖带,其余像素仍在原锚点上;同时该函数按锚点渲染这条竖带,因此竖带与其余像素
|
||
// 纵向同源。若在这里把锚点重置为当前 `yMax`,残余的纵向偏移会被静默清零、而像素
|
||
// 并未上移 → 整个网格的波形纵向错位("垂直拖动后再横向平移出现纵向跳变")。
|
||
const int stripWidth = std::min(std::abs(roll), bitmapWidth - 1);
|
||
const int stripX = (roll > 0) ? (bitmapWidth - stripWidth) : 0;
|
||
RenderLaneLayerRegion(laneIndex, layer, stripX, stripWidth, 0, 0, layer.wideMin,
|
||
layer.anchorYMax);
|
||
}
|
||
}
|
||
|
||
bool PlotView::BlitWideBuffer(gd::Graphics& g, const std::vector<double>& timeTicks)
|
||
{
|
||
const float plotWidth = m_plotRect.Width;
|
||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||
if (!(plotWidth > 8.0f) || !(span > 0.0))
|
||
{
|
||
return false;
|
||
}
|
||
const int width = static_cast<int>(std::lround(plotWidth));
|
||
const int margin = LayerMarginX();
|
||
const int bitmapWidth = width + 2 * margin;
|
||
const double scale = static_cast<double>(width) / span;
|
||
|
||
/** 一次图层贴图(所有参数在释放图层锁之前算好,避免持锁期间做 GDI 绘制)。 */
|
||
struct LayerBlit
|
||
{
|
||
HDC dc = nullptr;
|
||
int srcX = 0;
|
||
int srcY = 0;
|
||
int destX = 0;
|
||
int destY = 0;
|
||
int width = 0;
|
||
int height = 0;
|
||
};
|
||
|
||
std::vector<LayerBlit> blits;
|
||
std::vector<int> backing; // 本帧拿不到(完整)图层像素的网格
|
||
blits.reserve(m_lanes.size());
|
||
backing.reserve(m_lanes.size());
|
||
{
|
||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||
if (m_layerFront.size() != m_lanes.size())
|
||
{
|
||
return false;
|
||
}
|
||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||
{
|
||
const LaneLayer& layer = m_layerFront[index];
|
||
const gd::RectF& lane = m_lanes[index].rect;
|
||
const float top = std::max(lane.Y, m_plotRect.Y);
|
||
const float bottom = std::min(lane.Y + lane.Height, m_plotRect.Y + m_plotRect.Height);
|
||
if (bottom <= top)
|
||
{
|
||
continue; // 网格整体移出可视区
|
||
}
|
||
const int bandHeight = static_cast<int>(std::lround(bottom - top));
|
||
// 纵向映射是否仍然可用:图层高度必须等于"当前网格高 + 上下预渲染余量",
|
||
// 且时间跨度必须与当前视界一致(跨度不同则像素的时间映射已经不对)。
|
||
const int expectBitmapHeight =
|
||
static_cast<int>(std::ceil(lane.Height)) + 2 * LayerMarginY(lane.Height);
|
||
bool usable = (layer.valid && layer.dc != nullptr && layer.width == bitmapWidth &&
|
||
layer.height == expectBitmapHeight && layer.span > 0.0 &&
|
||
std::fabs(layer.span - span) <= 1.0e-12);
|
||
|
||
LayerBlit blit;
|
||
if (usable)
|
||
{
|
||
// ⚠️ **纵向偏移**:图层带「上下各半个网格高」的预渲染余量,幅值平移只改这里的
|
||
// 源行偏移(`anchorYMax` 与当前 `yMax` 之差换算成像素),不必重画图层 ——
|
||
// 这就是"纵向拖动与横向拖动同样是 O(1) 贴图"的关键。
|
||
int vOffset = 0;
|
||
if (layer.pxPerValue > 0.0)
|
||
{
|
||
vOffset = static_cast<int>(
|
||
std::llround((layer.anchorYMax - m_lanes[index].yMax) * layer.pxPerValue));
|
||
}
|
||
int srcY = static_cast<int>(std::lround(top - lane.Y)) + layer.marginY + vOffset;
|
||
int height = bandHeight;
|
||
int destTopOffset = 0;
|
||
// 超出画布的上下两端只贴**有像素的那一段**(其余由网格底衬补齐)。
|
||
if (srcY < 0)
|
||
{
|
||
destTopOffset = -srcY;
|
||
height -= destTopOffset;
|
||
srcY = 0;
|
||
}
|
||
if (srcY + height > layer.height)
|
||
{
|
||
height = layer.height - srcY;
|
||
}
|
||
// ⚠️ **局部贴图**:图层只覆盖"视界 ± 半屏",视界可能有一部分落在图层之外。
|
||
// 这里只贴**有像素的那一段**,其余由底衬补齐;整块跳过才是"网格瞬间变空"的直接原因。
|
||
const double subX = (Tab().ViewMin() - layer.wideMin) * scale;
|
||
int srcX = static_cast<int>(std::lround(subX));
|
||
int destOffset = 0; // 从视界左端起偏移多少像素开始贴
|
||
int blitWidth = width;
|
||
if (srcX < 0)
|
||
{
|
||
destOffset = -srcX;
|
||
blitWidth -= destOffset;
|
||
srcX = 0;
|
||
}
|
||
if (srcX + blitWidth > bitmapWidth)
|
||
{
|
||
blitWidth = bitmapWidth - srcX;
|
||
}
|
||
if (height > 0 && blitWidth > 0)
|
||
{
|
||
blit.dc = layer.dc;
|
||
blit.srcX = srcX;
|
||
blit.srcY = srcY;
|
||
blit.destX = static_cast<int>(std::lround(m_plotRect.X)) + destOffset;
|
||
blit.destY = static_cast<int>(std::lround(top)) + destTopOffset;
|
||
blit.width = blitWidth;
|
||
blit.height = height;
|
||
blits.push_back(blit);
|
||
// 横向 / 纵向只有**部分覆盖**:该网格的其余部分也要靠底衬补齐。
|
||
if (destOffset > 0 || blitWidth < width || height < bandHeight)
|
||
{
|
||
backing.push_back(static_cast<int>(index));
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// ⚠️⚠️ **必须兜底补齐网格底衬,绝不能留白**:旧实现在这里直接 `continue`
|
||
//(网格空白)或用旧跨度像素 `StretchBlt` 拉伸占位(深度放大时把一小段像素放大
|
||
// 几十倍 → 整块网格变成一片模糊底色)—— 两者在用户眼里都是
|
||
// 「网格叠底消失,没有任何内容」。改为就地补画底衬(底色 + 网格线 + 分界线):
|
||
// 代价极低,可以每帧执行,因此**网格底衬永远不会空白/闪烁**(用户 2026-10-17 要求)。
|
||
backing.push_back(static_cast<int>(index));
|
||
}
|
||
}
|
||
|
||
if (blits.empty())
|
||
{
|
||
// ⚠️⚠️ **绝不能因为"本帧一个网格都贴不出图层像素"就退化成整幅重绘**
|
||
//(用户 2026-10-17 报告:滚动过渡动画在巨型信号处卡顿)。
|
||
// 这一情形在滚动到**从未构建过图层的区域**时必然出现(上百个网格时绝大多数
|
||
// 网格的图层都还没建立):旧实现在这里 `return false` ⇒ 落到 `Paint` 末尾的
|
||
// **整幅重绘** ⇒ 把**本帧全部可见网格的波形**(可能包含上千万采样的巨型信号)
|
||
// 每帧重画一遍 —— 实测单帧 **20~35ms**,而手动拖滚动条只要 ~6ms。
|
||
// 正确行为:本帧只画**网格底衬**(廉价,见下方),波形等视界真正停下之后再按预算补
|
||
//(这正是用户要求的“没来得及画信号时就按只有网格叠底的状态继续滚动”)。
|
||
if (backing.empty())
|
||
{
|
||
// 连底衬都没得画(布局变化 / 图层数组与网格数不符)→ 交给整幅重绘兜底。
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ⚠️ 顺序:**先画网格底衬(GDI+),再取得 HDC 做 BitBlt** ——
|
||
// `g.GetHDC()` 之后再调用 GDI+ 绘制接口是未定义行为。
|
||
for (const int laneIndex : backing)
|
||
{
|
||
PaintLaneGridBacking(g, laneIndex, timeTicks);
|
||
}
|
||
|
||
HDC target = g.GetHDC();
|
||
if (target == nullptr)
|
||
{
|
||
return true;
|
||
}
|
||
for (const LayerBlit& blit : blits)
|
||
{
|
||
BitBlt(target, blit.destX, blit.destY, blit.width, blit.height, blit.dc, blit.srcX, blit.srcY,
|
||
SRCCOPY);
|
||
}
|
||
g.ReleaseHDC(target);
|
||
return true;
|
||
}
|
||
|
||
} // namespace jdi
|