Performance: Essential Must-Knows
written by Fidcal, some additions by Sotha
This article relates to performance issues for mappers. For performance information for players, see Performance Tweaks
All mappers should know the following but remember not to blindly follow every rule but consider the balance between performance and presentation on a case by case basis. Don't spoil a beautiful view or an interesting interaction to gain efficiency unless it is worth the trade.
- 1 Index
- 2 Useful Cvars
- 3 Build Airtight. Seal out the Void. Avoid Leaks
- 4 Render-Sectioning using Visportals
- 5 func_static v worldspawn Brushes
- 6 Caulk
- 7 Collision Model Removal on some func_static Entities
- 8 Patches, Fixed Tessellation
- 9 AI Performance Considerations
- 10 Entity Management
- 11 Texture Performance Considerations
- 12 Lighting Performance Considerations
- 12.1 Lighting overlaps
- 12.2 BlendLight Keyword
- 12.3 Virtual Ambient Light
- 12.4 Shadow Casting Removal
- 12.5 Moving Lights vs. Static Lights
- 12.6 No Dynamic Interactions
- 12.7 shadowopt : use it with dmap
- 13 Sound Performance
- 14 Quick Performance Improvement Checklist
- 15 Other References
Frames Per Second
The console command com_showFps 1 allows you to see the framerate at any part of your map, so that you have an idea of how changes you make affect the FPS in a given area. Generally, it is a problem if FPS drops below 30 fps.
To identify the locations where problems are reported, use these two cvars to render the positions as an overlay display while playing:
g_showviewpos 1 con_noPrint 0
Draw Calls, Triangle Counts
type "r_showPrimitives 1" in the console to see a list of draw calls, tris, etc, that are currently on screen.
It is useful to set "con_noprint 0" in the console then leave the console and watch the counters on-screen as an overlay while you roam around.
The more drawcalls, the more of a performance impact there is, in general terms. Allowing drawcalls to get higher than 4000 is usually a bad idea. But the Shadowcasting calls (shdw) are far more significant to performance. In general, trying to keep the shadows below 80k is desirable.
r_showTris ... enables wireframe rendering of the world, 1 = only draw visible ones, 2 = draw all front facing, 3 = draw all
r_showLightCount ... 1 = colors surfaces based on light count, 2 = also count everything through walls, 3 = also print overdraw
red = 0, green = 1, blue = 2, cyan = 3, pink = 4, white = 5+
If possible, try to keep light overlaps to 3 or lower, depending on the complexity of the geometry and light sizes.
Shadow Casting lights are far more of a factor with this metric, noShadows or ambient lights can probably be set to fairly high counts.
r_showShadows ... 1 = wireframes, 2 semi-solid volumes
There are 7 different colours used in the engine for drawing shadow volumes, and the colour tells you how the shadow volume got calculated. Quite cool. 4 of them are probably never used any more, they're for GPUs that don't support shader programs. The ones we're likely to see are:
- Green: worldspawn shadows generated and optimized during dmapping. These are independent models in their own right,
they don't "belong" to the worldspawn that generated them in the same way that dynamic shadows belong to the model that casts them.
- Orange: turbo shadows, which are dynamically generated shadows that got drawn by the shadow.vp shader program.
- Red: turbo shadows where the player's view is partly or fully inside the shadow volume, so they need to draw extra shadow tris (end caps)
to avoid errors in working out where the shadow hits. This is the same complication that was behind the z-pass / z-fail patent controversy.
r_showInteractions ...1 = shows the number of light/surface interactions that were generated that frame, as well as the number of shadows that were generated.
It is (also) useful to set "con_noprint 0" in the console then leave the console and watch the counters on-screen as an overlay while you roam around.
Build Airtight. Seal out the Void. Avoid Leaks
Probably the first thing to learn when building a map (mission) with the Doom3 software engine is that you are building in a void and your map must be airtight, sealed against the void, much like a spaceship. Nothing to do with air of course but with the way the software renders your map. These are the rules:
- Your map must not have any gaps in its outer walls leading to the void. The tiniest hairline, near invisible gap caused by two angled lines that are not grid snapped can be enough to cause a leak.
- The origin coordinates of any entity must not be in, or exposed to, the void. Keep in mind that it is possible for the origin of an entity to be outside its visible shape. Such an entity may appear to be inside while its origin is outside in the void. And of course no entity must form part of the outer wall so if you convert a beam or group of brushes to func_static for instance, they must not form part of the outer wall.
- Some materials (textures) such as nodraw do not seal against the void. Such textures must not be used on the surfaces of any brush open to the void.
- There is a rare type of leak caused if the worldspawn entity has the 'angle' property - just delete that property.
The above rules also apply between areas within your map separated by Visportals to reduce what needs to be rendered at any one time. So one area can leak to another area within the outer walls of your map. This is like internal bleeding - there may be no visible symptom but the patient is not functioning very well! With an external leak you will get an error when compiling your map with dmap and the compilation will abort. But with an internal leak no error is reported and the map will compile if it is otherwise OK. The map will work but performance will be reduced because the software will waste effort rendering areas that are not needed. So, there should be no gaps in the separating walls; no entity must form part or all of the separating walls; textures like nodraw should not be used on those walls.
The smaller your grid size, the easier it is to create small gaps without noticing. Your world-sealing geometry should generally be created using a large grid size of 8 or even 16.
How to detect and fix leaks
Note there is more on fixing leaks at Leaking maps
External Leaks to the Void
If your map leaks to the void then you will get a 'Leaked' error in the console while trying to dmap and the compilation will abort and generate a 'pointfile' with a filename prefix the same as your map and the suffix .lin instead of .map in the same folder as your map. In Dark Radiant you can use this to track the path of the leak, determine the cause, and fix it. Use the File menu option 'Pointfile' and a red line is drawn along the path of the leak. You can use Ctrl+Shift+L to move the camera along the line towards the leak to the void (useful to spot an actual leak) and Ctrl+Shift+K to move towards the entity at the other end (useful if an entities origin is outside in the void.) With the above rules in mind you will be looking for gaps, entities making up part of the outer walls or else an entities origin outside the walls in the void. If you think you have found and fixed the problem but get the error again check carefully as it might be a different leak or you missed the first one.
Internal leaks between visportalled areas do not cause a formal error or stop map compilation but they will affect performance and sound propagation. No pointfile is generated so how can you detect them or even know they have occurred? You might realize you have such a leak two ways:
- If you have a visportal that remains open when you would expect it to be closed or unprocessed yet it seems to be set up correctly (see Visportals.)
- You hear sounds from another area as if they were coming directly through walls.
Springheel has created a video demonstrating how to test for internal leaks:
or, follow the steps below:
Whenever a pointfile is created, the red line always starts at the lowest numbered entity not sealed from the leak. We use that to our advantage in this system. First, you need to find out which entity the pointfile comes from by intentionally opening a leak and looking at the pointfile. Whatever is at the beginning of the red line is your "Origin Entity" (OE).
The following method requires moving your OE from zone to zone checking for internal leaks.
To test an internal visportalled area that has no external walls to the void....
- Deliberately make a leak in the outer wall (to the void) of your map by just peeling back a brush a few units.
- Move your OE inside the first area you want to test for internal leaks (a room, for example).
- Find the visportals that seal that zone, and replace the visportal texture with something else. Make it distinctive, and make sure it seals against the void (NOT a non-shadowcasting texture (with _ns at the end), caulk, or a texture with transparencies). If you have visportals inside entity doors then you need to hide those doors temporarily to change the visportal texture. If there are no leaks, this should compeletely seal the zone.
- Save the map with a new name so the original is kept as a backup (always play safe)
- Run dmap. You will get a "leaked" error.
- Back in Dark Radiant, select Pointfile from the file menu. If the red line starts from your OE, then that zone has a leak allowing the red line to get from your OE to the leak you intentionally opened in the outside of your map. Follow the red line (use Shift+Ctrl+K and Shift+Ctrl+L to move the camera along the red line) to see where the leak is, save, dmap, and repeat as necessary. If the red line does NOT start from your OE, then that means there is no path from your OE to the outer leak. In other words, the zone you're testing is perfectly sealed.
- Do this for as many zones as you need to test. I usually test all of mine as one of the last things I do in a map. Don't forget to retexture your visportals when you're done!
If you do not have a consistent OE to work with, alternatively you can export just the worldspawn brushes into a separate map file.
- Filter World geometry, select everything else and delete it.
- Place a player start in the area to test (which will be the only entity there).
- Save as a separate map file (careful not to overwrite your current map version!)
- The player start will be the only entity, so the pointfile will come from there if any leak, and lead through to your deliberate opening to void.
Render-Sectioning using Visportals
If visportals are not used then the entire map will be rendered at once wherever the player is - greatly reducing performance to the point where most maps are not likely to be playable at all. It is crucial that the map should be compartmentalized using visportals. Study the Visportals tutorial for details.
In scenarios where you would like to force visportals closed while in view, you can use Func_Portals.
In addition to having more control over portal closure, you can also target a textured (or cubemap-ed, etc) patch to obscure the portal on closure.
func_static v worldspawn Brushes
By converting worldspawn brushes to func_static...
- You can add the property noshadows = 1 so that it doesn't cast shadows, improving frame rates.
- It avoids creation of millions of polys in certain situations where the engine converts a brush into loads.
- It reduces pathfinding complexity (see Preventing and Reducing Pathfinding Complexity in Select Locations in Pathfinding)
But be careful to use r_showtris = 3 in the console to check the results. It sometimes happens that large func_statics get processed through visportals. For example, an L-shaped room with the ceiling criss-crossing with beams all converted to one giant func_static and all within the portal area sealed by the door. Yet outside one could clearly see all their tris. By splitting the beams into two groups, one in each 'leg' of the L so making two separate func_statics, they were effectively as good and no longer showed outside. So don't be tempted eg, to convert all trim throughout a house into one giant func_static.
- Many of the same trade-offs between func_static and worldspawn apply to comparing brushes (worldspawn) to models.
- Models can be converted to worldspawn at map compile via the "inline 1" spawnarg. This should be used sparingly as there are many caveats the least of which is that you lose the lower memory footprint of models vs brushes.
- See Models vs Brushes
Read Caulk about when to use the texture common > caulk to remove faces from the rendering process.
There are differing opinions about using Caulk as a performance optimization. Tests have shown it to have a minimal benefit, at best.
It should be noted that even though it shouldn't be a relied upon by sloppy builders, careful caulk placement is likely to reduce the chance for map leaks and thus improve performance in that aspect.
Collision Model Removal on some func_static Entities
For models that neither the player nor any AI nor any object can reach or interact with then some memory can be saved by setting the property:
All moving game elements would then pass through the model so only normally use on inaccessible model features.
This should only be applied to Func_Statics as it would remove ALL collision from Worldspawn if applied there.
(This can be a critical memory saver for LARGE maps that are near the dmap memory limit. )
Patches, Fixed Tessellation
Setting patches to fixed tessellation instead of automatic can significantly decrease the number of polygons.
A related topic, though of more concern with regard to modeling, is triangulation. Essentially, graphic hardware likes to render large equilateral triangles. If your geometry is composed of tall thin triangles it will be significantly more punishing to the graphic processor, much more than a stack of small triangles that cover the same area. Of course, brush-work will be cut however the map compiler sees fit so you would need to employ Brush Carving to gain any control over the resultant triangles. Fixed Patch tessellation can also reduce the chance of poor triangulation.
AI Performance Considerations
Pathfinding Complexity Reduction
see Preventing and Reducing Pathfinding Complexity in Select Locations in Pathfinding
Adding "neverdormant" "0" to an AI will cause it to suspend all activity when not in view. Use sparingly, since AI with this on will not react to anything when out of the player's view.
There are several things that can be done to reduce the resource usage caused by entities.
Entities can be dynamically merged, changed, or removed to enhance performance.
The Dark Mod has a dedicated procedural content generator which also acts as an Entity Manager.
See the SEED wiki.
Level of Detail. Objects can swap models for lower poly versions or versions with materials that have lower performance impact.
See the LOD wiki.
The most basic LOD attribute that you can specify for entities is at what distance they should disappear.
Simply add "dist_check_period" "hide_distance" spawnargs (with associated values) to the desired entities as such.
"dist_check_period" ".5" edit: There is a default spawnarg already, so this does not need to be added to the entity. "hide_distance" "2500"
This is an extremely useful tool for mappers to improve performance. Many small models have lots of shadow-casting polys, and they continue to be drawn even when the player is so far away that he cannot see them. Putting a hide_distance spawnarg can solve that problem. Likewise, bar shelf might be full of high poly objects that can't be seen from most places in the room, but they are still being drawn. Putting a hide_distance 300 on them means they won't hurt performance when the player is across the room, but they will be visible to the player when they round the counter and can see the shelves.
This (also) mainly applies to particle effect management. You can use Location Entities (see Location Settings ) to manage whether particle effects like Moon Beams and Torches are rendered out of view.
Combining several Func_Static entities into one can improve performance.
Risks \ Caveats:
- If the size of the entity when combined is too great it can overwhelm your GPU.
- If the new merged entity crosses any visportals it will not be split. (See Func_static vs Worldspawn )
- If the new merged entity is hit with too many lights the overall light count may sap performance (See Light Overlaps )
Use this method carefully (or use the SEED system).
Stim Response and Custom Scripted Entities
You can also manage Entities via the "Stim Response" system or custom scripting. Just be careful about the number of these custom entity checks and the "check periods" like "sr_time_interval_N". (And, of course, carefully debug any script conditionals and loops.)
Texture Performance Considerations
This is probably the single most disappointing limit after the limits of overlapping lights. The more texture variety, the more draw batches and thus more draw calls. One way around this limit is to combine multiple textures into a texture atlas ( Texture Sheet ). Another approach would be to use careful decal placement to break-up the monotony of repeat textures rather than using more diverse texture sets. Blendlight projection can also be used to have visual elements span a variety of materials, but it would usually be less expensive to use decals.
You can see the total size of all loaded images by using printMemInfo console command. It dumps detailed results to several text files near the map file. Also briefly described here: .
Another thing that can be done with Func_Static is to batch repeated textures together.
A mapper would prefer that a game engine work like: Texture + Texture + Texture + Polygon (or group of polys) Nearly all game engines and video hardware will prefer: Polygon + Polygon + Polyon + Texture
Anything you can do to make your map render like the latter example will improve CPU performance. The old advised ratio was 500 to 1000 polygons per texture but this may have moved up to 2000 or more with newer GPUs. Too many polygons per batch will increase your CPU performance but decrease your GPU performance. You must find the right balance for your target hardware.
If your scene has lots of the same texture on decorative features or geometry that is not split by too many visportals then it may improve rendering speed to combine all the geometry in the scene that has the same texture (or a large percentage of the same texture) into a single Func_Static. One specific but common scenario is a scene that has lots of decal patches with the same texture. These can be combined into Func_Static's to reduce draw calls. Lunaran's "Strombine" map for Quake 4 used this method and improved performance to the point that his map was rendering "as fast as if it had a third of the polygons in the worst corners".
- Quake 4 has a built-in r_showBatchSize command that can be used to diagnose batching performance in detail.
- SEED can serve the same purpose as func_static for the above "decal scenario" and has a lower memory footprint.
Yes, this is a TERRIBLE option... but a valid one nonetheless.
If your texture is scaled to fine detail level it means more rendering work (more pixels per poly).
(...and more frame-buffer memory consumption)
If you can get away with scaling\stretching a couple of textures a bit and need a little boost, then do it (just don't admit to it).
[edit: Is there any actual evidence this has a noticeable benefit? I find it hard to believe this would accomplish anything. - Springheel]
One of the most expensive material operations that the any engine must perform is to evaluate transparency. Use alpha materials sparingly.
The more stages in your material the more passes are required to render it.
While animating textures or normal maps are more expensive than static ones, you can reduce their performance impact by creating a "Film strip" texture of all the frames and using scroll and a time offset or snap table. Using conditional parms to invoke individual frames is far more expensive.
Lighting Performance Considerations
While Doom 3 has arguably one of the fastest "dynamic" light methods, it comes at the expense of the overall performance benefit
of "baked lighting" or "light maps" You should carefully consider light and geometry placement and work to ensure that you make the most
of the lights placed in your scene.
Lights require a hell of a lot more than just a GL status change: the rendering algorithm processes lights one by one, and accumulates the result.
Therefore there is no performance advantage to using the same texture on lots of lights, because each light (and its contained geometry)
is still going to be processed in its own individual pass.
Shadow casting is the main cost for light rendering. It both consumes Fillrate for the z-buffer prepass
and CPU \ IO bandwidth for shadow skinning operations.
Moving lights are far more expensive than stationary ones as stationary lights re-use geometry in VBO cache.
What does Doom 3 want?
The most common answer to this question, inferred by the Doom 3 game map design, is that Doom 3 wants maps to have lots of
small non-overlapping lights close to the world geometry. This is a natural fit for the electronic filled laboratory areas of the
Doom 3 setting but it has been a frustration to mappers as light placement is a tedious process.
The real answer:
(Presuming you do not want large swaths of darkness punctuated by sparse illumination...)
If you wanted to illuminate a scene in a way that the Doom 3 engine would render the fastest you would create razor thin light volumes that cover
as much of the surface geometry as possible without exceeding the triangle batch size limit. Smaller volumes means that the CPU and GPU have to
spend less resources evaluating what each volume contains. These may also be broken-up by material as best as possible (one light per texture is the best possible).
"What about dynamic objects and AI?" you say... Well they would be handled by either a few sparse projected lights, or large "in air" light volumes that do not touch the world geometry.
"How would those "razor thin" "surface hugging" volumes look any good?" you ask... It wouldn't be easy but if each were either over-bright or had a custom texture
it should be comparable or better than using a more natural volume distribution.
As one of the few saving graces in this example, you can take advantage of the fact that intersecting light volumes
have almost no negative performance impact if they do not intersect with geometry. Yes, this convoluted light example would be very unwieldy
and it would be hard to utilize dynamic shadows and directional light.
It would be virtually impossible to live up to the ideals in this example but you may use it as a guide.
Alternately, you would create tunnel-like geometry around all light sources, the fastest rendering being a string of
non-overlapping point-lights that recedes into the distance contained within a long hallway or tunnel.
If you know that an area of your map is purely decorative (no player or AI can reach it), you may try to use the former scenario as a guide.
If you feel that your structure can be made to be more tunnel-like, then the latter alternative can be used as a guide.
Now you know what Doom 3 wants.
Generally be cautious about the light from more than one source falling into the same geometry triangle.
This overlapping lighting makes the rendering calculations more complex and reduces game performance resulting in slowdown.
The console command r_showtris 2 shows what is currently rendered (as triangular wireframes) so you can check any suspect areas.
Counter to common intuition large flat surfaces can be big offenders for light performance considerations.
Often, large surface areas span several lights and the map compiler does not split the triangles at light boundaries.
This means that the entire surface has the same light count (a total of every light that makes contact).
You can add trim and other detail to split up large triangles to reduce this (see Brush Carving below).
Non-directional lighting which can pass through walls is usually seen as a problem but this property can be used to your advantage
by placing lights inside of structures and projecting them outward so they only affect the desire surface.
Note polygonal facing does not matter, lights hitting backs of polygons have performance impications,
"...the engine treats the light as a volumetric data set which is evaluated against both facing and non-facing polygons."
While most mappers will use the light entities "as is" the default radius of these entities necessarily means that lights up against walls will overlap into external areas. This can cause overdraw related performance drops as well as additional light passes for off-screen overlaps.
You can mitigate this problem by altering the size and position of the light radius so it remains in the room\area where it belongs. Though, you will shortly find out that such a change will look "wrong". The source of illumination and flame particles will always follow the origin of the light volume. To address this you can change the light_center spawnarg to relocate the source of illumination then you can use the attach_pos_origin_N (Where N is the attachment position number. For the flame on the torch it is 1.) spawnarg to relocate the flame particles.
The name given for splitting up Brushes so that light counts are kept in-check is Brush Carving. There are many methods to achieve this result including:
- Add trim or detail to break-up large surfaces
- Convert some of the adjacent brushes to (caulked) patches.
- Changing a material parameter for adjacent polygons.
- Using the "Discrete" and "NoFragment" keywords in the material definitions to control splits (use sparingly)
- Dmap using the LightCarve keyword (primarily useful for maps with large open-space areas with large light volumes and simple geometry, a large warehouse map, etc.)
Some related hints from the LightCarve wiki that likely apply to all light optimization:
- Always make light radii multiples of the grid size (default 8). The default light radius of 300 is not a multiple of 8, and should be changed to 296 or 304. By doing this you ensure that lights can be lined up with each other perfectly.
- Activate the “Show light volumes” button towards the right-hand side of the main toolbar. This allows you to see all of the light volumes in the level, not just the currently selected light.
- Two light volumes that stop a small distance short of each other will create extra triangles. Adjust their sizes and positions so that the volumes touch each other exactly, so that the cut can be shared between the two.
- Similarly, a light volume that stops a small distance short of a wall, floor, visportal or other brush is creating wasted triangles. Move it so that its volume shares the cut with the natural level geometry.
- Do not place a light such that its boundary intersects a piece of very complex brushwork. Position it so that the brushwork is either contained within or outside the light volume.
Related Doom3world (wayback archive) topic: http://web.archive.org/web/20070509181516/http://www.doom3world.org/phpbb2/viewtopic.php?t=4924
You can isolate specific lights to specific materials with the use of the spectrum keyword.
In this way lights that would normally overlap will not but at the expense of cumulative lighting from lights that are not of the same spectrum. This is usually used for "special effect" lighting but could potentially be used to control illumination in tricky areas to keep the light counts low. Yes, VERY cumbersome. Spectrum Keyword at Modwiki
Blend Lights are the lowest impact lighting method possible in Doom 3 other than emissive textures. They are non interactive and can be thought of as a volumetric decals.
Virtual Ambient Light
Dark Mod provides a virtual main ambient light to help performance on low end systems. See Virtual Darkness
Other than the ambient_world (see entry above) you may use ambient lights to add subtle lighting variation to areas. They do not produce normals or specular so it is recommended that you do not use these as primary light sources but instead for faked radiance. Ambient lighting has less performance impact than other light methods.
Shadow Casting Removal
Where appropriate, remove shadows from lights, entity brushes, and models by settings the property noshadows = 1 (see func_static v worldspawn Brushes)
- Further details: Turning Shadows Off
Just as you can create a simple shadow-casting mesh for a detailed model to save on performance, you can also set a complex entity brush noshadows = 1 then build a simpler entity brush with shadowed nodraw brushes (texture common caulk shadow and shadow2 ) inside it. Discussion
Moving Lights vs. Static Lights
Moving lights have a considerable performance impact. Be sure to use this effect sparingly.
No Dynamic Interactions
This is a last resort optimization.
If you are sure that no dynamic lighting will hit a surface and that neither the player nor AI can use a lantern\torch or activate a light of any kind near a surface, then you may set the property "noDynamicInteractions = 1".
The lighting, shaders, specular, etc will be baked-into the affected entity or brush. No light will affect the appearance; it will always have the same appearance no matter what. This can be used for distant "decorative" geometry or scenarios where a large amount of terrain is being traversed by a player with a vehicle with multiple dynamic lights (see the Monorail map in Doom 3 for an example).
shadowopt : use it with dmap
CAUTION! THIS FEATURE CAUSED PATHFINDING ERRORS IN ONE MAP
For better performance, when you compile your map with Dmap use...
dmap shadowopt 2 <mapname>
You can also try values 0 to 5 but at the time of writing 2 is considered likely the best.
This page also refers : https://modwiki.xnet.fi/Dmap_%28console_command%29
I has recently been determined that the radius of sound emitters can have a severe effect on CPU performance due to the path-finding code complexity for sound occlusion.
Where possible, try using the s_occlusion 1 key-pair in your sound shader.
See also Pathfinding
Quick Performance Improvement Checklist
If you have low fps areas in your map, it may help analyzing the scene. Console command "r_showprimitives 1" gives an output what is being drawn. Especially check the amount of tris and shadow tris. In a heavily patched areas you can save performance by reducing the patch tessellation with the patch inspector. The default auto-tessellated patches have a LOT of redundant tris generated. You can easily save a lot of tris by setting the tessellation to manual and 2x2 or 3x3, without much reducing the scene details visually. Setting complex patchwork or models into "noshadows 1" can also help a lot without having any effects on visuals. Finally, the most common culprit for performance degradation is multiple lights shining on the same area. Ie, lights overlapping. Never have more lights overlapping than two or three. Just adjust your lights so that they aren't overlapping and you gain performance without changing scene visuals much. Making worldspawn things into func_static also reduces tris.
Tl; Dr or simple scene optimization (assuming visportalling has been already done correctly.)
- 0) have your map to have boxy worldspawn geometry with all the details as func_statics.
- 1) reduce lights overlapping
- 2) reduce patch tessellation
- 3) make things into noshadows. (lights and func_static objects).
This is a useful article on optimising... https://modwiki.xnet.fi/Optimising_maps
Optimizing maps thread: Optimizing Maps
Quake 4 performance guide from id software:
More related TDM articles: