WPF 渲染小结

在上一篇文章D3DImage - 它能做啥、解决了什么问题、有哪些瓶颈、怎么最佳实践最后,提到DropShadowEffect严重影响到D3DImage的渲染性能问题,导致程序在渲染8分屏(8个远端视频)的时候,出现严重的性能下降,渲染卡顿。要知道,在使用原生窗口渲染方案渲染8分屏,CPU占用和内存占用也不过25%和~200Mb,稍差一点,使用D3DImage优化后方案渲染,CPU占用并没有出现多大的跳跃,大约在30%左右。即使是添加了DropShadowEffect的情况下,CPU占用和内存占用好像都没有多大变化;既然在CPU和内存占用都没有多大变化的情况下,WPF渲染卡顿,那肯定(可能吧)是“帧生成”时间过长的锅。

帧生成时间

玩游戏的人都知道,影响游戏帧数的一个关键因素是帧生成时间,帧生成时间过长必定导致游戏FPS下降,游戏不流畅。帧生成时间并不等同帧更新时间,这个需要搞清楚。例如一个游戏锁帧60FPS,那么帧更新时间为1000/60=16.6ms,通常来说,如果你硬件性能足够强劲,那么帧生成时间要小于16.6ms才能保证游戏运行在60FPS的帧率下,否则会掉帧。类似的,WPF的渲染帧率下降可能(无责任猜想)也是同样的因素导致。But why?

DropShadowEffect 的锅

不要误会,DropShadowEffect并没有什么过错。只是在特定情境下,DropShadowEffect(及其他所有Effect类),就是WPF渲染瓶颈的关键:

  • 将Effect应用到时刻变化的元素
  • 在应用了Effect的元素上,叠加了其他时刻变化的兄弟元素

远程会议的问题就是碰到了第二种情景,我们以为只要不直接应用DropShadowEffect到D3DImage这种时刻更新帧的元素上,应该就能避免渲染瓶颈,然而被打脸。
说了这么久,好像还是没有说为什么;年轻人,不要这么着急,继续往下看。

WPF 的渲染知识两则

  • 当WPF在渲染一个窗口的时候,它只更新需要更新的区域,称为脏区(DirtyRect)。
  • 显存的占用与渲染面积成正相关

使用下面这个例子来模拟导致问题的场景:

左边是应用了DropShadowEffect的Grid,中间是应用ColorAnimation的Grid,右边是3个视频渲染。暂时来说,情况看起来还是可以的,没有出现明显的渲染卡顿,整个界面的渲染都维持在一个比较高的帧数。

那么,将中间的元素叠加在左边的元素上看看:

问题出现了,帧率下降严重,视频出现卡顿,ColorAnimation变得不平滑:

在这两种情况下,脏区数量都是一样的,分别是始终变化的ColorAnimation Grid和视频区,唯一不同的是,ColorAnimation Grid的位置变了,与应用了DropShadowEffect的Grid部分重叠了,这导致每帧渲染多了一个HW IRT(hardware intermediate render target),对于WPF来说,HW IRT是一个代价高昂的渲染过程,比它更惨的是SW IRT,如果你的WPF程序在渲染过程中出现多个这种渲染过程,那么可以肯定你的程序需要完成大量的工作来渲染你的程序。

那么,什么是IRT?

Intermediate Render Target。在现代的图形处理单元(GPU)中,我们可以将我们要进行渲染的内容先在Render Target中渲染,然后像素着色器可以通过处理这个Render Target来添加特定的效果,这个过程完成后才将处理完的数据储存到后台缓存(Back Buffer),这个时候渲染线程(Render Thread)可以将back buffer拷贝到前台缓存(Front Buffer)进行显示。对应到上面的例子,动态元素在拥有DropShadowEffect的元素上刷新,引起脏区更新,这个脏区有关DropShadwoEffect,DropShadowEffect需要像素着色器渲染指令(因为它本身就是由HLSL创建的),嘣!!!,IRT就来了。但是,IRT在一次渲染中是很正常的啊,有些WPF程序在一次渲染中可能存在几个IRT都不会引起这么明显的性能下降。4K是性能的试金石,要知道,我们的程序是运行在4K下的,变化的脏区面积足够大,才引起了显著的性能下降,而且,不要忘了,WPF在使用像素着色器时有天生的缺陷,这篇文章有详细说明,其中提到的关键一点:

WPF has an extensible pixel shader API, along with some build in effects. This allows developers to really add some very unique effects to their UI. In Direct3D when you apply a shader to an existing texture, it’s very typical to use an intermediate rendertarget…after all you can’t sample from a texture you are writing to! WPF does this also, but unfortunately it will create a totally new texture EACH FRAME and destroy it when it’s done. Creating and destroying GPU resources is one of the slowest things you can do on a per frame basis. I wouldn’t even typically do this with system memory allocations of that size. There would be a considerable performance increase on the use of shaders if somehow these intermediate surfaces can be reused. If you’ve ever wondered why you get noticeable CPU usage with these hardware accelerated shaders, this is why.

至此,WPF的渲染相关文章结束。

评论