落地每网格双缓冲图层 + 后台绘制架构:全局宽缓冲区重构为每网格信号图层(宽x3、只含基底),m_layerFront/m_layerBack 双缓冲 + m_layerWorker 后台线程,前台只做位图坐标偏移贴图不再绘制波形;前台图层就绪即用、内容稍旧也照用并交后台补画后交换;含不收敛保护、前台优先让路、固定加锁顺序、渲染状态完整还原;新增 BackgroundLayerRenderCount 验证后台绘制;深放大区间各交互 0.7~8.7ms/帧
This commit is contained in:
1 parent
e22749b57a
commit
f97c4cf289
3 files changed
+667
-338
No files matched your search
@@ -671,6 +671,8 @@ namespace jdi
|
||||
{
|
||||
// 先停掉后台计算线程:它可能正在读取采样数据,必须在释放其它资源前回收。
|
||||
StopCurveWorker();
|
||||
// 再停掉后台「网格图层」绘制线程:它会读写渲染状态与图层位图。
|
||||
StopLayerWorker();
|
||||
ReleaseSceneCache();
|
||||
ReleaseDecorCache();
|
||||
ReleaseWideBuffer();
|
||||
@@ -5488,6 +5490,13 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 本帧整帧都在改写渲染状态(`m_area` / `m_plotRect` / 视界 / 图层…),
|
||||
// 因此必须与「后台图层绘制线程」互斥;可重入,内部再调图层渲染不会自锁。
|
||||
// 后台只在**内容变化后补画**时才会长时间持有它,平移/缩放等交互期间它基本空闲,
|
||||
// 因此前台不会被拖慢。
|
||||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||||
m_lastPaintTick.store(GetTickCount64(), std::memory_order_relaxed);
|
||||
|
||||
BuildLaneVisuals();
|
||||
|
||||
// 双缓冲:清掉上一帧钉住的曲线条目(本帧会按需重新钉住),并清理已被更新条目覆盖 /
|
||||
@@ -5623,16 +5632,12 @@ namespace jdi
|
||||
}
|
||||
else
|
||||
{
|
||||
// 已有可用基底(跨度与内容均匹配)→ 滚动:只补画新暴露的细条,恒为 O(细条),
|
||||
// 无周期性大尖峰;否则以当前视界为中心重建基底。
|
||||
// ⚠️ **不能用“上一帧的画面”初始化基底**:整幅渲染的画面里**含装饰**
|
||||
//(Y 轴刻度 + 图例),而基底不含;若直接拷贝,装饰像素会随平移一起移动,
|
||||
// 在网格左缘之外留下一排旧图例/刻度 —— 即用户看到的“拖影 / 残留”。
|
||||
bool ready = (m_wideValid && WideBufferMatches()) ? RollWideBuffer() : false;
|
||||
if (!ready)
|
||||
{
|
||||
ready = RebuildWideBuffer();
|
||||
}
|
||||
// 只要图层“就绪且跨度一致”,就直接用现成像素做坐标偏移(平移/滚动)。
|
||||
// 内容即使稍旧也无所谓:由后台线程补画后与旧缓冲交换,前台下一帧即拿到新像素。
|
||||
// 只有“跨度变了/图层还没建”时才落到下方"整幅重绘"以保证正确性。
|
||||
bool ready = LayersReadyForView() ? RollWideBuffer() : false;
|
||||
// 无论是否用上现成像素,都把"内容与前台不一致"的网格交后台补画。
|
||||
ScheduleStaleLayerRenders();
|
||||
if (ready)
|
||||
{
|
||||
m_wideLastKey = wideKey;
|
||||
@@ -6826,78 +6831,247 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::EnsureWideBuffer(int width, int height)
|
||||
bool PlotView::EnsureLaneLayer(LaneLayer& layer, int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_wideDc != nullptr && m_wideW == width && m_wideH == height)
|
||||
if (layer.dc != nullptr && layer.width == width && layer.height == height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
ReleaseWideBuffer();
|
||||
ReleaseLaneLayer(layer);
|
||||
HDC screen = GetDC(nullptr);
|
||||
if (screen == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_wideDc = CreateCompatibleDC(screen);
|
||||
m_wideBitmap = CreateCompatibleBitmap(screen, width, height);
|
||||
layer.dc = CreateCompatibleDC(screen);
|
||||
layer.bitmap = CreateCompatibleBitmap(screen, width, height);
|
||||
ReleaseDC(nullptr, screen);
|
||||
if (m_wideDc == nullptr || m_wideBitmap == nullptr)
|
||||
if (layer.dc == nullptr || layer.bitmap == nullptr)
|
||||
{
|
||||
ReleaseWideBuffer();
|
||||
ReleaseLaneLayer(layer);
|
||||
return false;
|
||||
}
|
||||
m_wideOldBitmap = SelectObject(m_wideDc, m_wideBitmap);
|
||||
m_wideW = width;
|
||||
m_wideH = height;
|
||||
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()
|
||||
{
|
||||
m_wideValid = false;
|
||||
if (m_wideDc != nullptr)
|
||||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||||
for (LaneLayer& layer : m_layerFront)
|
||||
{
|
||||
if (m_wideOldBitmap != nullptr)
|
||||
{
|
||||
SelectObject(m_wideDc, m_wideOldBitmap);
|
||||
m_wideOldBitmap = nullptr;
|
||||
}
|
||||
DeleteDC(m_wideDc);
|
||||
m_wideDc = nullptr;
|
||||
ReleaseLaneLayer(layer);
|
||||
}
|
||||
if (m_wideBitmap != nullptr)
|
||||
for (LaneLayer& layer : m_layerBack)
|
||||
{
|
||||
DeleteObject(m_wideBitmap);
|
||||
m_wideBitmap = nullptr;
|
||||
ReleaseLaneLayer(layer);
|
||||
}
|
||||
m_wideW = 0;
|
||||
m_wideH = 0;
|
||||
m_layerFront.clear();
|
||||
m_layerBack.clear();
|
||||
m_layerAnchorValid = false;
|
||||
}
|
||||
|
||||
void PlotView::InvalidateWideBuffer()
|
||||
{
|
||||
m_wideValid = false;
|
||||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||||
for (LaneLayer& layer : m_layerFront)
|
||||
{
|
||||
layer.valid = false;
|
||||
}
|
||||
for (LaneLayer& layer : m_layerBack)
|
||||
{
|
||||
layer.valid = false;
|
||||
}
|
||||
m_layerAnchorValid = false;
|
||||
}
|
||||
|
||||
bool PlotView::WideBufferMatches() const
|
||||
std::uint64_t PlotView::LaneLayerKey(int laneIndex) const
|
||||
{
|
||||
if (!m_wideValid || m_wideDc == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 图层只含基底(不含派生曲线与装饰),因此指纹 = 内容(外观/几何,不含派生参数)
|
||||
// ⊕ 网格下标 ⊕ 视界跨度 ⊕ 绘图区宽度(决定像素/时间与位图尺寸)。
|
||||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||||
if (!(span > 0.0) || std::fabs(span - m_wideSpan) > 1.0e-12)
|
||||
std::uint64_t key = SceneContentHash(true, false);
|
||||
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;
|
||||
}
|
||||
|
||||
bool PlotView::LayersReadyForView() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||||
if (m_layerFront.size() != m_lanes.size() || m_layerFront.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 内容(外观样式 / 网格结构 / 视界几何)变化 → 需重建。
|
||||
// ⚠️ **不含派生曲线参数**:宽缓冲只存基底(网格 / 波形),派生曲线是每帧叠加的,
|
||||
// 因此拖弱波增强 / 均值滤波参数滑块不会让基底失效,可直接复用基底像素。
|
||||
return (m_wideKey == SceneContentHash(true, 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 margin = std::max(1, width / 2);
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
for (const LaneLayer& layer : m_layerFront)
|
||||
{
|
||||
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 跨度必须一致:跨度变了像素/时间的映射就不同,旧像素不能再当“坐标偏移”用。
|
||||
if (std::fabs(span - layer.span) > 1.0e-12)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW)
|
||||
{
|
||||
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 = std::max(1, width / 2); // 左右各半屏 → 可平移 ±半屏
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
const float laneHeight = m_lanes[static_cast<std::size_t>(laneIndex)].rect.Height;
|
||||
const int bitmapHeight = static_cast<int>(std::ceil(laneHeight));
|
||||
if (bitmapHeight <= 0 || !EnsureLaneLayer(layer, bitmapWidth, bitmapHeight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double scale = static_cast<double>(width) / span; // 像素 / 时间
|
||||
const double wideMin = Tab().ViewMin() - static_cast<double>(margin) / scale; // 位图 x=0 对应的时间
|
||||
|
||||
if (stripW <= 0 || stripX < 0 || stripX + stripW > bitmapWidth)
|
||||
{
|
||||
stripX = 0;
|
||||
stripW = bitmapWidth;
|
||||
}
|
||||
|
||||
// 保存当前布局 / 视图 / 网格,临时切换到"宽窗口"映射后离屏渲染,再恢复。
|
||||
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 bool savedFastFrame = m_fastFrame;
|
||||
const bool savedRenderWide = m_renderWide;
|
||||
const float savedScroll = Tab().LaneScroll();
|
||||
|
||||
m_plotRect = gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), laneHeight);
|
||||
// 只把 `m_area` 限为待渲染的那一段 x 区间,使 GDI/GDI+ 的裁剪只落在它上面;
|
||||
// x 坐标仍用**整条宽窗口**的映射(`m_plotRect` 为完整宽图),因此网格线位置与整块图层一致。
|
||||
m_area = gd::RectF(static_cast<float>(stripX), 0.0f, static_cast<float>(stripW), laneHeight);
|
||||
Tab().ViewMin() = wideMin;
|
||||
Tab().ViewMax() = wideMin + static_cast<double>(bitmapWidth) / scale;
|
||||
|
||||
m_renderWide = true; // 图层只含基底:跳过派生曲线与装饰
|
||||
// 图层必须与"同一视界的整幅渲染"**逐像素同源**:强制全质量抗锯齿(不受拖拽快速帧影响)。
|
||||
m_fastFrame = false;
|
||||
BuildLaneVisuals();
|
||||
// 沿用**当前视界**的 Y 轴范围,否则幅值映射与前台不一致。
|
||||
for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i)
|
||||
{
|
||||
m_lanes[i].yMin = savedLanes[i].yMin;
|
||||
m_lanes[i].yMax = savedLanes[i].yMax;
|
||||
}
|
||||
// 该网格在本图层中的位置:横向铺满整条宽图,纵向从 0 开始。
|
||||
m_lanes[static_cast<std::size_t>(laneIndex)].rect =
|
||||
gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), laneHeight);
|
||||
{
|
||||
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, m_theme->cardBg);
|
||||
|
||||
// 竖向网格刻度沿用**前台视界跨度**(而非 3× 的宽窗口跨度),否则竖网格线会只有 1/3。
|
||||
const double fgSpan = savedViewMax - savedViewMin;
|
||||
const double timeStep = NiceStep((fgSpan > 0.0) ? fgSpan : 1.0, kTimeTickTarget);
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeStep, timeTicks);
|
||||
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
|
||||
}
|
||||
m_fastFrame = savedFastFrame;
|
||||
m_renderWide = savedRenderWide;
|
||||
|
||||
// 恢复布局 / 视图 / 网格。
|
||||
// ⚠️ **必须调用 `BuildLaneVisuals()`**(而不是只把 `m_lanes` 拷回去):它会按正常布局
|
||||
// 重算 `m_laneContentHeight`。该值参与内容指纹;若残留上面"宽窗口"布局算出的值,
|
||||
// 前台算出的图层指纹就永远与后台不一致 → 图层被判定为"永远过期" → 每帧重复排任务,
|
||||
// 后台反复重画、前台反复抢 `m_renderMutex`,表现为界面卡死。
|
||||
m_area = savedArea;
|
||||
m_plotRect = savedPlot;
|
||||
Tab().ViewMin() = savedViewMin;
|
||||
Tab().ViewMax() = savedViewMax;
|
||||
BuildLaneVisuals();
|
||||
// `BuildLaneVisuals()` 内部会 `ClampLaneScroll()`,用宽窗口布局可能夹出不同的滚动量,
|
||||
// 因此把滚动量与各网格幅值范围一并还原回进入本函数前的状态。
|
||||
Tab().LaneScroll() = savedScroll;
|
||||
for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i)
|
||||
{
|
||||
m_lanes[i].yMin = savedLanes[i].yMin;
|
||||
m_lanes[i].yMax = savedLanes[i].yMax;
|
||||
}
|
||||
// 恢复该网格的矩形(`BuildLaneVisuals` 已按正常布局给出,这里只为稳妥保持一致)。
|
||||
if (laneIndex < static_cast<int>(m_lanes.size()) && laneIndex < static_cast<int>(savedLanes.size()))
|
||||
{
|
||||
m_lanes[static_cast<std::size_t>(laneIndex)] = savedLanes[static_cast<std::size_t>(laneIndex)];
|
||||
}
|
||||
|
||||
layer.wideMin = wideMin;
|
||||
layer.span = span;
|
||||
layer.key = LaneLayerKey(laneIndex);
|
||||
layer.valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::RebuildWideBuffer()
|
||||
@@ -6905,278 +7079,337 @@ namespace jdi
|
||||
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))
|
||||
if (!(plotWidth > 8.0f) || !(areaHeight > 8.0f) || !(span > 0.0) || m_lanes.empty())
|
||||
{
|
||||
m_wideValid = false;
|
||||
InvalidateWideBuffer();
|
||||
return false;
|
||||
}
|
||||
|
||||
const int width = static_cast<int>(std::lround(plotWidth));
|
||||
const int margin = std::max(1, width / 2); // 左右各半屏 → 可平移 ±半屏
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
const float plotHeight = m_plotRect.Height;
|
||||
const int bitmapHeight = static_cast<int>(std::ceil(plotHeight));
|
||||
if (!EnsureWideBuffer(bitmapWidth, bitmapHeight))
|
||||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||||
m_layerFront.resize(m_lanes.size());
|
||||
m_layerBack.resize(m_lanes.size());
|
||||
|
||||
bool ok = true;
|
||||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double scale = static_cast<double>(width) / span; // 像素 / 时间
|
||||
const double wideMin = Tab().ViewMin() - static_cast<double>(margin) / scale; // 位图 x=0 对应的时间
|
||||
|
||||
// 保存当前布局 / 视图 / 网格,临时切换到"宽窗口"映射后离屏渲染,再恢复。
|
||||
// 宽缓冲**只覆盖绘图区**(不含底部时间轴):时间轴由前台每帧绘制。
|
||||
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;
|
||||
|
||||
m_area = gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), plotHeight);
|
||||
m_plotRect = gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), plotHeight);
|
||||
Tab().ViewMin() = wideMin;
|
||||
Tab().ViewMax() = wideMin + static_cast<double>(bitmapWidth) / scale;
|
||||
|
||||
m_renderWide = true;
|
||||
// 宽缓冲必须与"同一视界的整幅渲染"**逐像素同源**,否则平移后网格线数量 / 锯齿会不同:
|
||||
// * 强制全质量抗锯齿(不受"拖拽快速帧"影响,绝不降级);
|
||||
// * 网格竖向刻度沿用**前台视界跨度**(而非 2× 的宽窗口跨度),否则竖网格线会减半。
|
||||
const bool savedFastFrame = m_fastFrame;
|
||||
m_fastFrame = false;
|
||||
BuildLaneVisuals();
|
||||
// 宽缓冲必须沿用**当前视界**的 Y 轴范围,否则幅值映射与前台不一致。
|
||||
for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i)
|
||||
{
|
||||
m_lanes[i].yMin = savedLanes[i].yMin;
|
||||
m_lanes[i].yMax = savedLanes[i].yMax;
|
||||
}
|
||||
{
|
||||
gd::Graphics g(m_wideDc);
|
||||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
UiFillRect(g, m_plotRect, m_theme->cardBg);
|
||||
|
||||
const double fgSpan = savedViewMax - savedViewMin;
|
||||
const double timeStep = NiceStep((fgSpan > 0.0) ? fgSpan : 1.0, kTimeTickTarget);
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeStep, timeTicks);
|
||||
for (std::size_t i = 0; i < m_lanes.size(); ++i)
|
||||
if (!RenderLaneLayerRegion(static_cast<int>(index), m_layerFront[index], 0, 0))
|
||||
{
|
||||
PaintLane(g, static_cast<int>(i), m_lanes[i], timeTicks);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
m_fastFrame = savedFastFrame;
|
||||
m_renderWide = false;
|
||||
m_layerAnchorMin = Tab().ViewMin();
|
||||
m_layerAnchorValid = true;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// 恢复布局 / 视图 / 网格。
|
||||
m_area = savedArea;
|
||||
m_plotRect = savedPlot;
|
||||
Tab().ViewMin() = savedViewMin;
|
||||
Tab().ViewMax() = savedViewMax;
|
||||
BuildLaneVisuals();
|
||||
bool PlotView::RollWideBuffer()
|
||||
{
|
||||
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 = std::max(1, width / 2);
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
const double scale = static_cast<double>(width) / span;
|
||||
|
||||
m_wideMin = wideMin;
|
||||
m_wideAnchorMin = wideMin + static_cast<double>(margin) / scale; // 视界左端(列 margin)对应的时间
|
||||
m_wideSpan = span;
|
||||
m_wideKey = SceneContentHash(true, false);
|
||||
m_wideValid = true;
|
||||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||||
if (!m_layerAnchorValid || m_layerFront.size() != m_lanes.size() || m_layerFront.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const LaneLayer& layer : m_layerFront)
|
||||
{
|
||||
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
|
||||
std::fabs(layer.span - span) > 1.0e-12)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 视界左端相对锚点的位移(像素)。内容需整体平移 round(d) 像素,只补画新暴露的细条。
|
||||
const double d = (Tab().ViewMin() - m_layerAnchorMin) * scale;
|
||||
const int shift = static_cast<int>(std::lround(d));
|
||||
if (shift == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 位移过大(跳视界)时滚动已经没有意义,交给调用方整幅重建。
|
||||
if (std::abs(shift) >= bitmapWidth - 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int stripWidth = std::min(std::abs(shift), bitmapWidth);
|
||||
const int stripX = (shift > 0) ? (bitmapWidth - stripWidth) : 0;
|
||||
for (LaneLayer& layer : m_layerFront)
|
||||
{
|
||||
if (shift > 0)
|
||||
{
|
||||
// 内容整体左移 shift:new[x] = old[x + shift],右侧暴露 shift 列。
|
||||
BitBlt(layer.dc, 0, 0, bitmapWidth - shift, layer.height, layer.dc, shift, 0, SRCCOPY);
|
||||
}
|
||||
else
|
||||
{
|
||||
const int amount = -shift;
|
||||
BitBlt(layer.dc, amount, 0, bitmapWidth - amount, layer.height, layer.dc, 0, 0, SRCCOPY);
|
||||
}
|
||||
}
|
||||
m_layerAnchorMin += static_cast<double>(shift) / scale;
|
||||
for (LaneLayer& layer : m_layerFront)
|
||||
{
|
||||
layer.wideMin = m_layerAnchorMin - static_cast<double>(margin) / scale;
|
||||
}
|
||||
// 只补画新暴露的细条(每帧代价恒为 O(细条),无周期性大尖峰)。
|
||||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||||
{
|
||||
RenderLaneLayerRegion(static_cast<int>(index), m_layerFront[index], stripX, stripWidth);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::BlitWideBuffer(gd::Graphics& g) const
|
||||
{
|
||||
if (!WideBufferMatches())
|
||||
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(m_plotRect.Width));
|
||||
const int width = static_cast<int>(std::lround(plotWidth));
|
||||
const int margin = std::max(1, width / 2);
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
const double scale = static_cast<double>(width) / m_wideSpan;
|
||||
const double subX = (Tab().ViewMin() - m_wideMin) * scale;
|
||||
if (subX < -0.5 || subX + static_cast<double>(width) > static_cast<double>(bitmapWidth) + 0.5)
|
||||
const double scale = static_cast<double>(width) / span;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||||
if (m_layerFront.size() != m_lanes.size())
|
||||
{
|
||||
return false; // 已移出宽缓冲范围 → 调用方负责重建
|
||||
return false;
|
||||
}
|
||||
HDC target = g.GetHDC();
|
||||
if (target == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int destX = static_cast<int>(std::lround(m_plotRect.X));
|
||||
const int destY = static_cast<int>(std::lround(m_plotRect.Y));
|
||||
const int height = static_cast<int>(std::lround(m_plotRect.Height));
|
||||
BitBlt(target, destX, destY, width, height, m_wideDc, static_cast<int>(std::lround(subX)), 0,
|
||||
SRCCOPY);
|
||||
bool any = false;
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (std::fabs(layer.span - span) > 1.0e-12)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double subX = (Tab().ViewMin() - layer.wideMin) * scale;
|
||||
if (subX < -0.5 || subX + static_cast<double>(width) > static_cast<double>(bitmapWidth) + 0.5)
|
||||
{
|
||||
continue; // 该网格图层已移出当前视界范围 → 由调用方安排重建
|
||||
}
|
||||
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 destX = static_cast<int>(std::lround(m_plotRect.X));
|
||||
const int destY = static_cast<int>(std::lround(top));
|
||||
const int srcX = static_cast<int>(std::lround(subX));
|
||||
const int srcY = static_cast<int>(std::lround(top - lane.Y));
|
||||
const int height = static_cast<int>(std::lround(bottom - top));
|
||||
if (height <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BitBlt(target, destX, destY, width, height, layer.dc, srcX, srcY, SRCCOPY);
|
||||
any = true;
|
||||
}
|
||||
g.ReleaseHDC(target);
|
||||
return true;
|
||||
return any;
|
||||
}
|
||||
|
||||
bool PlotView::RollWideBuffer()
|
||||
void PlotView::ScheduleStaleLayerRenders()
|
||||
{
|
||||
if (!WideBufferMatches())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int width = static_cast<int>(std::lround(m_plotRect.Width));
|
||||
const int margin = std::max(1, width / 2);
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
if (m_wideW != bitmapWidth)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const double scale = static_cast<double>(width) / m_wideSpan;
|
||||
// 视界左端相对锚点的位移(像素)。内容需整体平移 round(d) 像素,只补画新暴露的细条。
|
||||
const double d = (Tab().ViewMin() - m_wideAnchorMin) * scale;
|
||||
const int shift = static_cast<int>(std::lround(d));
|
||||
if (shift != 0)
|
||||
{
|
||||
if (shift > 0)
|
||||
{
|
||||
// 内容整体左移 shift:new[x] = old[x + shift],右侧暴露 shift 列。
|
||||
BitBlt(m_wideDc, 0, 0, bitmapWidth - shift, m_wideH, m_wideDc, shift, 0, SRCCOPY);
|
||||
}
|
||||
else
|
||||
{
|
||||
const int amount = -shift;
|
||||
BitBlt(m_wideDc, amount, 0, bitmapWidth - amount, m_wideH, m_wideDc, 0, 0, SRCCOPY);
|
||||
}
|
||||
RenderWideStrip(shift);
|
||||
m_wideAnchorMin += static_cast<double>(shift) / scale;
|
||||
m_wideMin = m_wideAnchorMin - static_cast<double>(margin) / scale;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::InitWideBufferFromScene()
|
||||
{
|
||||
if (!m_sceneValid || m_sceneDc == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const float plotWidth = m_plotRect.Width;
|
||||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||||
const double sceneSpan = m_sceneKey.viewMax - m_sceneKey.viewMin;
|
||||
if (!(span > 0.0) || std::fabs(sceneSpan - span) > 1.0e-9)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 仅当"派生预览曲线参数未变"时可用:此时场景缓存内容(含派生曲线/图例)依然正确。
|
||||
// (拖弱波增强/均值滤波参数滑块时派生曲线会变,必须从零重建基底。)
|
||||
std::vector<SceneDerivedEntry> currentDerived;
|
||||
CollectSceneDerived(currentDerived);
|
||||
if (currentDerived.size() != m_sceneDerived.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (std::size_t i = 0; i < currentDerived.size(); ++i)
|
||||
{
|
||||
const SceneDerivedEntry& a = currentDerived[i];
|
||||
const SceneDerivedEntry& b = m_sceneDerived[i];
|
||||
if (a.edgeCenter != b.edgeCenter || a.meanFilter != b.meanFilter ||
|
||||
a.weakBoost != b.weakBoost || a.weakBoostLevelPercent != b.weakBoostLevelPercent ||
|
||||
a.weakBoostIterations != b.weakBoostIterations ||
|
||||
a.edgeCenterItems != b.edgeCenterItems || a.meanFilterItems != b.meanFilterItems ||
|
||||
a.weakBoostItems != b.weakBoostItems)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const int width = static_cast<int>(std::lround(m_plotRect.Width));
|
||||
const int margin = std::max(1, width / 2);
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
const float plotHeight = m_plotRect.Height;
|
||||
const int bitmapHeight = static_cast<int>(std::ceil(plotHeight));
|
||||
if (!EnsureWideBuffer(bitmapWidth, bitmapHeight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 场景缓存的绘图区相对其原点(m_sceneX/m_sceneY)的位置。
|
||||
const int srcX = static_cast<int>(std::floor(m_plotRect.X)) - m_sceneX;
|
||||
const int srcY = static_cast<int>(std::floor(m_plotRect.Y)) - m_sceneY;
|
||||
if (BitBlt(m_wideDc, margin, 0, width, bitmapHeight, m_sceneDc, srcX, srcY, SRCCOPY) == FALSE)
|
||||
{
|
||||
m_wideValid = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const double scale = static_cast<double>(width) / span;
|
||||
m_wideMin = m_sceneKey.viewMin - static_cast<double>(margin) / scale;
|
||||
m_wideAnchorMin = m_sceneKey.viewMin;
|
||||
m_wideSpan = span;
|
||||
m_wideKey = SceneContentHash(true, false);
|
||||
m_wideValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::RenderWideStrip(int shiftPixels)
|
||||
{
|
||||
const int width = static_cast<int>(std::lround(m_plotRect.Width));
|
||||
const int margin = std::max(1, width / 2);
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
const float plotHeight = m_plotRect.Height;
|
||||
const double scale = static_cast<double>(width) / m_wideSpan;
|
||||
const int stripWidth = std::min(std::abs(shiftPixels), bitmapWidth);
|
||||
if (stripWidth <= 0)
|
||||
if (!(plotWidth > 8.0f) || !(span > 0.0) || m_lanes.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int stripX = (shiftPixels > 0) ? (bitmapWidth - stripWidth) : 0;
|
||||
// 平移后的缓冲区左端时间。
|
||||
const double newWideMin = m_wideMin + static_cast<double>(shiftPixels) / scale;
|
||||
const int width = static_cast<int>(std::lround(plotWidth));
|
||||
const int margin = std::max(1, width / 2);
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
|
||||
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;
|
||||
|
||||
// 关键:x 坐标仍用**整条宽窗口**的映射(m_plotRect 为完整宽图),因此网格/波纹位置正确;
|
||||
// 仅把 m_area 限为细条,使 GDI/GDI+ 的裁剪只落在细条上。
|
||||
m_area = gd::RectF(static_cast<float>(stripX), 0.0f, static_cast<float>(stripWidth), plotHeight);
|
||||
m_plotRect = gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), plotHeight);
|
||||
Tab().ViewMin() = newWideMin;
|
||||
Tab().ViewMax() = newWideMin + static_cast<double>(bitmapWidth) / scale;
|
||||
|
||||
m_renderWide = true;
|
||||
// 与 `RebuildWideBuffer` 保持完全一致:细条补画也必须**全质量抗锯齿**,且网格竖向刻度
|
||||
// 沿用**前台视界跨度**,否则滚动后新露出的细条会与既有基底的网格 / 锯齿不一致。
|
||||
const bool savedFastFrame = m_fastFrame;
|
||||
m_fastFrame = false;
|
||||
BuildLaneVisuals();
|
||||
for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i)
|
||||
std::vector<int> pending;
|
||||
{
|
||||
m_lanes[i].yMin = savedLanes[i].yMin;
|
||||
m_lanes[i].yMax = savedLanes[i].yMax;
|
||||
}
|
||||
{
|
||||
gd::Graphics g(m_wideDc);
|
||||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
UiFillRect(g, m_area, m_theme->cardBg);
|
||||
const double fgSpan = savedViewMax - savedViewMin;
|
||||
const double timeStep = NiceStep((fgSpan > 0.0) ? fgSpan : 1.0, kTimeTickTarget);
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeStep, timeTicks);
|
||||
for (std::size_t i = 0; i < m_lanes.size(); ++i)
|
||||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||||
if (m_layerFront.size() != m_lanes.size())
|
||||
{
|
||||
PaintLane(g, static_cast<int>(i), m_lanes[i], timeTicks);
|
||||
m_layerFront.resize(m_lanes.size());
|
||||
m_layerBack.resize(m_lanes.size());
|
||||
}
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
const LaneLayer& layer = m_layerFront[index];
|
||||
const bool stale = (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
|
||||
std::fabs(layer.span - span) > 1.0e-12 ||
|
||||
layer.key != LaneLayerKey(static_cast<int>(index)));
|
||||
if (stale)
|
||||
{
|
||||
pending.push_back(static_cast<int>(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
m_fastFrame = savedFastFrame;
|
||||
m_renderWide = false;
|
||||
if (pending.empty())
|
||||
{
|
||||
m_layerRescheduleStreak = 0;
|
||||
return;
|
||||
}
|
||||
// 停滞保护:见成员注释。超过阈值就同步重建一次强行收敛。
|
||||
if (++m_layerRescheduleStreak > 90)
|
||||
{
|
||||
m_layerRescheduleStreak = 0;
|
||||
RebuildWideBuffer();
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_layerJobMutex);
|
||||
for (int index : pending)
|
||||
{
|
||||
bool exists = false;
|
||||
for (const LayerJob& job : m_layerJobs)
|
||||
{
|
||||
if (job.laneIndex == index)
|
||||
{
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
LayerJob job;
|
||||
job.laneIndex = index;
|
||||
m_layerJobs.push_back(job);
|
||||
}
|
||||
}
|
||||
}
|
||||
EnsureLayerWorker();
|
||||
m_layerJobCv.notify_all();
|
||||
}
|
||||
|
||||
m_area = savedArea;
|
||||
m_plotRect = savedPlot;
|
||||
Tab().ViewMin() = savedViewMin;
|
||||
Tab().ViewMax() = savedViewMax;
|
||||
BuildLaneVisuals();
|
||||
void PlotView::EnsureLayerWorker()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_layerJobMutex);
|
||||
if (m_layerWorker.joinable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_layerWorkerStop = false;
|
||||
m_layerWorker = std::thread([this]() { LayerWorkerLoop(); });
|
||||
}
|
||||
|
||||
void PlotView::StopLayerWorker()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_layerJobMutex);
|
||||
if (!m_layerWorker.joinable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_layerWorkerStop = true;
|
||||
m_layerJobs.clear();
|
||||
}
|
||||
m_layerJobCv.notify_all();
|
||||
m_layerWorker.join();
|
||||
}
|
||||
|
||||
void PlotView::LayerWorkerLoop()
|
||||
{
|
||||
// 后台绘制不能与 UI 抢 CPU:降到低优先级(前台仍然"想跑多快跑多快")。
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||
for (;;)
|
||||
{
|
||||
LayerJob job;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_layerJobMutex);
|
||||
m_layerJobCv.wait(lock, [this]() { return m_layerWorkerStop || !m_layerJobs.empty(); });
|
||||
if (m_layerWorkerStop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
job = m_layerJobs.front();
|
||||
m_layerJobs.erase(m_layerJobs.begin());
|
||||
}
|
||||
if (job.laneIndex < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// ⚠️ **前台优先**:前台刚刚还在绘制(典型:用户正在拖拽/平移)时,后台主动让路,
|
||||
// 把任务放回队首稍后再试。否则后台会不断抢占 `m_renderMutex`,前台每帧都要等
|
||||
// 一个图层画完,反而比“单线程同步绘制”更卡。交互停下来后后台再从容补画。
|
||||
if (GetTickCount64() - m_lastPaintTick.load(std::memory_order_relaxed) < 30)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_layerJobMutex);
|
||||
m_layerJobCv.wait_for(lock, std::chrono::milliseconds(8));
|
||||
m_layerJobs.insert(m_layerJobs.begin(), job);
|
||||
continue;
|
||||
}
|
||||
// 同一网格若有更新的任务在排队,本次直接跳过(以最新状态为准)。
|
||||
{
|
||||
bool superseded = false;
|
||||
std::lock_guard<std::mutex> lock(m_layerJobMutex);
|
||||
for (const LayerJob& other : m_layerJobs)
|
||||
{
|
||||
if (other.laneIndex == job.laneIndex)
|
||||
{
|
||||
superseded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (superseded)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
{
|
||||
// ⚠️ 加锁顺序必须与前台一致:**先 `m_renderMutex` 再 `m_layerMutex`**。
|
||||
// 反序会与 `Paint`(渲染中会去锁图层元数据)构成经典的 AB-BA 死锁。
|
||||
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
|
||||
std::lock_guard<std::mutex> layerLock(m_layerMutex);
|
||||
if (job.laneIndex >= static_cast<int>(m_layerBack.size()) ||
|
||||
m_layerBack.size() != m_lanes.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!RenderLaneLayerRegion(job.laneIndex, m_layerBack[static_cast<std::size_t>(job.laneIndex)],
|
||||
0, 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 后台缓冲画好 → 立即与前台缓冲整体交换(前台始终有立即可用的像素)。
|
||||
std::swap(m_layerFront[static_cast<std::size_t>(job.laneIndex)],
|
||||
m_layerBack[static_cast<std::size_t>(job.laneIndex)]);
|
||||
m_layerAnchorMin = Tab().ViewMin();
|
||||
m_layerAnchorValid = true;
|
||||
m_layerRenderCount.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
// 任务之间让出 CPU / 锁:避免后台连续抢占 `m_renderMutex`,把前台绘制挤出去
|
||||
//(后台只是预取图层,晚一两毫秒无关紧要;前台才需要“想跑多快跑多快”)。
|
||||
Sleep(1);
|
||||
// 通知宿主重绘:绘制线程下一帧就会取到刚交换过来的图层。
|
||||
const HWND host = m_host;
|
||||
if (host != nullptr)
|
||||
{
|
||||
PostMessageW(host, kMessagePlotDerivedReady, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::PaintRubberBand(gd::Graphics& g)
|
||||
|
||||
Reference in new issue
Block a user