旧版CAD缩放延时的原因分析
我看过当前 D:\Project\CimsReporting 的代码了,本次没有修改代码,也没有执行构建。结论是:CAD 模式的缩放不是普通 WPF 图片缩放,而是每次滚轮都重新调用 CAD 引擎进行整帧栅格化,因此出现延时是符合当前实现路径的。
一、当前缩放链路
主要路径如下:
MouseWheel
-> ZoomCadViewport
-> ScheduleCadRender
-> RequestCadRenderAsync
-> Task.Run(RenderCadViewport)
-> WpfCadPanelService.RenderViewport
-> CADLib QueryFeatures
-> CAD2GDIPDirectTranslator
-> GDIPColorDirectRasterizer
-> 整帧像素合成
-> WriteableBitmap.WritePixels
对应位置:
每次缩放都会重新执行:
- 按当前 ROI 查询 CAD 特征;
- 对每个 CAD layer 重新创建 GDI 栅格器;
- 重新转换 CAD 图元;
- 重新生成完整的
ViewerHost尺寸位图; - 把原始 buffer 拷贝到托管数组;
- 执行背景、Step 轮廓、扫描遮罩等像素处理;
- 整幅写入
WriteableBitmap; - 重建静态和动态覆盖层。
所以延迟的核心不是 WPF Image 控件本身,而是每个缩放事件都会触发一次完整 CAD 渲染。
二、为什么放得越大越容易延时
这里有一个比较关键的因素。
在 WpfCadPanelService.cs:378-393 中:
var queryResult = preparedJob.CadJob.QueryFeatures(...);
...
var dpi = CADTranslator.CalculateDPI(query.ROI, pixelRect.Width, pixelRect.Height);
translator.TranslateBatch(..., dpi, ...);
缩放放大时:
- 输出位图尺寸基本不变,仍然是整个可视区域的像素尺寸;
- 但 CAD 的 ROI 变小;
- 同样的屏幕像素要表示更小的 CAD 范围;
CalculateDPI得到的 DPI 会升高;- CAD 图元需要以更高精度重新转换和栅格化。
因此,放大后虽然屏幕像素数量没有明显增加,但 CAD 转换和栅格化的工作量可能增加,尤其是:
- 线段、圆弧、铜皮边界较多;
- 细小图元在高倍放大后开始进入可见范围;
- 叠加了多个钻孔、阻焊、VIA 等层;
- 单板或整板数据本身复杂。
这可以解释“放大的越大越延时”,但具体瓶颈仍需要结合运行日志确认。
三、当前实现中比较明显的性能问题
1. 缺少缩放过程中的即时预览
ZoomCadViewport() 修改 ROI 后,直接请求真实 CAD 重绘:
BoardImageWpfView.xaml.cs:2061-2086
当前没有先使用已有位图做即时缩放,而是等待 CADLib 完整渲染结束后才显示新画面。
这会导致用户感知到:
滚轮 -> 空白等待 -> 新 CAD 图像出现
仓库里实际上已经存在预览相关代码:
BoardImageWpfPlugin\Services\CadViewportPreview.csBoardImageCore\Services\CadViewportPreview.csBoardImageCore\Display\CadDisplaySession.cs
其中 BoardImageCore 还使用了 TransformedBitmap 做预览帧。但是目前 BoardImageWpfPlugin 的实际 CAD 显示路径没有使用这套预览机制。
这是当前最值得优先利用的优化点。
2. _cadRenderRequestId 只能丢弃结果,不能取消正在进行的 CAD 计算
当前代码在渲染期间收到新请求时,只会增加请求编号:
_cadRenderRequestId++;
if (_cadRenderInProgress)
return;
旧任务仍然会继续执行。完成后发现请求已经过期,才丢弃结果,然后重新启动最新请求:
BoardImageWpfView.xaml.cs:1173-1229
也就是说,如果用户连续滚轮:
第 1 次 CAD 渲染还没结束
第 2、3、4、5 次缩放请求进入
第 1 次仍然完整计算
第 1 次结束后,再计算最新一次
这会产生明显的“输入已经停止,但画面还在追赶”的感觉。
当前已有一定的 latest-request 机制,但它只是在结果层面丢弃旧结果,并没有停止昂贵的 QueryFeatures、CAD 转换和像素合成。
3. 每一帧会执行完整的像素后处理
BoardImageWpfView.xaml.cs:1265-1295
每帧都会:
- 分配新的完整像素数组;
Bitmap.CopyPixels;- 绘制 CAD 背景;
- 绘制 Step 轮廓;
- 扫描遮罩处理;
WritePixels整幅更新。
而且还有一处重复处理:
UpdateCadImagePixels()中调用一次DrawCadStaticPixels();- 随后
UpdateCadOverlay()又调用UpdateCadImageScanMask(); UpdateCadImageScanMask()再次复制基础像素并调用DrawCadStaticPixels();- 再次执行整幅
WritePixels()。
具体位置:
BoardImageWpfView.xaml.cs:1281-1289BoardImageWpfView.xaml.cs:2130-2137BoardImageWpfView.xaml.cs:1298-1317
这意味着一次完整 CAD 帧可能会重复做一轮比较重的像素处理。
4. 每帧重建 Overlay Canvas 和图元
BoardImageWpfView.xaml.cs:2149-2165
当前每次渲染都会:
- 新建缺陷 Canvas;
- 新建高亮 Canvas;
- 遍历缺陷;
- 新建 Polygon、Ellipse、TextBlock 等 WPF 元素;
- 替换原来的 Canvas 子元素。
静态覆盖层也会重新创建:
BoardImageWpfView.xaml.cs:2168-2185
如果整板图中缺陷、报废区域、注册点较多,这部分会放大 UI 延时。
5. 每层都重复进行 CAD 查询和栅格化
当前每帧会遍历:
foreach (var layer in cadPanel.Layers)
每层又会:
- 创建查询;
QueryFeatures;- 创建
GDIPColorDirectRasterizer; - 创建
CAD2GDIPDirectTranslator; - 转换;
- 拷贝原始 buffer;
- 逐像素合成。
现有的 PreparedCadJob 和 PreparedLayers 缓存只能避免 CAD 文件初始化和层准备重复执行,不能避免每次 viewport 改变时重新 QueryFeatures 和 Rasterize。
四、建议的优化优先级
P0:先确认真正的耗时段
代码已经有性能日志:
WPF PERF cad.renderViewport
WPF PERF cad.frame
WPF PERF cad.imagePixels
WPF PERF cad.staticOverlay
日志路径是应用目录下的:
logs\BoardImageWpfPlugin-YYYYMMDD.log
对应日志写入位置:
ExceptionLogService.cs:139-171
建议实际操作一次:
- CAD 初始整板图;
- 连续滚轮放大 5 次;
- 停止滚轮等待画面稳定;
- 再缩小 3 次;
- 查看上述四类耗时。
判断方式:
| 日志指标 | 主要耗时 | 说明 |
|---|---|---|
cad.renderViewport layerRasterMs 很高 |
CADLib 查询/转换/栅格化 | 优先优化 CAD 渲染策略 |
cad.renderViewport 不高,但 cad.imagePixels 很高 |
像素拷贝、遮罩、WritePixels | 优先优化 WPF buffer 处理 |
cad.staticOverlay 很高 |
覆盖层图元重建 | 优先缓存 Overlay |
每次 cad.frame 间隔明显大于单帧耗时 |
调度、排队、连续请求 | 优先做预览和防抖 |
目前仓库内没有找到实际运行日志,因此暂时不能给出“CAD 渲染占 80%”之类的确定结论。
P1:交互期间使用低成本预览,停止滚轮后再精确渲染
这是我最推荐的方案。
滚轮期间不要立即重新调用 CADLib,而是:
- 使用当前已经显示的 CAD 位图;
- 对现有位图做
ScaleTransform或TransformedBitmap; - 立即显示缩放结果;
- 以 30~80 ms 防抖;
- 用户停止滚轮后,再进行一次真实 CAD 精细渲染。
效果会从:
滚轮 -> 等 CAD 渲染 -> 显示
变成:
滚轮 -> 立即缩放旧图
停止滚轮 -> 后台精细刷新
用户感知会明显改善,即使后台 CAD 渲染本身仍然需要几百毫秒。
这个方案可以复用现有的:
CadViewportPreview.csBoardImageCore中的CadDisplaySession预览思路。
P2:增加缩放防抖和真正的 latest-only 调度
当前请求编号机制还不够,建议逻辑上改成:
收到滚轮
-> 只更新目标 ROI
-> 重置防抖计时器
-> 不立即启动新的 CAD 渲染
防抖计时结束
-> 只提交最后一个 ROI
-> 后台渲染
后台渲染期间收到新 ROI
-> 标记 pending
-> 当前任务结束后只渲染最新 ROI 一次
核心目标是:
- 一次连续滚轮操作最多产生一个最终 CAD 精细渲染;
- 不让旧的高成本任务连续追赶;
- 预览显示和精细渲染解耦。
仅仅依赖 _cadRenderRequestId 丢弃旧结果,无法解决 CPU 已经被旧渲染占用的问题。
P3:消除每帧重复的像素处理
重点检查:
UpdateCadImagePixels();UpdateCadImageScanMask();DrawCadStaticPixels()。
建议方向:
- 一帧内只做一次
DrawCadStaticPixels; - 避免每次都
Clone整个 buffer; - 扫描遮罩只处理变更区域;
- 背景和 CAD 原图分离;
- Step 轮廓、扫描遮罩尽量放到独立 Overlay,而不是每次修改整张像素图;
- 如果必须写位图,尽量只写 dirty rectangle,不要每次整幅
WritePixels。
这一组优化通常比更换 Image 控件或调整 NearestNeighbor 更有效。
P4:缓存 CAD 几何或建立多级细节缓存
如果日志显示主要耗时在 cad.renderViewport,建议从 CAD 渲染层解决:
方案 A:按缩放级别建立 LOD
例如:
- 整板视图:低精度;
- 中等缩放:中精度;
- 高倍缩放:高精度。
不要每个滚轮步长都使用最高精度。
方案 B:瓦片化渲染
将 CAD 按固定空间区域切成 tile:
整板 -> 低分辨率缩略图
局部放大 -> 只渲染当前可视 tile
继续平移/缩放 -> 只补充新进入区域
这比每次把完整 viewport 的所有层全部重新生成更适合整板图。
方案 C:缓存 CAD 查询结果
如果 CADLib 返回的 QueryFeatures 结果可以安全复用,可以考虑缓存:
CAD 文件 + Step + Layer + ROI/Tile + LOD
但这需要确认 CADLib 对查询对象、Placement 和线程安全的要求,不能直接假设所有结果都可以长期复用。
P5:Overlay 不要每次重新创建 WPF 图元
对于缺陷、注册点、报废区域等覆盖层,可以考虑:
- 图元只创建一次;
- 缩放时只更新 Canvas 的变换矩阵;
- 或者使用
DrawingVisual/ 单个自绘 Visual; - 不要每帧新建大量
Polygon、Ellipse、TextBlock; - Tooltip 和命中测试单独维护数据索引。
尤其当缺陷数较多时,这部分可以明显降低 UI 线程负担。
五、当前代码中已经做对的地方
当前实现并不是完全没有优化:
PreparedCadJob有缓存;- CAD layer 的准备过程有缓存;
- CAD 渲染放到了
Task.Run,避免完全阻塞 UI 线程; - 使用
_cadRenderRequestId避免显示过期结果; - 使用
NearestNeighbor,避免 WPF 对 CAD 位图做高成本平滑缩放; - 有完整的
cad.renderViewport、cad.imagePixels、cad.staticOverlay性能日志。
但是这些优化主要解决的是:
不阻塞 UI / 避免重复初始化 / 避免显示过期结果
没有解决最关键的:
缩放过程中每次都要重新做完整 CAD 精细栅格化
六、我建议的最小改造组合
如果目标是尽量少改架构、优先改善用户体验,我建议按这个顺序:
- 滚轮期间使用已有位图做即时缩放预览;
- 滚轮事件增加 30~80 ms 防抖;
- 停止滚轮后只提交最后一个 CAD 精细渲染请求;
- 避免
DrawCadStaticPixels在同一帧重复执行; - Overlay 图元缓存,缩放时只更新坐标或 Transform;
- 如果
cad.renderViewport仍然很高,再做 CAD tile/LOD/几何缓存。
其中前 3 项主要改善“感觉上的卡顿和延时”,第 4~6 项才是降低真实 CPU 渲染时间。
总体判断:最主要的问题是“CAD 实时重栅格化 + 没有缩放预览 + 无法取消旧渲染”,不是单纯的 WPF 图片显示性能问题。
评论 0
暂无评论,快来发表第一条评论吧!