预览曲线改为按整条信号计算(与构建为新信号逐位一致);新增平移装饰层缓存消除拖拽卡顿
1) 预览曲线(均值滤波/中心轴/弱波增强)不再按可见窗口近似,统一按整条信号计算,与 BuildDerivedSignal 调用同一函数、同一段数据,逐位一致(用户要求预览与构建结果一致);整段窗口超门槛时交后台计算。 2) 新增装饰层(图例 + Y 轴刻度)像素缓存:实测整幅重绘中二者合计过半,却与时间轴视图无关。全质量帧捕获、拖拽快速帧贴回,仅捕获装饰矩形本身(无鬼影);用不含派生参数的 SceneDecorHash 校验。2ms/大格拖拽平移 13ms→约 5ms。 3) 后台任务按 (信号,曲线种类) 去重、以最新参数为准,避免拖滑块每帧入队整段任务淹没线程池。 4) 后台线程优先级低于正常,UI 优先获得 CPU。
This commit is contained in:
1 parent
709cbe4a87
commit
389b4ec2cf
3 files changed
+289
-32
No files matched your search
@@ -1894,9 +1894,10 @@ namespace jdi
|
||||
* 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。
|
||||
*
|
||||
* @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。
|
||||
* @param [IN] includeDerived 是否把派生预览曲线的开关与参数计入指纹。
|
||||
* @return 指纹值。
|
||||
*/
|
||||
std::uint64_t SceneContentHash(bool includeStyles = true) const;
|
||||
std::uint64_t SceneContentHash(bool includeStyles = true, bool includeDerived = true) const;
|
||||
|
||||
/**
|
||||
* @brief 场景内容中**不含逐信号外观样式**的指纹。
|
||||
@@ -1913,6 +1914,16 @@ namespace jdi
|
||||
*/
|
||||
std::uint64_t SceneGeometryHash() const;
|
||||
|
||||
/**
|
||||
* @brief 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**。
|
||||
*
|
||||
* 拖弱波增强等级/迭代次数滑块时,图例与 Y 轴刻度不变,仅网格内预览曲线变化;
|
||||
* 因此装饰层缓存不应因参数变化而失效(否则拖滑块每帧都要重绘图例与刻度)。
|
||||
*
|
||||
* @return 指纹值。
|
||||
*/
|
||||
std::uint64_t SceneDecorHash() const;
|
||||
|
||||
/**
|
||||
* @brief 静态场景缓存中"网格内信号外观样式"的快照条目。
|
||||
*/
|
||||
@@ -2124,6 +2135,42 @@ namespace jdi
|
||||
*/
|
||||
bool m_sceneDegraded = false;
|
||||
|
||||
// ---- 「装饰层」像素缓存(水平平移时不变的内容:Y 轴刻度胶囊 + 图例)----
|
||||
// 平移会让视图每帧变化 → 静态场景缓存必然失效 → 每帧都要重画波形**以及**网格线、
|
||||
// Y 轴刻度文字、图例(实测后者占整幅重绘的一半以上)。但这些装饰内容**只取决于网格几何与
|
||||
// 信号外观,与时间轴视图无关**,平移时不变化。因此在每张「全质量帧」后把装饰区域的像素
|
||||
// 捕获下来,拖拽(快速帧)时直接贴回,省去每帧重绘文字与圆角底板的开销。
|
||||
// 仅在完全相同(`m_decorKey` 命中)时启用,避免几何/外观变化时用到过期像素。
|
||||
/**
|
||||
* @brief 确保装饰缓存位图尺寸正确(不足则重建)。
|
||||
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
|
||||
* @return 可用返回 true。
|
||||
*/
|
||||
bool EnsureDecorCache(int width, int height);
|
||||
/** @brief 释放装饰缓存位图。 */
|
||||
void ReleaseDecorCache();
|
||||
/**
|
||||
* @brief 把本帧记录下来的装饰矩形从后备缓冲区捕获进装饰缓存。
|
||||
* @param [IN,OUT] g 绘图上下文。 @return 捕获成功返回 true。
|
||||
*/
|
||||
bool CaptureDecorCache(gd::Graphics& g);
|
||||
/**
|
||||
* @brief 把装饰缓存里的装饰贴回后备缓冲区(快速帧用)。
|
||||
* @param [IN,OUT] g 绘图上下文。 @return 贴图成功返回 true。
|
||||
*/
|
||||
bool BlitDecorCache(gd::Graphics& g);
|
||||
|
||||
mutable HDC m_decorDc = nullptr; /**< 装饰缓存设备上下文。 */
|
||||
mutable HBITMAP m_decorBitmap = nullptr; /**< 装饰缓存位图。 */
|
||||
mutable HGDIOBJ m_decorOldBitmap = nullptr; /**< 装饰 DC 原选位图。 */
|
||||
mutable int m_decorW = 0; /**< 装饰缓存宽度。 */
|
||||
mutable int m_decorH = 0; /**< 装饰缓存高度。 */
|
||||
mutable bool m_decorValid = false; /**< 装饰缓存内容是否有效。 */
|
||||
mutable std::uint64_t m_decorKey = 0; /**< 装饰缓存对应的内容指纹。 */
|
||||
mutable bool m_useDecorCache = false; /**< 本帧是否跳过绘制装饰、改用缓存贴回。 */
|
||||
/** 本帧需要捕获 / 贴回的装饰矩形(各网格的 Y 轴胶囊与图例带)。 */
|
||||
mutable std::vector<gd::RectF> m_decorRects;
|
||||
|
||||
/**
|
||||
* @brief 实验性功能浮动面板的像素图层项。
|
||||
*
|
||||
|
||||
@@ -672,6 +672,7 @@ namespace jdi
|
||||
// 先停掉后台计算线程:它可能正在读取采样数据,必须在释放其它资源前回收。
|
||||
StopCurveWorker();
|
||||
ReleaseSceneCache();
|
||||
ReleaseDecorCache();
|
||||
ReleasePanelCache();
|
||||
}
|
||||
|
||||
@@ -2333,19 +2334,35 @@ namespace jdi
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_curveMutex);
|
||||
for (const CurveJob& job : m_curveJobs)
|
||||
// 同一「信号 + 曲线种类」只保留**一个**待算任务,并以**最新参数**为准:
|
||||
// 拖弱波增强等级/迭代次数滑块时每帧参数都变,若每次都入队会把队列灌满整段曲线任务,
|
||||
// 后台线程池被淹没 → 与 UI 抢 CPU → 拖动反而卡顿。这里把同 (signal, kind) 的旧待算任务
|
||||
// 全部移除(正在计算的那个由工作线程另行负责,其结果是旧参数,会被后续条目取代)。
|
||||
bool identicalPending = false;
|
||||
for (auto iterator = m_curveJobs.begin(); iterator != m_curveJobs.end();)
|
||||
{
|
||||
if (job.key == key)
|
||||
if (iterator->key.signal == key.signal && iterator->key.kind == key.kind)
|
||||
{
|
||||
return; // 该曲线已有在算任务,避免任务堆积
|
||||
if (iterator->key == key)
|
||||
{
|
||||
identicalPending = true;
|
||||
}
|
||||
iterator = m_curveJobs.erase(iterator);
|
||||
}
|
||||
else
|
||||
{
|
||||
++iterator;
|
||||
}
|
||||
}
|
||||
CurveJob job;
|
||||
job.key = key;
|
||||
job.windowBegin = windowBegin;
|
||||
job.windowEnd = windowEnd;
|
||||
job.generation = m_curveGeneration;
|
||||
m_curveJobs.push_back(job);
|
||||
if (!identicalPending)
|
||||
{
|
||||
CurveJob job;
|
||||
job.key = key;
|
||||
job.windowBegin = windowBegin;
|
||||
job.windowEnd = windowEnd;
|
||||
job.generation = m_curveGeneration;
|
||||
m_curveJobs.push_back(job);
|
||||
}
|
||||
}
|
||||
EnsureCurveWorker();
|
||||
m_curveCv.notify_one();
|
||||
@@ -5580,6 +5597,11 @@ namespace jdi
|
||||
// 图例命中矩形每帧重建,避免残留已消失的信号。
|
||||
m_legendHits.clear();
|
||||
|
||||
// 「装饰层」缓存决策:快速帧(拖拽)期间、且装饰内容与上次捕获完全一致时,
|
||||
// 跳过 Y 轴胶囊与图例的绘制(约占整幅重绘的一半),改用上次捕获的像素贴回。
|
||||
m_decorRects.clear();
|
||||
m_useDecorCache = m_fastFrame && m_decorValid && (m_decorKey == SceneDecorHash());
|
||||
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
PaintLane(g, static_cast<int>(index), m_lanes[index], timeTicks);
|
||||
@@ -5587,6 +5609,19 @@ namespace jdi
|
||||
|
||||
PaintTimeAxis(g, timeTicks, timeStep);
|
||||
|
||||
// 装饰层:全质量帧捕获,快速帧贴回(见成员注释)。
|
||||
if (!m_fastFrame)
|
||||
{
|
||||
if (CaptureDecorCache(g))
|
||||
{
|
||||
m_decorKey = SceneDecorHash();
|
||||
}
|
||||
}
|
||||
else if (m_useDecorCache)
|
||||
{
|
||||
BlitDecorCache(g);
|
||||
}
|
||||
|
||||
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
|
||||
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
||||
// ⚠️ 快速帧(交互期间,关闭了抗锯齿)**仍然捕获**(这样拖动参数滑块时能走
|
||||
@@ -5786,7 +5821,7 @@ namespace jdi
|
||||
return SignalRef();
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::SceneContentHash(bool includeStyles) const
|
||||
std::uint64_t PlotView::SceneContentHash(bool includeStyles, bool includeDerived) const
|
||||
{
|
||||
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
||||
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
||||
@@ -5843,7 +5878,9 @@ namespace jdi
|
||||
// 会改变网格内的曲线绘制。它们**只影响网格内容**(不影响网格结构/几何),
|
||||
// 因此只在 includeStyles 时计入 —— 于是拖参数滑块可以走“局部重画”快路径,
|
||||
// 而不必整幅重绘(`FindStyleOnlyLanes` 用 `m_sceneDerived` 定位受影响的网格)。
|
||||
if (includeStyles)
|
||||
// `includeDerived == false` 时连这些参数也不计入(用于装饰层指纹:图例/Y 轴刻度
|
||||
// 与派生曲线参数无关,拖滑块时装饰层无需失效)。
|
||||
if (includeStyles && includeDerived)
|
||||
{
|
||||
mix(Tab().Lanes()[laneIndex].edgeCenter ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].edgeCenterItems.size()));
|
||||
@@ -5910,6 +5947,13 @@ namespace jdi
|
||||
return SceneContentHash(false);
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::SceneDecorHash() const
|
||||
{
|
||||
// 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**
|
||||
//(拖弱波增强等级/迭代次数滑块时图例与刻度不变,装饰层缓存不应失效)。
|
||||
return SceneContentHash(true, false);
|
||||
}
|
||||
|
||||
void PlotView::CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const
|
||||
{
|
||||
out.clear();
|
||||
@@ -6507,6 +6551,129 @@ namespace jdi
|
||||
m_sceneHeight = 0;
|
||||
}
|
||||
|
||||
bool PlotView::EnsureDecorCache(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_decorDc != nullptr && m_decorW == width && m_decorH == height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
ReleaseDecorCache();
|
||||
HDC screen = GetDC(nullptr);
|
||||
if (screen == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_decorDc = CreateCompatibleDC(screen);
|
||||
m_decorBitmap = CreateCompatibleBitmap(screen, width, height);
|
||||
ReleaseDC(nullptr, screen);
|
||||
if (m_decorDc == nullptr || m_decorBitmap == nullptr)
|
||||
{
|
||||
ReleaseDecorCache();
|
||||
return false;
|
||||
}
|
||||
m_decorOldBitmap = SelectObject(m_decorDc, m_decorBitmap);
|
||||
m_decorW = width;
|
||||
m_decorH = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::ReleaseDecorCache()
|
||||
{
|
||||
m_decorValid = false;
|
||||
if (m_decorDc != nullptr)
|
||||
{
|
||||
if (m_decorOldBitmap != nullptr)
|
||||
{
|
||||
SelectObject(m_decorDc, m_decorOldBitmap);
|
||||
m_decorOldBitmap = nullptr;
|
||||
}
|
||||
DeleteDC(m_decorDc);
|
||||
m_decorDc = nullptr;
|
||||
}
|
||||
if (m_decorBitmap != nullptr)
|
||||
{
|
||||
DeleteObject(m_decorBitmap);
|
||||
m_decorBitmap = nullptr;
|
||||
}
|
||||
m_decorW = 0;
|
||||
m_decorH = 0;
|
||||
}
|
||||
|
||||
bool PlotView::CaptureDecorCache(gd::Graphics& g)
|
||||
{
|
||||
if (m_decorRects.empty())
|
||||
{
|
||||
m_decorValid = false;
|
||||
return false;
|
||||
}
|
||||
// 用绘图区矩形作为缓存位图尺寸(装饰都在绘图区内)。
|
||||
const int width = static_cast<int>(std::ceil(m_area.Width));
|
||||
const int height = static_cast<int>(std::ceil(m_area.Height));
|
||||
if (!EnsureDecorCache(width, height))
|
||||
{
|
||||
m_decorValid = false;
|
||||
return false;
|
||||
}
|
||||
HDC source = g.GetHDC();
|
||||
if (source == nullptr)
|
||||
{
|
||||
m_decorValid = false;
|
||||
return false;
|
||||
}
|
||||
const int originX = static_cast<int>(std::floor(m_area.X));
|
||||
const int originY = static_cast<int>(std::floor(m_area.Y));
|
||||
bool any = false;
|
||||
for (const gd::RectF& rect : m_decorRects)
|
||||
{
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BitBlt(m_decorDc, x0, y0, x1 - x0, y1 - y0, source, x0 + originX, y0 + originY, SRCCOPY);
|
||||
any = true;
|
||||
}
|
||||
g.ReleaseHDC(source);
|
||||
m_decorValid = any;
|
||||
return any;
|
||||
}
|
||||
|
||||
bool PlotView::BlitDecorCache(gd::Graphics& g)
|
||||
{
|
||||
if (!m_decorValid || m_decorDc == nullptr || m_decorRects.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
HDC target = g.GetHDC();
|
||||
if (target == nullptr)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BitBlt(target, x0 + originX, y0 + originY, x1 - x0, y1 - y0, m_decorDc, x0, y0, 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)
|
||||
@@ -7017,18 +7184,29 @@ namespace jdi
|
||||
pillZero.push_back(tick == 0.0);
|
||||
}
|
||||
|
||||
if (!pills.empty())
|
||||
// 记录胶囊矩形(供装饰层捕获/贴回;略微外扩以容纳圆角与文字边缘)。
|
||||
for (const gd::RectF& pill : pills)
|
||||
{
|
||||
UiFillRoundRects(g, pills, 3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xF0));
|
||||
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));
|
||||
}
|
||||
for (std::size_t index = 0; index < pills.size(); ++index)
|
||||
// 快速帧且装饰缓存命中时:跳过重绘(文字 + 圆角底板),改由装饰层贴回。
|
||||
if (!m_useDecorCache)
|
||||
{
|
||||
const gd::RectF& pill = pills[index];
|
||||
// 用 UiTextAtClipped 保证长文本不越过绘图区(GDI 文本不受 GDI+ 裁剪约束)。
|
||||
UiTextAtClipped(g, pillTexts[index], m_fonts->monoSmall, pill.X + 4.0f * m_scale,
|
||||
gd::RectF(pill.X + 4.0f * m_scale, pill.Y, pill.Width - 8.0f * m_scale,
|
||||
pill.Height),
|
||||
pillZero[index] ? m_theme->text : m_theme->textMuted, clipRectForLabels);
|
||||
if (!pills.empty())
|
||||
{
|
||||
UiFillRoundRects(g, pills, 3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xF0));
|
||||
}
|
||||
for (std::size_t index = 0; index < pills.size(); ++index)
|
||||
{
|
||||
const gd::RectF& pill = pills[index];
|
||||
// 用 UiTextAtClipped 保证长文本不越过绘图区(GDI 文本不受 GDI+ 裁剪约束)。
|
||||
UiTextAtClipped(g, pillTexts[index], m_fonts->monoSmall, pill.X + 4.0f * m_scale,
|
||||
gd::RectF(pill.X + 4.0f * m_scale, pill.Y, pill.Width - 8.0f * m_scale,
|
||||
pill.Height),
|
||||
pillZero[index] ? m_theme->text : m_theme->textMuted,
|
||||
clipRectForLabels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7117,6 +7295,18 @@ 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));
|
||||
@@ -8463,17 +8653,18 @@ namespace jdi
|
||||
// 与「边沿计数(中心轴)」共用同一段绘制逻辑
|
||||
//(同色相高对比度的派生配色 + 同透明度),只是曲线本身按多次迭代滤波计算。
|
||||
const std::size_t count = std::min(resolved.file->time.size(), resolved.data->values.size());
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = 0;
|
||||
VisibleIndexRange(*resolved.file, count, begin, end);
|
||||
if (end <= begin)
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ⭐ 预览曲线一律按**整条信号**计算(`[0, count]`),与「构建为新信号」使用的
|
||||
// `ComputeMovingAverageIterated(整段 values, ...)` 完全相同,因此预览与构建结果
|
||||
// **逐位一致**(用户明确要求"预览显示效果要和构建为新信号一致")。
|
||||
// 整段窗口超过异步门槛时由后台线程计算,绘制线程本帧用已算好的旧曲线顶替,
|
||||
// 因此不会阻塞交互;小信号同步计算。
|
||||
std::size_t spanBegin = 0;
|
||||
const std::vector<float>* curve = CurveSpanFor(DerivedCurveKind::MEAN_FILTER, *resolved.data,
|
||||
points, iterations, 0, begin, end, spanBegin,
|
||||
true);
|
||||
points, iterations, 0, 0, count, spanBegin, true);
|
||||
if (curve == nullptr)
|
||||
{
|
||||
return;
|
||||
@@ -8489,16 +8680,14 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
const std::size_t count = std::min(resolved.file->time.size(), resolved.data->values.size());
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = 0;
|
||||
VisibleIndexRange(*resolved.file, count, begin, end);
|
||||
if (end <= begin)
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 预览曲线一律按**整条信号**计算(与「构建为新信号」逐位一致),详见 `PaintMeanFilterCurve`。
|
||||
std::size_t spanBegin = 0;
|
||||
const std::vector<float>* curve = CurveSpanFor(DerivedCurveKind::WEAK_BOOST, *resolved.data, 0,
|
||||
iterations, levelPercent, begin, end, spanBegin,
|
||||
iterations, levelPercent, 0, count, spanBegin,
|
||||
true);
|
||||
if (curve == nullptr)
|
||||
{
|
||||
|
||||
@@ -236,6 +236,26 @@ Documents/ 项目文档(含本文件)
|
||||
- **平移(尤其"一大格=2ms")的固有下限**:平移会让视图每帧变化 → 静态场景缓存必然失效 → 每帧都要重画波形(**这部分无法异步**,因为 GDI+ 绘制必须在 UI 线程;异步只覆盖"派生预览曲线"的计算)。在 5 个网格、含 2576 万采样编码器方波的 `VERY_BIG_DATA` 上,实测**拖拽平移**约 6–15ms/帧(机器空闲/繁忙分别为下限/上限),已无尖峰。若日后要进一步降低,只能减少波形绘制本身(受"不牺牲显示效果"约束)或把整帧渲染也搬到离屏线程(改动很大、风险高)。
|
||||
- **预览曲线"看起来没画出来"的真相**:对**编码器方波这类强信号**,弱波增强几乎不改动波形,所以预览曲线与原信号**几乎重合**(读数胶囊里两者数值只差末位),视觉上"看不见"——这是**正确行为**。用 `mode=1` 导出可确认曲线确实被绘制(G1–G5 均有浅色叠加 +「(弱波增强)」读数)。小跨度渲染与旧实现**逐像素 0 差异**。
|
||||
|
||||
### 9.8 预览曲线严格等同「构建为新信号」+ 平移装饰层缓存(本仓库当前状态)
|
||||
**(一)预览曲线与「构建为新信号」逐位一致**
|
||||
- 原实现按「可见区间 ± 保护带」计算预览曲线(近似,边界可能略有差异)。用户明确要求"预览显示效果要和构建为新信号一致"。
|
||||
- 现改为**一律按整条信号计算**(`PaintMeanFilterCurve`/`PaintWeakBoostCurve` 传 `begin=0, end=count`):与 `BuildDerivedSignal` 调用的是**同一个** `ComputeWeakWaveBoost`/`ComputeMovingAverageIterated`、同一段 `values`,因此**逐位一致**。整段窗口超过异步门槛时由后台线程计算(绘制线程不阻塞)。
|
||||
|
||||
**(二)平移期间的「装饰层」像素缓存(`m_decorDc`)**
|
||||
- 实测(`VERY_BIG_DATA`,2ms/大格)整幅重绘约 10ms,其中 **图例 ≈3.5ms、Y 轴刻度 ≈1.5ms**(文字 + 圆角底板),二者合计过半;而它们**只随网格几何/信号外观变化,与时间轴视图无关**(平移时不变)。
|
||||
- 实现:每张"全质量帧"后,把**图例带**(编号徽标 + 底板 + 色块 + 文字)与**Y 轴刻度胶囊**的像素从后备缓冲区捕获进 `m_decorDc`;拖拽(快速帧)时**跳过重绘**、直接 `BitBlt` 贴回。用专用指纹 `SceneDecorHash()`(含外观样式、**不含派生曲线参数**)校验,内容一旦变化即失效并重新捕获,绝不使用过期像素。仅捕获**装饰矩形本身**(逐胶囊 + 图例带),矩形之间的波形照常绘制,因此**无错位/无鬼影**。
|
||||
- 效果:拖拽平移(2ms/大格)整帧 **13ms → 约 5ms**;0.4% 跨度 20→8ms。**注意**:该缓存只作用于「拖拽(快速帧)」——静止帧仍按全质量重绘装饰(保证最终画质)。
|
||||
- 拖弱波增强滑块时,`SceneDecorHash` 不含派生参数 → 装饰层不会因滑块每帧变参数而失效(否则拖滑块要重绘图例/刻度)。
|
||||
|
||||
**(三)后台任务去重(避免拖滑块淹没线程池)**
|
||||
- 拖滑块每帧改参数 → 若每帧都入队"整条曲线"任务,会灌满队列、后台线程池被淹没并与 UI 抢 CPU。现按「信号 + 曲线种类」只保留**一个**待算任务、**以最新参数为准**(旧的待算任务被移除;正在计算的那个结果由后续条目取代)。
|
||||
|
||||
**(四)后台线程优先级**
|
||||
- 见 9.7 第 3 点:后台线程为 `THREAD_PRIORITY_BELOW_NORMAL`,UI 需要运行时永远优先获得 CPU。
|
||||
|
||||
**(五)平移仍有固有下限(诚实说明)**
|
||||
- 平移**必然**让视图每帧变化 → 静态场景缓存失效 → 波形必须在 UI 线程重画(GDI+ 只能在 UI 线程绘制,**无法异步**)。装饰层缓存已把"与视图无关"的部分(图例/刻度)从每帧开销中移除,剩余开销是波形本身 + 网格线 + 时间轴,属"不牺牲显示效果"下的固有成本。
|
||||
|
||||
---
|
||||
|
||||
## 10. 变更日志(本文件维护记录)
|
||||
@@ -247,3 +267,4 @@ Documents/ 项目文档(含本文件)
|
||||
| 2026-10-11 | DeepSeek-V4.1-Flash | 深度修复「实验性功能预览曲线导致的交互卡顿」:大窗口改入缓存(静止重绘 2247ms→0.25ms)、交互期间延迟重算(弱波增强拖滑块 2303ms→18ms)、计算缓冲复用;显示逐像素不变。 |
|
||||
| 2026-10-11 | DeepSeek-V4.1-Flash | 架构升级:**派生曲线计算与 UI 绘制分离**(后台线程 + 双缓冲)。绘制线程永不等计算,弱波增强拖滑块/平移从 2303/2247ms 降到 ~15ms;结构变化前 `PauseDerivedAsync` 保证线程安全;小窗口同步路径与旧实现逐像素 0 差异。 |
|
||||
| 2026-10-11 | DeepSeek-V4.1-Flash | 完善异步:后台改**多线程池**(预览曲线并行计算、更快出现);`PaintLane` 分两遍把**预览曲线画在全部原信号之上**(不再被覆盖);**快速帧只用于改主体的拖拽**(拖光标/框选改为命中场景缓存,约 1–4ms)。小跨度逐像素 0 差异。 |
|
||||
| 2026-10-11 | DeepSeek-V4.1-Flash | 预览曲线改为**按整条信号计算**,与「构建为新信号」逐位一致;新增**平移装饰层像素缓存**(图例 + Y 轴刻度在拖拽时直接贴回,2ms/大格拖拽 13ms→约 5ms);后台任务按 (信号,种类) 去重以防拖滑块淹没线程池。 |
|
||||
Reference in new issue
Block a user