This article tries to shed some light on the building blocks of the BSP file format, and inner workings of the Half-Life compile tools which assemble it.
When you compile your map it actually goes through four stages or compilation:
- CSG: Turns the raw information from your MAP file into solid geometry and generated hull files.
- BSP: Cuts the map into segments, generating a few BSP trees (node networks) for rendering and collision purposes. Also culls any unseen/outside faces.
- VIS: Calculates visibility data between the nodes, to prevent rendering the whole map at once.
- RAD: Adds lighting information to the map.
You can use the
-chart
parameter to each of the different compilers to see an overview of the current resource usage. Here’s the resource chart for a very simple box map:
models 1/512 64/32768 ( 0.2%)
planes 20/32768 400/655360 ( 0.1%)
vertexes 8/65535 96/786420 ( 0.0%)
nodes 6/32767 144/786408 ( 0.0%)
texinfos 3/32767 120/1310680 ( 0.0%)
faces 6/65535 120/1310700 ( 0.0%)
* worldfaces 6/32768 0/0 ( 0.0%)
clipnodes 18/32767 144/262136 ( 0.1%)
leaves 2/32760 56/917280 ( 0.0%)
* worldleaves 1/8192 0/0 ( 0.0%)
marksurfaces 6/65535 12/131070 ( 0.0%)
surfedges 24/512000 96/2048000 ( 0.0%)
edges 13/256000 52/1024000 ( 0.0%)
texdata [variable] 48/33554432 ( 0.0%)
lightdata [variable] 2082/50331648 ( 0.0%)
visdata [variable] 1/8388608 ( 0.0%)
entdata [variable] 371/2097152 ( 0.0%)
* AllocBlock 1/64 0/0 ( 1.6%)
Each of the resources listed above (not marked
*) directly corresponds to a BSP lump, and the limits are almost always limits the lump structures impose on each other, because lump entries reference other lump entries by their indices in some way.
Let’s go through the different resources and see what does what. I will also determine if you are likely to reach this limit and what can be done to prevent running into problems.
Models
Risk level:
high
Other than you might expect, the ‘models’ here do not refer to weapons, monster or NPCs but rather to solid brush entities, such as
func_wall,
func_breakable, etc. This also applies to any brush tied to a trigger entity, such as
trigger_once,
trigger_push, etc.
The only exception is
func_detail. It technically isn’t an entity but rather a trick that the compiler uses to stop world geometry from cutting into each other. It is merged into world geometry when the BSP compiler finishes.
Even if you have no solid brush entities, the compiler will always use one slot for
worldspawn. Essentially, it is the first BSP model, and all world geometry that's not part of any brush entity is assigned to it.
To save on the model limit, you can combine separate object together into one entity. To the engine, they are now considered as one model. This also means that if you look at one object, then the other objects tied to the same entity will be rendered too. That’s why you should make sure that you tie objects together that are relatively close to one another.
When tying objects together, they will receive a new origin point [
incorrect? origin is (0 0 0) by default]. Make sure this point is not outside the world or inside a world brush. If it is, then your objects will always be rendered, even through the visibility system. You can determine if this is the case by using the
gl_wireframe 2
mode. This is also often the case when you see objects through the skybox.
The compiler maxes out at 511 brush entities (+
worldspawn), which is mostly enough even for larger maps. However, it’s more likely that you will run into a different limit that is directly connected to the model limit:
edicts
(entity dictionaries).
Edicts can be seen as slots where entities are stored. The edicts limit is the combination of brush entities and regular point entities, so if you’re making large and complex maps it’s very likely this will become your first problem. If you pass the limit, you’ll be greeted with the
'ED_alloc: No free edicts'
message.
For retail WON Half-Life the limit is 900 but that was raised to 1200 in the Half-Life 25th anniversary patch in November 2023. The game will likely also precache a few extra entities through the code, so realistically your budget for your map will be less than 1200. The compile chart doesn’t display the amount of entities in the map, so you’ll need to use external tools such as BSPguy.
If you’re making a MOD of your own, you can raise the edicts limit by adding the following line to your
liblist.gam file:
edicts "2000"
You cannot set the value higher than 2048, because that’s the hard limit of the entity index.
Planes
Risk level:
low
Planes are two-dimensional sheets that run along an axis. When compiling your map, CSG creates these planes to determine where the faces of your brushes are. Even with a medium-sized map, you will max out the planes limit in the CSG step. Luckily, BSP then greatly optimizes the planes by removing all outer planes that touch the void.
This hollow cube, for instance, will have 92 planes in CSG, which is then optimized to 20 in BSP. Looking at the shape, you would expect that this cube has 6 planes for each side but the compilers also add a number of extra planes for clipnode generation, so you will always have more planes than expected.
This makes it hard to predict what kind of effect your brushes have on this limit. If you can maintain a clean mapping style, you can expect BSP to do all the necessary optimizations for you. The risk of running into this limit is therefore quite low.
Vertexes
Risk level:
low
Vertexes (or vertices) are the corner joints that connect lines together. You’ll need two vertices to connect one line, four vertices to create a square and eight to make a cube.
During the BSP compile process all exterior vertices are removed leaving us with a simple hollow cube. These are then counted towards the limit. The only way to lower your vertex amount is to make your geometry more simple. However, the risk of reaching this limit is rather low. You’re more likely to run into other limits before.
Nodes
Risk level:
low
The BSP compiler performs Binary Space Partitioning to cut the map into segments and determine where there is open space for the player to move around in. It uses your world geometry as a guide on how to split the world in the most effective way.
The place where this split is made is called a
split plane
. This split plane has two sides: a front and a back side. It will check if any of those sides are pure solid or contain open space. If there is open space, it’ll continue looking for the next place to split and redo the process. Every time this occurs it creates nodes. The node network that BSP generates is also referred to as the
BSP tree
. You can see this in the image below.
Split plane #1 has completely solid wall on the backside (indicated in RED) and open space on the front (GREEN). It’ll generate two child nodes for both options. One of the options is completely solid, so that path doesn’t need to be examined any more. The player won’t be able to go there. You can see this indicated in the node network as a solid blue square.
The other option is still open and the compiler will look for another place to put a split plane. In this example it picks the opposite wall running along the same axis (#2). There is solid in the front and open area in the back of this plane. From this spot, two new child nodes are created. It then picks a spot on a different axis to split (#3). There is full open space in the back but also partial space in the front. It creates two new child nodes, #4 and #5.
Let’s follow split plane #5 first. There is solid in the back and open area in the front. With all previous splits combined, this now creates a new sector of which the compiler knows that this is an area that can be traversed by the player (indicated in yellow). It will need to do two more split planes (#4 and #6) to determine the same for the rest of the map. The area at the end of the node (and what it contains) are referred to as a ‘leaf’. Refer to the section about leaves for more info.
Next to world geometry, nodes will also apply to brush entities such as
func_wall since they also need to be rendered. Even if you turn them into non-solid brushes with
NULL
textures, the BSP compiler will still generate nodes to determine if this brush exists in your game world.
The risk of running into this limit is rather low. You can only keep the amount of nodes low by keeping your geometry very simple. As soon as you start making larger and more complex maps, you will run into other limits first.
Texinfos
Risk level:
low
Texinfo (texture info.) stores information regarding the orientations of textures that are applied to your brushes. Not every individual brush creates a new texinfo. Brushes that have similar world orientations and use the same shift/scale/rotation values can share the same texinfo. Brushes that use tool textures such as
NULL
,
AAATRIGGER
, etc will not generate texinfos.
Using
NULL
textures on unseen faces is a good way to reduce the amount of texinfos but you’re not very likely to ever hit this limit. There are more texture related limits that you’ll hit first.
Faces / worldfaces
Risk level:
medium
Every surface that has a texture applied to it is considered a face. A small cube has six sides and therefore six faces. However, by default the compiler will split faces every 240 pixels, when the texture scale is set to 1.00. This is called ‘face subdivision’. This is done because of the limit in the lightmap size. If you want to know more, please refer to the
Total Map Optimisation tutorial, section 3.4.
Every brush that has a texture will generate faces, including brush entities such as
func_wall.
Worldfaces
refers to faces on world geometry, basically everything that isn’t an entity. This includes
func_detail, because that’s technically not an entity.
An exception is made for tool textures, such as
AAATRIGGER
,
CLIP
,
SKIP
, etc. These are removed during the compile process. Therefore it’s important to use the
AAATRIGGER
texture on your triggers. You can use regular textures on trigger entities but they will then count towards the face limit.
The risk of passing the threshold of this resource is medium. If you use higher resolution textures such as 256x256 or 512x512, you’ll often need to reduce the texture scale to 0.50 or 0.25. This then sets the face subdivision to 120 and 60 pixels respectively. In turn, this will generate four or even sixteen times the amount of faces compared to a 128x128 texture on 1.00 scale.
Working with a low texture scale will cost a lot of face resources but ultimately you’re more likely to run into the AllocBlock limit first. Refer to that segment for more info.
A good practice to optimize this limit is to use
NULL
on faces players cannot see and use higher texture scales on textures that are far from the players sight. The face subdivision also goes up with higher texture scale: 480 units on scale 2.00, 960 on 4.00, etc.
Clipnodes
Risk level:
high
Clipnodes are similar in structure to the previously-discussed Nodes, but much simpler. It is used to simplify collision detection in GoldSrc. Each plane has three clipnodes for each of the three hulls that the player and monsters use: standing, crouching and large. So a simple six sided cube will end up with 18 clipnodes.
If you want to know more, please refer to the
Total Map Optimisation tutorial, section 6.2.
Leaves
Risk level:
high
As mentioned in the ‘nodes’ explanation, BSP cuts the world into segments and creates a tree.
At the end of each branch you’ll find a what’s called a
leaf
. The compiler divides those into
leaves
and
worldleaves
. The yellow boxes in the image above are determined to be traversable space and will generate a
worldleaf
. You can see two of these worldleaves in the map.
Every worldleaf will also count towards the general leaves limit. The other leaves are created by placing brush entities. For instance: If I create a six sided
func_breakable crate, then the amount of leaves will go up by six.
If you turn the crate into
func_detail then it becomes part of the world geometry. It will be partitioning the space around the crate into new square blocks and this create new worldleaves.
The worldleaves limit is only 8192, so if you use
func_detail a lot you’ll end up reaching the limit relatively fast. Especially with complex objects, the space around it will be partitioned into many small compartments. To fix this, you can turn these objects into
func_wall instead and put the load on the normal leaves instead.
However,
func_wall is considered an entity and those are very limited as well. You’ll have to balance
func_detail and
func_wall if you want to create a large and detailed map. Rule of thumb: complex =
func_wall, simple =
func_detail.
The worldleaves will also increase if you manually add
HINT
brushes to improve performance, because you are adding more split planes. If you want to know more about
HINT
, read chapter 4.2 of this
tutorial.
Regardless of what you do, the compiler will always start with one regular leaf.
Marksurfaces
Risk level:
low
Marksurfaces connect the visible surfaces in your map to the leaves in the BSP tree (see Nodes and Leaves for more info). Some surfaces can be connected to multiple leaves if they border a split plane, so there will always be more marksurface entries than actual surfaces.
The risk of running into this limit is low, as the maximum amount of entries is 65535. There is no good way to reduce your marksurface usage, except for trying to keep to a simple and clean style of mapping to minimize the number of leaves generated.
Edges / Surfedges
Risk level:
low
Edges are defined by connecting a straight line between two vertices. The surfedges resource is used to store the direction of the edge, which can be either from vertex A to B, or from B to A. Both values are used to help the engine render the faces in the most efficient way.
It’s very unlikely that you’ll run into issues with edges as the limit is 256000 and 512000 entries.
Texdata / lightdata / visdata / entdata
Risk level:
low
These resources are used to store texture, light, visibility and entity data in memory. All limits can be variable, because they do not directly rely on resources within the engine or BSP format but rather on your computer hardware. The default settings should be plenty for any system. In the early days, setting a high lightdata value could cause trouble for video cards with small amounts of video memory but this hasn’t been the case for many years.
Entdata (entity data) in particular stores all entity data in text format. It has its own limit for the parsing of the data, namely 8194 entities total, 32 characters for key names, and 1024 characters for key values. In particular,
worldspawn's "wad" value can exceed the keyvalue length limit, so keep the textures in a short path.
Texdata (texture data) stores information on textures used in the map. Each texture entry has two parts, metadata (name and dimensions) and actual pixel and palette data. The latter part is filled when the textures are
-wadinclude
d or using
-nowadtextures
, otherwise the engine looks for the textures in wad files defined in
worldspawn.
AllocBlock
Risk level:
medium
The Allocation Block is a subdivision of the lightdata, where the lighting data is stored. In the last stage of compilation (RAD), all the lighting is calculated and mapped onto the map’s surfaces. The engine has 64 of these blocks allocated to store all this lightmap data. Each block holds 128x128 lightmap pixels (luxels). Each luxel spans 16 normal texture pixels. You can see the lightmap pixels mapped on top of this 128x128 texture.
Because there are far less luxels than actual pixels, the light and shadows effects in Half-Life can become quite jagged. The lightmap scale is directly connected to the scale of the texture, so you can increase the relative amount of luxels by using a smaller scales of 0.50 or 0.25 on your textures. This, however, will drastically increase the amount of resources you’ll need to store all this light information. Using 256x256 textures on 0.50 scale by default makes the risk of running into the AllocBlock very high once your map starts to become larger. With 512x512 textures on 0.25 scale you will only be able to make small maps. If you generally stick to using the default HL textures on scale 1.00, you are not likely to run into this limit.
A very common mistake that contribute to maxing out AllocBlock limit is resizing brushes in Hammer/JACK with the texture scale lock or UV lock enabled.
Another contribution to lighting data are animated and switchable lights. Each light style, and each named light, adds another lightmap to a face, for a maximum of 4 lightmaps per face. Therefore, even with a simple map, having a lot of animated and switchable lights can easily double AllocBlock usage.
If you need to free up lightmap resources, you can set textures that are far from the players sight to a higher scale, which reduces the amount of luxels used. Using
NULL
to remove unseen faces will also remove lightmap data from that face. The compiler isn’t extremely efficient when it comes to mapping the lightmaps onto the allocated blocks but, as a mapper, there isn’t much you can do to optimize this.
Because the referencing of entries by other entries is done with indices into the lump using mostly 16-bit shorts, it places a hard limit on how many of the target lump's entries can exist before their indices becomes out of bounds for the data types used. In addition, Carmack makes a few of these data types do double (sometimes triple) duty e.g. positive values refer to one type, negative values means entirely different type, and 0 means null, so that halves the capacity of a type's max index. clipnodes suffer the most from this; an entire half (the negative half) of addressable values in 2^16 is used to assign only 2 significant values -1 and -2.
I've incorporated the information into the first section of this page. You can read the table as: