Metric Panda Games

One pixel at a time.

Reducing The Platform API Surface

Rival Fortress Update #44

This week I started work on the Windows platform layer for Rival Fortress, and while I was at it, I also reduced the number of functions exposed by the API.

The platform layer

As is common in many modern games, Rival Fortress doesn’t interact directly with the operating system, but make calls through a platform API that is a thin abstraction over OS services.

For example, the platform API exposes an AllocateMemory function that returns a chunk of memory, and behind the scenes uses a custom allocator for the OS based on VirtualAlloc on Windows or mmap on Linux/macOS.

The advantage of this approach is that it removes platform implementation details from game code and makes portability much easier: as long as the platform can expose all functions in the API, the game will run.

You may argue that the C/C++ standard library already provides this level of abstraction and portability, but having a tight API boundary gives you the flexibility of using whatever you want behind the scenes, like optimized system calls if standard library functions are not ideal.

Striving for a minimal API

Designing the The platform API is tricky. The number of functions exposed needs to strike a balance between just enough to get the job done efficiently and not too many as to negate the usefulness of the idea.

The platform API for Rival Fortress has gone through many iterations, and in its current state it looks like this:

typedef struct MPEPlatformAPI
{
  // NOTE: Memory
  PFN_AllocateMemory AllocateMemory;
  PFN_DeallocateMemory DeallocateMemory;


  // NOTE: Filesystem
  PFN_GetDirectoryList GetDirectoryList;
  PFN_FileOpen FileOpen;
  PFN_FileClose FileClose;
  PFN_FileRead FileRead;
  PFN_FileWrite FileWrite;

  // NOTE: Threading
  PFN_SemaphorePost SemaphorePost;
  PFN_SemaphoreWait SemaphoreWait;

  // NOTE: I/O
  PFN_PollEvents PollEvents;
  PFN_SubmitGraphicsAndSound SubmitGraphicsAndSound;

} MPEPlatformAPI;

PFN_ is the prefix I use for typedef-ed function pointers.

In debug builds the game code is built as a DLL, so platform API is passed as a struct that is copied to a global object, so that the game code can be unloaded and reloaded as I explain in the Hot Swappable Game Modules post.

In release builds the game and platform are built as a single executable, so function pointers are not needed and the API functions are called directly.

In code, platform calls go through the MPE_PLATFORM macro, that conditionally calls the function pointers or the functions directly like so:

#if MPE_BUILD_STANDALONE_EXE
#define MPE_PLATFORM(Name, ...) MPE_Platform##Name(__VA_ARGS__)
#else
static MPEPlatformAPI GlobalPlatformAPI;
#define MPE_PLATFORM(Name, ...) GlobalPlatformAPI.Name(__VA_ARGS__)
#endif