Deep immersion in the WPF rendering system
- Transfer
At first I did not want to publish this article. It seemed to me impolite - one should speak about the dead either well or nothing. But a few conversations with people whose opinion I really appreciate made me change my mind. Developers who have put a lot of effort into the Microsoft platform should be aware of the internal features of its work, so that, having reached a dead end, they can understand the reasons for what happened and more accurately formulate wishes for the platform developers. I consider WPF and Silverlight to be good technologies, but ... If you have been following my Twitter for the past few months, then some of the remarks might seem like groundless attacks on WPF and Silverlight performance. Why did I write this? After all, in the end, I invested thousands and thousands of hours of my own time for many years, promoting the platform, developing libraries, helping community members and so on. I am definitely personally interested. I want the platform to get better.

Performance, performance, performance
When designing an addictive, consumer-oriented, user interface, performance is most important to you. Without it, everything else makes no sense. How many times have you had to simplify the interface because it lags? How many times have you come up with a “new, revolutionary user interface model” that you had to throw in the trash, because the existing technology did not allow it to be implemented? How many times have you told your customers that you need a quad-core processor with a frequency of 2.4 GHz to work properly? Clients have repeatedly asked me why on WPF and Sliverlight they cannot get the same smooth interface as in the iPad application, even having a four times more powerful PC. These technologies may be suitable for business applications, but they are clearly not suitable for next-generation user applications.
But WPF uses hardware acceleration. Why do you consider it ineffective?
WPF really uses hardware acceleration and some aspects of its internal implementation are done very well. Unfortunately, GPU utilization is much lower than it could be. The WPF rendering system uses very brute force. I hope to explain this statement below.
We analyze a single pass of WPF rendering.
To analyze the performance, we need to understand what actually happens inside WPF. For this, I used PIX, the Direct3D profiler that ships with the DirectX SDK. PIX launches your D3D application and injects a number of interceptors into all Direct3D calls to analyze and monitor them.
I created a simple WPF application that displays two ellipses from left to right. Both ellipses are the same color (# 55F4F4F5) with a black outline.

And how does WPF render this?
First of all, WPF cleans up (# ff000000) the dirty area that it is about to redraw. Dirty areas are needed to reduce the number of pixels sent to the final merger stage in the GPU pipeline. We can even assume that this reduces the amount of geometry that will have to be re-tessellated, more on this later. After cleaning the dirty area, our frame looks like this

After that, WPF does something incomprehensible. First, it fills the vertex buffer, and then draws something that looks like a rectangle over a dirty area. Now the frame looks like this (exciting, isn't it?):

After that, it tessellates the ellipse on the CPU. Tessellation, as you may already know, is the transformation of the geometry of our 100x100 ellipse into a set of triangles. This is done for the following reasons: 1) triangles are a natural rendering unit for the GPU 2) ellipse tessellation can result in only a few hundred triangles, which is much faster than rasterizing 10,000 pixels with anti-aliasing by means of the CPU (which Silverlight does). The screenshot below shows what tessellation looks like. Readers familiar with 3D graphics might notice that these are triangle stripes. Note that in tessellation, the ellipse looks incomplete. As the next step, WPF takes tessellation, loads it into the vertex GPU buffer and makes another draw call using the pixel shader,

Remember that I noted the incompleteness of an ellipse? It really is. WPF generates what Direct3D programmers know as a “line list”. The GPU understands lines as well as triangles. WPF fills the vertex buffer with these lines and guess what? Correctly, making another draw call? The lineup looks like this:

Now, WPF has finished drawing an ellipse, isn't it? Not! You forgot about the circuit! A path is also a collection of lines. It is also sent to the vertex buffer and another draw call is made. The outline looks like this.

At this point, we drew one ellipse, so our frame looks like this: The

whole procedure must be repeated for each ellipse on the stage. In our case, two times.
I do not understand. Why is this bad for performance?
The first thing you might notice is that to render one ellipse we needed three draw calls and two calls to the vertex buffer. To explain the inefficiency of this approach, I will have to talk a little about the work of the GPU. For starters, modern GPUs work VERY FAST and asynchronously with the CPU. But with some operations, costly switching from user mode to kernel mode transitions occurs. When filling in the vertex buffer, it should be blocked. If the buffer is used by the GPU at this moment, it forces the GPU to synchronize with the CPU and dramatically reduces performance. A vertex buffer is created with D3DUSAGE_WRITEONLY | D3DUSAGE_DYNAMIC, but when it is blocked (which often happens), D3DLOCK_DISCARD is not used. This can cause a loss of speed (synchronization of the GPU and CPU) in the GPU, if the buffer is already in use by the GPU. In the case of a large number of draw calls, we have a high probability of getting many transitions to kernel mode and a large load in the drivers. To improve performance, we need to send as much work to the GPU as possible, otherwise your CPU will be busy and the GPU will be idle. Do not forget that in this example we were talking about only one frame. A typical WPF interface tries to output 60 frames per second! If you have ever tried to find out why your rendering stream is loading the processor so hard, you most likely found that most of the load comes from your GPU driver. To improve performance, we need to send as much work to the GPU as possible, otherwise your CPU will be busy and the GPU will be idle. Do not forget that in this example we were talking about only one frame. A typical WPF interface tries to output 60 frames per second! If you have ever tried to find out why your rendering stream is loading the processor so hard, you most likely found that most of the load comes from your GPU driver. To improve performance, we need to send as much work to the GPU as possible, otherwise your CPU will be busy and the GPU will be idle. Do not forget that in this example we were talking about only one frame. A typical WPF interface tries to output 60 frames per second! If you have ever tried to find out why your rendering stream is loading the processor so hard, you most likely found that most of the load comes from your GPU driver.
And what about cached composition? It increases productivity!
Without a doubt, increases. Cached build or BitmapCache caches objects in the GPU texture. This means that your CPU does not need to be resized, and the GPU does not need to be re-rasterized. When performing a single render pass, WPF simply uses the texture from the video memory, increasing performance. Here is the BitmapCache of the ellipse:

But WPF has dark sides in this case too . For each BitmapCache, it makes a separate draw call. I will not lie, sometimes you really need to make a draw call to render a single object (visual). Anything can happen. But let's imagine a scenario in which we have