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 a professional actor, programmer, and artist, with a bit of dabbling in music. || [Senior Core Tech Engineer]/[Motion Capture] at Deep Silver Dambuster Studios on: * Homefront: The Revolution * UNANNOUNCED || [Senior Graphics Programmer]/[Motion Capture] at Square Enix on: * Luminous Studio engine * Final Fantasy XV || [R&D Programmer] at tri-Ace on: * Phantasy Star Nova * Star Ocean: Integrity and Faithlessness * Silent Scope: Bone Eater * Danball Senki W || [Programmer] on: * Leisure Suit Larry: Beach Volley * Ghost Recon 2 Online * HOT PXL * 187 Ride or Die * Ready Steady Cook * Tennis Elbow || L. Spiro is currently a GPU performance engineer at Apple Inc. || Hyper-realism (pencil & paper): https://www.deviantart.com/l-spiro/gallery/4844241/Realism || Music (live-played classical piano, remixes, and original compositions): https://soundcloud.com/l-spiro/

12 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

  4. Irlan
    September 17, 2014 at 11:28 PM #

    I actually took the same way of letting the graphics be a static class for two reasons:

    Doing a Game is about translating low-level code into high-level;

    Doing a Game is about translating low-level code into high-level;

    • Irlan
      September 17, 2014 at 11:42 PM #

      Correction:

      *I actually took the same way of letting the graphics be a collection of static methods for two reasons:

  5. MattMatt
    August 9, 2016 at 6:06 PM #

    Hi L. Spriro; have you considered using the CRTP (curiously recurring template pattern) to create a base class, so virtual functions have no cost at all (especially on Ps4/WiiU since you’re planning to support those platforms); Although it would increase compile time, it would perform better :)

    • L. Spiro
      August 9, 2016 at 6:33 PM #

      That would be a way to avoid virtual functions, but it requires more effort to achieve the same result as what I am getting now (as I also do not have virtual functions). CRTP still requires separate classes to inherit from a base as I do now, but requires extra specialization as well as manually casting all function calls to that type of object.

      • MattMatt
        August 9, 2016 at 8:55 PM #

        Yes; but it can still be useful for simple systems like supporting different window frameworks; In my engine for instance, I have something like a base class IDisplay and other classes like CWin32Diplsay : public IDisplay, etc…
        BTW how do you avoid virtual functions ? If you use inheritance, your dtor must be virtual (and other api interface methods, considering CTexture defines all those) ?

        • L. Spiro
          August 9, 2016 at 9:45 PM #

          I use CRTP in my vector classes and a few other similar utility classes. For these types of macro-level classes it isn’t so suitable (though you may find it perfectly adequate).

          Inheritance does not mean virtual destructors. The constructors of my COpenGlTexture2d and CBaseTexture2d classes are private, and you are only able to create and destroy CTexture2d objects. There is never an ambiguity that needs to be resolved with virtual destructors (you can’t construct a CTexture2d * but delete a CBaseTexture2d *) and there are no virtual functions on the implementations.

          After re-reading my article, I can see why you think I actually do have virtuals, because I did say that the base class could define any if needed, but in practice you are likely not to have to define any.
          If you do, it wouldn’t be because one texture could be COpenGlTexture2d and another could be CDirectx12Texture2d; this could never happen at run-time. CRTP might be able to handle that case, but the case for virtuals in textures would be run-time differences that only virtuals could resolve, such as activating a texture in a slot or setting it as a render target. Even these cases don’t need virtuals under most designs.

          • MattMatt
            August 9, 2016 at 10:16 PM #

            Ok, I understand now; thx for the advice :)

Leave a Comment

Remember to play nicely folks, nobody likes a troll.