For the past week I’ve been doing a ton of cleaning up and refactoring. I’ve also set up a new build option that cuts build times in half.
In other words, this week’s post is very coding-heavy.
The client/server divide
Doomsday, being based on the DOOM engine, originally used a peer-to-peer networking model. Each player was equal in authority and multiplayer games progressed in synchronous lockstep. A few years into the project, Doomsday switched to a more client/server oriented approach where one of the peers is designated as the host and owns the “official” version of the game world, while the others receive information about the changes that occur in the world.
However, while this behavior has been in place for a number of years, at the source code level the separation is not at all clear. There are separate client and server executables, but most of the server is still using the same source code files as the client, with only preprocessor defines being used to exclude certain subsystems and routines. While it was relatively easy to achieve client/server behavior this way, the code base has suffered in terms of maintainability and clarity.
To address this problem, a few years ago I added a new shared library called “libdoomsday” that is used as the basis for both the server and the client apps. Since then we’ve been slowly breaking old client code apart and moving common functionality to the library.
My main objective last week was to break down the resource management code (that deals with such data as sprites, textures, color palettes, fonts, and MD2 models) into two independent sets of code: one that is specific to the client/GUI, and one that is applicable to both the server and client. In practice, I’ve been focusing on the huge ClientResources
class. I’ve managed to cut its size roughly in half, from 2904 code lines down to 1632. While this is not the most complex case of intermingled code (thanks to previous work by DaniJ), it is a significant step toward to a more elegant and easily workable code base.
Turbo
One approach to a clean, modular code base is to split everything into small and tidy, well-organized source files. In a C++ program, you might for instance put the implementation of each class in its own file. The end result is that you have a large (and growing) number of small files.
Unfortunately, C++ is somewhat notorious for being slow to compile because for each source file, one first needs to process a large number of header files (class and template definitions, including the C++ STL, for example). In a typical case you might process thousands of lines to headers to compile a few hundred lines of actual code of your own.
One way to work around this issue is called precompiled headers, where you take all (or most) of the headers needed by your program and preprocess them. This allows one to reuse the preprocessed data for each source file, cutting down compilation times. The problem with this is that each C++ compiler handles the feature somewhat differently and the benefits are not uniform across all platforms.
I’ve chosen to take advantage of the other common approach, which is merging many source files into a small number of large files, and then compiling the merged sources as usual. However, the crucial detail is to do this using an automated script during the build, leaving the original source files in their elegant, modular form. The end result is that less time is spent on redundant processing. There are some risks with this approach, as the usual file-scope rules no longer apply, but if the code is clean and follows reasonable conventions, the benefits greatly outweigh the risks.
The new build option DENG_ENABLE_TURBO now controls this source file merging during the build. It is enabled by default, and the result is that build times have been reduced by more than a half. For example, the Linux CI builds used to take about 45 minutes. Now they finish in 19 minutes.
Having fast builds is also a terrific boon to daily development, as you can more quickly try out whatever you’re working on and then iterate with more changes.