CAD缩放延时改善方案
看了之后有一个比较明确的收获:真正值得借鉴的不是“旧位图缩放预览”,而是 DisplayCore 的 CAD 渲染架构。
你们之前放弃“先缩放旧图、再刷新高清图”是合理的。框选缩放场景不需要模糊预览,应该重点降低鼠标松开后那一次精确 CAD 重绘的耗时。
一、DisplayCore 为什么框选缩放流畅
我重点看了这些源码:
DisplayManager.csDataCollectThread.csWpfDisplayManager.csDispRectZoomCmd.csCADSource.csSignalSource.csCADPresenterPool.csCADPresenter.cs
它的流畅性主要来自下面几个设计。
1. 框选过程中不重新渲染 CAD
旧代码的矩形缩放命令是:
鼠标按下
-> 创建操作层中的矩形
鼠标移动
-> 只更新操作层矩形
鼠标抬起
-> 计算最终 FOV
-> 只设置一次 DisplayManager.DataFov
所以框选拖动过程中,CAD 主图不会随着鼠标移动不断重新 Query 和栅格化。拖动时显示的只是一个很轻量的操作层矩形。
当前插件在这点上其实已经比较接近:
HandleCadMouseMove只更新选择框;FinishCadSelection在鼠标释放后才调用ScheduleCadRender()。
因此,当前插件框选拖动过程本身不应是主要瓶颈。主要延时应该发生在鼠标释放后开始的那次完整 CAD 重绘。
2. FOV 变化不会创建一串渲染任务,而是交给一个长期运行的渲染线程
DisplayCore 的 DataFov setter 只是更新当前绘制上下文,然后把最新 context 设置给 DataCollectThread:
DataCollectThread 是长期运行的后台线程:
关键特点是:
- 只有一个长期存在的渲染线程;
- 它读取当前最新的
DispDrawContext; - 新的 FOV 会覆盖旧的 FOV;
- 不会为每次操作启动一个新的
Task.Run; - 渲染线程每隔约 3 ms 检查一次状态;
- 只在
IsForceRefreshData或 layer dirty 时重新绘制。
当前插件是:
框选完成
-> ScheduleCadRender
-> Task.Run
-> RenderViewport
-> await 完成
-> 更新 UI
如果渲染期间又发生了新的请求,当前代码只能通过 _cadRenderRequestId 丢弃旧结果,但旧的 CAD 计算已经做完了。
这对鼠标滚轮影响比较大,对框选影响相对小,但说明两者的调度模型不同:
| DisplayCore | 当前 BoardImageWpfPlugin |
|---|---|
| 一个长期运行的渲染循环 | 每次请求启动一次异步任务 |
| 最新 FOV 覆盖旧 FOV | 旧任务继续运行,完成后再丢弃 |
| 状态合并 | 请求结果丢弃 |
| 渲染资源长期复用 | 每帧内部大量创建对象 |
3. CAD 栅格器和 Translator 是长期复用的
这是我认为最重要的发现。
旧 CAD 显示不是每次 FOV 变化都重新创建栅格器,而是通过 CADPresenterPool 复用 CADPresenter:
CADPresenter 初始化时只创建一次:
_rasterizer = new GDIPDirectRasterizer();
_translator = new CAD2GDIPDirectTranslator(_job, _rasterizer);
只有 CAD Job 发生变化时,才重新 AcceptJob():
每次 FOV 变化时,旧代码主要做的是:
复用 Presenter
-> 创建/调整当前 ROI Query
-> QueryFeaturesCustom
-> TranslateBatch
-> 直接写入目标 buffer
例如:
而当前插件的 WpfCadPanelService.cs:363-403 每次渲染每个 layer 都会重新创建:
using (var rasterizer = new GDIPColorDirectRasterizer())
{
...
var translator = new CAD2GDIPDirectTranslator(...);
...
}
也就是每次框选完成后:
每个 layer
-> 新建 GDIP rasterizer
-> 初始化颜色
-> CreateHostImage
-> 新建 translator
-> QueryFeatures
-> TranslateBatch
-> 复制 buffer
-> 销毁 rasterizer
这和 DisplayCore 的差异非常大。
建议一:优先复用 CADPresenter 模式
不要把优化重点放在旧图缩放,而是考虑把当前 CAD 后端改成类似:
一个 BoardImage CAD 会话
-> 一个长期存在的 Presenter
-> Presenter 内部持有 CADJob、Rasterizer、Translator
-> 每次框选只更新 ROI
-> 直接渲染到复用的目标 buffer
如果一个界面可能同时有多个 CAD 显示会话,再使用 Presenter Pool;如果只有一个整板图窗口,一个长期复用的 Presenter 也可以。
这是我认为从 DisplayCore 中得到的最有价值的优化方向。
4. 旧代码的 CAD Source 已经封装了高效的 ROI 渲染
DisplayCore 本身只负责显示调度,真正的 CAD 渲染效率还来自旁边的 CADSource:
它的接口很清晰:
GetFovData(fov, displaySize, resultBuffer)
CAD source 直接把当前 FOV 渲染到给定的 RawBitmapData 中:
presenter.PresentReference(
...,
new CADRect(fov),
dispRect.Size,
...,
result.Pixels,
result.Length);
这和当前插件的模式不同。
当前插件的流程是:
CADLib 返回/生成 BitmapSource
-> CopyPixels 到 byte[]
-> DrawCadStaticPixels
-> 扫描遮罩
-> Copy/Clone
-> WriteableBitmap.WritePixels
DisplayCore 更接近:
CAD Presenter
-> 直接写入复用的 RawBitmapData
-> DisplayCore 组合 layer
-> 直接更新 WPF BackBuffer
也就是说,当前插件中间多了很多层:
BitmapSourceCopyPixelsbyte[]Marshal.Copy- 像素后处理
WritePixels
这些不会改变 CADLib 本身的查询耗时,但会增加额外的内存分配和数据拷贝。
5. WPF 显示 buffer 是直接绑定 BackBuffer,并且有双 buffer
DisplayCore 的 WpfDisplayManager.cs:227-243 会把 WriteableBitmap.BackBuffer 直接交给 DispDrawSurface:
WriteableBitmap.BackBuffer
-> DispDrawSurface
-> GDI Graphics
-> 复用的 native buffer
同时 DataCollectThread 使用两个 DispDrawSurface 循环复用:
而当前插件每次 CAD 渲染都会:
BitmapSource.CopyPixels;- 分配新的完整 byte 数组;
- 在数组中进行多轮处理;
- 再
WritePixels一整张图。
对应:
BoardImageWpfView.xaml.cs:1265-1295
建议二:考虑复用 native buffer 和双缓冲
不建议继续使用:
BitmapSource -> CopyPixels -> byte[] -> WritePixels
作为每次精确 CAD 刷新的主要路径。
可以借鉴 DisplayCore:
- 固定尺寸后复用渲染 buffer;
- 复用像素内存;
- 后台绘制 back buffer;
- 完成后交换 front/back;
- 尽量直接操作
WriteableBitmap.BackBuffer或等价的 native buffer。
这和模糊预览完全不同,最终输出仍然可以保持精确 CAD 质量。
二、当前插件和 DisplayCore 的最大差异
| 环节 | DisplayCore | 当前 BoardImageWpfPlugin |
|---|---|---|
| 框选拖动 | 只画操作层矩形 | 只画选择框,类似 |
| 最终 FOV 更新 | 更新状态,后台线程读取 | 启动一次 Task.Run |
| CAD 渲染资源 | Presenter/Rasterizer/Translator 长期复用 | 每层每帧重新创建 |
| CAD 查询 | PresentReference/CADQueryFactory 封装 |
直接 QueryFeatures |
| 输出 buffer | 复用 RawBitmapData |
新建 BitmapSource、byte 数组 |
| WPF 更新 | BackBuffer + 双 buffer | CopyPixels + WritePixels |
| Layer 更新 | source/layer dirty 管理 | 每次整帧后处理 |
| Overlay | DisplayCore layer 体系 | 每次重建 Canvas 和 WPF 图元 |
所以,我现在的判断比上一轮更明确:
框选缩放的主要问题不是交互预览,也不是鼠标事件太频繁,而是鼠标释放后,当前插件采用了“每层重新初始化 CAD 栅格器 + 重新生成 BitmapSource + 多次整帧复制/后处理”的路径。
三、针对框选缩放最值得做的优化
第一优先级:引入长期复用的 CAD Presenter
建议优先评估以下方案:
CADJob 加载一次
CADPresenter 创建一次
GDIP Rasterizer 创建一次
CAD Translator 创建一次
每次框选完成:
只更新 ROI
复用 Presenter 查询和渲染
写入同一块输出 buffer
具体可借鉴:
这项优化不需要引入模糊预览,也不会改变用户的框选操作方式。
第二优先级:复用渲染 buffer,减少中间 Bitmap 转换
建议让 CAD renderer 直接面向:
ROI + 输出尺寸 + 目标像素 buffer
而不是返回 BitmapSource。
尽量避免每次:
BitmapSource.CopyPixels
byte[] 分配
byte[] Clone
Marshal.Copy
WritePixels
尤其当前还有重复处理:
UpdateCadImagePixels()调用一次DrawCadStaticPixels();- 紧接着
UpdateCadOverlay()中的UpdateCadImageScanMask()又重新复制和处理一次。
这一部分可以参考 DisplayCore 的:
第三优先级:使用“最新 FOV 状态”而不是“任务请求”模型
即使框选通常只在鼠标释放后触发一次,我仍建议将渲染调度改成 DisplayCore 的思路:
当前 ROI 是一个可覆盖状态
后台只有一个 CAD render worker
worker 始终读取最新 ROI
好处是:
- 不会积累旧任务;
- 窗体尺寸变化、切层、连续框选时更稳定;
- 后台线程启动开销只发生一次;
- 后续如果恢复滚轮缩放,也不会出现多个旧任务追赶。
这项是调度优化,不是位图预览。
第四优先级:借鉴 CADSource 的查询和渲染封装
当前插件自己维护:
- Layer 查询;
- Placement;
QueryFeatures;- Rasterizer;
- Translator;
- 每层颜色;
- Buffer 合成。
DisplayCore 的 CADSource 则将这些封装在 Presenter 内部,并使用:
PresentReference
PresentExtraLayers
建议评估能否复用或等价实现这种接口,特别是:
- 使用
CADQueryFactory.CreateSignalQuery; - 使用
CADPresenter.PresentReference; - 额外层使用
PresentExtraLayers; - 避免每个 layer 在外部重复搭建查询和栅格化环境。
当前插件并不是简单缺少一个缓存字典,而是CAD renderer 的生命周期层级放错了:资源生命周期被放到了单次 viewport render 内,而 DisplayCore 放在显示会话/Presenter 生命周期内。
四、哪些方向我现在不建议优先做
1. 不建议重新启用“旧图缩放预览”
既然实际试过效果不好,而且用户明显感知到模糊,就不建议把它作为主方案。
可以保持:
框选过程只显示清晰的选择矩形
鼠标释放后直接生成新的清晰 CAD 图
优化目标应该是缩短“鼠标释放到新清晰图出现”的时间。
2. 不建议仅仅增加防抖
防抖对滚轮连续事件有帮助,但对框选缩放不是主因。当前框选本身已经是在鼠标释放后才调用一次 ScheduleCadRender()。
如果只加防抖,而仍然保留:
- 每次新建 Rasterizer;
- 每次新建 Translator;
- 每次返回 BitmapSource;
- 每次整幅复制;
- 每次重复像素处理;
用户的框选延迟不会有本质改善。
3. 不建议先并行渲染多个 Layer
当前 WpfCadPanelService 使用 _syncRoot 把整个 CAD 渲染过程串行保护起来。直接把各层并行化存在风险:
- CADLib 对同一
CADJob是否完全线程安全需要确认; - Rasterizer/Translator 不能随便跨线程共用;
- 最终像素合成仍然有竞争;
- CPU 可能被多个高 DPI CAD 任务打满。
DisplayCore 的做法也不是简单地把所有层并行,而是使用 CADPresenterPool 管理独立 Presenter,并在单个显示刷新中有序合成。
五、最终建议排序
如果只给一套建议,我建议顺序是:
- 不改变框选交互,不做模糊预览;
- 把 CAD Rasterizer/Translator 从“每次 RenderViewport 创建”提升到“CAD 显示会话级长期复用”;
- 优先评估复用
CADPresenter/CADPresenterPool以及PresentReference、PresentExtraLayers路径; - 把 CAD 输出改为直接写入复用的 native buffer,减少
BitmapSource -> byte[] -> WritePixels中间链路; - 使用一个长期运行的 latest-ROI render worker,避免每次请求启动新 Task;
- 把扫描遮罩、Step 轮廓、缺陷覆盖层从 CAD 基础图的重复像素处理中拆开;
- 最后才考虑 tile、LOD 或 CAD 几何缓存。
最核心的一句话
DisplayCore的流畅性主要来自 Presenter/Rasterizer/Translator 复用、FOV 状态合并、后台单渲染循环、复用 native buffer,而不是来自模糊预览。
当前 BoardImageWpfPlugin 最应该借鉴的是这套精确渲染路径和资源生命周期,这样可以在不牺牲框选后清晰度的情况下,降低实际延时。
评论 0
暂无评论,快来发表第一条评论吧!