落地每网格双缓冲图层 + 后台绘制架构:全局宽缓冲区重构为每网格信号图层(宽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 轴刻度胶囊 + 图例 + 网格分界线)。
|
||||
|
||||
Reference in new issue
Block a user