The Blog

Organization for Multi-platform Support

Overview

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:

	LSG_INLINE LSGUINT32 LSG_CALL CTexture::ConvertFormatToPixelFormat( LSG_TEXTURE_FORMAT _tfFormat ) {
#ifdef LSG_USINGOPENGL
		static LSGUINT32 ui32Types[] = {
			GL_RGB,
			GL_RGB,
			GL_RGBA,
			GL_RGBA,
#ifndef LSG_ES
			GL_BGR,
#else
			GL_RGB,			// Mode for ES.
#endif	// #ifndef LSG_ES
			GL_RGBA,
			GL_ALPHA,
		};
#elif defined( LSG_USINGDIRECTX )
		static LSGUINT32 ui32Types[] = {
			DX_FMT_R3G3B2,
			DX_FMT_R5G6B5,
			DX_FMT_A4R4G4B4,
			DX_FMT_A1R5G5B5,
			DX_FMT_R8G8B8,
			DX_FMT_A8R8G8B8,
			DX_FMT_A8,
		};
#endif	// #ifdef LSG_USINGOPENGL
		if ( _tfFormat >= sizeof( ui32Types ) / sizeof( ui32Types[0] ) ) { return 0; }
		return ui32Types[_tfFormat];
	}

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 COpenGlStandardTextureCDirectX9StandardTextureCDirectX10StandardTextureCDirectX11StandardTexture, 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:

	class CStandardTexture : public
#if defined( LSG_DX9 )
		CDirectX9StandardTexture
#elif defined( LSG_OGL )
		COpenGlStandardTexture
#endif	// #if defined( LSG_DX9 )


	private :
#if defined( LSG_DX9 )
		typedef CDirectX9StandardTexture		Parent;
#elif defined( LSG_OGL )
		typedef COpenGlStandardTexture			Parent;
#endif	// #if defined( LSG_DX9 )
	};

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.

#ifdef LSG_OGL

namespace lsg {

	/**
	 * Class COpenGlStandardTexture
	 * \brief A standard OpenGL texture.
	 *
	 * Description: A standard OpenGL texture.
	 */
	class COpenGlStandardTexture : public CTextureBase {
…
	protected :
		// == Functions.
		/**
		 * Create an OpenGL texture and fill it with our texel data.  Mipmaps are generated if necessary.
		 *
		 * \return Returns tue if the creation and filling of the texture succeeds.  False indicates a resource error.
		 */
		LSBOOL LSE_CALL							CreateApiTexture();
	private :
		typedef CTextureBase						Parent;
	};

}	// namespace lsg

#endif	// #ifdef LSG_OGL

The CStandardTexture class acts as the controller for the API classes.  It handles all of the common texture operations and then calls CreateApiTexture() when ready:

	/**
	 * Send the graphics data to the hardware.  If the texture is set-only it cannot be modified after calling this.
	 *
	 * \return Returns true if the upload to the graphics hardware succeeded.
	 */
	LSBOOL LSE_CALL CStandardTexture::Finalize() {
		if ( --m_ui32Lock == 0UL ) {
			m_bFinalized = true;
			return CreateApiTexture();
		}
		return true;
	}

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.

 

 

L. Spiro

About L. Spiro

L. Spiro is veteran of the gaming industry and currently makes video games in Tokyo, Japan, as an R&D programmer for tri-Ace (http://research.tri-ace.com/). L. Spiro has worked on Ghost Recon 2 Online, 187 Ride or Die, Catz 5, Dogz 5, Imagine Happy Cooking, Ready Steady Cook, Leisure Suit Larry Beach Volley, HOT PIXEL, After Dark: Flying Toaster and more, for Ubisoft, Atari, Lucas Arts, Eidos Entertainment, Vivendi Universal, Konami, and more.

5 Awesome Comments So Far

Don't be a stranger, join the discussion by leaving your own comment
  1. kemicza
    July 31, 2011 at 12:45 AM #

    Nice post, thanks.

  2. Aurelien
    August 21, 2011 at 7:51 PM #

    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).

    • L. Spiro
      August 21, 2011 at 8:17 PM #

      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.

  3. Crank
    May 13, 2012 at 1:48 PM #

    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?

    • L. Spiro
      May 13, 2012 at 2:12 PM #

      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.

      L. Spiro

Leave a Comment

Remember to play nicely folks, nobody likes a troll.