[Tutorial] Compiler charts and limits Last edited 11 months ago2024-01-09 15:56:27 UTC

You are viewing an older revision of this wiki page. The current revision may be more detailed and up-to-date. Click here to see the current revision of this page.
This article tries to shed some light on the inner workings of the Half-Life compile tools. When you compile your map it actually goes through four stages or compilation: 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%)
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 npc’s 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.
User posted image
The only exception is func_detail. func_detail technically isn’t an entity but rather a trick that the compiler uses to stop world geometry from cutting into each other.

Even if you have no solid brush entities, the compiler will always use one slot for the information stored in worldspawn. This is where the map stores values such as which sky to use or the viewable distance.
User posted image
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. 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 512 brush entities, 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.

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.
User posted image
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.
User posted image
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 compiler uses 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.
User posted image
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 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’. If you want to know more, please refer to chapter 3 of this tutorial.

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 using to detect collision in Half-Life. 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 chapter 6.2 of this tutorial.

Leaves

*Risk level: high*

As mentioned in the ‘nodes’ explanation, BSP cuts the world into segments and creates a tree.
User posted image
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.
User posted image
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.
User posted image
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: [font color=green]low[/green]

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.

Edges / Surfedges

Risk level: [font color=green]low[/green]

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: [font color=green]low[/green]

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.

AllocBlock

Risk level: medium

The Allocation Blocks is where are 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. Each lightmap pixel spans 16 normal texture pixels. You can see the lightmap pixels mapped on top of this 128x128 texture.
User posted image
Because there are far less light pixels 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 light pixels 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.

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 lightmap pixels 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.

1 Comment

Commented 11 months ago2024-01-09 22:56:28 UTC Comment #105855
I wrote a short journal entry on how all 15 lumps relate to each other. I used the relational model to visualize it.

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:
The resource A has a limit of X because it is referenced by JKL with data type T, using [Remark]
The resource clipnodes has a limit of 32768 because it is referenced by other clipnodes with data type short, using positive values > 0

You must log in to post a comment. You can login or register a new account.