📜 ⬆️ ⬇️

Quake III source code

image

[Translator's note: the translation of the first part of this article is already on Habré, but for some reason its author has not completed the work.]

Quake III Renderer


The Quake III renderer was an evolutionary development of the hardware-accelerated Quake II renderer: the classic part is built on the “binary split” / “potentially visible set” architecture, but two new notable key aspects are added:
')

Architecture


The renderer.lib completely contained in renderer.lib and is statically linked to quake3.exe :



The overall architecture repeats Quake Classic: it uses the famous combination of BSP / PVS / lighting maps:


The stage of multitexturing and lighting maps is clearly visible if you change the slider and display only one or the other:

Texture drawn by level designer / artists:



Light map generated by q3light.exe :



The final result when connecting using multitexturing at runtime:



The rendering architecture was reviewed by Brian Hook at the Game Developer Conference in 1999. Unfortunately, video from GDC Vault is no longer available! [But it is on youtube .]

Shaders


The shader system is built on top of the fixed OpenGL 1.X pipeline, and therefore is very expensive. Developers can program vertex modifications, but also add texture passes. This is covered in detail in the Quake 3 Shader bible bible shaders:


Multicore renderer and SMP (symmetric multiprocessing)


Many do not know that Quake III Arena was released with SMP support using cvariable r_smp . Frontend and backend exchange information through the standard Producer-Consumer scheme. When r_smp is set to 1, the surfaces being drawn are alternately stored in a double buffer located in RAM. The frontend (which in this example is called Main thread ) alternately writes to one of the buffers, while the other reads the backend (in this example, it is called the Renderer thread ).

An example demonstrates how everything works:

t0-t1:



t1-t2: processes start everywhere:

Notice that at t2:

This case (when the Renderer thread blocks the Main thread) often occurs when playing Quake III:
Let us demonstrate limiting the blocking of one of the OpenGL API methods.



After t2:



Note: Synchronization is done through the Windows Event Objects in winglimp.c (the part with SMP acceleration below).

Network model


The Quake3 network model is, without a doubt, the most elegant part of the engine. At a low level, Quake III still abstracts data exchange with the NetChannel module that first appeared in Quake World . The most important thing to understand is:

In environments with a fast rhythm of change, any information not received during the first transmission is not worth re-sending, because it will still be outdated.

Therefore, as a result, the engine uses UDP / IP: there are no TCP / IP traces in the code, because “reliable transmission” creates an unacceptable delay. The network stack has been enhanced by two mutually exclusive layers:



But the most amazing design is on the server side, where the elegant system minimizes the size of each UDP datagram and compensates for the unreliability of UDP: the snapshot history generates delta parquets using memory introspection.

Architecture


The client side of the network model is quite simple: the client sends commands to the server every frame and receives game state updates. The server side is a bit more complicated, because it must transfer the general state of the game to each client, taking into account the lost UDP packets. This mechanism contains three main elements:




When the server decides to send an update to the client, it uses all three elements in order to generate a message, which is then transmitted through NetChannel.

An interesting fact: storing such a number of game states for each player takes up a large amount of memory: in my measurements, 8 MB for four players.

Snapshot system


To understand the system of snapshots, I will give an example with the following conditions:

Frame 1 server:

The server received several updates from each client. They influenced the overall state of the game (green). It is time to transfer the status to Client1 client:



To generate a message, the network module ALWAYS does the following:
  1. Copies the general state of the game in the next slot of the client's history.
  2. Compares it to another snapshot.

This is what we see in the next image.

  1. The overall game state (Master gamestate) is copied with index 0 into Client1 history: it is now called “Snapshot1”.
  2. Since this is the first update in Client1 history of correct snapshots, therefore, the engine uses an empty “Dummy snapshot” snapshot, in which all fields are set to zero. This results in a FULL update because each field is sent to NetChannel.



The most important thing to understand here is that if there are no valid snapshots in the client's history, the engine takes an empty snapshot to generate a delta message. This results in a full update sent to the client in 132 bits (each field is preceded by a bit marker ): [1 A_on32bits 1 B_on32bits 1 B_on32bits 1 C_on32bits] .

Frame 2 servers:
Now let's move a little bit to the future: here is the second frame of the server. As we can see, each client sent commands, and all of them influenced the overall state of the game Master gamestate: Client2 moved along the Y axis, so now pos [1] is equal to E (blue). Client1 also sent commands, but, more importantly, it acknowledged receipt of the previous update, so Snapshot1 was marked as confirmed (“ACK”):



The process is the same:

  1. The overall state of the game is copied to the following client history slot: (index 1): this is a Snapshot2
  2. This time we have the right snapshot in our client history (snapshot1). Compare these two snapshots

As a result, only a partial update is sent over the network (pos [1] = E). This is the beauty of this design: the process is always the same.



Note: since each field is preceded by a bit marker (1 = changed, 0 = did not change), 36 bits are used for the partial update from the example above: [0 1 32bitsNewValue 0 0] .

Frame 3 servers:
Let's take another step forward to see how the system deals with lost packages. Now we are in frame 3. Clients continue to send commands to the server.
Client2 suffered damage and health is now equal to H. But Client1 did not confirm the last update. It may be that the UDP server is lost, the client ACK may be lost, but as a result it cannot be used.



Despite this, the process remains the same:
  1. We copy the general state of the game into the following client history slot: (index 2): this is a Snapshot3
  2. Compare the last valid confirmed snapshot (snapshot1).



As a result, the message sends it partially and contains a combination of old and new changes: (pos [1] = E and health = H). Note that snapshot1 may be too outdated to use. In this case, the engine again uses "empty snapshot", which leads to a complete update.

The beauty and elegance of the system is in its simplicity. One algorithm automatically:


Introspection Memory on C


You may be wondering how Quake3 compares introspection snapshots ... because in C it doesn't exist.

The answer is the following: each field location for netField_t is pre-created using an array and smart preprocessing directives:

  typedef struct { char *name; int offset; int bits; } netField_t; //        ... #define NETF(x) #x,(int)&((entityState_t*)0)->x netField_t entityStateFields[] = { { NETF(pos.trTime), 32 }, { NETF(pos.trBase[0]), 0 }, { NETF(pos.trBase[1]), 0 }, ... } 

The complete code for this part is in MSG_WriteDeltaEntity from snapshot.c . Quake3 doesn't even know what it compares: it blindly uses the index, the offset and the size of the entityStateFields and sends differences across the network.

Pre-fragmentation


Having gone deep into the code, you can see that the NetChannel module cuts messages into blocks of 1400 bytes ( Netchan_Transmit ), even though the maximum size of the UDP datagram is 65,507 bytes. So the engine avoids packet breaking by routers when transmitting over the Internet, because most networks have a maximum packet size (MTU) of 1500 bytes. Getting rid of fragmentation in routers is very important because:


Messages with reliable and unreliable transmission


Although the snapshot system compensates for UDP datagrams lost in the network, some messages and commands must be delivered (for example, when a player leaves the game or when the server needs the client to download a new level).

Such binding is abstracted by the NetChannel module: I wrote about this in a previous post .

Recommended reading


One of the developers, Brian Hook, wrote a short article on the network model .

By Unlagged Neil "haste" Toronto, Neil "haste" Toronto also described it .

Virtual machine


If the previous engines gave the virtual machine only gameplay, then idtech3 entrusts it with much more important tasks. Among other things:


Moreover, its design is much more complex: it combines the protection / portability of the Quake1 virtual machine with the high performance of Quake2 native DLLs. This is achieved by compiling on-the-fly bytecode to x86 commands.

An interesting fact: the virtual machine was originally supposed to be a simple bytecode interpreter, but the performance was very low. Therefore, the development team wrote a runtime x86 compiler. According to the .plan file of August 16, 1999, this task was accomplished in one day.

Architecture


The Quake III virtual machine is called QVM. Its three parts are constantly loaded:




QVM insides


Before we start using QVM, let's check how the bytecode is generated. As usual, I prefer to explain with illustrations and a short accompanying text:



quake3.exe and its bytecode interpreter are generated using Visual Studio, but the bytecode VM uses a completely different approach:

  1. Each .c file (translation module) is compiled separately using LCC.
  2. LCC is used with a special parameter, due to which the output is not carried out to PE (Windows Portable Executable), but to an intermediate representation, which is a text assembly of a stack machine. Each file created consists of text , data and bss with the export and import of characters.
  3. The id Software special tool called q3asm.exe gets all the text assembly files and compiles them together into a .qvm file. In addition, it converts all information from text to binary (for speed, in case it is impossible to apply native converted files). Also, q3asm.exe recognizes methods called by the system.
  4. After downloading the binary bytecode, quake3.exe converts it to x86 commands (not necessarily required).

LCC internals


Here is a specific example starting with the function that we need to run in the virtual machine:

  extern int variableA; int variableB; int variableC=0; int fooFunction(char* string){ return variableA + strlen(string); } 

The module.c lcc.exe in the translation module.c lcc.exe called with a special flag to avoid generating a Windows PE object and perform output to an intermediate representation. This is the output file .obj LCC, corresponding to the above C function:

  data export variableC align 4 LABELV variableC byte 4 0 export fooFunction code proc fooFunction 4 4 ADDRFP4 0 INDIRP4 ARGP4 ADDRLP4 0 ADDRGP4 strlen CALLI4 ASGNI4 ARGP4 variableA INDIRI4 ADDRLP4 0 INDIRI4 ADDI4 RETI4 LABELV $1 endproc fooFunction 4 4 import strlen bss export variableB align 4 LABELV variableB skip 4 import variableA 

A few notes:


Such a text file is generated for each .c file in the VM module.

Internals q3asm.exe


q3asm.exe gets the text files of the LCC intermediate view and assembles them together into a .qvm file:



Here you can see the following:


QVM: how it works


Again, a drawing showing a unique entry point and a unique exit point that dispatch:



Some details:

Messages (Quake3 -> VM) are sent to the virtual machine as follows:


The list of messages sent by the client VM and server VM is presented at the end of each file.

System calls (VM -> Quake3) are performed as follows:


The list of system calls provided by the client VM and server VM is at the beginning of each file.

Interesting fact: parameters are always very simple types, they are either primitive (char, int, float), or are pointers to primitive types (char *, int []). I suspect that this has been done to minimize the problems of struct communication between Visual Studio and LCC.

An interesting fact: Quake3 VM does not perform a dynamic connection, so the developer of the QVM mod did not have access to any libraries, even the standard C library (strlen, memset is here, but in fact are system calls). Some managed to emulate them with a predefined buffer: Malloc in QVM .

Unprecedented freedom


Thanks to the transfer of functions to a virtual machine, the modder community has gained much more opportunities. In Nela Toronto's Unlagged , the prediction system was rewritten using "reverse agreement ".

Performance problem and its solution


Because of such a long toolchain, VM code development was difficult:


Therefore, idTech3 also had the ability to load native DLLs for VM parts, and this solved all the problems:



In general, the VM system was very flexible because the virtual machine has the ability to execute:


Recommended reading








Artificial Intelligence


The modders community has written bots for all previous idTech engines. At one time, two systems were quite famous:


But for idTech3, the bots system was a fundamental part of the gameplay, so it needed to be developed within the company and it had to be present in the game initially. But serious problems arose during the development:

Source : page 275 of the book “Masters of Doom”:

— . — , . , . Quake III, , . .

, . , , , . .

, , . , , . . 1999 , .

Architecture


As a result, Jean-Paul van Waverin (Mr.Elusive) worked on the bots, and it's funny, because he wrote Omicron and Gladiator. This explains why part of the server bots code is highlighted in a separate project bot.lib:



I could write about this, but Jean-Paul van Waveren himself wrote a
103-page work with a detailed explanation. Moreover, Alex J. Champandard created an overview of the bot system code , which describes the location of each module mentioned in the work of van Waverin. These two documents are sufficient for understanding Quake3 AI.

Source: https://habr.com/ru/post/330818/


All Articles