Compare commits

..
4 Commits
Author SHA1 Message Date
DeepSeek V4.1 Flash c794967b02 记录离线逐像素校验结论:整段预览与旧窗口预览的全质量帧 0 差异(含 GetPixel 踩坑提醒) 2026-10-11 15:44:14 +08:00
DeepSeek V4.1 Flash 389b4ec2cf 预览曲线改为按整条信号计算(与构建为新信号逐位一致);新增平移装饰层缓存消除拖拽卡顿
1) 预览曲线(均值滤波/中心轴/弱波增强)不再按可见窗口近似,统一按整条信号计算,与 BuildDerivedSignal 调用同一函数、同一段数据,逐位一致(用户要求预览与构建结果一致);整段窗口超门槛时交后台计算。
2) 新增装饰层(图例 + Y 轴刻度)像素缓存:实测整幅重绘中二者合计过半,却与时间轴视图无关。全质量帧捕获、拖拽快速帧贴回,仅捕获装饰矩形本身(无鬼影);用不含派生参数的 SceneDecorHash 校验。2ms/大格拖拽平移 13ms→约 5ms。
3) 后台任务按 (信号,曲线种类) 去重、以最新参数为准,避免拖滑块每帧入队整段任务淹没线程池。
4) 后台线程优先级低于正常,UI 优先获得 CPU。
2026-10-11 15:33:37 +08:00
DeepSeek V4.1 Flash 709cbe4a87 后台计算线程优先级降为低于正常,确保拖动时 UI 优先获得 CPU
派生曲线计算是可延迟的后台工作,不能与 UI/绘制线程抢核。后台线程池的每个线程设为 THREAD_PRIORITY_BELOW_NORMAL,UI 线程需要运行时系统优先调度 UI,计算只在 UI 空闲时占用 CPU。
2026-10-11 14:23:42 +08:00
DeepSeek V4.1 Flash 226943921b 完善异步架构:后台线程池 + 预览曲线上层绘制 + 快速帧只用于改主体的拖拽
1) 后台派生曲线计算改为多线程池(线程数 = clamp(硬件并发-1,1,4)),多条预览曲线并行计算、更快出现(原单线程顺序处理 5 条整段弱波增强约 1.8s,用户长时间看不到曲线)。
2) PaintLane 分两遍:先画全部原信号,再画全部派生预览曲线,保证预览曲线在本网格全部原信号之上、不被覆盖(用户要求)。
3) 「快速帧(关抗锯齿)」只用于会改变画面主体的拖拽(PAN/SCROLL/LANE_REORDER/MOVE_SIGNAL/BOOST_*);拖时间光标、选区框选、拖面板这类只改叠加层的交互改为复用静态场景缓存(贴图约 1ms),实测拖光标从整幅重画降到约 1-4ms。
小跨度渲染与旧实现逐像素 0 差异。
2026-10-11 14:15:26 +08:00
3 changed files with 343 additions and 35 deletions

No files matched your search

