Handmade Penguin Chapter 11: Making Graphics Portable

This chapter covers roughly the content in the The Basics of Platform API Design part of the Handmade Hero course, under the Linux operating system.

<-- Chapter 10 | Back to Index | Chapter 12 -->

Be warned! This chapter is not yet finished. Maybe new stuff will appear if you keep hitting refresh!

Platform Layers and Portability

This is the episode you've all been waiting for: we're going to be reorganising and designing our code so that we can write code without caring which platform it runs on. This means we want to separate our platform-specific code from our cross-platform code. This is why we have files like win32_handmade.cpp and sdl_handmade.cpp: they'll store our Windows and SDL specific code respectively.

What we ultimately need to do is have different versions of the platform-specific code. One technique (which is best used sparingly) is to use platform specific #defines. For example, you would have code like:

#ifdef HANDMADE_WIN32
DoWin32Thing();
#elif HANDMADE_SDL
DoSDLThing();
#else
#error Don't have code for this platform!
#endif
When we're compiling for Windows, the code within HANDMADE_WIN32 will be compiled. When we're compiling for an SDL platform, the code within the HANDMADE_SDL block will be compiled. To set this up, we need to modify our build.sh file to define HANDMADE_SDL. We do this with the -D option for gcc, which works the same way as Visual Studio's:
c++ -DHANDMADE_SDL=1 ...

This technique has some issues, however. Notably, we end up with a lot of these #ifdef blocks around our code: this makes it difficult to read (there's a lot of code that won't be compiled in any situation). It also mixes the code for different platforms up: a single file will have cross-platform code, Windows code, SDL code and more. It can be difficult to find all of the platform code. Finally, it means that the specific code for all platforms need to have similar control flow: which can restrict us needlessly. We'll be keeping it to a minimum.

A better way of handling things is just to keep all of our platform-specific code in their own files, and have cross-platform code separate. What we're trying to do is have a boundary between the platform-specific code and our cross-platform code. To start, we'll want to create a file for our platform-independent code, handmade.cpp. We'll also need to define some sort of interface layer between our platform-specific and playform-independent code: this is basically a set of functions (and structures, etc) that are implemented by either the game (and called by the platform layer) or by each platform layer (and called by the game). This will be placed in the handmade.h file. We'll #include both of these in our sdl_handmade.cpp file.

Be warned! Compiling multiple .cpp files by including them in the main file is called a unity build and is quite an uncommon technique. Unity builds simplify the build system significantly, though they can be slower on very large or complex codebases.

There are a couple of different ways we can think of our platform layer, and these all ultimately boil down to having a different interface for them to implement. One way would be to define an interface where the platform layer provides functions to do all of the required platform-specific things, like create windows and initialise sound devices. This is, in fact, basically what SDL is: a platform layer that totally abstracts the operating system.

Another technique, and the one we'll be using, is rather than abstracting out the operating system, to abstract out the game. The game will provide functions for updating and rendering, and the platform-specific layer will call those when needed. This gives the platform-specific code more flexibility, at the cost of making the game less flexible. Which solution is better depends on the sort of game you're making (and it's much more of a sliding scale than a binary decision) — if you're going to need multiple windows, then obviously a function to create a window will need to be provided by the platform layer.

Cross-platform Rendering

Now that we know how we're going to be making our code cross-platform, let's move our rendering code from the platform-specific sdl_handmade.cpp to the handmade.cpp platform-independent file. It turns out, our RenderWeirdGradient() function only has one dependency on SDL: the sdl_offscreen_buffer struct passed in. We'll need to replace that with something platform-independent. This means that, as the interface must be the same for different platforms, we'll need to replace it exactly with the game_offscreen_buffer struct from the Windows version:

struct game_offscreen_buffer
{
    void *Memory;
    int Width;
    int Height;
    int Pitch;
};
Similarly, we'll need to replicate the GameUpdateAndRender() function exactly. This is the function we'll call from our SDL platform layer:
internal void GameUpdateAndRender(game_offscreen_buffer *Buffer, int BlueOffset, int GreenOffset);

Be warned! Having our functions be internal (which, if you remember, is our aliass for static) would not work if we were not using a unity build. In a traditional build, these functions would not be accessible outside of the file in which they were defined.

The final thing we need to do to our platform layer is to call the GameUpdateAndRender() function. This is pretty straightforward, the only trick being converting our sdl_offscreen_buffer to a game_offscreen_buffer:

game_offscreen_buffer Buffer = {};
Buffer.Memory = GlobalBackbuffer.Memory;
Buffer.Width = GlobalBackbuffer.Width; 
Buffer.Height = GlobalBackbuffer.Height;
Buffer.Pitch = GlobalBackbuffer.Pitch; 
GameUpdateAndRender(&Buffer, XOffset, YOffset);

We then just need to move RenderWeirdGradient() from sdl_handmade.cpp to handmade.cpp and call it from our GameUpdateAndRender() function. Our rendering code is then cross-platform! If we apply the same changes to sdl_handmade_queueaudio.cpp, we can see our work in action. If we want to change the rendering code, we only have to change it once to have it work across all platforms!

End of Lesson!

Did anyone notice we didn't actually have to write any platform-specific code today? This tells us that we're getting really close to having a cross-platform game. Tune in tomorrow as we make our sound code cross-platform!

If you've bought Handmade Hero, the source for the Linux version can be downloaded here. Note that you will require the official source code for handmade.h and handmade.cpp.


<-- Chapter 10 | Back to Index | Chapter 12 -->