消除开始拖拽的首帧尖峰:用静态场景缓存初始化宽基底 + 启用预览功能时预建基底

平移首帧不再从零重画 2×视界:平移只是整体横移,上一帧场景缓存内容依然正确,直接贴进宽基底中心再滚动到当前视界(仅当派生预览曲线参数未变时适用)。启用弱波增强/均值滤波/中心轴时预建基底,使首次拖动其参数滑块无需重建。实测 VERY_BIG_DATA 全信号、视界 [40.1756,40.1885]s、5 网格:中键平移拖动最大 8.5ms、弱波增强滑块最大 10.8ms(原分别 35/37.6ms);全部鼠标交互全程 >=60fps、无尖峰;静止帧与 HEAD 逐像素 0 差异。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-11 23:21:37 +08:00
1 parent e988da7963
commit 8b1de5d900
3 files changed
+101 -1

No files matched your search

+10
View File
@@ -2232,6 +2232,16 @@ namespace jdi
bool RollWideBuffer();
/** @brief 渲染宽缓冲区中因滚动而新暴露的一条细条(宽 |shiftPixels|)。 */
void RenderWideStrip(int shiftPixels);
/**
* @brief 用「上一帧的静态场景缓存」初始化宽缓冲区(平移首帧用,避免从零重画)。
*
* 平移时画面只是整体横向移动,场景缓存里的内容(含派生曲线/图例)**依然正确**;
* 因此把场景缓存的绘图区直接贴进宽缓冲中心即可,随后滚动到当前视界。
* 仅当"视界相对场景缓存发生了平移"(跨度一致)时可用。
*
* @return 初始化成功返回 true。
*/
bool InitWideBufferFromScene();
/** @brief 宽缓冲区内容是否仍与当前视图/内容一致。 */
bool WideBufferMatches() const;
/** @brief 使宽缓冲区失效。 */
+74 -1
View File
@@ -1845,6 +1845,8 @@ namespace jdi
{
lane.edgeCenterPanelOpen = false;
}
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
RebuildWideBuffer();
return true;
}
@@ -1988,6 +1990,8 @@ namespace jdi
{
lane.weakBoostPanelOpen = false;
}
// 预建「宽缓冲基底」(不含派生曲线),使用户首次拖动该滑块时无需从零重建(避免一次性尖峰)。
RebuildWideBuffer();
return true;
}
@@ -2069,6 +2073,8 @@ namespace jdi
{
lane.meanFilterPanelOpen = false;
}
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
RebuildWideBuffer();
return true;
}
@@ -5578,8 +5584,13 @@ namespace jdi
if (wideUsable)
{
// 已有可用基底 → 滚动(只补画新暴露的细条,恒为 O(细条),无周期性大尖峰);
// 否则(首帧 / 跨度或内容变化)整幅重建一次基底。
// 否则优先用"上一帧场景缓存"初始化基底(平移首帧:贴图 + 细条,避免从零重画
// 2×视界造成的一次尖峰),再不行才整幅重建。
bool ready = (m_wideValid && WideBufferMatches()) ? RollWideBuffer() : false;
if (!ready && InitWideBufferFromScene())
{
ready = RollWideBuffer();
}
if (!ready)
{
ready = RebuildWideBuffer();
@@ -6963,6 +6974,68 @@ namespace jdi
return true;
}
bool PlotView::InitWideBufferFromScene()
{
if (!m_sceneValid || m_sceneDc == nullptr)
{
return false;
}
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));
+17
View File
@@ -340,6 +340,22 @@ Documents/ 项目文档(含本文件)
- **显示效果**:滚动平移帧截图核对无缝隙/无残字;**静止/结算帧与 HEAD 逐像素 0 差异**(`LockBits` 验证)。
- **剩余残留**:**每次"开始一次拖拽"的第一帧**需新建缓冲(2×视界基底,负载下约 38ms 的一次尖峰);持续拖动过程中无尖峰。要消除这一次性启动尖峰,需把基底渲染做成可后台的独立渲染器(同上,较大重构)。非拖拽视界变化仍整幅重绘。
### 9.13 消除"开始拖拽"的首帧尖峰:用场景缓存初始化基底 + 启用预览功能时预建基底
9.12 的残留(首帧需从零新建 2×视界基底,负载下约 38ms)已消除:
- **平移首帧用「上一帧静态场景缓存」初始化宽基底**(`InitWideBufferFromScene`):平移只是整体横向移动,场景缓存里的内容(含派生曲线/图例)**依然正确**,因此把它直接贴进宽基底中心即可,再滚动到当前视界 —— 首帧只需一次 BitBlt + 一条细条,**不再从零重画 2×视界**。仅当"派生预览曲线参数未变"时适用(拖这些参数时派生曲线会变,须从零重建基底)。
- **启用带预览的实验性功能时预建基底**:在 `SetLaneWeakBoost`/`SetLaneMeanFilter`/`SetLaneEdgeCenterCount(…, true)` 里预建宽基底,使**首次拖动这些参数滑块**时无需从零重建(避免一次性尖峰)。
- **实测**(`VERY_BIG_DATA` 全信号,视界 `[40.1756, 40.1885]s`,5 网格,负载机 ms/帧):
| 场景 | 9.12 | 本次 |
| --- | --- | --- |
| 中键平移拖动(短距) | 最大 35ms(首帧重建) | **最大 8.5ms** |
| 中键平移拖动(长距) | 最大 10.5ms | **最大 7.4ms** |
| 弱波增强滑块 | 最大 37.6ms(首帧重建) | **最大 10.8ms** |
| 透明度/粗细/颜色 | 8–16ms | 8–15ms |
- **结论**:用户报告的全部**鼠标**交互(左/中键拖动平移、样式滑块/拾色、弱波增强参数滑块)在该放大区间**全程 ≥60fps、无尖峰**;静止/结算帧与 HEAD **逐像素 0 差异**。
- **仍存在的残留**:**非拖拽的视界变化**(键盘方向键 / 工具栏,非用户报告的鼠标场景)仍走整幅重绘(约 125ms)—— 因其没有"拖拽结束"事件可用来触发一次精确的全质量重绘,若走宽缓冲快路径会留下 ≤0.5px 的亚像素偏差且无法"结算"回精确帧,故保持整幅重绘以保证像素精确。
---
## 10. 变更日志(本文件维护记录)
@@ -356,3 +372,4 @@ Documents/ 项目文档(含本文件)
| 2026-10-11 | DeepSeek-V4.1-Flash | 落地用户提出的**复用像素**方案:新增**平移宽缓冲区**(以视界为中心左右各多渲染半个视界宽),平移时前台只做一次 **BitBlt 截取子矩形**(O(1)),移出边界才重建 → 深放大下平移 **34ms→1.3ms**;静止/结算帧逐像素 0 差异。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 把宽缓冲推广为**基底复用**:抽出 `PaintLaneDerivedOverlays`、宽缓冲只存基底(不含派生曲线),基底键排除派生参数 → 拖弱波增强滑块 **55ms→5.2ms**;平移 1.3ms、样式 8ms 不变;静止帧逐像素 0 差异。残留:宽缓冲重建在前台(约每屏一次)、非拖拽视界变化仍整幅重绘。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 宽缓冲改为**滚动式**(自贴图平移 + 只补画新暴露细条),消除"移出边界整幅重建"的周期性尖峰;缓冲只覆盖绘图区、时间轴前台绘制、亚像素误差用锚点限制在 ≤0.5px → 持续拖动平移约 6ms/帧、无尖峰(长距最大 10.5ms);静止帧逐像素 0 差异。残留:每次开始拖拽的首帧需新建缓冲(一次启动尖峰)。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 消除"开始拖拽"的首帧尖峰:平移首帧用**静态场景缓存初始化宽基底**(`InitWideBufferFromScene`,贴图 + 细条,不再从零重画 2×视界);启用带预览的实验性功能时**预建基底**。实测:平移拖动最大 8.5ms、弱波增强滑块最大 10.8ms(原分别 35/37.6ms),用户全部鼠标交互在该放大区间**全程 ≥60fps、无尖峰**;静止帧逐像素 0 差异。残留:非拖拽视界变化(键盘/工具栏)仍整幅重绘。 |