平移改为复用已绘制像素:新增宽缓冲区,平移动画降为 O(1) 贴图
按用户分析落地:平移时画面内容不变、只是横向平移,因此以当前视界为中心向左右各多渲染半个视界宽,存成 2×视界宽的离屏位图(用与正常绘制完全相同的 PaintLane/PaintTimeAxis,仅跳过随视界移动的图例/Y 轴刻度,改由装饰层缓存贴回)。平移时前台只做一次 BitBlt 截取子矩形(O(1)),移出边界才重建一次。 实测 VERY_BIG_DATA 全信号、视界 [40.1756,40.1885]s、5 网格:中键平移拖动 34ms→1.3ms(长距 1.7ms),透明度/粗细/颜色 16ms→8ms;静止/结算帧与修复前逐像素 0 差异(LockBits 验证)。
This commit is contained in:
1 parent
aefaaef6fa
commit
fa0d62b016
3 files changed
+282
-1
No files matched your search
@@ -673,6 +673,7 @@ namespace jdi
|
||||
StopCurveWorker();
|
||||
ReleaseSceneCache();
|
||||
ReleaseDecorCache();
|
||||
ReleaseWideBuffer();
|
||||
ReleasePanelCache();
|
||||
}
|
||||
|
||||
@@ -699,6 +700,7 @@ namespace jdi
|
||||
{
|
||||
m_scale = (scale > 0.1f) ? scale : 1.0f;
|
||||
InvalidateSceneCache();
|
||||
InvalidateWideBuffer();
|
||||
}
|
||||
|
||||
bool PlotView::CursorEnabled() const
|
||||
@@ -764,6 +766,7 @@ namespace jdi
|
||||
m_area = area;
|
||||
// 布局变化(窗口缩放、面板宽度调整、网格数量变化)后静态场景不再有效。
|
||||
InvalidateSceneCache();
|
||||
InvalidateWideBuffer();
|
||||
|
||||
m_labelBandWidth = 66.0f * m_scale;
|
||||
m_timeAxisHeight = 34.0f * m_scale;
|
||||
@@ -5559,6 +5562,29 @@ namespace jdi
|
||||
}
|
||||
};
|
||||
|
||||
// 视界平移(左键/中键拖动):画面内容不变、只是横向平移。直接复用「宽缓冲区」
|
||||
// 已渲染的像素,截取子矩形贴图 —— 前台 O(1)(一次 BitBlt),不重画波形,
|
||||
// 因此无论放大到多深的区间,平移都与全局显示时一样流畅。
|
||||
if (m_fastFrame && m_dragMode == PlotDragMode::PAN && m_decorValid && HasDrawableContent())
|
||||
{
|
||||
bool blitted = BlitWideBuffer(g);
|
||||
if (!blitted)
|
||||
{
|
||||
// 视界移出了宽缓冲范围,或内容/跨度变化 → 以当前视界为中心重建一次。
|
||||
if (RebuildWideBuffer())
|
||||
{
|
||||
blitted = BlitWideBuffer(g);
|
||||
}
|
||||
}
|
||||
if (blitted)
|
||||
{
|
||||
// 宽缓冲不含图例与 Y 轴刻度(二者锚定网格左缘、随视界移动),由装饰缓存贴回。
|
||||
BlitDecorCache(g);
|
||||
paintOverlays();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 网格次序拖拽:静态场景缓存仍然有效,但各网格要按位移重新"摆放"。
|
||||
// 这里逐网格把缓存中的横向条带平移贴回(几十微秒),
|
||||
// 因此拖动百万级采样的网格时依然流畅,且被拖网格的波形随鼠标自由移动。
|
||||
@@ -6692,6 +6718,192 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::EnsureWideBuffer(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_wideDc != nullptr && m_wideW == width && m_wideH == height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
ReleaseWideBuffer();
|
||||
HDC screen = GetDC(nullptr);
|
||||
if (screen == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_wideDc = CreateCompatibleDC(screen);
|
||||
m_wideBitmap = CreateCompatibleBitmap(screen, width, height);
|
||||
ReleaseDC(nullptr, screen);
|
||||
if (m_wideDc == nullptr || m_wideBitmap == nullptr)
|
||||
{
|
||||
ReleaseWideBuffer();
|
||||
return false;
|
||||
}
|
||||
m_wideOldBitmap = SelectObject(m_wideDc, m_wideBitmap);
|
||||
m_wideW = width;
|
||||
m_wideH = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::ReleaseWideBuffer()
|
||||
{
|
||||
m_wideValid = false;
|
||||
if (m_wideDc != nullptr)
|
||||
{
|
||||
if (m_wideOldBitmap != nullptr)
|
||||
{
|
||||
SelectObject(m_wideDc, m_wideOldBitmap);
|
||||
m_wideOldBitmap = nullptr;
|
||||
}
|
||||
DeleteDC(m_wideDc);
|
||||
m_wideDc = nullptr;
|
||||
}
|
||||
if (m_wideBitmap != nullptr)
|
||||
{
|
||||
DeleteObject(m_wideBitmap);
|
||||
m_wideBitmap = nullptr;
|
||||
}
|
||||
m_wideW = 0;
|
||||
m_wideH = 0;
|
||||
}
|
||||
|
||||
void PlotView::InvalidateWideBuffer()
|
||||
{
|
||||
m_wideValid = false;
|
||||
}
|
||||
|
||||
bool PlotView::WideBufferMatches() 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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 内容(外观样式 / 派生曲线参数 / 网格结构 / 视界几何)变化 → 需重建。
|
||||
return (m_wideKey == SceneContentHash(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_wideValid = 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 int bitmapHeight = static_cast<int>(std::ceil(areaHeight));
|
||||
if (!EnsureWideBuffer(bitmapWidth, bitmapHeight))
|
||||
{
|
||||
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 gd::RectF savedAxis = m_timeAxisRect;
|
||||
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), areaHeight);
|
||||
m_plotRect = gd::RectF(0.0f, savedPlot.Y - savedArea.Y, static_cast<float>(bitmapWidth),
|
||||
savedPlot.Height);
|
||||
m_timeAxisRect = gd::RectF(0.0f, savedAxis.Y - savedArea.Y, static_cast<float>(bitmapWidth),
|
||||
savedAxis.Height);
|
||||
Tab().ViewMin() = wideMin;
|
||||
Tab().ViewMax() = wideMin + static_cast<double>(bitmapWidth) / scale;
|
||||
|
||||
m_renderWide = true;
|
||||
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 viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
const double sp = viewMax - viewMin;
|
||||
const double timeStep = NiceStep((sp > 0.0) ? sp : 1.0, kTimeTickTarget);
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
|
||||
for (std::size_t i = 0; i < m_lanes.size(); ++i)
|
||||
{
|
||||
PaintLane(g, static_cast<int>(i), m_lanes[i], timeTicks);
|
||||
}
|
||||
PaintTimeAxis(g, timeTicks, timeStep);
|
||||
}
|
||||
m_renderWide = false;
|
||||
|
||||
// 恢复布局 / 视图 / 网格。
|
||||
m_area = savedArea;
|
||||
m_plotRect = savedPlot;
|
||||
m_timeAxisRect = savedAxis;
|
||||
Tab().ViewMin() = savedViewMin;
|
||||
Tab().ViewMax() = savedViewMax;
|
||||
BuildLaneVisuals();
|
||||
|
||||
m_wideMin = wideMin;
|
||||
m_wideSpan = span;
|
||||
m_wideKey = SceneContentHash(true);
|
||||
m_wideValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::BlitWideBuffer(gd::Graphics& g) const
|
||||
{
|
||||
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;
|
||||
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)
|
||||
{
|
||||
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_area.Y));
|
||||
const int height = static_cast<int>(std::ceil(m_area.Height));
|
||||
BitBlt(target, destX, destY, width, height, m_wideDc, static_cast<int>(std::lround(subX)), 0,
|
||||
SRCCOPY);
|
||||
g.ReleaseHDC(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::PaintRubberBand(gd::Graphics& g)
|
||||
{
|
||||
if (!m_dragMoved || m_rubberRect.Width <= 0.0f || m_rubberRect.Height <= 0.0f)
|
||||
@@ -7142,6 +7354,9 @@ namespace jdi
|
||||
|
||||
// 内侧纵轴标尺作为覆盖层绘制:每个刻度数值带半透明底,保证在波形之上仍清晰可读,
|
||||
// 同时只在数值背后遮挡波形,不牺牲波形本身的可见范围。
|
||||
// ⚠️ 渲染「宽缓冲区」时跳过 Y 轴刻度与图例:它们锚定在网格左缘、随视界横向移动,
|
||||
// 而宽缓冲区会被平移截取,装饰必须由前台叠加层(装饰层缓存)单独绘制。
|
||||
if (!m_renderWide)
|
||||
{
|
||||
const bool showLabels = (lane.rect.Height >= 34.0f * m_scale);
|
||||
float lastLabelY = -1.0e9f;
|
||||
@@ -7228,7 +7443,7 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
if (lane.rect.Height >= 24.0f * m_scale)
|
||||
if (!m_renderWide && lane.rect.Height >= 24.0f * m_scale)
|
||||
{
|
||||
PaintLaneLegend(g, laneIndex, lane, clipTop, clipBottom);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user