I’ve approached this ‘remake’ of Quake with two main goals:
- Understand the original code and their approach to solving game engine related challenges
- Use it as an opportunity to spot the ways in which modern C++ can be used to make code efficient, safe, and concise
The original quake is very much a product of C-style coding in that, at best, it is very efficient, and at worst, it has dangerous coding practices. For example, it uses static allocation for most strings, and uses fixed size buffers for dynamic ones, keeping string operations speedy. On the other hand, it also relies heavily on passing around
char * pointers without ever indicating memory ownership and requiring every string be null terminated lest functions read out of bounds. And of course, what happens when the data exceeds the size of the buffer? Pointers to lists without proper lengths are the infamous cause of many modern bugs/security flaws/etc. (see Heartbleed), so it’s interesting to spot them in the Quake source. Take lines 677-695 of
MSG_ReadFloat fails to check bounds on
net_message.data, so it could quite happily read out of bounds. This function is usually used to read time values, so improper calls could easily throw these values out of whack, or simply crash the game, leading to some cryptic bugs.
The C++ world tried to address the
char * problem using the
std::string class, which keeps track of its length and ensures memory is cleaned up accordingly. Unfortunately, the implementation (in this case Microsoft’s 32-bit version of the STL) is pretty quirky unless you dig into how it works. The short version is that
std::string maintains a fixed 16 byte buffer (15 characters + 1 null) for short string optimization. Any operations for a string less than or equal to that size happen internally. Once the string grows beyond that, the buffer is instead used as a pointer to memory from the free store (or the associated allocator). For games, where every byte and allocation needs to be accounted for, and especially for consoles without virtual memory systems, this behind-the-scenes behaviour is probably one of the motivations for avoiding STL. With that said, I’m still trying to understand the animosity towards STL, other than that enough bad things were said about it until the point that game developers wrote it off. STL definitely isn’t perfect, but most times it’s more efficient than the hand-coded alternative. But I digress.
My approach in ‘remaking’ Quake has been to replace the C-style idioms with C++ ones (
char * ->
std::string, iterators, const-correctness, RAII where possible), although I’ve discovered that it isn’t always easy. One difficulty has been dealing with the static allocations since many STL containers insist on dynamically allocating their memory at runtime. Recent C++ features like
constexpr allow for compile-time wrappers such as the
conststr class shown in the example here, which helps mitigate some of this. Overall, I’m trying to keep the original API intact while cleaning up the implementations.
The first chunk of code I went through was the system functions (specifically Windows ones). Quake uses prefix-based namespacing to group functionality, although it gets a bit confusing when some of the function definitions are spread through files with different names. The entry point for Quake is broken down into separate compilation units used based on the system it is being compiled for.
sys_win.c is used for Windows,
sys_linux.c for Linux,
sys_dos.c for DOS, and so-forth. They all conform to a standard interface defined in
sys.h which contains function declarations. These are mainly wrappers around system functions for reading files, printing messages, etc. They’re pretty much what you would expect - create a window, parse the command line, run the (variable) timing loop. There’s some playing with the CPU’s floating point precision for, what I assume is, a speed boost. Like most files in Quake, it makes heavy use of global variables, much to the chagrin of most modern developers. My biggest contribution was switching an integer based file handle system to a
File_handle object that lets me query the handle’s validity without relying on
file_handle < 0 as it’s only check.