Compare commits

...
5 Commits
Author SHA1 Message Date
DeepSeek V4.1 Flash 1c9c43233d 非拖拽视界变化(键盘/工具栏)也走宽基底快路径:124.6ms→8.6ms;新增 wideKey 结算帧恢复像素精确;分支顺序固定为静态贴图→宽基底→样式补丁;宽基底强制全质量抗锯齿且竖向刻度改用前台跨度;像素对比验证 7 张静态/结算帧与 HEAD 字节一致、底座管线忠实(meanAbs 0.68/255) 2026-10-12 00:04:01 +08:00
DeepSeek V4.1 Flash 8b1de5d900 消除开始拖拽的首帧尖峰:用静态场景缓存初始化宽基底 + 启用预览功能时预建基底
平移首帧不再从零重画 2×视界:平移只是整体横移,上一帧场景缓存内容依然正确,直接贴进宽基底中心再滚动到当前视界(仅当派生预览曲线参数未变时适用)。启用弱波增强/均值滤波/中心轴时预建基底,使首次拖动其参数滑块无需重建。实测 VERY_BIG_DATA 全信号、视界 [40.1756,40.1885]s、5 网格:中键平移拖动最大 8.5ms、弱波增强滑块最大 10.8ms(原分别 35/37.6ms);全部鼠标交互全程 >=60fps、无尖峰;静止帧与 HEAD 逐像素 0 差异。
2026-10-11 23:21:37 +08:00
DeepSeek V4.1 Flash e988da7963 宽缓冲改为滚动式:消除平移过程中的周期性重建尖峰
视界每帧移动 d 像素时,把缓冲内容整体自贴图平移 round(d) 像素并只补画新暴露的 |round(d)| 像素宽细条(细条渲染复用整条宽窗口的映射,故网格/波形 x 坐标与刻度密度均正确)。用锚点把内容相对时间轴的亚像素偏移限制在半个像素内;缓冲只覆盖绘图区,时间轴由前台每帧绘制。实测 VERY_BIG_DATA 全信号、视界 [40.1756,40.1885]s、5 网格:持续平移拖动约 6ms/帧、长距最大 10.5ms(原为每屏一次 33ms 尖峰);弱波增强滑块 6ms、样式 8ms;静止帧与 HEAD 逐像素 0 差异。
2026-10-11 23:08:46 +08:00
DeepSeek V4.1 Flash b04167a9d6 把宽缓冲区推广为基底复用:拖弱波增强参数滑块降为 O(1)(55ms 到 5ms)
抽出 PaintLaneDerivedOverlays;宽缓冲只存基底(网格/波形/时间轴,跳过派生曲线与图例/Y轴刻度);基底校验键改为 SceneContentHash(true,false)(含外观样式、排除派生参数),因此拖弱波增强等级/迭代滑块不会让基底失效,可复用基底像素 + 只重画派生曲线叠加层。判定为结构未变且外观样式未变(仅视图或派生参数变化)时启用;外观样式变化仍走样式补丁。

实测 VERY_BIG_DATA 全信号、视界 [40.1756,40.1885]s、5 网格:弱波增强滑块 55ms 到 5.2ms,平移 1.3ms、透明度/粗细/颜色 8ms 不变;静止/结算帧与 HEAD 逐像素 0 差异(LockBits)。
2026-10-11 22:55:45 +08:00
DeepSeek V4.1 Flash fa0d62b016 平移改为复用已绘制像素:新增宽缓冲区,平移动画降为 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 验证)。
2026-10-11 22:36:42 +08:00
3 changed files with 705 additions and 61 deletions

No files matched your search

