网格底衬永不空白 + 动画流畅性根治 + 光标随首个信号重现 + 「放大自动点线」新判据
一、网格底衬消失(半屏/局部/全屏,且要等一会才刷新好)—— 两个机制叠加 1) BlitWideBuffer 对「跨度已过期」的图层原用 StretchBlt 放大占位:深度放大时把一小段像素放大 几十倍 ⇒ 整块网格变成一片无网格的模糊色块(用户看到的就是「网格叠底消失、没有任何内容」)。 修法:删除占位拉伸;对拿不到(完整)图层像素的网格【就地补画网格底衬】 (新增 PaintLaneGridBacking + m_renderGridOnly,复用 PaintLane 的网格部分,跳过波形/散点/ 派生曲线/Y 轴刻度/图例)。底衬代价极低,可每帧执行 ⇒ 网格永不空白/闪烁。 ⚠️ 顺序:先画底衬(GDI+)再 g.GetHDC() 做 BitBlt;只有全部网格都贴不出像素时才交给整幅重绘。 2) 待重画队列按下标顺序消费(一次 1 个)⇒ 上百个网格时可视网格要等上百帧。 修法:RenderPendingLaneRebuilds 里做稳定划分,【可视网格优先】(LaneVisuallyRelevant); 「结算」判据由 LanesAllReadyForView() 改为 LanesVisiblyReadyForView()。 二、过渡动画卡顿/不平顺/直接闪到终态 —— 三处根因 1) 「m_layerRebuildPending 非空就每帧请求再送一帧」:上百个网格时 700ms 内挤出 124 次重绘, 而 WM_TIMER 优先级低于投递消息 ⇒ 动画定时器被饿死(实测 frames=124 / timerTicks=1)。 修法:新增 AnyVisibleLanePending(),只用「还有没有可视网格待补」决定是否请求再送一帧; 离屏网格改为机会式预热(实测 frames=25 / timerTicks=16)。 2) SetTimer 的间隔会被量化到系统 tick(默认 15.6ms):请求 16ms 实际等到下一个 tick ⇒ 实测帧距 ~26ms(只有约 38fps)。修法:kScrollTimerPeriodMs 16 → 10(实测 ~15.5ms ≈ 64fps; 真实消息循环帧距 ~25ms → ~16ms)。实测映射:8→15.8 / 10→15.5 / 12→16.5 / 16→26.4 / 31→37.9。 ⚠️ timeBeginPeriod(1) 在本机无效(已 A/B 验证)。 3) 网格滚动联动 / 「回到最上方」每帧都改 LaneScroll(),而它原本参与「宽基底可用性」判据 (SceneGeometryHash)⇒ 每一帧都退化成整幅重绘。修法:SceneContentHash 新增 includeLaneScroll 形参, 宽基底可用性判据不计网格滚动量/各网格屏幕纵向位置(它们只是图层的贴图落点); 静态场景缓存的指纹仍然计入。实测网格滚动动画单帧 18.36ms → 1.19ms。 三、光标工具激活且没有任何信号绘制时,首个信号出现后按规则重新出现(用户要求) 新增 PlotView::ResetCursorPlacementOnFreshContent()(规则与 CycleCursorMode() 的「首次出现」同源: 单光标置于视图中心;双光标第二条紧邻第一条且落在可见范围内),由 Application::SynchronizeView 的「首次出现可见信号」分支在 FitAll() 之后调用。 实测:单光标 stale=4.60 → 新的视图中心 20.92;双光标 stale=(2.89,7.18) → (20.92,26.07)。 四、「放大自动点线」规则改为:任一小格内数据点少于 5 个即自动切换(用户重新定义) 新增 PlotView::AutoPointLineNeeded()(推翻此前「一屏内数据点 < N×4」):按 GridMinorCellCount() 切成横向小格,只要存在任何一个小格内有效数据点 < kAutoPointLineMinPointsPerCell(=5) 就切「点线」。 实现:先「总有效点 < N×5」鸽巢 O(1) 充分判定;单调时间轴逐小格两次二分取区间(单格累计到 5 个即早退, 另给 kAutoPointLineCellScanCap=512 封顶);非单调时间轴一次有界扫描分桶。保留「视界内至少一个可见点」保护。 ⚠️ 光标吸附仍用旧的整屏判据 SignalSnapEnabled(用户明确要求「其它规则不变」)。 验证(全部离屏,不占用鼠标/键盘/屏幕) - 新增 .preflight/perf/gridanim_main.cpp(gridanim.exe)55/55: [A] 缩放/适应视图/「预热未完成再滚动」等每个用例逐帧断言「每个可视网格都有网格底衬 (有竖直锐边、无模糊占位、无未绘制带)」;[B] 直接驱动 + 真实消息循环两条路径统计每帧耗时、 帧距均值/最大与进度单调性;[C] 光标重现;[D] 自动点线新判据(稠密不切、稀疏切换、恢复原样式)。 - 既有回归:verify 49/49、panelanim 156/156、datacheck 69/69、edgecheck 46/46、errcheck 19/19。 - make release 零错误零警告(使用 TARGET_FILE_NAME=TargetCheck 以避免与用户正在运行的实例冲突)。 文档:Documents/agent_mem.md 新增 §9.42、§11.2 第 117–124 条与变更日志一行。
This commit is contained in:
1 parent
f845616938
commit
34375481de
7 files changed
+702
-168
No files matched your search
@@ -822,7 +822,10 @@ namespace jdi
|
||||
// * **全部图层就绪**:还有网格没画好时不能结算,否则结算帧之后的格子又是旧的。
|
||||
// * 记录已消费本 key,避免下一帧重复结算。
|
||||
const bool viewStopped = (m_wideLastValid && m_wideLastKey == wideKey);
|
||||
if (viewStopped && m_dragMode == PlotDragMode::NONE && LanesAllReadyForView())
|
||||
// ⚠️ 结算只用**可视网格**的就绪状态:上百个网格时若要求全部离屏图层都就绪,
|
||||
// 队列按下标顺序推进会让“结算”拖到上百帧之后(期间既没有静态场景缓存可用、
|
||||
// 又每帧都在做无谓的图层重建),而离屏图层的就绪与否与画面无关。
|
||||
if (viewStopped && m_dragMode == PlotDragMode::NONE && LanesVisiblyReadyForView())
|
||||
{
|
||||
m_wideLastValid = false; // 结算:落到下方整幅重绘(非拖拽 → 全质量)
|
||||
}
|
||||
@@ -852,24 +855,31 @@ namespace jdi
|
||||
// 且**仍按预算逐个补**(不用一次把全部过期网格塞进同一帧):实测同一台机器上 4 线程
|
||||
// 真并行(3.98x),但**图层光栅化(GDI+)本身会被串行化**,并行重建 5 个网格 ≈ 逐个重建
|
||||
//(1.03x)→ 一次性补齐只会把一帧从 ~10ms 抬到 ~31ms(更容易掉帧)。
|
||||
// 因此保持"视界停后**每帧补一个网格**"(`RenderPendingLaneRebuilds` 在 `budget>1` 时才走并行)。
|
||||
const bool morePending =
|
||||
viewStopped ? RenderPendingLaneRebuilds(1) : !m_layerRebuildPending.empty();
|
||||
// 仍按预算逐个补:不用一次把全部过期网格塞进同一帧(并行重建≈逐个重建)。
|
||||
// ⚠️ **“是否再来一帧”只看还有没有【可视】网格待补**:上百个离屏网格若每帧都
|
||||
// 请求重绘,会形成持续一秒以上的高频重绘,把动画定时器饿死(见 `AnyVisibleLanePending`)。
|
||||
if (viewStopped)
|
||||
{
|
||||
RenderPendingLaneRebuilds(1);
|
||||
}
|
||||
const bool morePending = AnyVisibleLanePending();
|
||||
// 绘图区底色(覆盖时间轴与侧边留白区域)。
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
// 贴基底(绘图区:网格 + 波形)。图层没覆盖到的部分先留空,后续帧补齐。
|
||||
if (BlitWideBuffer(g))
|
||||
// 时间轴(宽缓冲不含它,前台按当前视图绘制)—— 必须在贴图**之前**算好:
|
||||
// `BlitWideBuffer` 在图层像素缺失时要就地补画网格底衬,而底衬的竖向网格线
|
||||
// 与时间轴共用同一套主刻度。
|
||||
const double wideViewMin = Tab().ViewMin();
|
||||
const double wideViewMax = Tab().ViewMax();
|
||||
const double wideSpanNow = wideViewMax - wideViewMin;
|
||||
const double wideTimeStep =
|
||||
NiceStep((wideSpanNow > 0.0) ? wideSpanNow : 1.0, kTimeTickTarget);
|
||||
std::vector<double> wideTimeTicks;
|
||||
BuildTicks(wideViewMin, wideViewMax, wideTimeStep, wideTimeTicks);
|
||||
// 贴基底(绘图区:网格 + 波形)。拿不到图层像素的网格由底衬兜底,永不空白。
|
||||
if (BlitWideBuffer(g, wideTimeTicks))
|
||||
{
|
||||
m_wideLastKey = wideKey;
|
||||
m_wideLastValid = true;
|
||||
// 时间轴:宽缓冲不含它,前台按当前视图绘制。
|
||||
const double wideViewMin = Tab().ViewMin();
|
||||
const double wideViewMax = Tab().ViewMax();
|
||||
const double wideSpanNow = wideViewMax - wideViewMin;
|
||||
const double wideTimeStep =
|
||||
NiceStep((wideSpanNow > 0.0) ? wideSpanNow : 1.0, kTimeTickTarget);
|
||||
std::vector<double> wideTimeTicks;
|
||||
BuildTicks(wideViewMin, wideViewMax, wideTimeStep, wideTimeTicks);
|
||||
PaintTimeAxis(g, wideTimeTicks, wideTimeStep);
|
||||
// 网格分界线:图层里**不含**它(它锚定在网格的屏幕位置,会被纵向偏移贴图带走),
|
||||
// 因此在这里补画一次(与整幅重绘同一套颜色与线宽)。
|
||||
@@ -1253,7 +1263,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::SceneContentHash(bool includeStyles, bool includeDerived,
|
||||
bool includeLaneRange) const
|
||||
bool includeLaneRange, bool includeLaneScroll) const
|
||||
{
|
||||
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
||||
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
||||
@@ -1287,10 +1297,17 @@ namespace jdi
|
||||
mixFloat(m_plotRect.Height);
|
||||
mixFloat(m_laneContentHeight);
|
||||
mix(m_scrollBarVisible ? 1ull : 0ull);
|
||||
// ⚠️ 网格纵向滚动量必须参与指纹:它只改变各网格的绘制位置。
|
||||
// ⚠️ 网格纵向滚动量必须参与**静态场景缓存**的指纹:它只改变各网格的绘制位置。
|
||||
// 若不参与,平移宽缓冲区 / 静态场景缓存会把旧滚动位置的像素当成当前内容贴回,
|
||||
// 表现为“网格位置错位 / 拖影 / 残留”等错误元素。
|
||||
mixFloat(Tab().LaneScroll());
|
||||
// ⚠️ 但它是**纯屏幕落点**(不影响任何网格内容),因此“宽基底可用性”判据
|
||||
//(`SceneGeometryHash`)把它排除:网格滚动时 `BlitWideBuffer` 本来就按 `lane.rect`
|
||||
// 逐网格贴图,内容完全复用 —— 这样“网格滚动到可视区 / 回到最上方”这类
|
||||
// **每帧都在改滚动量的动画**才不会退化成每帧整幅重绘(用户 2026-10-17 报告卡顿)。
|
||||
if (includeLaneScroll)
|
||||
{
|
||||
mixFloat(Tab().LaneScroll());
|
||||
}
|
||||
|
||||
// 选中信号会改变图例条目的底色与文字颜色(绘制在缓存的静态场景里),
|
||||
// 因此必须参与指纹:否则在左侧信号面板点选后图例不会高亮联动。
|
||||
@@ -1304,7 +1321,11 @@ namespace jdi
|
||||
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
|
||||
{
|
||||
const LaneVisual& visual = m_lanes[laneIndex];
|
||||
mixFloat(visual.rect.Y);
|
||||
// ⚠️ 各网格的屏幕纵向位置同样只是“贴图落点”(见 `includeLaneScroll` 的说明)。
|
||||
if (includeLaneScroll)
|
||||
{
|
||||
mixFloat(visual.rect.Y);
|
||||
}
|
||||
mixFloat(visual.rect.Height);
|
||||
// ⚠️ 幅值范围只在 `includeLaneRange` 时计入:拖拽平移时的纵向分量会每帧小幅改动它,
|
||||
// 若参与“宽基底可用性”判据,每一帧都会退化成整幅重绘(质量下降 + 卡顿)。
|
||||
@@ -1395,8 +1416,12 @@ namespace jdi
|
||||
std::uint64_t PlotView::SceneGeometryHash() const
|
||||
{
|
||||
// 外观样式不参与;各网格的幅值范围也不参与(那由逐网格图层指纹负责,
|
||||
// 这样拖拽时的纵向分量不会把整幅重绘的快路径打掉)。
|
||||
return SceneContentHash(false, true, false);
|
||||
// 这样拖拽时的纵向分量不会把整幅重绘的快路径打掉);
|
||||
// ⚠️ 网格纵向滚动量 / 各网格屏幕纵向位置也不参与:那只是图层的**贴图落点**,
|
||||
// 不影响任何网格的像素内容(`BlitWideBuffer` 按 `lane.rect` 逐网格贴图),
|
||||
// 因此滚动网格时可以继续复用宽基底(否则“滚动到可视区 / 回到最上方”的
|
||||
// 每一帧都会退化成整幅重绘 —— 用户 2026-10-17 报告的滚动动画卡顿)。
|
||||
return SceneContentHash(false, true, false, false);
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::SceneDecorHash() const
|
||||
@@ -2415,6 +2440,57 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::LaneVisuallyRelevant(int laneIndex) const
|
||||
{
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!(m_plotRect.Height > 0.0f))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const gd::RectF& rect = m_lanes[static_cast<std::size_t>(laneIndex)].rect;
|
||||
// 用一个网格高的余量:即将滚入视区的网格也算相关,避免滚动时出现一帧空档。
|
||||
const float margin = std::max(rect.Height, 0.0f);
|
||||
return (rect.Y + rect.Height + margin > m_plotRect.Y) &&
|
||||
(rect.Y - margin < m_plotRect.Y + m_plotRect.Height);
|
||||
}
|
||||
|
||||
bool PlotView::LanesVisiblyReadyForView() const
|
||||
{
|
||||
if (m_lanes.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool anyRelevant = false;
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
if (!LaneVisuallyRelevant(static_cast<int>(index)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
anyRelevant = true;
|
||||
if (!LaneLayerReady(static_cast<int>(index)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return anyRelevant;
|
||||
}
|
||||
|
||||
bool PlotView::AnyVisibleLanePending() const
|
||||
{
|
||||
for (const int laneIndex : m_layerRebuildPending)
|
||||
{
|
||||
if (LaneVisuallyRelevant(laneIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PlotView::QueueLaneRebuild(int laneIndex)
|
||||
{
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
@@ -2437,6 +2513,12 @@ namespace jdi
|
||||
{
|
||||
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` 足够大时一次取完(视界已停时调用方会传很大的预算)。
|
||||
const int available = static_cast<int>(m_layerRebuildPending.size());
|
||||
@@ -2906,7 +2988,10 @@ namespace jdi
|
||||
// 登记"跨度 / 内容不一致"的网格。空闲时视界未变 ⇒ `RollWideBuffer` 只登记、不做滚动,
|
||||
// 也不会走"位移超出画布 → 立刻整块重画"那条分支。
|
||||
RollWideBuffer();
|
||||
const bool morePending = RenderPendingLaneRebuilds(1);
|
||||
RenderPendingLaneRebuilds(1);
|
||||
// ⚠️ 只在还有**可视**网格待补时才请求再送一帧:离屏网格的数量可能上百,
|
||||
// 若按“队列非空”每帧请求重绘,会持续一秒以上地高频重绘并饿死动画(见 `AnyVisibleLanePending`)。
|
||||
const bool morePending = AnyVisibleLanePending();
|
||||
if (morePending)
|
||||
{
|
||||
const HWND host = m_host;
|
||||
@@ -3082,7 +3167,7 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
bool PlotView::BlitWideBuffer(gd::Graphics& g) const
|
||||
bool PlotView::BlitWideBuffer(gd::Graphics& g, const std::vector<double>& timeTicks)
|
||||
{
|
||||
const float plotWidth = m_plotRect.Width;
|
||||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||||
@@ -3095,146 +3180,164 @@ namespace jdi
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
const double scale = static_cast<double>(width) / span;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||||
if (m_layerFront.size() != m_lanes.size())
|
||||
/** 一次图层贴图(所有参数在释放图层锁之前算好,避免持锁期间做 GDI 绘制)。 */
|
||||
struct LayerBlit
|
||||
{
|
||||
HDC dc = nullptr;
|
||||
int srcX = 0;
|
||||
int srcY = 0;
|
||||
int destX = 0;
|
||||
int destY = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
std::vector<LayerBlit> blits;
|
||||
std::vector<int> backing; // 本帧拿不到(完整)图层像素的网格
|
||||
blits.reserve(m_lanes.size());
|
||||
backing.reserve(m_lanes.size());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_layerMutex);
|
||||
if (m_layerFront.size() != m_lanes.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||||
{
|
||||
const LaneLayer& layer = m_layerFront[index];
|
||||
const gd::RectF& lane = m_lanes[index].rect;
|
||||
const float top = std::max(lane.Y, m_plotRect.Y);
|
||||
const float bottom = std::min(lane.Y + lane.Height, m_plotRect.Y + m_plotRect.Height);
|
||||
if (bottom <= top)
|
||||
{
|
||||
continue; // 网格整体移出可视区
|
||||
}
|
||||
const int bandHeight = static_cast<int>(std::lround(bottom - top));
|
||||
// 纵向映射是否仍然可用:图层高度必须等于"当前网格高 + 上下预渲染余量",
|
||||
// 且时间跨度必须与当前视界一致(跨度不同则像素的时间映射已经不对)。
|
||||
const int expectBitmapHeight =
|
||||
static_cast<int>(std::ceil(lane.Height)) + 2 * LayerMarginY(lane.Height);
|
||||
bool usable = (layer.valid && layer.dc != nullptr && layer.width == bitmapWidth &&
|
||||
layer.height == expectBitmapHeight && layer.span > 0.0 &&
|
||||
std::fabs(layer.span - span) <= 1.0e-12);
|
||||
|
||||
LayerBlit blit;
|
||||
if (usable)
|
||||
{
|
||||
// ⚠️ **纵向偏移**:图层带「上下各半个网格高」的预渲染余量,幅值平移只改这里的
|
||||
// 源行偏移(`anchorYMax` 与当前 `yMax` 之差换算成像素),不必重画图层 ——
|
||||
// 这就是"纵向拖动与横向拖动同样是 O(1) 贴图"的关键。
|
||||
int vOffset = 0;
|
||||
if (layer.pxPerValue > 0.0)
|
||||
{
|
||||
vOffset = static_cast<int>(
|
||||
std::llround((layer.anchorYMax - m_lanes[index].yMax) * layer.pxPerValue));
|
||||
}
|
||||
int srcY = static_cast<int>(std::lround(top - lane.Y)) + layer.marginY + vOffset;
|
||||
int height = bandHeight;
|
||||
int destTopOffset = 0;
|
||||
// 超出画布的上下两端只贴**有像素的那一段**(其余由网格底衬补齐)。
|
||||
if (srcY < 0)
|
||||
{
|
||||
destTopOffset = -srcY;
|
||||
height -= destTopOffset;
|
||||
srcY = 0;
|
||||
}
|
||||
if (srcY + height > layer.height)
|
||||
{
|
||||
height = layer.height - srcY;
|
||||
}
|
||||
// ⚠️ **局部贴图**:图层只覆盖"视界 ± 半屏",视界可能有一部分落在图层之外。
|
||||
// 这里只贴**有像素的那一段**,其余由底衬补齐;整块跳过才是"网格瞬间变空"的直接原因。
|
||||
const double subX = (Tab().ViewMin() - layer.wideMin) * scale;
|
||||
int srcX = static_cast<int>(std::lround(subX));
|
||||
int destOffset = 0; // 从视界左端起偏移多少像素开始贴
|
||||
int blitWidth = width;
|
||||
if (srcX < 0)
|
||||
{
|
||||
destOffset = -srcX;
|
||||
blitWidth -= destOffset;
|
||||
srcX = 0;
|
||||
}
|
||||
if (srcX + blitWidth > bitmapWidth)
|
||||
{
|
||||
blitWidth = bitmapWidth - srcX;
|
||||
}
|
||||
if (height > 0 && blitWidth > 0)
|
||||
{
|
||||
blit.dc = layer.dc;
|
||||
blit.srcX = srcX;
|
||||
blit.srcY = srcY;
|
||||
blit.destX = static_cast<int>(std::lround(m_plotRect.X)) + destOffset;
|
||||
blit.destY = static_cast<int>(std::lround(top)) + destTopOffset;
|
||||
blit.width = blitWidth;
|
||||
blit.height = height;
|
||||
blits.push_back(blit);
|
||||
// 横向 / 纵向只有**部分覆盖**:该网格的其余部分也要靠底衬补齐。
|
||||
if (destOffset > 0 || blitWidth < width || height < bandHeight)
|
||||
{
|
||||
backing.push_back(static_cast<int>(index));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️⚠️ **必须兜底补齐网格底衬,绝不能留白**:旧实现在这里直接 `continue`
|
||||
//(网格空白)或用旧跨度像素 `StretchBlt` 拉伸占位(深度放大时把一小段像素放大
|
||||
// 几十倍 → 整块网格变成一片模糊底色)—— 两者在用户眼里都是
|
||||
// 「网格叠底消失,没有任何内容」。改为就地补画底衬(底色 + 网格线 + 分界线):
|
||||
// 代价极低,可以每帧执行,因此**网格底衬永远不会空白/闪烁**(用户 2026-10-17 要求)。
|
||||
backing.push_back(static_cast<int>(index));
|
||||
}
|
||||
}
|
||||
|
||||
if (blits.empty())
|
||||
{
|
||||
// 一个网格都贴不出像素(典型:布局变化后所有图层都失效)→ 交给调用方走整幅重绘,
|
||||
// 避免画面停留在"只有网格底衬、没有波形"的状态。
|
||||
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 false;
|
||||
return true;
|
||||
}
|
||||
bool any = false;
|
||||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||||
for (const LayerBlit& blit : blits)
|
||||
{
|
||||
const LaneLayer& layer = m_layerFront[index];
|
||||
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const gd::RectF& lane = m_lanes[index].rect;
|
||||
// 纵向映射是否仍然可用:图层高度必须等于"当前网格高 + 上下预渲染余量"。
|
||||
// 高度变了(布局变化)时像素无法映射,只能等整块重画。
|
||||
const int expectBitmapHeight =
|
||||
static_cast<int>(std::ceil(lane.Height)) + 2 * LayerMarginY(lane.Height);
|
||||
if (layer.height != expectBitmapHeight)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// ⚠️ **跨度过期 = 该图层是"旧跨度"的像素**(典型:选区放大 / 键盘缩放的瞬间)。
|
||||
// 旧实现直接 `continue` → 整个网格在重画完成前**空白**,且触发"同步整批重建"
|
||||
// (实测 70~80ms)把前台卡住。
|
||||
// 现在改为把它当作**占位图**用一次 StretchBlt 拉出来:旧图层覆盖的时间段
|
||||
// [layer.wideMin, layer.wideMin + layer.span] 重新映射到新的视界上 ——
|
||||
// 画面略糊(低分辨率),但**立即可用**;精确像素由后续按预算重画逐个替换。
|
||||
const bool spanStale = (std::fabs(layer.span - span) > 1.0e-12);
|
||||
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));
|
||||
int height = static_cast<int>(std::lround(bottom - top));
|
||||
if (height <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// ⚠️ **纵向偏移**:图层带「上下各半个网格高」的预渲染余量,幅值平移只改这里的
|
||||
// 源行偏移(`anchorYMax` 与当前 `yMax` 之差换算成像素),不必重画图层 ——
|
||||
// 这就是"纵向拖动与横向拖动同样是 O(1) 贴图"的关键。
|
||||
int srcY = 0;
|
||||
{
|
||||
int vOffset = 0;
|
||||
if (layer.pxPerValue > 0.0)
|
||||
{
|
||||
vOffset = static_cast<int>(
|
||||
std::llround((layer.anchorYMax - m_lanes[index].yMax) * layer.pxPerValue));
|
||||
}
|
||||
srcY = static_cast<int>(std::lround(top - lane.Y)) + layer.marginY + vOffset;
|
||||
}
|
||||
// 超出画布的上下两端只贴**有像素的那一段**(其余由底色填充,本帧的预算内重画会补齐)。
|
||||
int destTopOffset = 0;
|
||||
if (srcY < 0)
|
||||
{
|
||||
destTopOffset = -srcY;
|
||||
height -= destTopOffset;
|
||||
srcY = 0;
|
||||
}
|
||||
if (srcY + height > layer.height)
|
||||
{
|
||||
height = layer.height - srcY;
|
||||
}
|
||||
if (height <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (spanStale)
|
||||
{
|
||||
// 占位拉伸:旧图层的时间跨度映射到当前视界(横向)。纵向映射不变。
|
||||
if (!(layer.span > 0.0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double dx0 = static_cast<double>(destX) + (layer.wideMin - Tab().ViewMin()) * scale;
|
||||
const double dx1 = dx0 + layer.span * scale;
|
||||
if (!(dx1 > dx0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double cx0 = std::max(dx0, static_cast<double>(destX));
|
||||
const double cx1 =
|
||||
std::min(dx1, static_cast<double>(destX) + static_cast<double>(width));
|
||||
if (cx1 - cx0 < 1.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double sx0 = (cx0 - dx0) / (dx1 - dx0) * static_cast<double>(layer.width);
|
||||
const double sx1 = (cx1 - dx0) / (dx1 - dx0) * static_cast<double>(layer.width);
|
||||
const int dstX = static_cast<int>(std::lround(cx0));
|
||||
const int dstW = std::max(1, static_cast<int>(std::lround(cx1 - cx0)));
|
||||
const int srcX2 = static_cast<int>(std::lround(sx0));
|
||||
const int srcW = std::max(1, static_cast<int>(std::lround(sx1 - sx0)));
|
||||
// HALFTONE 让拉伸后的占位图明显更接近成图(下采样尤其明显);置一次即可。
|
||||
SetStretchBltMode(target, HALFTONE);
|
||||
SetBrushOrgEx(target, 0, 0, nullptr);
|
||||
StretchBlt(target, dstX, destY + destTopOffset, dstW, height, layer.dc, srcX2, srcY,
|
||||
srcW, height, SRCCOPY);
|
||||
any = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ⚠️ **局部贴图**:图层只覆盖"视界 ± 半屏",视界可能有一部分落在图层之外。
|
||||
// 这里只贴**有像素的那一段**(其余留给底色填充,后续帧由预算内重画补齐),
|
||||
// 而不是整块跳过 —— 跳过会让整个网格瞬间变空("断线 / 空白"的直接原因)。
|
||||
const double subX = (Tab().ViewMin() - layer.wideMin) * scale;
|
||||
int srcX = static_cast<int>(std::lround(subX));
|
||||
int destOffset = 0; // 从视界左端起偏移多少像素开始贴
|
||||
int blitWidth = width;
|
||||
if (srcX < 0)
|
||||
{
|
||||
destOffset = -srcX;
|
||||
blitWidth -= destOffset;
|
||||
srcX = 0;
|
||||
}
|
||||
if (srcX + blitWidth > bitmapWidth)
|
||||
{
|
||||
blitWidth = bitmapWidth - srcX;
|
||||
}
|
||||
if (blitWidth <= 0)
|
||||
{
|
||||
continue; // 完全没有可用像素(该网格待重画)
|
||||
}
|
||||
BitBlt(target, destX + destOffset, destY + destTopOffset, blitWidth, height, layer.dc, srcX,
|
||||
srcY, SRCCOPY);
|
||||
any = true;
|
||||
BitBlt(target, blit.destX, blit.destY, blit.width, blit.height, blit.dc, blit.srcX, blit.srcY,
|
||||
SRCCOPY);
|
||||
}
|
||||
g.ReleaseHDC(target);
|
||||
return any;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 为单个网格绘制网格底衬(底色 + 横向/竖向网格线 + 网格分界线)。
|
||||
*
|
||||
* 复用 `PaintLane` 的网格部分(通过 `m_renderGridOnly` 跳过波形 / 散点 / 派生曲线 /
|
||||
* Y 轴刻度 / 图例),因此底衬与整幅重绘**逐像素同源** —— 兜底帧与就绪帧之间不会出现
|
||||
* 网格线跳变。它是"网格底衬永不空白"的唯一执行点(见 `BlitWideBuffer`)。
|
||||
*/
|
||||
bool PlotView::PaintLaneGridBacking(gd::Graphics& g, int laneIndex, const std::vector<double>& timeTicks)
|
||||
{
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()) || m_theme == nullptr ||
|
||||
m_fonts == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const bool savedGridOnly = m_renderGridOnly;
|
||||
m_renderGridOnly = true;
|
||||
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
|
||||
m_renderGridOnly = savedGridOnly;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::PaintRubberBand(gd::Graphics& g)
|
||||
@@ -3508,6 +3611,11 @@ namespace jdi
|
||||
{
|
||||
// 只画装饰:网格线与波形已由宽基底提供,这里跳过。
|
||||
}
|
||||
else if (m_renderGridOnly)
|
||||
{
|
||||
// 只画网格底衬(宽基底快路径的兜底帧):跳过波形、散点与派生预览曲线。
|
||||
// 网格线在上方已经画好(`!m_renderDecorOnly` 分支)。
|
||||
}
|
||||
else if (laneSignals.empty())
|
||||
{
|
||||
const gd::RectF visibleRect(lane.rect.X, clipTop, lane.rect.Width, clipBottom - clipTop);
|
||||
@@ -3681,7 +3789,8 @@ namespace jdi
|
||||
// 同时只在数值背后遮挡波形,不牺牲波形本身的可见范围。
|
||||
// ⚠️ 渲染「宽缓冲区」时跳过 Y 轴刻度与图例:它们锚定在网格左缘、随视界横向移动,
|
||||
// 而宽缓冲区会被平移截取,装饰必须由前台叠加层(装饰层缓存)单独绘制。
|
||||
if (!m_renderWide)
|
||||
// ⚠️ 「只画网格底衬」时同样跳过(底衬的定义就是不含装饰)。
|
||||
if (!m_renderWide && !m_renderGridOnly)
|
||||
{
|
||||
const bool showLabels = (lane.rect.Height >= 34.0f * m_scale);
|
||||
float lastLabelY = -1.0e9f;
|
||||
@@ -3767,7 +3876,7 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_renderWide && lane.rect.Height >= 24.0f * m_scale)
|
||||
if (!m_renderWide && !m_renderGridOnly && lane.rect.Height >= 24.0f * m_scale)
|
||||
{
|
||||
PaintLaneLegend(g, laneIndex, lane, clipTop, clipBottom);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user