1: Performance Issues with ScreenSpaceLines3D
This entry is part 1 of a 12-part series on WPF
Problems with Lines
WPF 3D doesn't know how to draw lines.
Shocking as it may seem, WPF 3D considers triangles to be
more fundamental than lines. Everything in 3D is a triangle. If you want to
draw a line, you have to somehow do it by using triangles.
Actually, this is not all that shocking once you really think
about it. The problem with a line is that it's too fundamental.
Geometrically speaking, a line has zero width. If you asked WPF to draw a
line, it would have nothing to draw. A triangle is the simplest bit of
geometry that actually has any surface area.
We think that drawing a line should be simple. In 2D
graphics, it is. But in 3D graphics, where the picture on your screen is a 2D
projection of a 3D world which uses a completely different coordinate system,
But still, sometimes we want to draw something that we
think of as a line. Maybe we want a wireframe view of our 3D scene. Maybe we
just want to highlight the edges of a solid figure to make it easier to see. In
practical terms, we know intuitively what we want when we think of "a line".
Usually, we just want it to be one pixel wide on the screen.
But WPF 3D has a pedantic posture. In the WPF 3D coordinate
system, a line is just a very thin rectangle. Or rather, it is two very thin
triangles, put together to make a rectangle.
This is tedious for three reasons:
- We need those triangles to be facing the camera. A
triangle has zero thickness. If we're looking at it sideways, then it's
- We need the width of those triangles to be set in 3D
coordinates such that after the scene is projected onto the 2D display
surface of our screen, the resulting "line" is one pixel wide.
- We need both of the above to stay true even when the scene
is rotated or zoomed.
Some clever folks have produced a 3D Tools library which contains
several classes designed to make WPF 3D programming easier. One of these
classes is called ScreenSpaceLines3D.
It solves all three of the problems above.
And it works beautifully. To create a line, all you have to
do is instantiate this class and give it two endpoints (in 3D coordinates) and
a thickness (in screen pixels). The resulting line looks just right, even if
you rotate or zoom the view.
A perfect solution to the problem, right?
I've warned my readers before about using libraries that
present an abstraction without really understanding what is going on under the
hood. ScreenSpaceLines3D is a perfect example.
Stop and wonder how ScreenSpaceLines3D does its magic. The
math isn't actually, the tricky part. Oh, it's tricky, but it's not that bad.
It simply crawls up to find the dimensions of the Viewport3D in which it
resides. Then it uses that information to calculate how wide something should
be if it wants to be one pixel in the 2D projection. Tricky, but that's not
The question isn't so much how to do this calculation,
but when. The line needs to be rescaled whenever anything about the
The way ScreenSpaceLines3D handles this is to add a hook to CompositionTarget.Rendering.
This handy feature of WPF allows us to register a callback function which gets
called whenever something is being rendered. However, we need to remember that
this gets called a LOT. Like 60 times per second, or so.
Here's the problem: ScreenSpaceLines3D adds one handler to
CompositionTarget.Rendering for every instance. And this handler never gets
So I noticed one day that Sawdust uses about 10-20% of the
CPU even when it's not doing anything. In fact, the longer I use Sawdust in a
single session, the more CPU it uses. If I bring it up and force it to draw
several hundred pictures, it gets to the point where it is consuming 100% of
the CPU even while idle.
Why? Because CompositionTarget.Rendering is being called 60
times per second, and every time it is called, it is rescaling every instance
of ScreenSpaceLines3D that has ever existed.
Joel would say that ScreenSpaceLines3D is an abstraction
I can think of several ways to try and fix this problem. For
example, I could add code to ScreenSpaceLines3D to unregister the hook function
when it is no longer needed.
Instead, I have decided that I don't want to use CompositionTarget.Rendering
for this situation. I hacked my copy of ScreenSpaceLines3D.cs to remove it. Then
I had lines that never got scaled properly. So I took the OnRender() function,
made it public and changed its name to Rescale(). Then I added stuff to call
Rescale() whenever I really need to.
This approach was a bit tedious, but it works pretty well.
Lines get scaled, but the app uses no CPU when it's idle, and it doesn't get
slower and slower the longer it runs.