Valve Developer Union

r_speeds and Optimizing Quake and GoldSrc Levels

(September 22, 2017)


"I swear r_speeds are still relevant—"

Okay, you probably don't need to take this guide as seriously as I did when I wrote it. What can I say, I'm a purist and I like to make my maps look like the game they came from. If you are trying to get something that looks period-appropriate, the information on visblocking and polycounts still applies.

While the thought of optimizing a Half-Life level for modern computers might seem laughable, if you're going to map for a game which will run without a graphics card, you might as well go all the way. Not to mention, games that old have a tendency to break if you push them hard enough, with disappearing walls, brush entities, and things generally going rather glitchy. Both Quake and GoldSrc games (which will work identically with the following instructions) use the same commands to test the optimization of your map, the main one being r_speeds.


The r_speeds ("render speeds") command displays the number of polygons, both entity and world, being rendered. World polygons are anything brush-related, while entity polygons are enemies, NPCs, viewmodels, muzzleflashes, and so on. The Source engine does not use r_speeds; it has a much more in-depth and complicated visual for how well your maps run, the Showbudget panel.

run r_speeds 1 in the console to enable it. What you get will vary depending on if you're running the game with the software renderer or the OpenGL renderer. If the former, the console will begin to be spammed with the following:

27.3 ms 260/161/ 16 poly 283 surf
28.3 ms 265/181/ 24 poly   8 surf
28.5 ms 291/189/ 33 poly  10 surf
29.2 ms 290/193/ 45 poly  14 surf

The first number is the number of milliseconds a single frame takes to render. As discussed in the Showbudget guide, a steady 60fps would require each frame to render in under 16ms. For 30fps, which is about the playable minimum, each frame needs to take, at maximum, 33ms. Any longer, and you'll start to see some pretty big framerate drops.

The three numbers in the middle all refer to polycounts. Something to keep in mind is that the BSP program splits nice, clean walls and brushwork into tens of ugly polygons. We'll get into the details in a moment. For now, keep in mind that the first number (260) refers to all world polygons in the potentially visible set, the second number (161) refers to those in the PVS that are being drawn, and the third number (16) is the number of undrawn polygons in the PVS.

The final number (283 surf) refers to the amount of those polygons which are being dynamically-lit—that is, any kind of non-static light is hitting them. This includes lights compiled to flicker or turn on with a targetname, as well as muzzle flashes. It's best to keep these as low as possible, or some of the lights will simply refuse to work altogether.

If you're testing your map in the OpenGL renderer, your console spam will look more like this:

0 ms   315 wpoly 1890 epoly   0 lmap
0 ms   315 wpoly 1890 epoly   0 lmap
0 ms   313 wpoly 1890 epoly   0 lmap
0 ms   309 wpoly 1890 epoly   0 lmap

Much simpler. The first number still refers to the amount of time a frame takes to render. 315 wpoly means 315 world polygons are currently being rendered, and 1890 epoly means 1,890 entity polygons are currently being rendered. The last number, 0 lmap still refers to the amount of dynamically-lit polygons currently being rendered.


Do I have to keep pausing to see my r_speeds?

While Quake will display r_speeds in the top-left corner of your screen automatically, GoldSrc games require you to run developer 1 in the console before they'll display. Otherwise, you'll only be able to see them in the console when you pause the game.

Lowering r_speeds and speeding up rendering

So r_speeds is a bunch of numbers, yes. Perhaps an overwhelming bunch of numbers. r_speeds is remarkably simple when you get down to it, however; unless you have a good reason, keep the amount of world polygons being rendered under 500 or so. This is about the sweet spot for these engines, and while modern computers and modern source ports will play high-poly maps with no issue, it's still relatively poor practice. Plus, if you're working on a mod, people might choose to play it on an older machine for authenticity. If there's a chance map performance might be an issue, why risk it? Optimize, optimize, optimize.

