旧版CAD缩放延时的原因分析

我看过当前 D:\Project\CimsReporting 的代码了,本次没有修改代码,也没有执行构建。结论是:CAD 模式的缩放不是普通 WPF 图片缩放,而是每次滚轮都重新调用 CAD 引擎进行整帧栅格化,因此出现延时是符合当前实现路径的。

一、当前缩放链路

主要路径如下:

MouseWheel
  -> ZoomCadViewport
  -> ScheduleCadRender
  -> RequestCadRenderAsync
  -> Task.Run(RenderCadViewport)
  -> WpfCadPanelService.RenderViewport
  -> CADLib QueryFeatures
  -> CAD2GDIPDirectTranslator
  -> GDIPColorDirectRasterizer
  -> 整帧像素合成
  -> WriteableBitmap.WritePixels

对应位置:

每次缩放都会重新执行:

  1. 按当前 ROI 查询 CAD 特征;
  2. 对每个 CAD layer 重新创建 GDI 栅格器;
  3. 重新转换 CAD 图元;
  4. 重新生成完整的 ViewerHost 尺寸位图;
  5. 把原始 buffer 拷贝到托管数组;
  6. 执行背景、Step 轮廓、扫描遮罩等像素处理;
  7. 整幅写入 WriteableBitmap;
  8. 重建静态和动态覆盖层。

所以延迟的核心不是 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 图像出现

仓库里实际上已经存在预览相关代码:

其中 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 整幅更新。

而且还有一处重复处理:

  1. UpdateCadImagePixels() 中调用一次 DrawCadStaticPixels();
  2. 随后 UpdateCadOverlay() 又调用 UpdateCadImageScanMask();
  3. UpdateCadImageScanMask() 再次复制基础像素并调用 DrawCadStaticPixels();
  4. 再次执行整幅 WritePixels()。

具体位置:

这意味着一次完整 CAD 帧可能会重复做一轮比较重的像素处理。


4. 每帧重建 Overlay Canvas 和图元

BoardImageWpfView.xaml.cs:2149-2165

当前每次渲染都会:

  • 新建缺陷 Canvas;
  • 新建高亮 Canvas;
  • 遍历缺陷;
  • 新建 Polygon、Ellipse、TextBlock 等 WPF 元素;
  • 替换原来的 Canvas 子元素。

静态覆盖层也会重新创建:

BoardImageWpfView.xaml.cs:2168-2185

如果整板图中缺陷、报废区域、注册点较多,这部分会放大 UI 延时。


5. 每层都重复进行 CAD 查询和栅格化

WpfCadPanelService.cs:178-205

当前每帧会遍历:

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

建议实际操作一次:

  1. CAD 初始整板图;
  2. 连续滚轮放大 5 次;
  3. 停止滚轮等待画面稳定;
  4. 再缩小 3 次;
  5. 查看上述四类耗时。

判断方式:

日志指标 主要耗时 说明
cad.renderViewport layerRasterMs 很高 CADLib 查询/转换/栅格化 优先优化 CAD 渲染策略
cad.renderViewport 不高,但 cad.imagePixels 很高 像素拷贝、遮罩、WritePixels 优先优化 WPF buffer 处理
cad.staticOverlay 很高 覆盖层图元重建 优先缓存 Overlay
每次 cad.frame 间隔明显大于单帧耗时 调度、排队、连续请求 优先做预览和防抖

目前仓库内没有找到实际运行日志,因此暂时不能给出“CAD 渲染占 80%”之类的确定结论。


P1:交互期间使用低成本预览,停止滚轮后再精确渲染

这是我最推荐的方案。

滚轮期间不要立即重新调用 CADLib,而是:

  1. 使用当前已经显示的 CAD 位图;
  2. 对现有位图做 ScaleTransform 或 TransformedBitmap;
  3. 立即显示缩放结果;
  4. 以 30~80 ms 防抖;
  5. 用户停止滚轮后,再进行一次真实 CAD 精细渲染。

效果会从:

滚轮 -> 等 CAD 渲染 -> 显示

变成:

滚轮 -> 立即缩放旧图
停止滚轮 -> 后台精细刷新

用户感知会明显改善,即使后台 CAD 渲染本身仍然需要几百毫秒。

这个方案可以复用现有的:


P2:增加缩放防抖和真正的 latest-only 调度

当前请求编号机制还不够,建议逻辑上改成:

收到滚轮
  -> 只更新目标 ROI
  -> 重置防抖计时器
  -> 不立即启动新的 CAD 渲染

防抖计时结束
  -> 只提交最后一个 ROI
  -> 后台渲染

后台渲染期间收到新 ROI
  -> 标记 pending
  -> 当前任务结束后只渲染最新 ROI 一次

核心目标是:

  • 一次连续滚轮操作最多产生一个最终 CAD 精细渲染;
  • 不让旧的高成本任务连续追赶;
  • 预览显示和精细渲染解耦。

仅仅依赖 _cadRenderRequestId 丢弃旧结果,无法解决 CPU 已经被旧渲染占用的问题。


P3:消除每帧重复的像素处理

重点检查:

  • UpdateCadImagePixels();
  • UpdateCadImageScanMask();
  • DrawCadStaticPixels()。

建议方向:

  1. 一帧内只做一次 DrawCadStaticPixels;
  2. 避免每次都 Clone 整个 buffer;
  3. 扫描遮罩只处理变更区域;
  4. 背景和 CAD 原图分离;
  5. Step 轮廓、扫描遮罩尽量放到独立 Overlay,而不是每次修改整张像素图;
  6. 如果必须写位图,尽量只写 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 精细栅格化

六、我建议的最小改造组合

如果目标是尽量少改架构、优先改善用户体验,我建议按这个顺序:

  1. 滚轮期间使用已有位图做即时缩放预览;
  2. 滚轮事件增加 30~80 ms 防抖;
  3. 停止滚轮后只提交最后一个 CAD 精细渲染请求;
  4. 避免 DrawCadStaticPixels 在同一帧重复执行;
  5. Overlay 图元缓存,缩放时只更新坐标或 Transform;
  6. 如果 cad.renderViewport 仍然很高,再做 CAD tile/LOD/几何缓存。

其中前 3 项主要改善“感觉上的卡顿和延时”,第 4~6 项才是降低真实 CPU 渲染时间。

总体判断:最主要的问题是“CAD 实时重栅格化 + 没有缩放预览 + 无法取消旧渲染”,不是单纯的 WPF 图片显示性能问题。

分享文章

评论 0

验证码
💬

暂无评论,快来发表第一条评论吧!