Tuesday, February 08, 2005

Optimizing Computer Games

We keep our game running at 30 frames per second. When it dips below this, we take the time to figure out the problem is and fix it. This goes against my general philosophy:
 
Optimize last.
 
Optimizing early is a waste of resources because I find that it is quite common for the project to go in a different direction. Sometimes the code I optimize isn't around at the end of the project. That's just wasted time. There is value to adding a feature and then after using it, deciding that we don't like it. This makes a better game. However, optimizing code that gets removed really doesn't get us anything.
 
Keeping the frame rate high is a different story. It definitely does waste time, but the advantages we get are very valuable. When the game is always at 30 frames per second, the artists know what the performance impact of their art is. If they add something with too many polygons, the frame rate slows down and they can see this immediately. If the game was running at 5 frames per second normally, they might not notice their art had too many polygons. This way artists can optimize art in the game. One time I saw one of our artist tweaking the number of billboard polygons in grass areas so the he could get as many as possible without lowering the frame rate. You make a better game when artists can do things like that.
 
The first thing to do when you start optimizing is to find the bottleneck. Normally there is one thing that is really slow. With a graphical game this can be:
  • GPU: Vertex bound (Too many complicated polygons)
  • GPU: Pixel bound (Too many complicated pixels)
  • CPU: Driver Bound (Too many batches)
  • CPU: Game Code
In our game we tend to have a balance where all these things are normally right on the edge of being a performance issue. When the frame rate dips it's usually because one thing changed and caused the slow down. It a few minutes to find the culprit.
 
The first thing we do is to find a reproducible slow case. I see people try to optimize all the time without doing this and they just get nowhere. Without a reproducible case, optimizing is like shooting in the dark. You need to be able to see the problem to fix it.
 
Now we determine if it's a CPU or GPU problem. This is easy. We look at the CPU utilization. If it's 100% (or 50% on hyper-threaded machines) then the problem is CPU, otherwise it's the GPU.
 
If we determine the issue is with the GPU, they next few steps are annoying. It takes a little trial and error. GPU's don't give you enough debugging information. Hopefully in the future the GPU vendors will put performance meters on their pixel and vertex pipelines so you can see exactly what is slow, but for now we have do some voodoo and guess. First thing to do is change the resolution to something very small. 640x480 usually works because it's small enough and we can still read our on screen performance numbers. If the frame rate goes up dramatically when we do this, then we are pixel bound. We need to fix overdraw or turn off so many per-pixel lights or something. If the rate doesn't go up when we shrink the screen, then we are vertex bound. We need to get rid of some polygons or something.
 
If we are CPU bound we need to figure out if it's the driver or our code. One way we do this is to put the game in "null render" mode. In this mode all the game and graphics code run, but we don't call the graphics API. If switching to null render mode speeds the game up significantly, then we have too many graphics batches or we are changing too much render state or something. If null render mode doesn't speed up the game then we are happy because we are bound by our code and that is easy to fix. We run VTune and it usually show us the functions that we are calling too much.