CAD缩放延时改善方案

看了之后有一个比较明确的收获:真正值得借鉴的不是“旧位图缩放预览”,而是 DisplayCore 的 CAD 渲染架构。

你们之前放弃“先缩放旧图、再刷新高清图”是合理的。框选缩放场景不需要模糊预览,应该重点降低鼠标松开后那一次精确 CAD 重绘的耗时。

一、DisplayCore 为什么框选缩放流畅

我重点看了这些源码:

它的流畅性主要来自下面几个设计。

1. 框选过程中不重新渲染 CAD

旧代码的矩形缩放命令是:

鼠标按下
  -> 创建操作层中的矩形

鼠标移动
  -> 只更新操作层矩形

鼠标抬起
  -> 计算最终 FOV
  -> 只设置一次 DisplayManager.DataFov

对应 DispRectZoomCmd.cs:12-27。

所以框选拖动过程中,CAD 主图不会随着鼠标移动不断重新 Query 和栅格化。拖动时显示的只是一个很轻量的操作层矩形。

当前插件在这点上其实已经比较接近:

因此,当前插件框选拖动过程本身不应是主要瓶颈。主要延时应该发生在鼠标释放后开始的那次完整 CAD 重绘。


2. FOV 变化不会创建一串渲染任务,而是交给一个长期运行的渲染线程

DisplayCore 的 DataFov setter 只是更新当前绘制上下文,然后把最新 context 设置给 DataCollectThread:

DisplayManager.cs:89-103

DataCollectThread 是长期运行的后台线程:

DataCollectThread.cs:25-72

关键特点是:

  • 只有一个长期存在的渲染线程;
  • 它读取当前最新的 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:

CADPresenterPool.cs:9-39

CADPresenter 初始化时只创建一次:

_rasterizer = new GDIPDirectRasterizer();
_translator = new CAD2GDIPDirectTranslator(_job, _rasterizer);

对应 CADPresenter.cs:36-54。

只有 CAD Job 发生变化时,才重新 AcceptJob():

CADPresenter.cs:67-92

每次 FOV 变化时,旧代码主要做的是:

复用 Presenter
  -> 创建/调整当前 ROI Query
  -> QueryFeaturesCustom
  -> TranslateBatch
  -> 直接写入目标 buffer

例如:

CADPresenter.cs:1076-1153

而当前插件的 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:

CADSource.cs:86-109

它的接口很清晰:

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

也就是说,当前插件中间多了很多层:

  • BitmapSource
  • CopyPixels
  • byte[]
  • 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 循环复用:

DataCollectThread.cs:14-22

而当前插件每次 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,并在单个显示刷新中有序合成。


五、最终建议排序

如果只给一套建议,我建议顺序是:

  1. 不改变框选交互,不做模糊预览;
  2. 把 CAD Rasterizer/Translator 从“每次 RenderViewport 创建”提升到“CAD 显示会话级长期复用”;
  3. 优先评估复用 CADPresenter / CADPresenterPool 以及 PresentReference、PresentExtraLayers 路径;
  4. 把 CAD 输出改为直接写入复用的 native buffer,减少 BitmapSource -> byte[] -> WritePixels 中间链路;
  5. 使用一个长期运行的 latest-ROI render worker,避免每次请求启动新 Task;
  6. 把扫描遮罩、Step 轮廓、缺陷覆盖层从 CAD 基础图的重复像素处理中拆开;
  7. 最后才考虑 tile、LOD 或 CAD 几何缓存。

最核心的一句话

DisplayCore 的流畅性主要来自 Presenter/Rasterizer/Translator 复用、FOV 状态合并、后台单渲染循环、复用 native buffer,而不是来自模糊预览。

当前 BoardImageWpfPlugin 最应该借鉴的是这套精确渲染路径和资源生命周期,这样可以在不牺牲框选后清晰度的情况下,降低实际延时。

分享文章

评论 0

验证码
💬

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