// 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(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(std::floor(left)); const int targetY = static_cast(std::floor(top)); const int width = static_cast(std::ceil(right)) - targetX; const int height = static_cast(std::ceil(bottom)) - targetY; if (width <= 0 || height <= 0) { return; } HaloLayerCache& cache = m_haloLayer; // 键 = 决定光晕几何的全部输入(与 `AcquirePolyline` 的键同源)。 // ⚠️ **不含网格的屏幕位置**:滚动网格时几何不变,只需按新位置贴同一张位图。 const bool sameSize = cache.bitmap && static_cast(cache.bitmap->GetWidth()) == width && static_cast(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(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(-targetX), static_cast(-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(cache.bits); for (int y = 0; y < height; ++y) { const unsigned char* source = static_cast(data.Scan0) + static_cast(y) * data.Stride; unsigned char* row = destination + static_cast(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( (static_cast(source[x * 4 + 0]) * alpha + 128u) * 257u >> 16); row[x * 4 + 1] = static_cast( (static_cast(source[x * 4 + 1]) * alpha + 128u) * 257u >> 16); row[x * 4 + 2] = static_cast( (static_cast(source[x * 4 + 2]) * alpha + 128u) * 257u >> 16); row[x * 4 + 3] = static_cast(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& points, const std::vector& 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( ClampDouble(static_cast(resolved.style->lineWidth) * kScatterRadiusScale, static_cast(kMinScatterRadius), static_cast(kMaxScatterRadius)) * m_scale); const float ringRadius = radius + 2.5f * m_scale; const std::size_t scatterStride = std::max(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(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(std::floor(m_area.X)); const int y = static_cast(std::floor(m_area.Y)); const int right = static_cast(std::ceil(m_area.X + m_area.Width)); const int bottom = static_cast(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(GetProcAddress(gdi, "GdiAlphaBlend")); } if (cached == nullptr) { HMODULE msimg = LoadLibraryW(L"msimg32.dll"); if (msimg != nullptr) { cached = reinterpret_cast(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(std::ceil(m_area.X + m_area.Width)); const int height = static_cast(std::ceil(m_area.Y + m_area.Height)); if (!EnsureDecorCache(width, height)) { return false; } // 清空为全透明(圆角之外必须保持透明,贴回时才不会盖住波形)。 std::memset(m_decorBits, 0, static_cast(width) * static_cast(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 noTicks; for (std::size_t index = 0; index < m_lanes.size(); ++index) { PaintLane(g, static_cast(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(std::floor(rect.X))); const int y0 = std::max(0, static_cast(std::floor(rect.Y))); const int x1 = std::min(width, static_cast(std::ceil(rect.X + rect.Width))); const int y1 = std::min(height, static_cast(std::ceil(rect.Y + rect.Height))); for (int y = y0; y < y1; ++y) { unsigned char* row = static_cast(m_decorBits) + (static_cast(y) * static_cast(width)) * 4u; for (int x = x0; x < x1; ++x) { unsigned char* p = row + static_cast(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(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(std::floor(rect.X)); const int y = static_cast(std::floor(rect.Y)); const int w = static_cast(std::ceil(rect.X + rect.Width)) - x; const int h = static_cast(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 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(std::ceil(m_area.X + m_area.Width)); const int height = static_cast(std::ceil(m_area.Y + m_area.Height)); if (!EnsureDerivedCache(width, height)) { return false; } const std::size_t bytes = static_cast(width) * static_cast(height) * 4u; if (!m_derivedStripActive) { // 整块重画:先把全部像素清成透明。 std::memset(m_derivedBits, 0, bytes); } else { // 补条带:只把这条竖带清成透明(其余像素是滚动过来的有效内容)。 const int x0 = std::max(0, static_cast(std::floor(m_derivedStripX0))); const int x1 = std::min(width, static_cast(std::ceil(m_derivedStripX1))); for (int y = 0; y < height; ++y) { unsigned char* row = static_cast(m_derivedBits) + (static_cast(y) * static_cast(width)) * 4u; if (x1 > x0) { std::memset(row + static_cast(x0) * 4u, 0, static_cast(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(height)), gd::CombineModeIntersect); } for (std::size_t index = 0; index < m_lanes.size(); ++index) { PaintDerivedCurvesFor(g, static_cast(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(proc); HDC target = g.GetHDC(); if (target == nullptr) { return false; } // 缓存位图原点 = 客户区 (0,0):源与目标坐标相同。只贴绘图区(曲线被夹在网格内)。 const int x = std::max(0, static_cast(std::floor(m_plotRect.X))); const int y = std::max(0, static_cast(std::floor(m_plotRect.Y))); const int w = std::min(m_derivedW - x, static_cast(std::ceil(m_plotRect.Width))); const int h = std::min(m_derivedH - y, static_cast(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(m_plotRect.Width) / span; const int shift = static_cast( 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(width) * 4u; const std::size_t moveBytes = rowBytes - static_cast(std::abs(shift)) * 4u; unsigned char* bits = static_cast(m_derivedBits); if (bits != nullptr && moveBytes > 0) { for (int y = 0; y < m_derivedH; ++y) { unsigned char* row = bits + static_cast(y) * rowBytes; if (shift > 0) { // 内容右移:从本行右端往前搬(`memmove` 自带重叠安全)。 std::memmove(row + static_cast(shift) * 4u, row, moveBytes); } else { // 内容左移:源在本行右侧 —— 目标**不能**越过行首(否则越界)。 std::memmove(row, row + static_cast(-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(shift) + 1.0f); } else { m_derivedStripX0 = std::max(plotLeft, plotRight + static_cast(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(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 lock(m_layerMutex); for (LaneLayer& layer : m_layerFront) { ReleaseLaneLayer(layer); } m_layerFront.clear(); m_layerRebuildPending.clear(); } void PlotView::InvalidateWideBuffer() { std::lock_guard 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(laneIndex + 1) * 1315423911ull; key *= 1099511628211ull; key ^= static_cast(std::llround(span * 1.0e6)); key *= 1099511628211ull; key ^= static_cast(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(m_lanes.size())) { return hash; } const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; mixFloat(visual.rect.Height); mixFloat(static_cast(visual.yMax - visual.yMin)); const std::vector& signals = Tab().Lanes()[static_cast(laneIndex)].signals; mix(signals.size()); for (const SignalRef& ref : signals) { const ResolvedSignal resolved = ResolveSignal(ref); mix(static_cast(ref.groupIndex)); mix(static_cast(ref.signalIndex)); mix(resolved.visible ? 1ull : 0ull); if (resolved.data == nullptr || resolved.style == nullptr) { continue; } mix(static_cast(resolved.style->color)); mixFloat(resolved.style->opacity); mixFloat(resolved.style->lineWidth); mix(static_cast(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(resolved.style->offsetTime)); } return hash; } int PlotView::LayerMarginX() const { const int width = static_cast(std::lround(m_plotRect.Width)); return std::max(1, width / 2); // 左右各半屏 } int PlotView::LayerMarginY(float laneHeight) const { // 上下各半个网格高:一整屏的额外内容已足够覆盖实际拖拽范围, // 又不至于让每个横向补条 / 整块重画按三倍高度放大(横向平移因此不受影响)。 const int half = static_cast(std::lround(static_cast(laneHeight) * 0.5)); return std::max(1, half); } double PlotView::LayerDefaultWideMin() const { // 以当前视界为中心:位图左右各留半个视界宽,因此可向两侧平移 ±半屏。 const double span = Tab().ViewMax() - Tab().ViewMin(); const int width = static_cast(std::lround(m_plotRect.Width)); if (!(span > 0.0) || width <= 0) { return Tab().ViewMin(); } const double scale = static_cast(width) / span; return Tab().ViewMin() - static_cast(LayerMarginX()) / scale; } bool PlotView::LayersStructurallyReady() const { std::lock_guard 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(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(std::ceil(laneHeight)) + 2 * LayerMarginY(laneHeight); if (layer.height != bitmapHeight) { return false; } } return true; } bool PlotView::LayersAllocated() const { std::lock_guard 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(m_lanes.size())) { return false; } std::lock_guard lock(m_layerMutex); if (laneIndex >= static_cast(m_layerFront.size())) { return false; } const LaneLayer& layer = m_layerFront[static_cast(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(std::lround(plotWidth)); const int bitmapWidth = width + 2 * LayerMarginX(); const float laneHeight = m_lanes[static_cast(laneIndex)].rect.Height; const int bitmapHeight = static_cast(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(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(m_lanes.size())) { return false; } if (!(m_plotRect.Height > 0.0f)) { return false; } const gd::RectF& rect = m_lanes[static_cast(laneIndex)].rect; // 可视判定本身留一个网格高的余量(即将滚入视区的网格也算相关); // 预取带在其上再按屏高放大(见头文件说明)。 const float margin = std::max(rect.Height, 0.0f) + static_cast(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(index))) { continue; } anyRelevant = true; if (!LaneLayerReady(static_cast(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(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(m_layerRebuildPending.size()); const int take = std::min(budget, available); std::vector batch; batch.reserve(static_cast(take)); for (int index = 0; index < take; ++index) { const int laneIndex = m_layerRebuildPending[static_cast(index)]; if (laneIndex >= 0 && laneIndex < static_cast(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 renderLock(m_renderMutex); std::lock_guard layerLock(m_layerMutex); if (m_layerFront.size() != m_lanes.size()) { break; } LaneLayer& layer = m_layerFront[static_cast(laneIndex)]; // ⚠️ **跨度过期(缩放瞬间)也在这里就地整块重画**(按**新跨度**)。 // 旧实现遇到跨度不符就 `continue`(把该网格推给"下一次整批重建"), // 而"整批重建"正是那个 70~80ms 的同步卡顿源;现在改为一帧补一个网格, // 其余网格由 `BlitWideBuffer` 的占位拉伸顶着 → 前台永不因图层未就绪而滞后。 // 整块重画:整个图层都对齐到"当前 `yMax`"(锚点 = 当前 yMax)。 if (!RenderLaneLayerRegion(laneIndex, layer, 0, 0, 0, 0, defaultWideMin, m_lanes[static_cast(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(hardware == 0 ? 2u : hardware), 4)); if (m_layerWorkerCount == want && static_cast(m_layerWorkers.size()) == want) { // 字体 / 主题 / 缩放 / 数据源可能已变化:刷新一次绑定(不重建线程上下文)。 for (int index = 0; index < want; ++index) { m_layerWorkers[static_cast(index)]->Setup(m_fonts, m_theme); m_layerWorkers[static_cast(index)]->SetScale(m_scale); m_layerWorkers[static_cast(index)]->m_dataSet = m_dataSet; m_layerWorkers[static_cast(index)]->m_dataSetWritable = m_dataSetWritable; } return; } ReleaseLayerWorkers(); m_layerWorkers.reserve(static_cast(want)); for (int index = 0; index < want; ++index) { auto worker = std::make_unique(); 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& 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 renderLock(m_renderMutex); std::lock_guard layerLock(m_layerMutex); if (m_layerFront.size() != m_lanes.size()) { return; } for (const int laneIndex : laneIndices) { if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) { continue; } RenderLaneLayerRegion(laneIndex, m_layerFront[static_cast(laneIndex)], 0, 0, 0, 0, wideMin, m_lanes[static_cast(laneIndex)].yMax); } return; } { std::lock_guard renderLock(m_renderMutex); std::lock_guard 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 laneSnapshot = m_lanes; // 目标图层位图数组(已确认尺寸与网格数一致,线程内不会扩容)。 std::vector* const front = &m_layerFront; const int workerCount = m_layerWorkerCount; std::atomic next{0}; std::vector threads; threads.reserve(static_cast(workerCount)); for (int workerIndex = 0; workerIndex < workerCount; ++workerIndex) { threads.emplace_back([&, workerIndex]() { PlotView* worker = m_layerWorkers[static_cast(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(laneSnapshot.size()) || laneIndex >= static_cast(tabSnapshot.Lanes().size())) { continue; } // 隔离上下文:自带标签页副本 + 只含该网格 + 只读的数据源 / 字体 / 主题。 worker->m_workerTab = tabSnapshot; worker->m_workerTab.Lanes() = {tabSnapshot.Lanes()[static_cast(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(laneIndex)]}; // ⚠️ 工作线程用**自己的** `m_workerTab` / `m_area` / `m_plotRect` / `m_lanes` 渲染, // 因此不会与 UI 线程或其它工作线程互相覆盖渲染状态(这正是 2026-10-12 移除 // "后台图层线程"的根因)。目标图层位图按 `laneIndex` 各自不同,可并发写入。 worker->RenderLaneLayerRegion(0, (*front)[static_cast(laneIndex)], 0, 0, 0, 0, wideMin, laneSnapshot[static_cast(laneIndex)].yMax); } }); } for (std::thread& thread : threads) { if (thread.joinable()) { thread.join(); } } // 合并(主线程):工作线程内部以 `laneIndex = 0` 渲染,因此图层内容指纹要按**真实网格下标**重算。 std::lock_guard 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(m_layerFront.size())) { continue; } LaneLayer& target = m_layerFront[static_cast(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(m_lanes.size())) { return false; } std::lock_guard 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(std::lround(plotWidth)); const int margin = LayerMarginX(); // 左右各半屏 → 可平移 ±半屏 const int bitmapWidth = width + 2 * margin; const float laneHeight = m_lanes[static_cast(laneIndex)].rect.Height; const int lanePixelHeight = static_cast(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(width) / span; // 像素 / 时间 const double laneSpan = m_lanes[static_cast(laneIndex)].yMax - m_lanes[static_cast(laneIndex)].yMin; const double pxPerValue = (laneSpan > 0.0) ? static_cast(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 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(bitmapWidth), static_cast(bitmapHeight)); // 只把 `m_area` 限为待渲染的那一段 x 区间,使 GDI/GDI+ 的裁剪只落在它上面; // x 坐标仍用**整条宽窗口**的映射(`m_plotRect` 为完整宽图),因此网格线位置与整块图层一致。 m_area = gd::RectF(static_cast(stripX), static_cast(stripY), static_cast(stripW), static_cast(stripH)); // 横向补条(窄竖带):把折线的构建与描边裁剪到条带的 x 范围(见 `m_stripRenderActive`)。 // 纵向补条(整宽 × 窄 y)不裁剪 x —— 那一方向本来就需要整条折线的顶点。 const bool stripIsHorizontalBand = (static_cast(stripW) < static_cast(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(stripX); m_stripX1 = static_cast(stripX + stripW); Tab().ViewMin() = wideMin; Tab().ViewMax() = wideMin + static_cast(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(laneIndex)].yMax = anchorYMax; m_lanes[static_cast(laneIndex)].yMin = anchorYMax - laneSpan; // 该网格在本图层中的位置:横向铺满整条宽图,纵向铺满整张画布。 // 画布 = 「可见网格 + 上下各 `marginY` 行余量」。**上下扩展量按"像素行"精确换算** //(而不是上下各扩展固定的数值量):位图高度是整数(`ceil(laneHeight) + 2*marginY`), // 只有当「扩展后总跨度 / 画布行数」严格等于前台的 px/数值 时,可见网格在画布里的映射 // 才与前台整幅渲染**逐像素同源**(否则会差一个 <1px 的纵向缩放误差, // 让拖拽期间的像素残差明显变大)。 if (pxPerValue > 0.0) { const double topExtend = static_cast(marginY) / pxPerValue; const double bottomRows = static_cast(bitmapHeight - marginY) - static_cast(laneHeight); const double bottomExtend = std::max(0.0, bottomRows) / pxPerValue; m_lanes[static_cast(laneIndex)].yMin -= bottomExtend; m_lanes[static_cast(laneIndex)].yMax += topExtend; } m_lanes[static_cast(laneIndex)].rect = gd::RectF(0.0f, 0.0f, static_cast(bitmapWidth), static_cast(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 timeTicks; BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeStep, timeTicks); PaintLane(g, laneIndex, m_lanes[static_cast(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 renderLock(m_renderMutex); std::lock_guard layerLock(m_layerMutex); m_layerFront.resize(m_lanes.size()); m_layerRebuildPending.clear(); } // 全部网格统一用"当前视界居中"的映射(与 `BlitWideBuffer` 的逐网格 `wideMin` 相容)。 const double wideMin = LayerDefaultWideMin(); std::vector allLanes; allLanes.reserve(m_lanes.size()); for (std::size_t index = 0; index < m_lanes.size(); ++index) { allLanes.push_back(static_cast(index)); } // **并行**重建全部网格(各网格用隔离上下文,多核时约等于一块网格的耗时)。 RebuildLaneLayersParallel(allLanes, wideMin); std::lock_guard 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 renderLock(m_renderMutex); std::lock_guard 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(std::lround(plotWidth)); const int margin = LayerMarginX(); const int bitmapWidth = width + 2 * margin; const double scale = static_cast(width) / span; std::lock_guard renderLock(m_renderMutex); std::lock_guard 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(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(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(std::llround(desired - static_cast(margin))); // 内容(外观 / 网格几何 / 幅值**跨度** / 可见性…)与当前状态不一致。 const bool contentStale = (layer.key != LaneLayerKey(laneIndex)); // 纵向:幅值**平移**不改内容,只改贴图偏移(与横向同构)。 int vOffset = 0; if (layer.pxPerValue > 0.0) { vOffset = static_cast( 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(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(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& 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(std::lround(plotWidth)); const int margin = LayerMarginX(); const int bitmapWidth = width + 2 * margin; const double scale = static_cast(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 blits; std::vector backing; // 本帧拿不到(完整)图层像素的网格 blits.reserve(m_lanes.size()); backing.reserve(m_lanes.size()); { std::lock_guard 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(std::lround(bottom - top)); // 纵向映射是否仍然可用:图层高度必须等于"当前网格高 + 上下预渲染余量", // 且时间跨度必须与当前视界一致(跨度不同则像素的时间映射已经不对)。 const int expectBitmapHeight = static_cast(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( std::llround((layer.anchorYMax - m_lanes[index].yMax) * layer.pxPerValue)); } int srcY = static_cast(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(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(std::lround(m_plotRect.X)) + destOffset; blit.destY = static_cast(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(index)); } continue; } } // ⚠️⚠️ **必须兜底补齐网格底衬,绝不能留白**:旧实现在这里直接 `continue` //(网格空白)或用旧跨度像素 `StretchBlt` 拉伸占位(深度放大时把一小段像素放大 // 几十倍 → 整块网格变成一片模糊底色)—— 两者在用户眼里都是 // 「网格叠底消失,没有任何内容」。改为就地补画底衬(底色 + 网格线 + 分界线): // 代价极低,可以每帧执行,因此**网格底衬永远不会空白/闪烁**(用户 2026-10-17 要求)。 backing.push_back(static_cast(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