+92
View File
@@ -1076,6 +1076,18 @@ namespace jdi
void PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane,
const std::vector<double>& timeTicks);
/**
* @brief 绘制某网格的全部**派生预览曲线**(均值滤波 / 中心轴 / 弱波增强),叠加在原信号之上。
*
* 从 `PaintLane` 抽出,供「宽缓冲区」前台路径复用:宽缓冲只存"基底"(网格/波形),
* 派生曲线每帧作为叠加层单独绘制,因此拖派生参数滑块时无需重画波形。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] laneIndex 网格下标。
* @param [IN] lane 网格绘制信息。
*/
void PaintLaneDerivedOverlays(gd::Graphics& g, int laneIndex, const LaneVisual& lane) const;
/**
* @brief 绘制底部时间轴。
* @param [IN,OUT] g 绘图上下文。
@@ -1933,6 +1945,13 @@ namespace jdi
float opacity = 1.0f; /**< 不透明度。 */
float lineWidth = 1.0f; /**< 线宽。 */
int mode = 0; /**< 绘制模式。 */
/** @brief 相等比较(用于判断"逐信号外观样式是否变化")。 */
bool operator==(const SceneStyleEntry& other) const
{
return color == other.color && opacity == other.opacity &&
lineWidth == other.lineWidth && mode == other.mode;
}
};
/**
@@ -2171,6 +2190,79 @@ namespace jdi
/** 本帧需要捕获 / 贴回的装饰矩形(各网格的 Y 轴胶囊与图例带)。 */
mutable std::vector<gd::RectF> m_decorRects;
// ---- 平移「宽缓冲区」:平移只改变绘制坐标、不改变画面内容 ----
// 视界平移时整个波形并不改变,只是横向平移。因此以当前视界为中心,向左右各多渲染
// 半个视界宽(位图宽度 = 视界宽 ×2),保存为一张**更宽**的离屏位图;平移时前台只需
// 从中**截取对应的横向子矩形贴图**(O(1),一次 BitBlt),而不必每帧重算重画。
// 当视界移动到该缓冲区边缘(超出已渲染范围)时,才以新视界为中心重建一次。
/**
* @brief 确保宽缓冲区位图尺寸正确(不足则重建)。
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
* @return 可用返回 true。
*/
bool EnsureWideBuffer(int width, int height);
/** @brief 释放宽缓冲区位图。 */
void ReleaseWideBuffer();
/**
* @brief 以当前视界为中心重建宽缓冲区(离屏渲染一张更宽的图像)。
*
* 渲染时临时把绘制区域的横向映射"加宽",因此调用的是与正常绘制**完全相同**的
* `PaintLane`/`PaintTimeAxis`,保证像素与正常绘制一致;仅**跳过图例与 Y 轴刻度**
*(它们锚定在网格左缘、随视界移动,需由前景叠加层绘制)。
*
* @return 重建成功返回 true。
*/
bool RebuildWideBuffer();
/**
* @brief 若当前视界落在宽缓冲区内,则截取子矩形贴图到后备缓冲区。
* @param [IN,OUT] g 绘图上下文。
* @return 命中并贴图返回 true(调用方随后只画叠加层即可)。
*/
bool BlitWideBuffer(gd::Graphics& g) const;
/**
* @brief 让宽缓冲区"滚动"以跟随视界:按整数像素平移已渲染内容,只补画新暴露的细条。
*
* 视界每帧移动 d 像素时,把缓冲区内容整体挪动 `round(d)` 像素(自贴图),
* 再只渲染新暴露出来的 `|round(d)|` 像素宽细条。这样**每帧代价恒为 O(细条)**,
* 不会像"移出边界就整幅重建 2×视界"那样出现周期性大尖峰。
* 用一个亚像素残差累加器把"内容相对时间轴的偏移"始终限制在半个像素内。
*
* @return 缓冲区仍可用返回 true。
*/
bool RollWideBuffer();
/** @brief 渲染宽缓冲区中因滚动而新暴露的一条细条(宽 |shiftPixels|)。 */
void RenderWideStrip(int shiftPixels);
/**
* @brief 用「上一帧的静态场景缓存」初始化宽缓冲区(平移首帧用,避免从零重画)。
*
* 平移时画面只是整体横向移动,场景缓存里的内容(含派生曲线/图例)**依然正确**;
* 因此把场景缓存的绘图区直接贴进宽缓冲中心即可,随后滚动到当前视界。
* 仅当"视界相对场景缓存发生了平移"(跨度一致)时可用。
*
* @return 初始化成功返回 true。
*/
bool InitWideBufferFromScene();
/** @brief 宽缓冲区内容是否仍与当前视图/内容一致。 */
bool WideBufferMatches() const;
/** @brief 使宽缓冲区失效。 */
void InvalidateWideBuffer();
HDC m_wideDc = nullptr; /**< 宽缓冲区设备上下文。 */
HBITMAP m_wideBitmap = nullptr; /**< 宽缓冲区位图(比视界更宽)。 */
HGDIOBJ m_wideOldBitmap = nullptr; /**< 宽缓冲 DC 原选位图。 */
int m_wideW = 0; /**< 宽缓冲区位图宽度(像素)。 */
int m_wideH = 0; /**< 宽缓冲区位图高度(像素)。 */
double m_wideMin = 0.0; /**< 宽缓冲区最左侧对应的时间。 */
double m_wideAnchorMin = 0.0; /**< 视界左端当前在宽缓冲中的锚点时间(滚动基准)。 */
double m_wideSpan = 0.0; /**< 宽缓冲区制作时的视界跨度。 */
bool m_wideValid = false; /**< 宽缓冲区内容是否有效。 */
std::uint64_t m_wideKey = 0; /**< 宽缓冲区对应的内容指纹(外观/派生参数等)。 */
bool m_renderWide = false; /**< 正在渲染宽缓冲区(重入 `PaintLane` 时跳过装饰)。 */
/** 上一次走"宽缓冲快路径"时的内容指纹(含视界)。当前帧指纹与之相同 → 视界已停止变化,
* 此时改走一次整幅重绘("结算"),保证最终呈现的是**像素精确**的全质量帧。 */
std::uint64_t m_wideLastKey = 0;
bool m_wideLastValid = false;
/**
* @brief 实验性功能浮动面板的像素图层项。
*
+508 -61
View File
@@ -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;
@@ -1842,6 +1845,8 @@ namespace jdi
{
lane.edgeCenterPanelOpen = false;
}
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
RebuildWideBuffer();
return true;
}
@@ -1985,6 +1990,8 @@ namespace jdi
{
lane.weakBoostPanelOpen = false;
}
// 预建「宽缓冲基底」(不含派生曲线),使用户首次拖动该滑块时无需从零重建(避免一次性尖峰)。
RebuildWideBuffer();
return true;
}
@@ -2066,6 +2073,8 @@ namespace jdi
{
lane.meanFilterPanelOpen = false;
}
// 预建「宽缓冲基底」,避免首次拖动参数滑块时的一次性重建尖峰。
RebuildWideBuffer();
return true;
}
@@ -5579,6 +5588,82 @@ namespace jdi
paintOverlays();
return;
}
// 视界平移 / 派生预览曲线参数变化:画面**基底**(网格/波形)不变,只是横向平移或
// 叠加的派生曲线变化。直接复用「宽缓冲区」已渲染的**基底**像素,只把派生曲线作为
// 叠加层重画 —— 前台 O(1),不重画波形,因此无论放大到多深都保持跟手。
// 判定:网格结构未变(几何指纹一致)且**逐信号外观样式未变** → 属视图/派生变化,可用宽基底;
// 外观样式(颜色/粗细/透明度/模式)变化 → 交给下方"样式补丁"分支局部重画。
// ⚠️ 位置必须在"静态场景贴图"**之后**:视界与样式都没变时(典型:鼠标悬停移动)
// 应由静态场景贴图命中,绝不能落到这里 —— 否则每两帧就会触发一次"结算整幅重绘",
// 悬停反而变卡。而它又必须在"样式补丁"**之前**:派生曲线参数变化(拖弱波增强 /
// 均值滤波滑块)同样只需叠加重画派生曲线,走本分支比"逐网格补丁"更快且质量更高。
// ⚠️ **不要求"正在拖拽"**:非拖拽的视界变化(键盘方向键 / 工具栏)同样走此快路径,
// 避免该放大区间"整幅重绘约 124ms"的卡顿。
bool wideUsable = false;
if (m_decorValid && HasDrawableContent() && m_sceneValid &&
m_sceneGeometryHash == SceneGeometryHash())
{
std::vector<std::vector<SceneStyleEntry>> currentStyles;
CollectSceneStyles(currentStyles);
wideUsable = (currentStyles == m_sceneStyles);
}
if (wideUsable)
{
// 内容(含视界)与本帧上一条快路径帧相同 → 视界已停止变化 → 走一次整幅"结算"
// 全质量帧(保证最终呈现**像素精确**;此后命中静态场景缓存,不再回到本分支)。
std::uint64_t wideKey = SceneContentHash(true);
wideKey ^= static_cast<std::uint64_t>(std::llround(Tab().ViewMin() * 1.0e6));
wideKey *= 1099511628211ull;
wideKey ^= static_cast<std::uint64_t>(std::llround(Tab().ViewMax() * 1.0e6));
if (m_wideLastValid && m_wideLastKey == wideKey)
{
m_wideLastValid = false; // 结算:落到下方整幅重绘
}
else
{
// 已有可用基底 → 滚动(只补画新暴露的细条,恒为 O(细条),无周期性大尖峰);
// 否则优先用"上一帧场景缓存"初始化基底(平移首帧:贴图 + 细条,避免从零重画
// 2×视界造成的一次尖峰),再不行才整幅重建。
bool ready = (m_wideValid && WideBufferMatches()) ? RollWideBuffer() : false;
if (!ready && InitWideBufferFromScene())
{
ready = RollWideBuffer();
}
if (!ready)
{
ready = RebuildWideBuffer();
}
if (ready)
{
m_wideLastKey = wideKey;
m_wideLastValid = true;
// 绘图区底色(覆盖时间轴与侧边留白区域)。
UiFillRect(g, m_area, m_theme->plotBg);
// 贴基底(绘图区:网格 + 波形)。
BlitWideBuffer(g);
// 时间轴:宽缓冲不含它,前台按当前视图绘制。
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);
// 派生预览曲线作为叠加层重画(很轻);随后装饰层与其它叠加层。
for (std::size_t i = 0; i < m_lanes.size(); ++i)
{
PaintLaneDerivedOverlays(g, static_cast<int>(i), m_lanes[i]);
}
BlitDecorCache(g);
paintOverlays();
return;
}
}
}
// 只有"外观样式"变化(典型场景:拖动自定义颜色滑块实时调色):网格结构、视图几何、
// 波形采样都没变,只是颜色/不透明度/线宽/模式不同 —— 此时只重画受影响的网格,
// 再写回缓存。(VERY_BIG_DATA 实测:整幅重绘 30ms+,单网格重画约 4ms。)
@@ -5586,9 +5671,9 @@ namespace jdi
std::vector<int> patchLanes;
if (FindStyleOnlyLanes(patchLanes) && BlitSceneCache(g))
{
// ⚠️ 样式/参数**正在连续变化**(拖透明度/粗细/拾色/实验性功能滑块):这本身就是
// 一种"交互中"。若不切换快速帧,本帧会用**抗锯齿**重画波形包络带 —— 实测
// 单条网格的包络带 AA 填充 ≈29ms(本机负载下 72ms),而关掉 AA 只要 ≈5.6ms。
// ⚠️ 样式**正在连续变化**(拖透明度 / 粗细 / 拾色滑块):这本身就是一种"交互中"。
// 若不切换快速帧,本帧会用**抗锯齿**重画波形包络带 —— 实测单条网格的包络带
// AA 填充 ≈29ms(本机负载下 72ms),而关掉 AA 只要 ≈5.6ms。
// 因此补丁阶段强制快速帧,并在补丁后把场景缓存标记为"已降级",
// 下一帧(参数稳定后)自动补一帧全质量 —— 既保证拖动手感,又不牺牲最终画质。
m_fastFrame = true;
@@ -6692,6 +6777,359 @@ 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, false));
}
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 float plotHeight = m_plotRect.Height;
const int bitmapHeight = static_cast<int>(std::ceil(plotHeight));
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 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)
{
PaintLane(g, static_cast<int>(i), m_lanes[i], timeTicks);
}
}
m_fastFrame = savedFastFrame;
m_renderWide = false;
// 恢复布局 / 视图 / 网格。
m_area = savedArea;
m_plotRect = savedPlot;
Tab().ViewMin() = savedViewMin;
Tab().ViewMax() = savedViewMax;
BuildLaneVisuals();
m_wideMin = wideMin;
m_wideAnchorMin = wideMin + static_cast<double>(margin) / scale; // 视界左端(列 margin)对应的时间
m_wideSpan = span;
m_wideKey = SceneContentHash(true, false);
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_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);
g.ReleaseHDC(target);
return true;
}
bool PlotView::RollWideBuffer()
{
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 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)
{
return;
}
const int stripX = (shiftPixels > 0) ? (bitmapWidth - stripWidth) : 0;
// 平移后的缓冲区左端时间。
const double newWideMin = m_wideMin + static_cast<double>(shiftPixels) / scale;
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)
{
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)
{
PaintLane(g, static_cast<int>(i), m_lanes[i], timeTicks);
}
}
m_fastFrame = savedFastFrame;
m_renderWide = false;
m_area = savedArea;
m_plotRect = savedPlot;
Tab().ViewMin() = savedViewMin;
Tab().ViewMax() = savedViewMax;
BuildLaneVisuals();
}
void PlotView::PaintRubberBand(gd::Graphics& g)
{
if (!m_dragMoved || m_rubberRect.Width <= 0.0f || m_rubberRect.Height <= 0.0f)
@@ -6940,15 +7378,8 @@ namespace jdi
// 只有拖拽中的信号光晕可以进入静态场景缓存(悬停光晕见 PaintHoverHighlight)。
const SignalRef haloRef = SceneHaloRef();
// 「边沿计数(中心轴)」开启时,为**被勾选**的信号各叠加一条均值滤波曲线
//(参数 = 该信号自己的条目:均值点数 X + 迭代次数 N,与「均值滤波」完全同一套语义)。
const bool centerOn = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].edgeCenter;
// 「均值滤波」开启时,为**被勾选**的信号各叠加一条多次迭代滑动平均曲线
//(均值点数 X、迭代次数 N 由该信号自己的条目给出)。
const bool meanFilterOn = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].meanFilter;
// 「弱波增强」开启时同样只为**被勾选**的信号叠加曲线
//(增强等级取整个网格共用的 `weakBoostLevel`)。
const bool weakBoostOn = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].weakBoost;
// 派生预览曲线(中心轴 / 均值滤波 / 弱波增强)在下方"第二遍"统一叠加绘制,
// 见 `PaintLaneDerivedOverlays`(抽出以便宽缓冲区前台路径复用)。
for (const SignalRef& ref : laneSignals)
{
@@ -7074,55 +7505,12 @@ namespace jdi
}
// 第二遍:把所有**派生预览曲线**统一绘制在**本网格全部原信号之上**。
// 若在同一个信号循环里就地叠加,后绘制的原信号会把先前信号的预览曲线盖住
//(用户明确要求"预览信号处于原信号上层不要被覆盖")。
for (const SignalRef& ref : laneSignals)
// 渲染「宽缓冲区」时跳过:宽缓冲只存"基底"(网格/波形,不含派生曲线与装饰),
// 派生曲线改由前台叠加层绘制(见 `PaintLaneDerivedOverlays`),
// 这样拖派生参数滑块时基底不变、可复用宽缓冲像素。
if (!m_renderWide)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
{
continue;
}
// 中心轴均值滤波曲线(「实验性功能 → 边沿计数(中心轴)」):与「均值滤波」同源,
// 只有被勾选的信号才绘制。
if (centerOn)
{
const Lane::EdgeCenterItem* centerItem =
Tab().Lanes()[static_cast<std::size_t>(laneIndex)].FindEdgeCenterItem(ref);
if (centerItem != nullptr)
{
PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations);
}
}
// 均值滤波预览曲线:只有被勾选的信号才绘制(参数取自它自己的条目)。
if (meanFilterOn)
{
const Lane::MeanFilterItem* item =
Tab().Lanes()[static_cast<std::size_t>(laneIndex)].FindMeanFilterItem(ref);
if (item != nullptr)
{
PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations);
}
}
// 弱波增强预览曲线:只有被勾选的信号才绘制(增强等级取自整个网格)。
if (weakBoostOn)
{
const Lane::WeakBoostItem* item =
Tab().Lanes()[static_cast<std::size_t>(laneIndex)].FindWeakBoostItem(ref);
if (item != nullptr)
{
const int levelPercent = static_cast<int>(std::lround(
std::max(0.0, std::min(1.0,
Tab().Lanes()[static_cast<std::size_t>(laneIndex)]
.weakBoostLevel)) *
100.0));
const int iterations = std::max(
kWeakBoostIterationsMin,
std::min(kWeakBoostIterationsMax,
Tab().Lanes()[static_cast<std::size_t>(laneIndex)].weakBoostIterations));
PaintWeakBoostCurve(g, resolved, lane, levelPercent, iterations);
}
}
PaintLaneDerivedOverlays(g, laneIndex, lane);
}
// g.Restore 也会还原绘制状态,此处直接恢复图形状态即可。
@@ -7142,6 +7530,9 @@ namespace jdi
// 内侧纵轴标尺作为覆盖层绘制:每个刻度数值带半透明底,保证在波形之上仍清晰可读,
// 同时只在数值背后遮挡波形,不牺牲波形本身的可见范围。
// ⚠️ 渲染「宽缓冲区」时跳过 Y 轴刻度与图例:它们锚定在网格左缘、随视界横向移动,
// 而宽缓冲区会被平移截取,装饰必须由前台叠加层(装饰层缓存)单独绘制。
if (!m_renderWide)
{
const bool showLabels = (lane.rect.Height >= 34.0f * m_scale);
float lastLabelY = -1.0e9f;
@@ -7228,7 +7619,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);
}
@@ -7250,6 +7641,62 @@ namespace jdi
return SignalRef();
}
void PlotView::PaintLaneDerivedOverlays(gd::Graphics& g, int laneIndex, const LaneVisual& lane) const
{
if (m_tab == nullptr || laneIndex < 0 ||
laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return;
}
const Lane& tabLane = Tab().Lanes()[static_cast<std::size_t>(laneIndex)];
const bool centerOn = tabLane.edgeCenter;
const bool meanFilterOn = tabLane.meanFilter;
const bool weakBoostOn = tabLane.weakBoost;
if (!centerOn && !meanFilterOn && !weakBoostOn)
{
return;
}
const int levelPercent = static_cast<int>(
std::lround(std::max(0.0, std::min(1.0, tabLane.weakBoostLevel)) * 100.0));
const int boostIterations =
std::max(kWeakBoostIterationsMin, std::min(kWeakBoostIterationsMax, tabLane.weakBoostIterations));
const int saveState = g.Save();
g.SetClip(lane.rect, gd::CombineModeIntersect);
for (const SignalRef& ref : tabLane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
{
continue;
}
if (centerOn)
{
const Lane::EdgeCenterItem* centerItem = tabLane.FindEdgeCenterItem(ref);
if (centerItem != nullptr)
{
PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations);
}
}
if (meanFilterOn)
{
const Lane::MeanFilterItem* item = tabLane.FindMeanFilterItem(ref);
if (item != nullptr)
{
PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations);
}
}
if (weakBoostOn)
{
const Lane::WeakBoostItem* item = tabLane.FindWeakBoostItem(ref);
if (item != nullptr)
{
PaintWeakBoostCurve(g, resolved, lane, levelPercent, boostIterations);
}
}
}
g.Restore(saveState);
}
void PlotView::PaintLaneLegend(gd::Graphics& g, int laneIndex, const LaneVisual& lane, float visibleTop,
float visibleBottom)
{
+105
View File
@@ -283,6 +283,106 @@ Documents/ 项目文档(含本文件)
- **结果(同一负载机器)**:透明度/粗细/颜色 **77ms → 16ms**;弱波增强滑块 **96–176ms → 26ms**;平移拖动 **34ms**(快速帧,空闲机 ≈8ms)。全质量/结算帧与修复前**逐像素 0 差异**(`LockBits` 对照验证)。
- **仍存在的固有下限**:整幅重绘中的**包络带 `FillPath`** 本身是 O(像素列数) 的 GDI+ 抗锯齿填充(≈29ms/网格/负载)。它只在"整幅重绘"或"结算帧"出现(平移快速帧已关抗锯齿 ≈5.4ms/网格)。若要在密集包络带上也关抗锯齿可再快 5×,但会轻微改变边缘观感,**需用户确认后再动**。
### 9.10 平移「宽缓冲区」:让平移的时间复杂度降为 O(1)(用户方案落地)
- **用户的判断(正确)**:平移视界时**画面内容并不改变,只是横向平移**;如果只是重画得快、而不复用已画好的像素,那么在"某些放大级别可见像素极多"时,重画必然卡住消息循环,交互就必然掉帧。正确做法是**复用已绘制的像素**,让前台始终只做 O(1) 的取用。
- **对现有代码的核对**:用户判断与仓库现状**基本一致** —— 之前的历次优化都是"加速绘制"(包络带 `FillPath`、装饰层、缓存),而**没有把"绘制"这一任务本身从交互帧里拆出去**(后台线程只算了派生曲线的数值,没有做波形光栅化)。因此一遇到"可见像素特别多"的放大级别(如 `[40.1756, 40.1885]s`),每帧重画就再次成为瓶颈。
- **可行性结论**:用户的"每网格双缓冲 + 后台填充 + 前台只贴已就绪缓冲"方向正确。要点补充:① GDI/GDI+ 绘制**可以在后台线程**进行(每个线程用**各自**的 `Bitmap`/`HDC`/`Graphics` 即可),所以后台光栅化可行;② 但本仓库的 `PaintLane` 会读写大量成员缓存(折线/曲线/图例命中/装饰矩形),直接搬到后台线程需要一次较大重构才能保证线程安全。③ 因此本次采用一个**风险更低、收益相同**的落地方式(见下)。
- **本次实现(平移 O(1))**:
- **宽缓冲区**:以当前视界为中心,向左右各多渲染**半个视界宽**,存成一张宽为 `2×视界宽` 的离屏位图(`m_wideDc`)。它用与正常绘制**完全相同**的 `PaintLane`/`PaintTimeAxis`(临时把横向映射"加宽"后渲染),因此像素与正常绘制一致;仅**跳过图例与 Y 轴刻度**(二者锚定网格左缘、随视界移动,改由已有的**装饰层缓存**贴回)。
- **平移 = 一次 BitBlt**:视界宽度不变、只是横向移动时,前台从宽缓冲区**截取对应横向子矩形**贴到后备缓冲区即可(O(1)),**完全不重画波形**。无论放大到多深,平移都与全局显示一样流畅。
- **边界重建**:当视界移出宽缓冲范围(或跨度/内容变化)时,才以新视界为中心**重建一次**(约占整幅重绘 2 倍,但只在每平移约半屏时发生一次)。
- **实测**(`VERY_BIG_DATA` 全信号,视界 `[40.1756, 40.1885]s`,5 网格;单位 ms/帧,负载机):
| 场景 | 修复前 | 修复后 |
| --- | --- | --- |
| 中键平移拖动(短距) | 34ms | **1.3ms** |
| 中键平移拖动(长距,含边界重建) | 34ms | **1.7ms**(峰值 50ms 为一次边界重建) |
| 透明度/粗细/颜色 | 16ms | 8ms |
| 弱波增强滑块 | 26–176ms | 约 15ms(空闲)/ 55ms(负载) |
- **显示效果**:**静止/结算帧与修复前逐像素 0 差异**(`LockBits` 对照);平移拖动帧为快速帧质量(关闭抗锯齿,与既有"拖动时低分辨率、松手补全质量"策略一致),松手后立即恢复全质量。
- **说明**:宽缓冲区重建目前在前台进行(每平移约半屏一次);若日后要连这一次重建也移出交互帧,需把 `PaintLane` 的成员缓存重构为可复制的渲染上下文再搬后台(较大改动)。弱波增强参数滑块仍有一次"重绘受影响网格 + 浮动面板"的开销(负载下约 55ms),是本轮未彻底消除的残留。
### 9.11 把「宽缓冲区」推广为**基底复用**:平移 + 拖参数滑块都降为 O(1)
承接 9.10,进一步把「宽缓冲区」从"只用于平移"推广为**通用基底复用**,彻底解决拖派生参数滑块的问题:
- **抽取派生曲线绘制**:把 `PaintLane` 里叠加派生预览曲线(均值滤波/中心轴/弱波增强)的"第二遍"抽成 `PaintLaneDerivedOverlays`。宽缓冲区渲染时**跳过派生曲线**,因此宽缓冲只存**基底**(网格/波形/时间轴,不含派生曲线与图例/Y 轴刻度)。
- **基底键与派生参数解耦**:宽缓冲的校验键改为 `SceneContentHash(true, false)`(含外观样式、**不含派生参数**)→ 拖弱波增强等级/迭代次数滑块**不会让基底失效**,可直接复用基底像素。
- **统一前台路径**:判定"网格结构未变(几何指纹一致)且逐信号外观样式未变"(即只有**视图**或**派生参数**变化)时 → 贴宽缓冲基底像素 + **只重画派生曲线叠加层** + 装饰层 → 前台 O(1),不重画波形。
- 外观样式(颜色/粗细/透明度/模式)变化仍走"样式补丁"(局部重画受影响网格)。
- **实测**(`VERY_BIG_DATA` 全信号,视界 `[40.1756, 40.1885]s`,5 网格;负载机 ms/帧):
| 场景 | 9.10 后 | 本次后 |
| --- | --- | --- |
| 中键平移拖动 | 1.3ms | **1.3ms** |
| 弱波增强滑块 | 55ms | **5.2ms** |
| 透明度/粗细/颜色 | 8ms | 8ms |
- **显示效果**:静止/结算帧与 HEAD **逐像素 0 差异**(`LockBits` 验证)。拖拽/拖参数帧为快速帧质量(关抗锯齿),松手后恢复全质量。
- **仍存在的残留**(如实记录):
1. **宽缓冲区重建**在视界移出缓冲范围(约每平移 1 屏一次)时于**前台**进行,负载下约 30–50ms 的**一次**尖峰。彻底消除需把"基底渲染"做成**不依赖成员状态的独立渲染器**再搬后台线程(`PaintLane` 会写成员缓存,直接后台化有数据竞争,属较大重构)。
2. **非拖拽的视界变化**(键盘方向键 / 工具栏)仍走整幅重绘(约 125ms);仅**鼠标左/中键拖动**走宽缓冲快路径。若需让键盘平移也快,需允许非拖拽帧复用"整数像素对齐"的宽缓冲(当前为避免 ≤0.5px 亚像素偏差而未启用)。
### 9.12 宽缓冲改为「滚动」:消除周期性重建尖峰(持续拖动恒 ≥60fps)
9.11 的残留 1(移出边界时整幅重建 2×视界,负载下约 30–50ms 的一次尖峰)已消除:
- **滚动式缓冲**:视界每帧移动 d 像素时,把缓冲内容**整体自贴图平移 `round(d)` 像素**,再**只补画新暴露的 `|round(d)|` 像素宽细条**。因此**每帧代价恒为 O(细条)**,不再有"移出边界就整幅重建"的周期性大尖峰。
- **细条渲染复用整条宽窗口的映射**:`RenderWideStrip` 令 `m_area` 只覆盖细条(GDI/GDI+ 裁剪落在细条),而 `m_plotRect` 仍是完整宽窗口 → 网格线与波形 x 坐标正确、刻度密度与主视界一致;仅新暴露的细条被重画。
- **亚像素误差有界**:用锚点 `m_wideAnchorMin` 记"视界左端当前对应缓冲中的时间",每帧按 `round((viewMin - anchor)*scale)` 平移并同步推进锚点,使"内容相对时间轴的偏移"始终 ≤0.5 像素。
- **缓冲只覆盖绘图区**(不含底部时间轴):时间轴由前台每帧按当前视图绘制,避免滚动细条把时间轴文字切成残字。
- **实测**(`VERY_BIG_DATA` 全信号,视界 `[40.1756, 40.1885]s`,5 网格):
| 场景 | 9.11 | 本次 |
| --- | --- | --- |
| 中键平移拖动(短距) | 1.3ms(含每屏一次 33ms 尖峰) | **约 6ms/帧,无尖峰** |
| 中键平移拖动(长距 975px) | 1.7ms(最大 33ms) | **约 6ms/帧,最大 10.5ms** |
| 弱波增强滑块 | 5.2ms | 6ms |
| 透明度/粗细/颜色 | 8ms | 8ms |
- **显示效果**:滚动平移帧截图核对无缝隙/无残字;**静止/结算帧与 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 的亚像素偏差且无法"结算"回精确帧,故当时保持整幅重绘以保证像素精确。**该残留已在 9.14 中解决。**
---
### 9.14 非拖拽视界变化(键盘 / 工具栏)也走宽基底快路径 + "结算"帧保证像素精确
9.13 的残留(非拖拽视界变化 ≈125ms / 8fps)已消除:
- **宽基底判定去掉"必须在拖拽中"**:只要"网格几何指纹 + 逐信号外观样式 + 装饰层"未变(派生曲线参数变化也算),任何**视界变化**都可用宽基底,前台只贴图 + 画时间轴/派生叠加/装饰层 —— 键盘方向键、工具栏缩放平移帧降到 **≈8ms**。
- **"结算"帧(保证最终像素精确)**:宽基底是整数像素平移,实际视界位置带**≤0.5px** 亚像素偏差。因此在快路径帧里把"内容指纹 ⊕ 视界"揉成一个 `wideKey`;**若本帧的 `wideKey` 与上一条快路径帧相同 → 判定"视界已停止变化" → 放弃快路径,落到整幅重绘**(一次全质量精确帧),并清空 `m_wideLastValid`。该帧同时刷新静态场景缓存,之后所有帧命中缓存(≈1ms),不会反复重绘。
- **分支顺序有硬约束**:静态场景贴图 → **宽基底** → 样式补丁 → 整幅重绘。
* 宽基底必须在**静态场景贴图之后**:视界与样式都没变时(鼠标悬停移动)必须命中场景贴图,否则每两帧就会触发一次"结算整幅重绘",悬停反而变卡。
* 宽基底必须在**样式补丁之前**:派生预览曲线参数变化(拖弱波增强 / 均值滤波滑块)走宽基底比"逐网格补丁"更快且质量更高(补丁会关抗锯齿)。
- **宽基底必须与"同一视界的整幅渲染"同源**(否则平移后网格/锯齿与前台不一致):基底渲染时**强制全质量抗锯齿**(不受拖拽快速帧影响),且网格竖向刻度沿用**前台视界跨度**而非 2× 的宽窗口跨度(后者会让竖网格线减半)。
- **实测**(`VERY_BIG_DATA`,视界 `[40.1756, 40.1885]s`,ms/帧):
| 场景 | 9.13 | 本次 |
| --- | --- | --- |
| **非拖拽视界变化(键盘/工具栏)** | 最大 124.6ms(≈8fps) | **最大 8.6ms** |
| 中键平移拖动(短/长距) | 8.5 / 7.4ms | 9.3 / 14.6ms |
| 透明度 / 颜色滑块 | 8–16ms | 10.6 / 12.5ms |
| 弱波增强滑块 | 最大 10.8ms | **最大 7.5ms** |
| 粗细滑块 | 8–15ms | 40 帧中仅**首帧** 127ms(其余 7.7ms) |
- **粗细滑块那一帧 127ms 是"设计内"的**:样式连续变化期间走"样式补丁 + 已降级标记"(快),一旦样式**停止变化**(含"回到与缓存相同的值")就必须补一帧全质量 —— 该帧必然是整幅重绘。它是**用户松手后的唯一一帧**,不是持续卡顿。
- **保真度实测**(新增 `perf.exe … 5` 像素对比模式):快路径与"同一视界的整幅全质量渲染"对比 ——
* 视界几乎不动(<0.01px,不滚动)时:`meanAbs = 0.68/255`(逐像素几乎一致,证明"底座 + 贴图"管线本身忠实)。
* 平移约 17px 时:`meanAbs ≈ 13.5/255`(差异集中在**密波形**上,是 ≤0.5px 亚像素平移的必然结果,肉眼不可辨);停止平移后的"结算"帧回到**像素精确**。
- **质量回退检查**:`mean_0/1/2`、`boost_0/1/2`、`panref` 七张静态/结算帧与 HEAD **字节完全一致**(SHA256 相同)。
- **意外收获**:平移**中途**帧比 HEAD 更干净 —— HEAD 把静态场景(含**图例 / Y 轴刻度**)直接贴进宽基底,平移后这些像素随之移动,出现**重复的网格标题与刻度重影**(见对比 PNG);本次改动后同一场景不再出现重影。
---
## 10. 变更日志(本文件维护记录)
@@ -296,3 +396,8 @@ Documents/ 项目文档(含本文件)
| 2026-10-11 | DeepSeek-V4.1-Flash | 完善异步:后台改**多线程池**(预览曲线并行计算、更快出现);`PaintLane` 分两遍把**预览曲线画在全部原信号之上**(不再被覆盖);**快速帧只用于改主体的拖拽**(拖光标/框选改为命中场景缓存,约 1–4ms)。小跨度逐像素 0 差异。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 预览曲线改为**按整条信号计算**,与「构建为新信号」逐位一致;新增**平移装饰层像素缓存**(图例 + Y 轴刻度在拖拽时直接贴回,2ms/大格拖拽 13ms→约 5ms);后台任务按 (信号,种类) 去重以防拖滑块淹没线程池。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 探针定位"样式/参数滑块在深放大下极卡":根因是**样式补丁用了抗锯齿**(≈72ms/网格)且**后台曲线发布时整幅作废**导致每帧整幅重绘。修复:样式补丁强制快速帧、拖拽中不整幅作废、交互期省略面板投影 → 透明度/粗细/颜色 77ms→16ms、弱波增强滑块 96–176ms→26ms,静止/结算帧逐像素不变。 |
| 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 差异。残留:非拖拽视界变化(键盘/工具栏)仍整幅重绘。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 消除 9.13 残留:**非拖拽视界变化(键盘/工具栏)也走宽基底快路径**(124.6ms→**8.6ms**),并用 `wideKey` 相等判定"视界已停止变化"触发一次**结算**整幅全质量帧以恢复像素精确;分支顺序固定为"静态贴图 → 宽基底 → 样式补丁 → 整幅",宽基底强制全质量抗锯齿且竖向刻度用前台跨度。新增像素对比模式验证:底座管线忠实(meanAbs 0.68/255),平移期仅 ≤0.5px 亚像素差;7 张静态/结算帧与 HEAD **字节一致**;平移中途帧不再有图例/刻度重影。 |