Back to Home

Deep immersion in the WPF rendering system

WPF · C # · .net · Windows

Deep immersion in the WPF rendering system

Original author: Jeremiah Morrill
  • Transfer
The translation of this article was prompted by a discussion of the recordings “Why is WPF Aliver than All Living?” and “Seven Years of WPF: What Has Changed?” The original article was written in 2011, when Silverlight was still alive, but WPF information has not lost its relevance.

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 havewith 300 animated BitmapCached ellipses. An advanced system will understand that it needs to render 300 textures and they are all z-ordered one after another. After that, she will collect their packages of maximum size, as far as I remember, DX9 can take up to 16 input elements (sampler inputs) at a time . In this case, we get 16 draw calls instead of 300, which will significantly reduce the load on the CPU. In terms of 60 frames per second, we will reduce the load from 18,000 draw calls per second to 1125. In Direct 3D 10, the number of input elements is much higher .

Okay, I read to this place. Tell me how WPF uses pixel shaders!

WPF has an extensible pixel shader API and some built-in effects. This allows developers to add truly unique effects to their user interface. When trying to apply a shader to an existing texture, Direct 3D usually uses an intermediate render target ... after all, you cannot use the texture you are writing to as a sample from! WPF does this too, but unfortunately, it creates a completely new texture EVERY FRAME and destroys it at the end. Creating and destroying GPU resources is one of the slowest things you can do when processing each frame. I usually don’t do this even with allocating a similar amount of system memory. By reusing these intermediate surfaces, a very significant increase in productivity could be achieved. If you ever wondered why your hardware-accelerated shaders create a noticeable load on the CPU, now you know the answer.

But maybe this is the way to render vector graphics on the GPU?

Microsoft made a lot of efforts to fix these problems, unfortunately this was not done in WPF, but in Direct 2D. Look at this group of 9 ellipses rendered by Direct2D:



Remember how many draw calls WPF needed to render one ellipse with a path? What about vertex buffer locks? Direct2D does this in ONE draw call. Tessellation looks like this



Direct 2D tries to draw as much as possible at a time, maximizing GPU usage and minimizing CPU load. Read Insights: Direct2D Rendering at the end of this page., there Mark Lawrence explains many of the internal details of Direct 2D. You may notice that despite all the speed of Direct 2D, there are even more areas where it will be improved in the second version. It is possible that version 2 of Direct 2D will use DX11 hardware acceleration tessellation.

Looking at the Direct 2D API, it can be assumed that a significant part of the code was taken from WPF. Check out this old Avalon video where Michael Wallent talks about developing a GDI replacement based on this technology. It has a very similar geometric API and terminology. Inside, it is similar, but very optimized and modern.

What about Silverlight?

I could do Silverlight, but that would be redundant. Silverlight's rendering performance is also low, but the reasons are different. It uses to draw the CPU (even for shaders, as far as I remember, they are partially written in assembler), but the CPU is at least 10-30 times slower than the GPU. This leaves you with much less processing power for rendering the user interface and even less for the logic of your application. Its hardware acceleration is very poorly developed and almost exactly repeats the cached construction of WPF and behaves in a similar way, making a draw call for each object with BitmapCache (BitmapCached visual).

So what do we do now?

This question is very often asked to me by clients who have encountered problems with WPF and Silverlight. Unfortunately, I do not have a definite answer. Those who can make their own frameworks tailored to their specific needs. The rest have to come to terms, since there are no alternatives to WPF and SL in their niches. If my clients simply develop business applications, then they do not have so many problems with speed and they simply enjoy the productivity of programmers. Those who want to build really interesting interfaces (that is, consumer apps or kiosk apps) have real problems.

After the start of the translation, news appearedabout the planned performance optimization and the use of DX10-11 in WPF 4.6. Whether the problems described in the article from the news will be resolved is not entirely clear.


Source article: A Critical Deep Dive into the WPF Rendering System

Read Next