Support for multiple platforms is a huge selling point for any game engine, and I consider this necessary for my project. This article is about how to handle support for many platforms without creating spaghetti code. L. Spiro Engine supports DirectX 9, DirectX 10, DirectX 11, OpenGL, and will support, OpenGL ES, Nintendo Wii U, PlayStation 3, and more. There are also changes to code for supporting Macintosh OS X and Linux, on top of the graphics API support. With all these targets in mind, it is clear that we need a good solution for organizing our code.

The Naive Approach

Most coders are tempted to take the straightforward approach of making small #ifdef blocks at the locations where the code needs to change to support one API or another. For example:

This quickly becomes spaghetti as more and more platforms are supported. Not only is it hard to see which code is active amongst the API code, but the non-API code also becomes obscure and hard to follow. On a project as large as a next-generation game engine, code organization is a serious issue, and this is simply not acceptable.

My Approach

For objects such as textures that need to have various parts changed to accommodate multiple API’s, my solution is to make a common base class, then one class for each API, all of which inherit from the base class, and finally the top-most class which inherits from one of the API classes depending on which API is active. For example, on the bottom I have CTextureBase, in the middle I have COpenGlStandardTexture, CDirectX9StandardTexture, CDirectX10StandardTexture, CDirectX11StandardTexture, etc., and then on top I have CStandardTexture, which represents a standard diffuse texture.

In this way the only #ifdef’ing I do in the CStandardTexture class is at the top of the file and at the bottom:

This keeps all of the code in all of the classes clean and free of #ifdef’s.

Next, each of the API classes have a specific set of functions that they are required to define (even if they might be empty for one API and not another). For example, all of the API texture classes have a function called CreateApiTexture(). The job of this function is to register the texture data with the graphics API. Each API class has a very clear job (to manage the interface with the respective graphics API) and are completely free of #ifdef’s, with the exception of one #ifdef at the top of the file.

Since all of the API classes will define this function, there is no need to clutter the code with #ifdef’s here (or anywhere else in the class).

The job of the base class, CTextureBase, is to hold the data that will be used by all of the upper classes and to define any virtual functions that may be needed. In my implementation, it holds the width, height, mip-mapping request, format, etc., but not the actual texel data. This is because cubemap textures will inherit from this base class also but they do not have texel data. They instead have an array of 6 other texture objects which themselves manage the texel data for cubemaps.

My base class also defines an Activate() function which is virtually overloaded by upper classes to provide a method for putting the texture into a texture slot (or unit in OpenGL terms).

Closing

This method may be a little more work than the straightforward way, but makes up for itself ten times over in the organization it offers. Making progress and tracking down API bugs is much simpler because my API-specific code is as easy to read as any other code. It is easy to update and updates are guaranteed not to interfere with other API code. Organization is vital to the success of any decent-sized project.

7 Awesome Comments So Far

Does it mean that you will have a different executable for direct x 9 and direct x 10? If yes, you would probably need to go deeper in abstraction with factories or dependency injections (not sure for the last one that it can be applied in c++ world).

Yes, each supported target has its own executable. In these cases, there is no run-time change in functionality; we are simply swapping out a mid-level class for one build or another. We also operate under the premise that the base classes will never be instantiated, and we enforce this with protected constructors.

This means the interfaces and usages of each class are well defined, so abstraction can be kept minimal as long as each API class implements the same API. Factories and dependency injections are not needed.

How do you handle passing around the D3D device/device context to use with the Activate() method? With OpenGL it’s easier to implement as the context is global, but in D3D there’s the explicit context object – do you store the device pointer with each texture object to use with Activate(), or do you instead forward the call to some other object that’s responsible for handling the D3D context?

To make compatibility between OpenGL and DirectX *, the devices and contexts are global. CDirectX9::GetDirectX9Device() and CDirectX9::GetDirectX9Object(). In OpenGL, the context is not exposed directly. COpenGl::MakeCurrent() is available, but it is only called once at start-up and once at shut-down.

In general, globals—especially singletons—are bad organization, and you can avoid their use entirely even in a full game engine. From an organizational standpoint, the fact that I have globals does irk me a bit, but in practice it doesn’t have the normal problems that globals usually have for a few reasons:
#1: Globals usually promote inappropriate dependencies, since any file can just include them and go, but this case is less prone to that pitfall because the reason for accessing these globals is very well defined: You need access to the DirectX device/object. Objects that need access to the DirectX device/object are also fairly well defined, so this isn’t one of those cases where the game class is global and it contains a pointer to the active sound manager and as a shortcut to getting the sound manager you include the game class instead.
#2: These functions are not singletons and they are not hiding any implementation details. Singletons are evil for a number of reasons, one of which is that they obfuscate what happens under the hood. These device and object are created during start-up and destroyed during shut-down. They have a well defined lifespan, and these static functions only provide read-only access to them.

You certainly can make a non-global implementation for this, but I have done so many years in the past and got quite tired of passing down devices etc., especially when there was nothing to pass down on the OpenGL side. For symmetry, and a little more productivity, I elected to make an exception here and go global, but at the same time making myself a promise not to let it get out of hand and turn into a mess of unrelated dependencies.