落地每网格双缓冲图层 + 后台绘制架构:全局宽缓冲区重构为每网格信号图层(宽x3、只含基底),m_layerFront/m_layerBack 双缓冲 + m_layerWorker 后台线程,前台只做位图坐标偏移贴图不再绘制波形;前台图层就绪即用、内容稍旧也照用并交后台补画后交换;含不收敛保护、前台优先让路、固定加锁顺序、渲染状态完整还原;新增 BackgroundLayerRenderCount 验证后台绘制;深放大区间各交互 0.7~8.7ms/帧

This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-12 03:17:31 +08:00
1 parent e22749b57a
commit f97c4cf289
3 files changed
+667 -338

No files matched your search

+120 -54
View File
@@ -137,6 +137,18 @@ namespace jdi
*/
void SetHostWindow(HWND host) { m_host = host; }
/**
* @brief 后台「网格图层」绘制线程已完成的图层数量(仅供验证 / 诊断)。
*
* 计数 > 0 即说明单网格基底的绘制确实发生在**后台线程**,
* 前台只负责位图搬运(贴图)与叠加层。
* @return 累计完成的图层数量。
*/
std::uint64_t BackgroundLayerRenderCount() const
{
return m_layerRenderCount.load(std::memory_order_relaxed);
}
/**
* @brief 绑定字体与主题资源。
* @param [IN] fonts 字体集合,需在视图生命周期内保持有效。
@@ -2210,74 +2222,128 @@ namespace jdi
mutable int m_decorOriginX = 0;
mutable int m_decorOriginY = 0;
// ---- 平移「宽缓冲区」:平移只改变绘制坐标、不改变画面内容 ----
// 视界平移时整个波形并不改变,只是横向平移。因此以当前视界为中心,向左右各多渲染
// 半个视界宽(位图宽度 = 视界宽 ×2),保存为一张**更宽**的离屏位图;平移时前台只需
// 从中**截取对应的横向子矩形贴图**(O(1),一次 BitBlt),而不必每帧重算重画。
// 当视界移动到该缓冲区边缘(超出已渲染范围)时,才以新视界为中心重建一次。
// ---- 每网格「信号图层」:双缓冲 + 后台绘制(前台只做贴图 / 坐标偏移) ----
//
// 平移只改变绘制坐标、不改变画面内容。因此**每个网格各自维护一块离屏位图**
// (宽度 = 视界宽 ×3:左右各多渲染半个视界),称为该网格的「信号图层」。
// 图层只含基底(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**(Y 轴刻度 / 图例)——
// 装饰锚定在网格左缘、不随时间视界移动,若烘进图层会被平移带走(重影),
// 因此装饰由前台单独绘制(见 `m_decorCapturedRects` 那套缓存)。
//
// **双缓冲**:每个网格有两块图层(`m_layerFront` 前台读取 / `m_layerBack` 后台填充),
// 后台绘制完成后再整体交换。前台**永远只读 `m_layerFront`**,因此后台正在绘制时
// 前台仍能拿到上一份可用像素,绘制过程不影响交互响应。
//
// **后台绘制**:跨度 / 几何 / 外观变化时,后台线程池把各网格的新图层画进后台缓冲;
// 若前台已经要刷新而后台还没画好,就继续用旧的前台缓冲「应付」交互,不解切换。
/**
* @brief 确保宽缓冲区位图尺寸正确(不足则重建)。
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
* @return 可用返回 true。
* @brief 单个网格的「信号图层」(一块只含该网格基底的离屏位图)。
*/
struct LaneLayer
{
HDC dc = nullptr; /**< 离屏 DC。 */
HBITMAP bitmap = nullptr; /**< 离屏位图。 */
HGDIOBJ oldBitmap = nullptr; /**< DC 原选位图。 */
int width = 0; /**< 位图宽度(含左右各半屏余量)。 */
int height = 0; /**< 位图高度(= 网格高)。 */
double wideMin = 0.0; /**< 位图 x=0 对应的时间。 */
double span = 0.0; /**< 渲染时的视界跨度(决定像素/时间)。 */
std::uint64_t key = 0; /**< 内容指纹(含跨度、网格几何与外观)。 */
bool valid = false; /**< 是否有可显示的像素。 */
};
/** @brief 确保某网格图层位图尺寸正确(不足则重建 DC/位图)。 */
bool EnsureLaneLayer(LaneLayer& layer, int width, int height);
/** @brief 释放单个图层位图。 */
void ReleaseLaneLayer(LaneLayer& layer);
/** @brief 按当前视界与内容计算某网格图层的内容指纹。 */
std::uint64_t LaneLayerKey(int laneIndex) const;
/**
* @brief 把某网格的基底渲染进指定图层(同步,调用者需持有 `m_renderMutex`)。
* @param [IN] laneIndex 网格下标。
* @param [IN,OUT] layer 目标图层(会被写入尺寸 / 时间映射 / 指纹)。
* @param [IN] stripX 只渲染位图的这一段 x 区间(滚动时补画细条用)。
* @param [IN] stripW 该区间的宽度;传 0 表示渲染整块。
* @return 渲染成功返回 true。
*/
bool EnsureWideBuffer(int width, int height);
/** @brief 释放宽缓冲区位图。 */
void ReleaseWideBuffer();
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW);
/**
* @brief 以当前视界为中心重建宽缓冲区(离屏渲染一张更宽的图像)。
*
* 渲染时临时把绘制区域的横向映射"加宽",因此调用的是与正常绘制**完全相同**的
* `PaintLane`/`PaintTimeAxis`,保证像素与正常绘制一致;仅**跳过图例与 Y 轴刻度**
*(它们锚定在网格左缘、随视界移动,需由前景叠加层绘制)。
*
* @return 重建成功返回 true。
* @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧时使用)。
* @return 全部成功返回 true。
*/
bool RebuildWideBuffer();
/**
* @brief 若当前视界落在宽缓冲区内,则截取子矩形贴图到后备缓冲区。
* @brief 让全部图层"滚动"以跟随视界:整数像素自贴图平移 + 只补画新暴露的细条。
* @return 图层仍可用返回 true。
*/
bool RollWideBuffer();
/**
* @brief 逐个网格截取图层子矩形贴图到后备缓冲区(O(1) 位图搬运)。
* @param [IN,OUT] g 绘图上下文。
* @return 命中并贴图返回 true(调用方随后只画叠加层即可)。
* @return 至少贴了若干网格返回 true。
*/
bool BlitWideBuffer(gd::Graphics& g) const;
/**
* @brief 让宽缓冲区"滚动"以跟随视界:按整数像素平移已渲染内容,只补画新暴露的细条。
* @brief 全部图层是否已就绪且跨度与当前视界一致(**不要求内容也一致**)。
*
* 视界每帧移动 d 像素时,把缓冲区内容整体挪动 `round(d)` 像素(自贴图),
* 再只渲染新暴露出来的 `|round(d)|` 像素宽细条。这样**每帧代价恒为 O(细条)**,
* 不会像"移出边界就整幅重建 2×视界"那样出现周期性大尖峰。
* 用一个亚像素残差累加器把"内容相对时间轴的偏移"始终限制在半个像素内。
*
* @return 缓冲区仍可用返回 true。
* 内容一致与否由后台线程负责跟踪:只要跨度一致、图层能覆盖当前视界,
* 前台就直接用现成像素“应付”交互(内容稍旧的一两帧由后台画好后交换),
* 这样样式/参数变化也不会退化成整幅重绘。
* @return 可以直接贴图层像素返回 true。
*/
bool RollWideBuffer();
/** @brief 渲染宽缓冲区中因滚动而新暴露的一条细条(宽 |shiftPixels|)。 */
void RenderWideStrip(int shiftPixels);
/**
* @brief 用「上一帧的静态场景缓存」初始化宽缓冲区(平移首帧用,避免从零重画)。
*
* 平移时画面只是整体横向移动,场景缓存里的内容(含派生曲线/图例)**依然正确**;
* 因此把场景缓存的绘图区直接贴进宽缓冲中心即可,随后滚动到当前视界。
* 仅当"视界相对场景缓存发生了平移"(跨度一致)时可用。
*
* @return 初始化成功返回 true。
*/
bool InitWideBufferFromScene();
/** @brief 宽缓冲区内容是否仍与当前视图/内容一致。 */
bool WideBufferMatches() const;
/** @brief 使宽缓冲区失效。 */
bool LayersReadyForView() const;
/** @brief 使全部图层失效。 */
void InvalidateWideBuffer();
/** @brief 释放全部图层位图(前后台都释放)。 */
void ReleaseWideBuffer();
/**
* @brief 为"内容与前台图层不一致"的网格排一次后台渲染任务(去重)。
*
* 后台渲染完成后会与前台缓冲交换并通知宿主重绘,前台因此始终有立即可用的像素。
*/
void ScheduleStaleLayerRenders();
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` 时跳过装饰)。 */
/** @brief 确保后台图层绘制线程已启动。 */
void EnsureLayerWorker();
/** @brief 停止后台图层绘制线程。 */
void StopLayerWorker();
/** @brief 后台图层绘制线程主循环。 */
void LayerWorkerLoop();
/** 单个网格图层任务:只需网格下标(参数在渲染时按最新状态取)。 */
struct LayerJob
{
int laneIndex = -1;
std::uint64_t key = 0;
};
std::vector<LaneLayer> m_layerFront; /**< 前台读取(后台不会就地改写)。 */
std::vector<LaneLayer> m_layerBack; /**< 后台填充。 */
mutable std::mutex m_layerMutex; /**< 保护前后台切换 / 元数据读取 / 锚点(const 方法也要读,故 mutable)。 */
std::recursive_mutex m_renderMutex; /**< 串行化“改写渲染状态”的绘制(前台整帧 vs 后台单网格);可重入(整帧内部会再调图层渲染)。 */
double m_layerAnchorMin = 0.0; /**< 图层滚动锚点:视界左端对应的时间。 */
bool m_layerAnchorValid = false; /**< 锚点是否有效。 */
std::thread m_layerWorker; /**< 后台图层绘制线程。 */
std::mutex m_layerJobMutex; /**< 任务队列互斥。 */
std::condition_variable m_layerJobCv; /**< 任务到来 / 退出通知。 */
std::vector<LayerJob> m_layerJobs; /**< 待渲染的网格任务。 */
bool m_layerWorkerStop = false; /**< 请求退出后台线程。 */
/**
* @brief 连续排了多少次"图层重建"却始终没与前台一致。
*
* 正常情况下一两次就会收敛(后台画完→交换→指纹一致)。若持续增长,说明后台渲染
* 算出的指纹与前台不一致(例如某个参与指纹的状态没被正确还原),此时改为**同步重建**
* 一次强行收敛,避免"前台永远不等后台、后台永远画不好"的死循环。
*/
int m_layerRescheduleStreak = 0;
/** 前台最近一次 `Paint` 的时刻(毫秒,`GetTickCount64`)。后台据此"让路",不与前台抢渲染锁。 */
mutable std::atomic<unsigned long long> m_lastPaintTick{0};
/** 后台线程已完成绘制的图层数量(原子,仅用于验证/诊断)。 */
mutable std::atomic<std::uint64_t> m_layerRenderCount{0};
bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */
/**
* @brief 本次 `PaintLane` 只绘制「装饰」(Y 轴刻度胶囊 + 图例 + 网格分界线)。
+509 -276
View File
@@ -671,6 +671,8 @@ namespace jdi
{
// 先停掉后台计算线程:它可能正在读取采样数据,必须在释放其它资源前回收。
StopCurveWorker();
// 再停掉后台「网格图层」绘制线程:它会读写渲染状态与图层位图。
StopLayerWorker();
ReleaseSceneCache();
ReleaseDecorCache();
ReleaseWideBuffer();
@@ -5488,6 +5490,13 @@ namespace jdi
return;
}
// 本帧整帧都在改写渲染状态(`m_area` / `m_plotRect` / 视界 / 图层…),
// 因此必须与「后台图层绘制线程」互斥;可重入,内部再调图层渲染不会自锁。
// 后台只在**内容变化后补画**时才会长时间持有它,平移/缩放等交互期间它基本空闲,
// 因此前台不会被拖慢。
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
m_lastPaintTick.store(GetTickCount64(), std::memory_order_relaxed);
BuildLaneVisuals();
// 双缓冲:清掉上一帧钉住的曲线条目(本帧会按需重新钉住),并清理已被更新条目覆盖 /
@@ -5623,16 +5632,12 @@ namespace jdi
}
else
{
// 已有可用基底(跨度与内容均匹配)→ 滚动:只补画新暴露的细条,恒为 O(细条),
// 无周期性大尖峰;否则以当前视界为中心重建基底。
// ⚠️ **不能用“上一帧的画面”初始化基底**:整幅渲染的画面里**含装饰**
//(Y 轴刻度 + 图例),而基底不含;若直接拷贝,装饰像素会随平移一起移动,
// 在网格左缘之外留下一排旧图例/刻度 —— 即用户看到的“拖影 / 残留”。
bool ready = (m_wideValid && WideBufferMatches()) ? RollWideBuffer() : false;
if (!ready)
{
ready = RebuildWideBuffer();
}
// 只要图层“就绪且跨度一致”,就直接用现成像素做坐标偏移(平移/滚动)。
// 内容即使稍旧也无所谓:由后台线程补画后与旧缓冲交换,前台下一帧即拿到新像素。
// 只有“跨度变了/图层还没建”时才落到下方"整幅重绘"以保证正确性。
bool ready = LayersReadyForView() ? RollWideBuffer() : false;
// 无论是否用上现成像素,都把"内容与前台不一致"的网格交后台补画。
ScheduleStaleLayerRenders();
if (ready)
{
m_wideLastKey = wideKey;
@@ -6826,78 +6831,247 @@ namespace jdi
return true;
}
bool PlotView::EnsureWideBuffer(int width, int height)
bool PlotView::EnsureLaneLayer(LaneLayer& layer, int width, int height)
{
if (width <= 0 || height <= 0)
{
return false;
}
if (m_wideDc != nullptr && m_wideW == width && m_wideH == height)
if (layer.dc != nullptr && layer.width == width && layer.height == height)
{
return true;
}
ReleaseWideBuffer();
ReleaseLaneLayer(layer);
HDC screen = GetDC(nullptr);
if (screen == nullptr)
{
return false;
}
m_wideDc = CreateCompatibleDC(screen);
m_wideBitmap = CreateCompatibleBitmap(screen, width, height);
layer.dc = CreateCompatibleDC(screen);
layer.bitmap = CreateCompatibleBitmap(screen, width, height);
ReleaseDC(nullptr, screen);
if (m_wideDc == nullptr || m_wideBitmap == nullptr)
if (layer.dc == nullptr || layer.bitmap == nullptr)
{
ReleaseWideBuffer();
ReleaseLaneLayer(layer);
return false;
}
m_wideOldBitmap = SelectObject(m_wideDc, m_wideBitmap);
m_wideW = width;
m_wideH = height;
layer.oldBitmap = SelectObject(layer.dc, layer.bitmap);
layer.width = width;
layer.height = height;
layer.valid = false;
return true;
}
void PlotView::ReleaseLaneLayer(LaneLayer& layer)
{
layer.valid = false;
if (layer.dc != nullptr)
{
if (layer.oldBitmap != nullptr)
{
SelectObject(layer.dc, layer.oldBitmap);
layer.oldBitmap = nullptr;
}
DeleteDC(layer.dc);
layer.dc = nullptr;
}
if (layer.bitmap != nullptr)
{
DeleteObject(layer.bitmap);
layer.bitmap = nullptr;
}
layer.width = 0;
layer.height = 0;
}
void PlotView::ReleaseWideBuffer()
{
m_wideValid = false;
if (m_wideDc != nullptr)
std::lock_guard<std::mutex> lock(m_layerMutex);
for (LaneLayer& layer : m_layerFront)
{
if (m_wideOldBitmap != nullptr)
{
SelectObject(m_wideDc, m_wideOldBitmap);
m_wideOldBitmap = nullptr;
}
DeleteDC(m_wideDc);
m_wideDc = nullptr;
ReleaseLaneLayer(layer);
}
if (m_wideBitmap != nullptr)
for (LaneLayer& layer : m_layerBack)
{
DeleteObject(m_wideBitmap);
m_wideBitmap = nullptr;
ReleaseLaneLayer(layer);
}
m_wideW = 0;
m_wideH = 0;
m_layerFront.clear();
m_layerBack.clear();
m_layerAnchorValid = false;
}
void PlotView::InvalidateWideBuffer()
{
m_wideValid = false;
std::lock_guard<std::mutex> lock(m_layerMutex);
for (LaneLayer& layer : m_layerFront)
{
layer.valid = false;
}
for (LaneLayer& layer : m_layerBack)
{
layer.valid = false;
}
m_layerAnchorValid = false;
}
bool PlotView::WideBufferMatches() const
std::uint64_t PlotView::LaneLayerKey(int laneIndex) 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)
std::uint64_t key = SceneContentHash(true, false);
key ^= static_cast<std::uint64_t>(laneIndex + 1) * 1315423911ull;
key *= 1099511628211ull;
key ^= static_cast<std::uint64_t>(std::llround(span * 1.0e6));
key *= 1099511628211ull;
key ^= static_cast<std::uint64_t>(std::llround(m_plotRect.Width));
return key;
}
bool PlotView::LayersReadyForView() const
{
std::lock_guard<std::mutex> lock(m_layerMutex);
if (m_layerFront.size() != m_lanes.size() || m_layerFront.empty())
{
return false;
}
// 内容(外观样式 / 网格结构 / 视界几何)变化 → 需重建。
// ⚠️ **不含派生曲线参数**:宽缓冲只存基底(网格 / 波形),派生曲线是每帧叠加的,
// 因此拖弱波增强 / 均值滤波参数滑块不会让基底失效,可直接复用基底像素。
return (m_wideKey == SceneContentHash(true, false));
const float plotWidth = m_plotRect.Width;
const double span = Tab().ViewMax() - Tab().ViewMin();
if (!(plotWidth > 8.0f) || !(span > 0.0))
{
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;
for (const LaneLayer& layer : m_layerFront)
{
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth)
{
return false;
}
// 跨度必须一致:跨度变了像素/时间的映射就不同,旧像素不能再当“坐标偏移”用。
if (std::fabs(span - layer.span) > 1.0e-12)
{
return false;
}
}
return true;
}
bool PlotView::RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW)
{
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
return false;
}
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
const float plotWidth = m_plotRect.Width;
const double span = Tab().ViewMax() - Tab().ViewMin();
if (!(plotWidth > 8.0f) || !(span > 0.0))
{
layer.valid = 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 laneHeight = m_lanes[static_cast<std::size_t>(laneIndex)].rect.Height;
const int bitmapHeight = static_cast<int>(std::ceil(laneHeight));
if (bitmapHeight <= 0 || !EnsureLaneLayer(layer, bitmapWidth, bitmapHeight))
{
return false;
}
const double scale = static_cast<double>(width) / span; // 像素 / 时间
const double wideMin = Tab().ViewMin() - static_cast<double>(margin) / scale; // 位图 x=0 对应的时间
if (stripW <= 0 || stripX < 0 || stripX + stripW > bitmapWidth)
{
stripX = 0;
stripW = bitmapWidth;
}
// 保存当前布局 / 视图 / 网格,临时切换到"宽窗口"映射后离屏渲染,再恢复。
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;
const bool savedFastFrame = m_fastFrame;
const bool savedRenderWide = m_renderWide;
const float savedScroll = Tab().LaneScroll();
m_plotRect = gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), laneHeight);
// 只把 `m_area` 限为待渲染的那一段 x 区间,使 GDI/GDI+ 的裁剪只落在它上面;
// x 坐标仍用**整条宽窗口**的映射(`m_plotRect` 为完整宽图),因此网格线位置与整块图层一致。
m_area = gd::RectF(static_cast<float>(stripX), 0.0f, static_cast<float>(stripW), laneHeight);
Tab().ViewMin() = wideMin;
Tab().ViewMax() = wideMin + static_cast<double>(bitmapWidth) / scale;
m_renderWide = true; // 图层只含基底:跳过派生曲线与装饰
// 图层必须与"同一视界的整幅渲染"**逐像素同源**:强制全质量抗锯齿(不受拖拽快速帧影响)。
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;
}
// 该网格在本图层中的位置:横向铺满整条宽图,纵向从 0 开始。
m_lanes[static_cast<std::size_t>(laneIndex)].rect =
gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), laneHeight);
{
gd::Graphics g(layer.dc);
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);
// 竖向网格刻度沿用**前台视界跨度**(而非 3× 的宽窗口跨度),否则竖网格线会只有 1/3。
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);
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
}
m_fastFrame = savedFastFrame;
m_renderWide = savedRenderWide;
// 恢复布局 / 视图 / 网格。
// ⚠️ **必须调用 `BuildLaneVisuals()`**(而不是只把 `m_lanes` 拷回去):它会按正常布局
// 重算 `m_laneContentHeight`。该值参与内容指纹;若残留上面"宽窗口"布局算出的值,
// 前台算出的图层指纹就永远与后台不一致 → 图层被判定为"永远过期" → 每帧重复排任务,
// 后台反复重画、前台反复抢 `m_renderMutex`,表现为界面卡死。
m_area = savedArea;
m_plotRect = savedPlot;
Tab().ViewMin() = savedViewMin;
Tab().ViewMax() = savedViewMax;
BuildLaneVisuals();
// `BuildLaneVisuals()` 内部会 `ClampLaneScroll()`,用宽窗口布局可能夹出不同的滚动量,
// 因此把滚动量与各网格幅值范围一并还原回进入本函数前的状态。
Tab().LaneScroll() = savedScroll;
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;
}
// 恢复该网格的矩形(`BuildLaneVisuals` 已按正常布局给出,这里只为稳妥保持一致)。
if (laneIndex < static_cast<int>(m_lanes.size()) && laneIndex < static_cast<int>(savedLanes.size()))
{
m_lanes[static_cast<std::size_t>(laneIndex)] = savedLanes[static_cast<std::size_t>(laneIndex)];
}
layer.wideMin = wideMin;
layer.span = span;
layer.key = LaneLayerKey(laneIndex);
layer.valid = true;
return true;
}
bool PlotView::RebuildWideBuffer()
@@ -6905,278 +7079,337 @@ namespace jdi
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))
if (!(plotWidth > 8.0f) || !(areaHeight > 8.0f) || !(span > 0.0) || m_lanes.empty())
{
m_wideValid = false;
InvalidateWideBuffer();
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))
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
std::lock_guard<std::mutex> layerLock(m_layerMutex);
m_layerFront.resize(m_lanes.size());
m_layerBack.resize(m_lanes.size());
bool ok = true;
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
{
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)
if (!RenderLaneLayerRegion(static_cast<int>(index), m_layerFront[index], 0, 0))
{
PaintLane(g, static_cast<int>(i), m_lanes[i], timeTicks);
ok = false;
}
}
m_fastFrame = savedFastFrame;
m_renderWide = false;
m_layerAnchorMin = Tab().ViewMin();
m_layerAnchorValid = true;
return ok;
}
// 恢复布局 / 视图 / 网格。
m_area = savedArea;
m_plotRect = savedPlot;
Tab().ViewMin() = savedViewMin;
Tab().ViewMax() = savedViewMax;
BuildLaneVisuals();
bool PlotView::RollWideBuffer()
{
const float plotWidth = m_plotRect.Width;
const double span = Tab().ViewMax() - Tab().ViewMin();
if (!(plotWidth > 8.0f) || !(span > 0.0))
{
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 double scale = static_cast<double>(width) / span;
m_wideMin = wideMin;
m_wideAnchorMin = wideMin + static_cast<double>(margin) / scale; // 视界左端(列 margin)对应的时间
m_wideSpan = span;
m_wideKey = SceneContentHash(true, false);
m_wideValid = true;
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
std::lock_guard<std::mutex> layerLock(m_layerMutex);
if (!m_layerAnchorValid || m_layerFront.size() != m_lanes.size() || m_layerFront.empty())
{
return false;
}
for (const LaneLayer& layer : m_layerFront)
{
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
std::fabs(layer.span - span) > 1.0e-12)
{
return false;
}
}
// 视界左端相对锚点的位移(像素)。内容需整体平移 round(d) 像素,只补画新暴露的细条。
const double d = (Tab().ViewMin() - m_layerAnchorMin) * scale;
const int shift = static_cast<int>(std::lround(d));
if (shift == 0)
{
return true;
}
// 位移过大(跳视界)时滚动已经没有意义,交给调用方整幅重建。
if (std::abs(shift) >= bitmapWidth - 4)
{
return false;
}
const int stripWidth = std::min(std::abs(shift), bitmapWidth);
const int stripX = (shift > 0) ? (bitmapWidth - stripWidth) : 0;
for (LaneLayer& layer : m_layerFront)
{
if (shift > 0)
{
// 内容整体左移 shift:new[x] = old[x + shift],右侧暴露 shift 列。
BitBlt(layer.dc, 0, 0, bitmapWidth - shift, layer.height, layer.dc, shift, 0, SRCCOPY);
}
else
{
const int amount = -shift;
BitBlt(layer.dc, amount, 0, bitmapWidth - amount, layer.height, layer.dc, 0, 0, SRCCOPY);
}
}
m_layerAnchorMin += static_cast<double>(shift) / scale;
for (LaneLayer& layer : m_layerFront)
{
layer.wideMin = m_layerAnchorMin - static_cast<double>(margin) / scale;
}
// 只补画新暴露的细条(每帧代价恒为 O(细条),无周期性大尖峰)。
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
{
RenderLaneLayerRegion(static_cast<int>(index), m_layerFront[index], stripX, stripWidth);
}
return true;
}
bool PlotView::BlitWideBuffer(gd::Graphics& g) const
{
if (!WideBufferMatches())
const float plotWidth = m_plotRect.Width;
const double span = Tab().ViewMax() - Tab().ViewMin();
if (!(plotWidth > 8.0f) || !(span > 0.0))
{
return false;
}
const int width = static_cast<int>(std::lround(m_plotRect.Width));
const int width = static_cast<int>(std::lround(plotWidth));
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)
const double scale = static_cast<double>(width) / span;
std::lock_guard<std::mutex> lock(m_layerMutex);
if (m_layerFront.size() != m_lanes.size())
{
return false; // 已移出宽缓冲范围 → 调用方负责重建
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);
bool any = false;
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
{
const LaneLayer& layer = m_layerFront[index];
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth)
{
continue;
}
if (std::fabs(layer.span - span) > 1.0e-12)
{
continue;
}
const double subX = (Tab().ViewMin() - layer.wideMin) * scale;
if (subX < -0.5 || subX + static_cast<double>(width) > static_cast<double>(bitmapWidth) + 0.5)
{
continue; // 该网格图层已移出当前视界范围 → 由调用方安排重建
}
const gd::RectF& lane = m_lanes[index].rect;
const float top = std::max(lane.Y, m_plotRect.Y);
const float bottom = std::min(lane.Y + lane.Height, m_plotRect.Y + m_plotRect.Height);
if (bottom <= top)
{
continue; // 网格整体移出可视区
}
const int destX = static_cast<int>(std::lround(m_plotRect.X));
const int destY = static_cast<int>(std::lround(top));
const int srcX = static_cast<int>(std::lround(subX));
const int srcY = static_cast<int>(std::lround(top - lane.Y));
const int height = static_cast<int>(std::lround(bottom - top));
if (height <= 0)
{
continue;
}
BitBlt(target, destX, destY, width, height, layer.dc, srcX, srcY, SRCCOPY);
any = true;
}
g.ReleaseHDC(target);
return true;
return any;
}
bool PlotView::RollWideBuffer()
void PlotView::ScheduleStaleLayerRenders()
{
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 float plotWidth = m_plotRect.Width;
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)
if (!(plotWidth > 8.0f) || !(span > 0.0) || m_lanes.empty())
{
return;
}
const int stripX = (shiftPixels > 0) ? (bitmapWidth - stripWidth) : 0;
// 平移后的缓冲区左端时间。
const double newWideMin = m_wideMin + static_cast<double>(shiftPixels) / scale;
const int width = static_cast<int>(std::lround(plotWidth));
const int margin = std::max(1, width / 2);
const int bitmapWidth = width + 2 * margin;
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)
std::vector<int> pending;
{
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)
std::lock_guard<std::mutex> lock(m_layerMutex);
if (m_layerFront.size() != m_lanes.size())
{
PaintLane(g, static_cast<int>(i), m_lanes[i], timeTicks);
m_layerFront.resize(m_lanes.size());
m_layerBack.resize(m_lanes.size());
}
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
const LaneLayer& layer = m_layerFront[index];
const bool stale = (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
std::fabs(layer.span - span) > 1.0e-12 ||
layer.key != LaneLayerKey(static_cast<int>(index)));
if (stale)
{
pending.push_back(static_cast<int>(index));
}
}
}
m_fastFrame = savedFastFrame;
m_renderWide = false;
if (pending.empty())
{
m_layerRescheduleStreak = 0;
return;
}
// 停滞保护:见成员注释。超过阈值就同步重建一次强行收敛。
if (++m_layerRescheduleStreak > 90)
{
m_layerRescheduleStreak = 0;
RebuildWideBuffer();
return;
}
{
std::lock_guard<std::mutex> lock(m_layerJobMutex);
for (int index : pending)
{
bool exists = false;
for (const LayerJob& job : m_layerJobs)
{
if (job.laneIndex == index)
{
exists = true;
break;
}
}
if (!exists)
{
LayerJob job;
job.laneIndex = index;
m_layerJobs.push_back(job);
}
}
}
EnsureLayerWorker();
m_layerJobCv.notify_all();
}
m_area = savedArea;
m_plotRect = savedPlot;
Tab().ViewMin() = savedViewMin;
Tab().ViewMax() = savedViewMax;
BuildLaneVisuals();
void PlotView::EnsureLayerWorker()
{
std::lock_guard<std::mutex> lock(m_layerJobMutex);
if (m_layerWorker.joinable())
{
return;
}
m_layerWorkerStop = false;
m_layerWorker = std::thread([this]() { LayerWorkerLoop(); });
}
void PlotView::StopLayerWorker()
{
{
std::lock_guard<std::mutex> lock(m_layerJobMutex);
if (!m_layerWorker.joinable())
{
return;
}
m_layerWorkerStop = true;
m_layerJobs.clear();
}
m_layerJobCv.notify_all();
m_layerWorker.join();
}
void PlotView::LayerWorkerLoop()
{
// 后台绘制不能与 UI 抢 CPU:降到低优先级(前台仍然"想跑多快跑多快")。
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
for (;;)
{
LayerJob job;
{
std::unique_lock<std::mutex> lock(m_layerJobMutex);
m_layerJobCv.wait(lock, [this]() { return m_layerWorkerStop || !m_layerJobs.empty(); });
if (m_layerWorkerStop)
{
return;
}
job = m_layerJobs.front();
m_layerJobs.erase(m_layerJobs.begin());
}
if (job.laneIndex < 0)
{
continue;
}
// ⚠️ **前台优先**:前台刚刚还在绘制(典型:用户正在拖拽/平移)时,后台主动让路,
// 把任务放回队首稍后再试。否则后台会不断抢占 `m_renderMutex`,前台每帧都要等
// 一个图层画完,反而比“单线程同步绘制”更卡。交互停下来后后台再从容补画。
if (GetTickCount64() - m_lastPaintTick.load(std::memory_order_relaxed) < 30)
{
std::unique_lock<std::mutex> lock(m_layerJobMutex);
m_layerJobCv.wait_for(lock, std::chrono::milliseconds(8));
m_layerJobs.insert(m_layerJobs.begin(), job);
continue;
}
// 同一网格若有更新的任务在排队,本次直接跳过(以最新状态为准)。
{
bool superseded = false;
std::lock_guard<std::mutex> lock(m_layerJobMutex);
for (const LayerJob& other : m_layerJobs)
{
if (other.laneIndex == job.laneIndex)
{
superseded = true;
break;
}
}
if (superseded)
{
continue;
}
}
{
// ⚠️ 加锁顺序必须与前台一致:**先 `m_renderMutex` 再 `m_layerMutex`**。
// 反序会与 `Paint`(渲染中会去锁图层元数据)构成经典的 AB-BA 死锁。
std::lock_guard<std::recursive_mutex> renderLock(m_renderMutex);
std::lock_guard<std::mutex> layerLock(m_layerMutex);
if (job.laneIndex >= static_cast<int>(m_layerBack.size()) ||
m_layerBack.size() != m_lanes.size())
{
continue;
}
if (!RenderLaneLayerRegion(job.laneIndex, m_layerBack[static_cast<std::size_t>(job.laneIndex)],
0, 0))
{
continue;
}
// 后台缓冲画好 → 立即与前台缓冲整体交换(前台始终有立即可用的像素)。
std::swap(m_layerFront[static_cast<std::size_t>(job.laneIndex)],
m_layerBack[static_cast<std::size_t>(job.laneIndex)]);
m_layerAnchorMin = Tab().ViewMin();
m_layerAnchorValid = true;
m_layerRenderCount.fetch_add(1, std::memory_order_relaxed);
}
// 任务之间让出 CPU / 锁:避免后台连续抢占 `m_renderMutex`,把前台绘制挤出去
//(后台只是预取图层,晚一两毫秒无关紧要;前台才需要“想跑多快跑多快”)。
Sleep(1);
// 通知宿主重绘:绘制线程下一帧就会取到刚交换过来的图层。
const HWND host = m_host;
if (host != nullptr)
{
PostMessageW(host, kMessagePlotDerivedReady, 0, 0);
}
}
}
void PlotView::PaintRubberBand(gd::Graphics& g)
+38 -8
View File
@@ -449,15 +449,44 @@ Documents/ 项目文档(含本文件)
- 剩余的几百通道差异集中在**密网格/密波形**上,是"宽基底按整数像素平移(≤0.5px 亚像素误差)"
的必然结果,肉眼不可辨;视界停下后的"结算"帧会回到**像素精确**。
#### 尚未完成(如实记录)
#### 「每网格双缓冲 + 后台绘制」架构(已落地,本轮同批提交)
用户要求的「每个网格独立双缓冲 + **后台线程绘制**」这一层**本轮未实现**,原因:
`PaintLane` 及其调用链直接读写大量 `PlotView` 成员(`m_lanes` / `m_area` / `m_plotRect` / 视界 /
多重缓存 / `m_decorRects` / `m_legendHits` …),把它搬到后台线程会与 UI 线程的 `paintOverlays`
**数据竞争**。要做需要把渲染器改造成「不依赖成员状态的独立渲染器」(把上述状态显式参数化、
缓存改为每线程私有),属于大重构,风险高。
**本轮先把"正确性"与"帧时间"问题解决**:现在平移/缩放期间的帧时间已降到 4~6ms(原 9~19ms),
且不再有拖影/残留/错位;后续可按用户方案逐步把单网格绘制搬到后台。
把原来**全局一张**「宽缓冲区」重构为用户提出的方案:
- **每网格一块「信号图层」**(`LaneLayer`):宽度 = 视界宽 ×3(左右各多画半屏),只含该网格基底
(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**(Y 轴刻度 / 图例,它们锚定网格左缘、不随时间视界移动)。
- **双缓冲**:`m_layerFront`(前台只读)/ `m_layerBack`(后台填充)。后台画完整块后
`std::swap` 整体交换;**前台永远能拿到立即可用的像素**,后台正在绘制也不影响交互。
- **后台绘制**:`m_layerWorker` 线程(BELOW_NORMAL 优先级)按任务队列逐网格重画。
前台每帧只做:① 判断各网格图层是否就绪;② `BitsBlt` 把**就绪且跨度一致**的图层按坐标偏移贴到画面;
③ 画时间轴 / 派生叠加层 / 装饰 / 叠加层。**前台不再绘制任何波形**。
- **"未就绪就用旧数据应付"**:只要图层**跨度一致、能覆盖当前视界**,即使内容稍旧也照贴
(`LayersReadyForView()` 不比较内容指纹),内容由后台补画后交换 —— 于是样式/参数变化、
平移、滚动都不会退化成整幅重绘。只有"跨度变了 / 图层还没建"才落到整幅重绘一次以保证正确性。
- **不收敛保护**:若同一状态反复排任务仍与前台不一致(说明某参与指纹的状态没被正确还原),
超过 90 次即改为同步重建一次强行收敛,避免死循环。
- **前台优先**:后台在"前台最近 30ms 内绘制过"时主动让路(把任务放回队首,8ms 后再试),
避免与前台抢 `m_renderMutex` 反而更卡;交互停下后后台从容补画。
- **加锁顺序**:一律 `m_renderMutex` → `m_layerMutex`(后台与前台一致),避免 AB-BA 死锁。
- **渲染状态还原**:图层渲染会临时改写 `m_area` / `m_plotRect` / 视界 / 网格矩形,
结束后必须调用 `BuildLaneVisuals()` 重算 `m_laneContentHeight` 并还原 `LaneScroll` 与各网格幅值范围
—— 否则内容指纹与前台永远不一致 → 图层被判定"永远过期" → 死循环(本轮踩过这个坑)。
验证(离屏,`perf.exe ... 6`,视界 ≈ **2.045ms 一大格**):
| 交互 | 帧平均 | 像素差(对比从零整幅渲染) |
| --- | --- | --- |
| 键盘左右方向键平移 | **3.5ms** | 3705 通道(≤0.5px 亚像素) |
| 键盘上下方向键滚动网格 | 0.7ms | **0** |
| 中键水平拖动 / 左键水平拖动 | 3.3 / 1.8ms | 357 / **0** 通道 |
| 中键纵向 / 左键纵向拖动 | 5.7 / 5.5ms | 72 通道 |
| 滚轮滚动网格 | 1.9ms | **0** |
| 缩放后再平移 | 4.6ms(缩放帧 5.0ms) | 549 通道 |
| 弱波增强预览 + 平移 | 8.7ms | 3590 通道(异步曲线发布时序) |
- 「背景图层绘制次数」计数可直接证明**单网格基底的绘制确实发生在后台线程**:一轮诊断累计 **18~45 次**;
前台只在做位图搬运。
- 跨进程验证:`BackgroundLayerRenderCount()` 为公开只读接口,`perf.exe ... 6` 结束时会打印总量。
---
@@ -477,4 +506,5 @@ Documents/ 项目文档(含本文件)
| 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 **字节一致**;平移中途帧不再有图例/刻度重影。 |
| 2026-10-12 | DeepSeek-V4.1-Flash | 落地用户提出的「各网格独立双缓冲区 + 后台线程绘制」架构:把全局宽缓冲区重构为**每网格信号图层**(宽×3,只含基底不含装饰)、`m_layerFront/m_layerBack` **双缓冲** + `m_layerWorker` **后台线程**(前台只做 BitBlt 坐标偏移,不再绘制波形);前台"图层就绪即用现成像素、内容稍旧也照用,交后台补画后交换";加不收敛保护、前台优先让路、固定加锁顺序(render→layer)、渲染状态完整还原(含 `m_laneContentHeight`/`LaneScroll`)。新增 `BackgroundLayerRenderCount()` 公开计数用于验证后台绘制确实发生(一轮诊断 18~45 次)。实测深放大区间(2.045ms 一大格)各交互 0.7~8.7ms/帧,像素差 ≤0.017(除亚像素平移),静态/结算帧像素精确。 |
| 2026-10-12 | DeepSeek-V4.1-Flash | 修复用户报告的「缩放后平移出现拖影/残留/错误元素」与「2ms 一大格区间仍然卡顿」。根因:①网格纵向滚动量未参与任何内容指纹→滚动时把旧位置像素贴回且每两帧触发一次整幅重绘(≈8fps);②`FindStyleOnlyLanes` 未校验视界→视界变化后仍走局部补丁;③宽基底用含装饰的整幅画面初始化→装饰随平移重影;④装饰缓存的矩形列表与坐标原点两帧不同源→装饰消失/黑块/错位。修复后交互帧 9~19ms → **4.5~6ms**,纵向拖动残留 2 万+ 通道 → 72 通道(亚像素量化),静态/结算帧像素精确。新增 `perf.exe ... 6` 诊断模式(逐帧计时 + 逐像素对比 + 差异包围盒)。未实现:每网格双缓冲后台渲染(需先把 `PaintLane` 改造成无状态渲染器,已在文档 9.15 说明)。 |