D3DImage - 它能做啥、解决了什么问题、有哪些瓶颈、怎么最佳实践

D3dImage,.Net Framework 3.5 之后,微软提供的一个全新的ImageSource对象,可以在WPF中很好的呈现DirectX内容;在此之前,你只能将DirectX内容直接渲染在Windows窗口之上,这必然引起令人头疼的AirSpace问题,为了在这些内容上面添加我们习以为常的WPF UI 元素,你只能使用Popup来承载这些内容,完全丧失WPF UI开发的灵活性,且有经验的WPF程序员都知道一个事实:WPF Popup就是一个深坑 - 你需要手动处理各种显示隐藏问题、因为其导致的焦点问题,显示层级问题以及最令人头疼的性能问题,特别是在4K屏幕下,因为我们都知道,Popup就是一个Window,为了解决Airspace问题而使用Popup来承载UI必定需要使其AllowTransparency=True,这就引起了另外一个问题,透明窗口占用内存与其面积成正相关,在4K屏幕下,你可能将整个程序大部分的内存占用贡献给了这些Popup UI。说了这么多,好像在诉控Popup有多么的垃圾(它的确如此,如果在做大量的UI容器时)。

年轻人,如果你觉得Airspace问题真的没有办法解决了,只能用Popup这种技术手段来规避了,那么听老人一句话,不要浪费时间在Popup上了,因为你在前期投入的时间来规避种种Popup UI导致的问题以及各种你意想不到的Bug,到最后总会碰到解决不了,完全不能规避的情况,从而导致整个Popup UI替换方案完全失败的情形。

什么是AirSpace问题

这是一个很复杂的问题,涉及到整个WPF的渲染机制,简单的来说,就是对于WPF窗口的子窗口,其并不是由父窗口负责渲染的,而是由其自己负责自己的渲染,子窗口不能像其他元素一样由父元素来进行布局、渲染(这很好理解),因此,如果我们要在一个WPF窗口中承载另外一个子窗口,那么其渲染并不受父窗口控制,它会渲染在父窗口所有元素之上,这就是导致上面提到的需要用Popup UI来规避Airspace问题的原因。那么问题来了,我为什么要在一个窗口里面渲染另外一个窗口?这不科学。这的确是一个不科学的操作,因为没人会这样做,除非你需要和DirectX内容打交道,特别是对于在WPF做多媒体应用开发的程序员,WPF和DirectX内容的交互 是一个不可避免的问题(虽然我觉得用WPF做多媒体应用本身就存在问题,但这不在本篇文章讨论的范围)。不仅是DirectX内容,在WPF中承载WinForm控件也有同样的问题,为了解决这种问题,微软曾在.Net Framework 4.5的某个预览版(如果没记错的话)中提供了对应的解决方案,但是在正式版中并没有保留,很可惜。

WPF多媒体程序开发的好帮手 - D3DImage

D3DImage就是一个全新的ImageSource,你可以完全根据你对ImageSource的理解来使用它D3DImage。为什么说它是WPF多媒体程序的好帮手?正如我上面提到的,我不觉得WPF是多媒体应用开发的第一选择,多媒体应用是一种性能敏感的程序类型,先不说使用WPF你很难触碰到底层的渲染机制,进行调优;而且,你不能原生访问Direct3D接口,即使,至少对于Windows平台下,多媒体内容大多都使用Direct3D API来进行渲染、绘制(更不用说OpenGL了,其对于WPF来说几乎就是一个不可能的选项)。因此我们需要D3DImage,其为你在WPF中使用Direct3D接口和内容提供了一种可能,尽管那是一种不那么直观的方式。
说到底,我们需要解决的问题是将Direct3D原生渲染的内容,以一种WPF熟悉的,原生的方式渲染在WPF上。在D3DImage之前,你只能直接渲染在窗口上,因为窗口是Direct3D渲染设备的必须参数。而D3DImage,不要误会,它并不是可以让Direct3D不用窗口进行渲染,而是为Direct3D渲染的内容提供了一个“通道”,这个”通道”使WPF可以将ID3DDevice上的渲染表面ID3DSurface更新到其渲染线程中(这种说法并不准确,但是你可以先这样理解,至于它们是怎样”共享“表面的,我们以后再谈)。

一切看起来都是那么完美,但是…

可以想象的到,D3DImage方式并不能提供比肩原生渲染的性能,不论是CPU占用还是内存占用。特别是当你需要兼顾Windows XP的时候,那性能就更加难看了。WPF虽然说是支持硬件加速的一个框架,但是它有原生缺陷,这篇文章描述了一些深入的问题。要知道,在Windows Vista之前,即Windows Xp,只能使用D3D9接口创建D3dDevice,我们甚至不能使用D3D9Ex接口,对于D3DImage来说,这是一个很大的性能损耗,因为由D3D9接口创建的D3dSurface与WPF的D3Dimage渲染过程中存在一个很恶心的过程,WPF需要将显存中的D3DSurface内容拷贝回系统内存,处理完后再拷贝回显存进行渲染,毫无疑问会导致CPU占用和内存占用上升。我们使用剪辑师来做一个实验,在关闭硬件加速的情况下,对比使用两种接口来创建渲染表面的性能表现:
在关闭硬件加速的情况下,使用剪辑师播放一个2K分辨率的视频在不同时刻的CPU占用:

可以看出存在明显的性能差别,在使用D3DEx接口创建渲染表面进行和WPF的交互操作时,并不存在上面提到的显存→系统内存拷贝问题,其直接在显存中进行拷贝,这样会降低CPU损耗。即使这样,它也不能提供比原生窗口渲染更好的性能,这在我们意料之内。

最佳实践

  • 不要支持Windows XP
  • 使用D3D9Ex以上接口来创建你要在D3DImage承载的Direct3D内容
  • 不要在UI上使用DropShadowEffect,一点都不可以
    关于这一点,又是一个可以深究的问题,但是这里就先简单的说一下。在远程会议的开发中,我们在渲染8分屏的时候,发现使用D3DImage进行视频渲染的时候,性能表现远远低于原生窗口渲染的方式,虽然知道D3DImage性能上与原生渲染存在差距,但是也不可能相差这么远。刚开始的时候我并没有怀疑DropShadowEffect,因为我知道它会带来性能问题,因此我还故意将其应用到一个空的Grid上,而不是直接应用到D3DImage上,而且这个Grid和Image并不是父子关系,在我的印象中,这样可以规避将整个D3DImage的像素数据进行DropShadowEffect的管道数据处理(DropShadowEffect本质是一个像素着色器,像素着色器是一个高性能组件,对于现代3D游戏来说,是一个必不可少的组件,但是这篇文章已经提到,WPF里的像素着色器并不完全是那么一回事),按理说,性能瓶颈并不应该出现在这里,然而,它的确是这里,原因是啥?我也不清楚,或许以后有结论之后我们再开一篇文章来谈谈。
评论