+49 -2
View File
@@ -1746,7 +1746,7 @@ namespace jdi
// 缩放等交互的反馈始终跟手,不会被计算拖住,同时把重计算放到后台充分利用多核。 // 缩放等交互的反馈始终跟手,不会被计算拖住,同时把重计算放到后台充分利用多核。
mutable std::mutex m_curveMutex; /**< 保护任务队列与已算好条目。 */ mutable std::mutex m_curveMutex; /**< 保护任务队列与已算好条目。 */
mutable std::condition_variable m_curveCv; /**< 唤醒后台线程。 */ mutable std::condition_variable m_curveCv; /**< 唤醒后台线程。 */
mutable std::thread m_curveWorker; /**< 后台计算线程(惰性启动)。 */ mutable std::vector<std::thread> m_curveWorkers; /**< 后台计算线程池(惰性启动,多核并行)。 */
mutable bool m_curveWorkerStop = false; /**< 请求后台线程退出。 */ mutable bool m_curveWorkerStop = false; /**< 请求后台线程退出。 */
mutable std::uint64_t m_curveGeneration = 0; /**< 数据代次(结构变化时自增)。 */ mutable std::uint64_t m_curveGeneration = 0; /**< 数据代次(结构变化时自增)。 */
mutable std::uint64_t m_curveSerial = 0; /**< 已发布条目序号。 */ mutable std::uint64_t m_curveSerial = 0; /**< 已发布条目序号。 */
@@ -1894,9 +1894,10 @@ namespace jdi
* 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。 * 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。
* *
* @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。 * @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。
* @param [IN] includeDerived 是否把派生预览曲线的开关与参数计入指纹。
* @return 指纹值。 * @return 指纹值。
*/ */
std::uint64_t SceneContentHash(bool includeStyles = true) const; std::uint64_t SceneContentHash(bool includeStyles = true, bool includeDerived = true) const;
/** /**
* @brief 场景内容中**不含逐信号外观样式**的指纹。 * @brief 场景内容中**不含逐信号外观样式**的指纹。
@@ -1913,6 +1914,16 @@ namespace jdi
*/ */
std::uint64_t SceneGeometryHash() const; std::uint64_t SceneGeometryHash() const;
/**
* @brief 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**。
*
* 拖弱波增强等级/迭代次数滑块时,图例与 Y 轴刻度不变,仅网格内预览曲线变化;
* 因此装饰层缓存不应因参数变化而失效(否则拖滑块每帧都要重绘图例与刻度)。
*
* @return 指纹值。
*/
std::uint64_t SceneDecorHash() const;
/** /**
* @brief 静态场景缓存中"网格内信号外观样式"的快照条目。 * @brief 静态场景缓存中"网格内信号外观样式"的快照条目。
*/ */
@@ -2124,6 +2135,42 @@ namespace jdi
*/ */
bool m_sceneDegraded = false; 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 实验性功能浮动面板的像素图层项。 * @brief 实验性功能浮动面板的像素图层项。
* *
+259 -33
View File
@@ -672,6 +672,7 @@ namespace jdi
// 先停掉后台计算线程:它可能正在读取采样数据,必须在释放其它资源前回收。 // 先停掉后台计算线程:它可能正在读取采样数据,必须在释放其它资源前回收。
StopCurveWorker(); StopCurveWorker();
ReleaseSceneCache(); ReleaseSceneCache();
ReleaseDecorCache();
ReleasePanelCache(); ReleasePanelCache();
} }
@@ -2333,13 +2334,28 @@ namespace jdi
} }
{ {
std::lock_guard<std::mutex> lock(m_curveMutex); 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;
} }
} }
if (!identicalPending)
{
CurveJob job; CurveJob job;
job.key = key; job.key = key;
job.windowBegin = windowBegin; job.windowBegin = windowBegin;
@@ -2347,23 +2363,34 @@ namespace jdi
job.generation = m_curveGeneration; job.generation = m_curveGeneration;
m_curveJobs.push_back(job); m_curveJobs.push_back(job);
} }
}
EnsureCurveWorker(); EnsureCurveWorker();
m_curveCv.notify_one(); m_curveCv.notify_one();
} }
void PlotView::EnsureCurveWorker() const void PlotView::EnsureCurveWorker() const
{ {
if (m_curveWorker.joinable()) if (!m_curveWorkers.empty())
{ {
return; return;
} }
// **多核并行**:后台线程池并发计算多条派生曲线(5 个网格各一条时,
// 串行要 5×368ms,并行只要约 1×368ms),预览曲线因此能很快出现。
// 线程数 = 硬件并发 - 1(给 UI/绘制线程留一个核),夹在 1~4。
unsigned int hardware = std::thread::hardware_concurrency();
int workers = static_cast<int>(hardware == 0 ? 2u : hardware) - 1;
workers = std::max(1, std::min(workers, 4));
m_curveWorkerStop = false; m_curveWorkerStop = false;
m_curveWorker = std::thread([this]() { CurveWorkerLoop(); }); m_curveWorkers.reserve(static_cast<std::size_t>(workers));
for (int index = 0; index < workers; ++index)
{
m_curveWorkers.emplace_back([this]() { CurveWorkerLoop(); });
}
} }
void PlotView::StopCurveWorker() const void PlotView::StopCurveWorker() const
{ {
if (!m_curveWorker.joinable()) if (m_curveWorkers.empty())
{ {
return; return;
} }
@@ -2373,11 +2400,23 @@ namespace jdi
m_curveJobs.clear(); m_curveJobs.clear();
} }
m_curveCv.notify_all(); m_curveCv.notify_all();
m_curveWorker.join(); for (std::thread& worker : m_curveWorkers)
{
if (worker.joinable())
{
worker.join();
}
}
m_curveWorkers.clear();
} }
void PlotView::CurveWorkerLoop() const void PlotView::CurveWorkerLoop() const
{ {
// 后台计算线程的优先级降到「低于正常」:派生曲线计算是**可延迟的后台工作**,
// 绝不能让它在拖动时与 UI / 绘制线程抢 CPU。降低优先级后,只要 UI 线程要运行,
// 系统就会优先调度 UI;计算只在 UI 空闲时占用 CPU,从而保证交互始终跟手
//(用户明确要求"无论如何 UI 及控件的响应需要是跟手的")。
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
for (;;) for (;;)
{ {
CurveJob job; CurveJob job;
@@ -5461,11 +5500,18 @@ namespace jdi
return; return;
} }
// 交互期间(拖拽 / 拖动滑块)改用「快速绘制」:关闭波形与派生曲线的抗锯齿。 // 「快速绘制(关闭抗锯齿)」只在**会改变画面主体(波形/网格几何)**的拖拽期间启用:
// 用户明确要求"先用低分辨率,绘制完成后再用高分辨率替换"—— // 这些拖拽每帧都要重画波形,关掉抗锯齿能显著加速(同一条包络带 27.6ms → 3.2ms)。
// 抗锯齿是单帧最贵的一项(同一条包络带实测 27.6ms → 3.2ms),几何完全不变; // ⚠️ 像「拖时间光标」「选区框选」「拖浮动面板」这类**只改叠加层、画面主体不变**的交互,
// 松手时宿主会整帧失效,因此全质量版本随即补上。 // 一旦启用快速帧就会给静态场景缓存打上「已降级」标记 → 缓存失效 → 每帧整幅重画
m_fastFrame = (m_dragMode != PlotDragMode::NONE); //(实测拖光标从约 1ms 骤升到十几毫秒)。因此这里**不**把它们算作快速帧,
// 让它们直接复用静态场景缓存(贴图 ~1ms)+ 叠加层,做到真正跟手。
const bool sceneChangingDrag =
(m_dragMode == PlotDragMode::PAN) || (m_dragMode == PlotDragMode::SCROLL_LANES) ||
(m_dragMode == PlotDragMode::LANE_REORDER) || (m_dragMode == PlotDragMode::MOVE_SIGNAL) ||
(m_dragMode == PlotDragMode::BOOST_LEVEL) ||
(m_dragMode == PlotDragMode::BOOST_ITERATIONS);
m_fastFrame = sceneChangingDrag;
// 贴图之后、静态场景之外的全部内容(随鼠标变化或位于最上层)。 // 贴图之后、静态场景之外的全部内容(随鼠标变化或位于最上层)。
// 三个分支(完全命中 / 局部重画 / 整幅重绘)共用同一段,保证观感完全一致。 // 三个分支(完全命中 / 局部重画 / 整幅重绘)共用同一段,保证观感完全一致。
@@ -5551,6 +5597,11 @@ namespace jdi
// 图例命中矩形每帧重建,避免残留已消失的信号。 // 图例命中矩形每帧重建,避免残留已消失的信号。
m_legendHits.clear(); 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) for (std::size_t index = 0; index < m_lanes.size(); ++index)
{ {
PaintLane(g, static_cast<int>(index), m_lanes[index], timeTicks); PaintLane(g, static_cast<int>(index), m_lanes[index], timeTicks);
@@ -5558,6 +5609,19 @@ namespace jdi
PaintTimeAxis(g, timeTicks, timeStep); PaintTimeAxis(g, timeTicks, timeStep);
// 装饰层:全质量帧捕获,快速帧贴回(见成员注释)。
if (!m_fastFrame)
{
if (CaptureDecorCache(g))
{
m_decorKey = SceneDecorHash();
}
}
else if (m_useDecorCache)
{
BlitDecorCache(g);
}
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容, // 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。 // 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
// ⚠️ 快速帧(交互期间,关闭了抗锯齿)**仍然捕获**(这样拖动参数滑块时能走 // ⚠️ 快速帧(交互期间,关闭了抗锯齿)**仍然捕获**(这样拖动参数滑块时能走
@@ -5757,7 +5821,7 @@ namespace jdi
return SignalRef(); return SignalRef();
} }
std::uint64_t PlotView::SceneContentHash(bool includeStyles) const std::uint64_t PlotView::SceneContentHash(bool includeStyles, bool includeDerived) const
{ {
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。 // FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效, // 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
@@ -5814,7 +5878,9 @@ namespace jdi
// 会改变网格内的曲线绘制。它们**只影响网格内容**(不影响网格结构/几何), // 会改变网格内的曲线绘制。它们**只影响网格内容**(不影响网格结构/几何),
// 因此只在 includeStyles 时计入 —— 于是拖参数滑块可以走“局部重画”快路径, // 因此只在 includeStyles 时计入 —— 于是拖参数滑块可以走“局部重画”快路径,
// 而不必整幅重绘(`FindStyleOnlyLanes` 用 `m_sceneDerived` 定位受影响的网格)。 // 而不必整幅重绘(`FindStyleOnlyLanes` 用 `m_sceneDerived` 定位受影响的网格)。
if (includeStyles) // `includeDerived == false` 时连这些参数也不计入(用于装饰层指纹:图例/Y 轴刻度
// 与派生曲线参数无关,拖滑块时装饰层无需失效)。
if (includeStyles && includeDerived)
{ {
mix(Tab().Lanes()[laneIndex].edgeCenter ? 1ull : 0ull); mix(Tab().Lanes()[laneIndex].edgeCenter ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].edgeCenterItems.size())); mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].edgeCenterItems.size()));
@@ -5881,6 +5947,13 @@ namespace jdi
return SceneContentHash(false); return SceneContentHash(false);
} }
std::uint64_t PlotView::SceneDecorHash() const
{
// 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**
//(拖弱波增强等级/迭代次数滑块时图例与刻度不变,装饰层缓存不应失效)。
return SceneContentHash(true, false);
}
void PlotView::CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const void PlotView::CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const
{ {
out.clear(); out.clear();
@@ -6478,6 +6551,129 @@ namespace jdi
m_sceneHeight = 0; 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) void PlotView::PaintRubberBand(gd::Graphics& g)
{ {
if (!m_dragMoved || m_rubberRect.Width <= 0.0f || m_rubberRect.Height <= 0.0f) if (!m_dragMoved || m_rubberRect.Width <= 0.0f || m_rubberRect.Height <= 0.0f)
@@ -6857,10 +7053,20 @@ namespace jdi
g.FillPath(&brush, &scatterPath); g.FillPath(&brush, &scatterPath);
} }
} }
}
// 中心轴均值滤波曲线(「实验性功能 → 边沿计数(中心轴)」)叠加绘制在原信号之上。 // 第二遍:把所有**派生预览曲线**统一绘制在**本网格全部原信号之上**。
// 它只是叠加显示的派生曲线,**不会**加入左侧信号列表; // 若在同一个信号循环里就地叠加,后绘制的原信号会把先前信号的预览曲线盖住
// **只有该信号被勾选时**才绘制(参数与「均值滤波」同源,曲线逐位一致)。 //(用户明确要求"预览信号处于原信号上层不要被覆盖")。
for (const SignalRef& ref : laneSignals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
{
continue;
}
// 中心轴均值滤波曲线(「实验性功能 → 边沿计数(中心轴)」):与「均值滤波」同源,
// 只有被勾选的信号才绘制。
if (centerOn) if (centerOn)
{ {
const Lane::EdgeCenterItem* centerItem = const Lane::EdgeCenterItem* centerItem =
@@ -6870,8 +7076,7 @@ namespace jdi
PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations); PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations);
} }
} }
// 均值滤波曲线(「实验性功能 → 均值滤波」):同样是仅用于预览的叠加曲线, // 均值滤波预览曲线:只有被勾选的信号才绘制(参数取自它自己的条目)。
// 但**只有该信号被勾选时**才绘制(参数取自它自己的条目)。
if (meanFilterOn) if (meanFilterOn)
{ {
const Lane::MeanFilterItem* item = const Lane::MeanFilterItem* item =
@@ -6881,8 +7086,7 @@ namespace jdi
PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations); PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations);
} }
} }
// 弱波增强曲线:同样是仅用于预览的叠加曲线,只在被勾选时绘制 // 弱波增强预览曲线:只有被勾选的信号才绘制(增强等级取自整个网格)。
//(增强等级取自整个网格)。
if (weakBoostOn) if (weakBoostOn)
{ {
const Lane::WeakBoostItem* item = const Lane::WeakBoostItem* item =
@@ -6980,6 +7184,15 @@ namespace jdi
pillZero.push_back(tick == 0.0); pillZero.push_back(tick == 0.0);
} }
// 记录胶囊矩形(供装饰层捕获/贴回;略微外扩以容纳圆角与文字边缘)。
for (const gd::RectF& pill : pills)
{
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()) if (!pills.empty())
{ {
UiFillRoundRects(g, pills, 3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xF0)); UiFillRoundRects(g, pills, 3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xF0));
@@ -6991,7 +7204,9 @@ namespace jdi
UiTextAtClipped(g, pillTexts[index], m_fonts->monoSmall, pill.X + 4.0f * m_scale, 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, gd::RectF(pill.X + 4.0f * m_scale, pill.Y, pill.Width - 8.0f * m_scale,
pill.Height), pill.Height),
pillZero[index] ? m_theme->text : m_theme->textMuted, clipRectForLabels); pillZero[index] ? m_theme->text : m_theme->textMuted,
clipRectForLabels);
}
} }
} }
@@ -7080,6 +7295,18 @@ namespace jdi
{ {
// 底板从编号徽标开始一直盖到最后一个图例条目(编号徽标一并被掠开底下的网格线)。 // 底板从编号徽标开始一直盖到最后一个图例条目(编号徽标一并被掠开底下的网格线)。
const float plateRight = cursorX + legendWidth + (legendWidth > 0.0f ? 2.0f * m_scale : 0.0f); 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, 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), 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, WithAlpha(m_theme->cardBg, 0xE8));
@@ -8426,17 +8653,18 @@ namespace jdi
// 与「边沿计数(中心轴)」共用同一段绘制逻辑 // 与「边沿计数(中心轴)」共用同一段绘制逻辑
//(同色相高对比度的派生配色 + 同透明度),只是曲线本身按多次迭代滤波计算。 //(同色相高对比度的派生配色 + 同透明度),只是曲线本身按多次迭代滤波计算。
const std::size_t count = std::min(resolved.file->time.size(), resolved.data->values.size()); const std::size_t count = std::min(resolved.file->time.size(), resolved.data->values.size());
std::size_t begin = 0; if (count == 0)
std::size_t end = 0;
VisibleIndexRange(*resolved.file, count, begin, end);
if (end <= begin)
{ {
return; return;
} }
// ⭐ 预览曲线一律按**整条信号**计算(`[0, count]`),与「构建为新信号」使用的
// `ComputeMovingAverageIterated(整段 values, ...)` 完全相同,因此预览与构建结果
// **逐位一致**(用户明确要求"预览显示效果要和构建为新信号一致")。
// 整段窗口超过异步门槛时由后台线程计算,绘制线程本帧用已算好的旧曲线顶替,
// 因此不会阻塞交互;小信号同步计算。
std::size_t spanBegin = 0; std::size_t spanBegin = 0;
const std::vector<float>* curve = CurveSpanFor(DerivedCurveKind::MEAN_FILTER, *resolved.data, const std::vector<float>* curve = CurveSpanFor(DerivedCurveKind::MEAN_FILTER, *resolved.data,
points, iterations, 0, begin, end, spanBegin, points, iterations, 0, 0, count, spanBegin, true);
true);
if (curve == nullptr) if (curve == nullptr)
{ {
return; return;
@@ -8452,16 +8680,14 @@ namespace jdi
return; return;
} }
const std::size_t count = std::min(resolved.file->time.size(), resolved.data->values.size()); const std::size_t count = std::min(resolved.file->time.size(), resolved.data->values.size());
std::size_t begin = 0; if (count == 0)
std::size_t end = 0;
VisibleIndexRange(*resolved.file, count, begin, end);
if (end <= begin)
{ {
return; return;
} }
// 预览曲线一律按**整条信号**计算(与「构建为新信号」逐位一致),详见 `PaintMeanFilterCurve`。
std::size_t spanBegin = 0; std::size_t spanBegin = 0;
const std::vector<float>* curve = CurveSpanFor(DerivedCurveKind::WEAK_BOOST, *resolved.data, 0, const std::vector<float>* curve = CurveSpanFor(DerivedCurveKind::WEAK_BOOST, *resolved.data, 0,
iterations, levelPercent, begin, end, spanBegin, iterations, levelPercent, 0, count, spanBegin,
true); true);
if (curve == nullptr) if (curve == nullptr)
{ {
+35
View File
@@ -228,6 +228,39 @@ Documents/ 项目文档(含本文件)
- **显示效果**:小窗口(同步)路径与旧实现**逐像素 0 差异**;大窗口(异步)路径的曲线像素与旧实现一致(差异仅出现在合成测试中一个与缓存相关的**旧实现的时轴陈旧**现象上,新实现按设定视图正确绘制)。后台完成后的全质量帧与同步结果相同。 - **显示效果**:小窗口(同步)路径与旧实现**逐像素 0 差异**;大窗口(异步)路径的曲线像素与旧实现一致(差异仅出现在合成测试中一个与缓存相关的**旧实现的时轴陈旧**现象上,新实现按设定视图正确绘制)。后台完成后的全质量帧与同步结果相同。
- **代价**:大窗口缓存条目可占用较多内存(整段弱波增强单条 ≈104MB/信号),由 `kCurveSpanCacheMaxBytes`(768MB)+ 条目数上限(32)兜底,最旧优先淘汰。 - **代价**:大窗口缓存条目可占用较多内存(整段弱波增强单条 ≈104MB/信号),由 `kCurveSpanCacheMaxBytes`(768MB)+ 条目数上限(32)兜底,最旧优先淘汰。
### 9.7 完善异步架构 + 预览曲线层级修正(本仓库当前状态)
在 9.6 基础上做了三项关键完善:
1. **后台线程池(真·多线程)**:原先是**单线程**顺序计算,5 个网格各一条整段弱波增强要 5×368ms≈1.8s,用户会长时间看不到预览曲线(看起来"完全没画出来")。改为 **`m_curveWorkers` 线程池**(`EnsureCurveWorker`,线程数 = `clamp(硬件并发-1, 1, 4)`,给 UI 留一个核),多条曲线**并行**计算,预览曲线因此很快出现。`StopCurveWorker` 会 join 全部线程。**后台线程优先级设为 `THREAD_PRIORITY_BELOW_NORMAL`**,保证拖动时 UI/绘制线程始终优先获得 CPU(计算只在 UI 空闲时占用),避免"多线程反而抢核导致卡顿"。
2. **预览曲线绘制在「本网格全部原信号之上」**:原先在**同一个信号循环里**就地叠加派生曲线,导致网格内有多条信号时,后绘制的原信号会把先前信号的预览曲线盖住(用户明确要求"预览信号处于原信号上层不要被覆盖")。现在 `PaintLane` 分**两遍**:第一遍画全部原信号(线/散点/光晕),第二遍画全部派生预览曲线。
3. **快速帧(关抗锯齿)只用于「会改变画面主体」的拖拽**:原先 `m_fastFrame = (拖拽中)`,会给静态场景缓存打上「已降级」标记 → 缓存失效 → 每帧整幅重画。像**拖时间光标 / 选区框选 / 拖浮动面板**这些**只改叠加层、主体不变**的交互因此被拖慢(实测拖光标从约 1ms 升到十几毫秒)。现在只有 `PAN / SCROLL_LANES / LANE_REORDER / MOVE_SIGNAL / BOOST_LEVEL / BOOST_ITERATIONS` 才算快速帧;其余拖拽直接复用静态场景缓存(贴图 ~1ms)+ 叠加层。实测拖光标 **全程命中缓存、约 1–4ms**。
- **平移(尤其"一大格=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 线程绘制,**无法异步**)。装饰层缓存已把"与视图无关"的部分(图例/刻度)从每帧开销中移除,剩余开销是波形本身 + 网格线 + 时间轴,属"不牺牲显示效果"下的固有成本。
**(六)离屏逐像素校验(含历史踩坑提醒)**
- 工具:`.preflight/perf/perf_<ver>.exe <csv> 1 <prefix>` 导出确定性 PNG;`.preflight/perf/compare.ps1` 用 `LockBits`+`Marshal.Copy` 逐字节比对。
- **踩坑**:比对若用 `[System.Drawing.Bitmap]::GetPixel` 且两侧位图**尺寸不一致或文件缺失**,会抛 `调用"GetPixel"时发生异常:"参数无效"`。根因通常是"旧版本"的 harness 链接失败(例如它引用了新版本才有的符号)导致对照图没生成。**务必**:①先确认两侧 PNG 都存在且尺寸一致;②用 `LockBits` 而非逐点 `GetPixel`(后者对上兆像素级图片极慢)。
- **实测结论**(`709cbe4` 的"可见窗口预览" vs `389b4ec` 的"整段预览",同 harness 同数据):全部**全质量帧**(均值滤波/弱波增强 × FitAll/2.5s/103ms 三个缩放)**逐像素 0 差异** —— 证明"改为整段计算"未带来任何显示回归,预览与"构建为新信号"一致;仅**拖拽中途**帧有 0.09% 差异(快速帧关抗锯齿 + 装饰层贴回的预期差异)。
--- ---
## 10. 变更日志(本文件维护记录) ## 10. 变更日志(本文件维护记录)
@@ -238,3 +271,5 @@ Documents/ 项目文档(含本文件)
| 2026-10-11 | DeepSeek-V4.1-Flash | 修复右键网格菜单后悬停光晕/提示气泡残留;修复「启用带预览曲线的实验性功能后平移/拖光标 300ms+ 卡顿」(`paintDerivedCapsule` 巨窗每帧重算,收窄请求窗口,显示效果零变化)。 | | 2026-10-11 | DeepSeek-V4.1-Flash | 修复右键网格菜单后悬停光晕/提示气泡残留;修复「启用带预览曲线的实验性功能后平移/拖光标 300ms+ 卡顿」(`paintDerivedCapsule` 巨窗每帧重算,收窄请求窗口,显示效果零变化)。 |
| 2026-10-11 | DeepSeek-V4.1-Flash | 深度修复「实验性功能预览曲线导致的交互卡顿」:大窗口改入缓存(静止重绘 2247ms→0.25ms)、交互期间延迟重算(弱波增强拖滑块 2303ms→18ms)、计算缓冲复用;显示逐像素不变。 | | 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 | 架构升级:**派生曲线计算与 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);后台任务按 (信号,种类) 去重以防拖滑块淹没线程池。 |