修复缩放后平移的拖影/残留/错误元素与深放大区间卡顿:网格纵向滚动量纳入内容指纹、样式补丁增加视界校验、宽基底不再用含装饰的画面初始化、装饰缓存改为每帧重捕获并记录矩形与坐标原点、装饰底板改不透明以支持跨视界复用;交互帧 9~19ms 降至 4.5~6ms,纵向拖动残留 2 万+ 通道降至 72 通道,静止/结算帧像素精确;新增离屏诊断模式 6(逐帧计时 + 逐像素对比)

This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-12 02:40:56 +08:00
1 parent 1c9c43233d
commit e22749b57a
3 files changed
+224 -58

No files matched your search

+39 -9
View File
@@ -2154,19 +2154,23 @@ namespace jdi
*/
bool m_sceneDegraded = false;
// ---- 「装饰层」像素缓存(水平平移时不变的内容:Y 轴刻度胶囊 + 图例)----
// 平移会让视图每帧变化 → 静态场景缓存必然失效 → 每帧都要重画波形**以及**网格线、
// Y 轴刻度文字、图例(实测后者占整幅重绘的一半以上)。但这些装饰内容**只取决于网格几何与
// 信号外观,与时间轴视图无关**,平移时不变化。因此在每张「全质量帧」后把装饰区域的像素
// 捕获下来,拖拽(快速帧)时直接贴回,省去每帧重绘文字与圆角底板的开销。
// 仅在完全相同(`m_decorKey` 命中)时启用,避免几何/外观变化时用到过期像素。
// ---- 「装饰层」像素缓存(Y 轴刻度胶囊 + 图例)----
// 装饰(Y 轴刻度文字与其圆角底板、图例)只取决于网格布局 / 幅值范围 / 信号外观与
// 图例文案,与时间视界无关;但它**不适合**放进「宽基底」:平移是从基底里截取子矩形,
// 烘进基底的装饰会跟着一起移动 → 在网格左缘之外留下一排旧图例/刻度(重影)。
// 因此单独缓存一帧**按当前视界绘制**的装饰像素,并在内容指纹完全一致时整块贴回。
//
// ⚠️ 关键不变量:**每一帧“真正绘制了装饰”都要重新捕获**(无论是否快速帧)。
// 只要保证“缓存像素”与“缓存指纹”永远同源,贴回就不可能过期;
// 反之(例如只在全质量帧捕获)会在几何/外观变化后贴回旧像素,
// 表现为残留 / 黑块 / 错位等错误元素。
/**
* @brief 确保装饰缓存位图尺寸正确(不足则重建)。
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
* @return 可用返回 true。
*/
bool EnsureDecorCache(int width, int height);
/** @brief 释放装饰缓存位图。 */
/** @brief 释放装饰缓存位图并作废缓存。 */
void ReleaseDecorCache();
/**
* @brief 把本帧记录下来的装饰矩形从后备缓冲区捕获进装饰缓存。
@@ -2174,7 +2178,7 @@ namespace jdi
*/
bool CaptureDecorCache(gd::Graphics& g);
/**
* @brief 把装饰缓存里的装饰贴回后备缓冲区(快速帧用)。
* @brief 把装饰缓存里的装饰贴回后备缓冲区(本帧跳过了装饰绘制时使用)。
* @param [IN,OUT] g 绘图上下文。 @return 贴图成功返回 true。
*/
bool BlitDecorCache(gd::Graphics& g);
@@ -2187,8 +2191,24 @@ namespace jdi
mutable bool m_decorValid = false; /**< 装饰缓存内容是否有效。 */
mutable std::uint64_t m_decorKey = 0; /**< 装饰缓存对应的内容指纹。 */
mutable bool m_useDecorCache = false; /**< 本帧是否跳过绘制装饰、改用缓存贴回。 */
/** 本帧需要捕获 / 贴回的装饰矩形(各网格的 Y 轴胶囊与图例带)。 */
/** 本帧需要捕获的装饰矩形(各网格的 Y 轴胶囊与图例带)。 */
mutable std::vector<gd::RectF> m_decorRects;
/**
* @brief 缓存里那批像素对应的装饰矩形(捕获时与像素同时保存)。
*
* ⚠️ 贴回必须用这一份,**不能**用当前的 `m_decorRects`:
* 命中缓存时各网格会跳过绘制、根本不会重建矩形列表(为空),
* 若用当前列表就会贴错位置或一个都不贴(表现为装饰消失 / 黑块 / 错位)。
*/
mutable std::vector<gd::RectF> m_decorCapturedRects;
/**
* @brief 捕获缓存像素时使用的绘图区原点(`m_area` 左上角)。
*
* ⚠️ 贴回时**必须用这一份**推算源偏移:宽缓冲区渲染会临时改写 `m_area`,
* 若捕获与贴回两帧的 `m_area` 不同,直接用当前 `m_area` 作原点会让整块装饰错位。
*/
mutable int m_decorOriginX = 0;
mutable int m_decorOriginY = 0;
// ---- 平移「宽缓冲区」:平移只改变绘制坐标、不改变画面内容 ----
// 视界平移时整个波形并不改变,只是横向平移。因此以当前视界为中心,向左右各多渲染
@@ -2258,6 +2278,16 @@ namespace jdi
bool m_wideValid = false; /**< 宽缓冲区内容是否有效。 */
std::uint64_t m_wideKey = 0; /**< 宽缓冲区对应的内容指纹(外观/派生参数等)。 */
bool m_renderWide = false; /**< 正在渲染宽缓冲区(重入 `PaintLane` 时跳过装饰)。 */
/**
* @brief 本次 `PaintLane` 只绘制「装饰」(Y 轴刻度胶囊 + 图例 + 网格分界线)。
*
* 宽缓冲区只存「基底」(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**:
* 因为装饰锚定在网格左缘、随视界横向移动,若烘进基底,平移截取时会产生重影。
* 因此前台每帧用本标志重画一次装饰(复用与整幅渲染**完全相同**的绘制代码,
* 保证逐像素一致),而不是去贴回任何缓存像素。
*/
bool m_renderDecorOnly = false;
/** 上一次走"宽缓冲快路径"时的内容指纹(含视界)。当前帧指纹与之相同 → 视界已停止变化,
* 此时改走一次整幅重绘("结算"),保证最终呈现的是**像素精确**的全质量帧。 */
std::uint64_t m_wideLastKey = 0;
+108 -49
View File
@@ -767,6 +767,7 @@ namespace jdi
// 布局变化(窗口缩放、面板宽度调整、网格数量变化)后静态场景不再有效。
InvalidateSceneCache();
InvalidateWideBuffer();
m_decorValid = false; // 装饰锚定在网格左缘,布局变了就必须重画装饰
m_labelBandWidth = 66.0f * m_scale;
m_timeAxisHeight = 34.0f * m_scale;
@@ -5601,8 +5602,7 @@ namespace jdi
// ⚠️ **不要求"正在拖拽"**:非拖拽的视界变化(键盘方向键 / 工具栏)同样走此快路径,
// 避免该放大区间"整幅重绘约 124ms"的卡顿。
bool wideUsable = false;
if (m_decorValid && HasDrawableContent() && m_sceneValid &&
m_sceneGeometryHash == SceneGeometryHash())
if (HasDrawableContent() && m_sceneValid && m_sceneGeometryHash == SceneGeometryHash())
{
std::vector<std::vector<SceneStyleEntry>> currentStyles;
CollectSceneStyles(currentStyles);
@@ -5623,14 +5623,12 @@ namespace jdi
}
else
{
// 已有可用基底 → 滚动(只补画新暴露的细条,恒为 O(细条),无周期性大尖峰);
// 否则优先用"上一帧场景缓存"初始化基底(平移首帧:贴图 + 细条,避免从零重画
// 2×视界造成的一次尖峰),再不行才整幅重建。
// 已有可用基底(跨度与内容均匹配)→ 滚动:只补画新暴露的细条,恒为 O(细条),
// 无周期性大尖峰;否则以当前视界为中心重建基底。
// ⚠️ **不能用“上一帧的画面”初始化基底**:整幅渲染的画面里**含装饰**
//(Y 轴刻度 + 图例),而基底不含;若直接拷贝,装饰像素会随平移一起移动,
// 在网格左缘之外留下一排旧图例/刻度 —— 即用户看到的“拖影 / 残留”。
bool ready = (m_wideValid && WideBufferMatches()) ? RollWideBuffer() : false;
if (!ready && InitWideBufferFromScene())
{
ready = RollWideBuffer();
}
if (!ready)
{
ready = RebuildWideBuffer();
@@ -5652,12 +5650,34 @@ namespace jdi
std::vector<double> wideTimeTicks;
BuildTicks(wideViewMin, wideViewMax, wideTimeStep, wideTimeTicks);
PaintTimeAxis(g, wideTimeTicks, wideTimeStep);
// 派生预览曲线作为叠加层重画(很轻);随后装饰层与其它叠加层。
// 派生预览曲线:基底不含它们(拖参数滑块时基底不变、可直接复用像素)。
for (std::size_t i = 0; i < m_lanes.size(); ++i)
{
PaintLaneDerivedOverlays(g, static_cast<int>(i), m_lanes[i]);
}
BlitDecorCache(g);
// 装饰(Y 轴刻度 + 图例):基底不含(否则平移会重影)。
// 内容指纹一致 → 直接整块贴回缓存(省掉占整幅渲染一半以上的文字绘制);
// 否则用 `m_renderDecorOnly` 重画一遍并**立即重新捕获**。
m_useDecorCache = m_decorValid && (m_decorKey == SceneDecorHash());
if (m_useDecorCache)
{
// 贴回用**捕获时**的矩形(命中缓存时各网格不会重建矩形列表)。
BlitDecorCache(g);
}
else
{
m_decorRects.clear();
m_renderDecorOnly = true;
for (std::size_t i = 0; i < m_lanes.size(); ++i)
{
PaintLane(g, static_cast<int>(i), m_lanes[i], wideTimeTicks);
}
m_renderDecorOnly = false;
if (CaptureDecorCache(g))
{
m_decorKey = SceneDecorHash();
}
}
paintOverlays();
return;
}
@@ -5700,10 +5720,10 @@ namespace jdi
// 图例命中矩形每帧重建,避免残留已消失的信号。
m_legendHits.clear();
// 「装饰层」缓存决策:快速帧(拖拽)期间、且装饰内容与上次捕获完全一致时,
// 「装饰层」缓存决策:快速帧(拖拽/平移)期间、且装饰内容与上次捕获完全一致时,
// 跳过 Y 轴胶囊与图例的绘制(约占整幅重绘的一半),改用上次捕获的像素贴回。
m_decorRects.clear();
m_useDecorCache = m_fastFrame && m_decorValid && (m_decorKey == SceneDecorHash());
m_useDecorCache = m_decorValid && (m_decorKey == SceneDecorHash());
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
@@ -5712,18 +5732,16 @@ namespace jdi
PaintTimeAxis(g, timeTicks, timeStep);
// 装饰层:全质量帧捕获,快速帧贴回(见成员注释)。
if (!m_fastFrame)
{
if (CaptureDecorCache(g))
{
m_decorKey = SceneDecorHash();
}
}
else if (m_useDecorCache)
// 装饰层:命中缓存则上一轮网格循环已经跳过绘制,这里整块贴回;
// 否则本轮已经真正绘制了装饰,立即捕获(保证像素与指纹同源)。
if (m_useDecorCache)
{
BlitDecorCache(g);
}
else if (CaptureDecorCache(g))
{
m_decorKey = SceneDecorHash();
}
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
@@ -5958,6 +5976,10 @@ namespace jdi
mixFloat(m_plotRect.Height);
mixFloat(m_laneContentHeight);
mix(m_scrollBarVisible ? 1ull : 0ull);
// ⚠️ 网格纵向滚动量必须参与指纹:它只改变各网格的绘制位置。
// 若不参与,平移宽缓冲区 / 静态场景缓存会把旧滚动位置的像素当成当前内容贴回,
// 表现为“网格位置错位 / 拖影 / 残留”等错误元素。
mixFloat(Tab().LaneScroll());
// 选中信号会改变图例条目的底色与文字颜色(绘制在缓存的静态场景里),
// 因此必须参与指纹:否则在左侧信号面板点选后图例不会高亮联动。
@@ -6091,6 +6113,23 @@ namespace jdi
return false;
}
// ⚠️ **必须额外确认“视图几何与缓存完全一致”**:`SceneGeometryHash()` 刻意不含视界,
// 所以它相等**并不能**说明视图没变。若视界已经变了还走“只重画受影响网格”的局部补丁,
// 就会把缓存里**旧视界**的画面(时间轴、其它网格、底色)留在屏上 —— 表现为用户反馈的
// “拖影 / 残留 / 错误元素”。因此这里逐项比较除内容指纹之外的缓存键。
{
const SceneCacheKey key = MakeSceneCacheKey();
if (key.viewMin != m_sceneKey.viewMin || key.viewMax != m_sceneKey.viewMax ||
key.laneScroll != m_sceneKey.laneScroll || key.laneCount != m_sceneKey.laneCount ||
key.areaX != m_sceneKey.areaX || key.areaY != m_sceneKey.areaY ||
key.areaW != m_sceneKey.areaW || key.areaH != m_sceneKey.areaH ||
key.scale != m_sceneKey.scale || key.haloFile != m_sceneKey.haloFile ||
key.haloSignal != m_sceneKey.haloSignal)
{
return false;
}
}
std::vector<std::vector<SceneStyleEntry>> current;
CollectSceneStyles(current);
if (current.size() != m_sceneStyles.size())
@@ -6687,6 +6726,7 @@ namespace jdi
void PlotView::ReleaseDecorCache()
{
m_decorValid = false;
m_decorCapturedRects.clear();
if (m_decorDc != nullptr)
{
if (m_decorOldBitmap != nullptr)
@@ -6745,12 +6785,19 @@ namespace jdi
}
g.ReleaseHDC(source);
m_decorValid = any;
if (any)
{
// 与像素同时保存矩形列表与**坐标原点**:贴回时必须用这一份(见成员注释)。
m_decorCapturedRects = m_decorRects;
m_decorOriginX = originX;
m_decorOriginY = originY;
}
return any;
}
bool PlotView::BlitDecorCache(gd::Graphics& g)
{
if (!m_decorValid || m_decorDc == nullptr || m_decorRects.empty())
if (!m_decorValid || m_decorDc == nullptr || m_decorCapturedRects.empty())
{
return false;
}
@@ -6759,19 +6806,21 @@ namespace jdi
{
return false;
}
const int originX = static_cast<int>(std::floor(m_area.X));
const int originY = static_cast<int>(std::floor(m_area.Y));
for (const gd::RectF& rect : m_decorRects)
// 用**捕获时**的原点推算源偏移,但按**绝对坐标**写入:
// 即使两帧的 `m_area` 不同(宽缓冲区渲染会临时改写它),装饰也不会错位。
for (const gd::RectF& rect : m_decorCapturedRects)
{
const int x0 = static_cast<int>(std::floor(rect.X)) - originX;
const int y0 = static_cast<int>(std::floor(rect.Y)) - originY;
const int x1 = static_cast<int>(std::ceil(rect.X + rect.Width)) - originX;
const int y1 = static_cast<int>(std::ceil(rect.Y + rect.Height)) - originY;
if (x1 <= x0 || y1 <= y0)
const int destX = static_cast<int>(std::floor(rect.X));
const int destY = static_cast<int>(std::floor(rect.Y));
const int srcX = destX - m_decorOriginX;
const int srcY = destY - m_decorOriginY;
const int w = static_cast<int>(std::ceil(rect.X + rect.Width)) - destX;
const int h = static_cast<int>(std::ceil(rect.Y + rect.Height)) - destY;
if (w <= 0 || h <= 0)
{
continue;
}
BitBlt(target, x0 + originX, y0 + originY, x1 - x0, y1 - y0, m_decorDc, x0, y0, SRCCOPY);
BitBlt(target, destX, destY, w, h, m_decorDc, srcX, srcY, SRCCOPY);
}
g.ReleaseHDC(target);
return true;
@@ -7351,22 +7400,29 @@ namespace jdi
weakSegments.insert(weakSegments.end(), {x, lane.rect.Y, x, lane.rect.Y + lane.rect.Height});
}
if (!minorSegments.empty())
if (!m_renderDecorOnly)
{
UiDrawSolidLines(g, gridClip, minorSegments.data(), minorSegments.size() / 4, minorLineColor);
}
if (!weakSegments.empty())
{
UiDrawSolidLines(g, gridClip, weakSegments.data(), weakSegments.size() / 4, weakLineColor);
}
if (!strongSegments.empty())
{
UiDrawSolidLines(g, gridClip, strongSegments.data(), strongSegments.size() / 4, strongLineColor);
if (!minorSegments.empty())
{
UiDrawSolidLines(g, gridClip, minorSegments.data(), minorSegments.size() / 4, minorLineColor);
}
if (!weakSegments.empty())
{
UiDrawSolidLines(g, gridClip, weakSegments.data(), weakSegments.size() / 4, weakLineColor);
}
if (!strongSegments.empty())
{
UiDrawSolidLines(g, gridClip, strongSegments.data(), strongSegments.size() / 4, strongLineColor);
}
}
// 波形绘制。
const std::vector<SignalRef>& laneSignals = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals;
if (laneSignals.empty())
if (m_renderDecorOnly)
{
// 只画装饰:网格线与波形已由宽基底提供,这里跳过。
}
else if (laneSignals.empty())
{
const gd::RectF visibleRect(lane.rect.X, clipTop, lane.rect.Width, clipBottom - clipTop);
UiText(g, L"(空网格)", m_fonts->uiSmall, visibleRect, m_theme->textMuted, TextAlign::CENTER);
@@ -7519,7 +7575,8 @@ namespace jdi
// 网格分界线:网格之间不再留间隔,改用较重的横线明确分隔相邻网格。
const float dividerY = lane.rect.Y;
if (dividerY >= m_plotRect.Y - 1.0f && dividerY <= m_plotRect.Y + m_plotRect.Height)
if (!m_renderDecorOnly && dividerY >= m_plotRect.Y - 1.0f &&
dividerY <= m_plotRect.Y + m_plotRect.Height)
{
const float dividerSegments[4] = {lane.rect.X, dividerY, lane.rect.X + lane.rect.Width, dividerY};
const std::uint32_t dividerColor =
@@ -7599,12 +7656,14 @@ namespace jdi
m_decorRects.push_back(gd::RectF(pill.X - 2.0f * m_scale, pill.Y - 1.0f * m_scale,
pill.Width + 4.0f * m_scale, pill.Height + 2.0f * m_scale));
}
// 快速帧且装饰缓存命中时:跳过重绘(文字 + 圆角底板),改由装饰层贴回。
// 命中装饰缓存:跳过重绘(文字 + 圆角底板),由上层整块贴回。
if (!m_useDecorCache)
{
if (!pills.empty())
{
UiFillRoundRects(g, pills, 3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xF0));
// 胶囊底板**不透明**:装饰像素因此与时间视界无关,可以跨视界整块复用;
// 半透明底板会把旧视界的波形混进缓存像素,平移贴回时在底板内部留下残影。
UiFillRoundRects(g, pills, 3.0f * m_scale, m_theme->cardBg | 0xFF000000u);
}
for (std::size_t index = 0; index < pills.size(); ++index)
{
@@ -7760,21 +7819,21 @@ namespace jdi
{
// 底板从编号徽标开始一直盖到最后一个图例条目(编号徽标一并被掠开底下的网格线)。
const float plateRight = cursorX + legendWidth + (legendWidth > 0.0f ? 2.0f * m_scale : 0.0f);
// 记录图例装饰矩形(含编号徽标;供装饰层捕获/贴回):从徽标左缘到图例右缘。
// 记录图例装饰矩形(含编号徽标):从徽标左缘到图例右缘。
{
const float left = std::min(badgeRect.X, legendBand.X - 4.0f * m_scale) - 2.0f * m_scale;
const float right = std::max(badgeRect.X + badgeRect.Width, plateRight) + 2.0f * m_scale;
m_decorRects.push_back(gd::RectF(left, top - 4.0f * m_scale, right - left,
height + 8.0f * m_scale));
}
// 快速帧且装饰缓存命中:整块图例(底板 + 徽标 + 色块 + 文字)改由装饰层贴回,跳过重绘。
// 命中装饰缓存:整块图例(底板 + 徽标 + 色块 + 文字)改由装饰层贴回,跳过重绘。
if (m_useDecorCache)
{
return;
}
UiFillRoundRect(g, gd::RectF(legendBand.X - 4.0f * m_scale, top - 2.0f * m_scale,
plateRight - legendBand.X + 4.0f * m_scale, height + 4.0f * m_scale),
3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xE8));
3.0f * m_scale, m_theme->cardBg | 0xFF000000u);
}
// 编号徽标绘制在底板之上(与信号面板的 GX 徽标使用同一套配色与圆角)。