Easier said than done though. Sure, it's easy to keep the detail level relatively consistent with what's already in the game. Likely, your bigger issue will be visibility control. Here's a scenario: you're hammering away at a map when you discover your r_speeds through a certain area are especially high, even though nothing in view is especially detailed. To understand why, let's explain a bit about how VIS does its thing.

Visleaves and the potentially visible set

I mentioned something called a potentially visible set earlier. Simply put, walls block views. As a result, if you're somewhere along the wall, you won't see what's behind it. Potentially. There's no guarantee. You could be on the side of that wall and see all around it. Let's extend the wall with an invisible line. This line marks a plane where your view might be blocked. Now imagine every wall in your level doing this. This is the premise behind visleaves and the potentially visible set. So we don't get confused, here's a visual in the level editor (you can load this view by going to "Map > Load Portal File"):

A view of visleaves in the level editor
A view of visleaves in the level editor

Notice that the wall in the very back creates a bubble through the room. Each wall will split the space you can walk around in into these bubbles. VIS determines which bubbles you can see from any other bubble. After all, there's no point in the game rendering what you can't see, right? Now replace "bubble" with "visleaf" and you have the way VIS works. Problem is, so your VIS times aren't longer than they already are, VIS is conservative about it. Even if something really can't be seen from one point, VIS might say it can anyway. This avoids any kind of world holes or rendering artifacts, but isn't as efficient.

In short: walls split rooms into bubbles. Some bubbles can be seen from other bubbles. Some can't. VIS calculates all of that, but doesn't get it exactly right. As a result, even if you can't see into a room, VIS might be able to. Keep in mind that doors and windows don't block VIS from seeing into a room. To VIS, it's like the doors don't even exist.

Using the PVS to your advantage

The solution to all your woes is more walls. If VIS can see into another room, you can use more walls to make it so it can't. Here's a sample map of two rooms:

Two rooms that'd run quite poorly in a proper map
Two rooms that'd run quite poorly in a proper map

Even though there's a func_door in the way, VIS will render both rooms at the same time. If you stuffed them both full of details without realizing it, you could have r_speeds in the 750s or higher. The solution is to use a "visblocker" wall, as seen here:

Two rooms with a visblocker
Two rooms with a visblocker

As clumsy as that might look, it's actually quite common in official id maps, notably DM3:

DM3's visblocker
DM3's visblocker

The other solution is to use corners, specifically a corner hallway. If you can afford to warp the brushwork, put the rooms at 90-degree angles and connect them using an L-shaped hallway:

Two rooms with a corner hallway to block visibility
Two rooms with a corner hallway to block visibility

Both of these will block VIS and allow you to stuff both rooms full of detail. Be careful that the bend in the hallway doesn't become the new lynch point in being able to see both rooms.

There's two more commands that help when it comes to testing visibility, r_drawflat and r_draworder. r_drawflat replaces all polygons, or in the case of GoldSrc, world polygons, with a brightly colored solid texture. Remember what I said earlier about QBSP splitting up solid faces into polygon messes? This is what I mean:

A map being run with r_drawflat enabled
A map being run with r_drawflat enabled

This command is especially helpful if you have complex geometry splitting a wall in two. You can see exactly where the polygons are and why they're broken up the way they are.

The other command, r_draworder, requires a software renderer, so either put your GoldSrc game into software mode in the settings or, for Quake, run your map with a software-rendered engine like WinQuake or Mark V WinQuake. Then, in the console, enable r_draworder. This inverts the order in which rooms are rendered, causing faraway rooms to be rendered over close walls. Here it is, showing two rooms that really need a visblocker, with r_speeds being rendered in the corner for maximum developeriness:

A map being run without r_draworder The same map with r_draworder enabled
Left: A map being run without r_draworder
Right: The same map with r_draworder enabled

Another caviat about GoldSrc debug commands

r_drawflat and r_draworder will only work in GoldSrc games when set to the software renderer. GLQuake has this same restriction, but at least r_drawflat should be patched in more modern source ports. (QuakeSpasm is one such port.)

Get creative with the way you block visibility, and you can have good-looking maps that run well, even on old, dated machines.