落地每网格双缓冲图层 + 后台绘制架构:全局宽缓冲区重构为每网格信号图层(宽x3、只含基底),m_layerFront/m_layerBack 双缓冲 + m_layerWorker 后台线程,前台只做位图坐标偏移贴图不再绘制波形;前台图层就绪即用、内容稍旧也照用并交后台补画后交换;含不收敛保护、前台优先让路、固定加锁顺序、渲染状态完整还原;新增 BackgroundLayerRenderCount 验证后台绘制;深放大区间各交互 0.7~8.7ms/帧
This commit is contained in:
1 parent
e22749b57a
commit
f97c4cf289
3 files changed
+667
-338
No files matched your search
@@ -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 轴刻度胶囊 + 图例 + 网格分界线)。
|
||||
|
||||
@@ -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
@@ -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 说明)。 |
|
||||
Reference in new issue
Block a user