Jump to content
  • Advertisement
Cryonic

C++ Abstracting Platform differences (OpenGL/Vulkan)

Recommended Posts

Hello GameDev community,

currently I am trying to implement an abstraction layer (using no virtual functions, no PIMPL)
for OpenGL/DirectX and have a problem.

Let's say I want to abstract the Renderer Initialization for OpenGL/Vulkan, my current structure is:

  • renderer.h/.cc - Graphics API Abstraction layer (contains platform independent code)
     
  • win32_renderer_opengl.h/.cc - Win32 implementation of OpenGL 
  • win32_renderer_vulkan.h/.cc - Win32 implementation of Vulkan
     
  • linux_renderer_opengl.h/.cc - Linux implementation of OpenGL
  • linux_renderer_vulkan.h/.cc - Linux implementation of Vulkan

renderer.h

enum RendererType
{
	RENDERER_TYPE_OPENGL,
	RENDERER_TYPE_VULKAN
};

struct PlatformRenderer
{
	RendererType renderer_type;
};

Now assume I have methods to create a Win32/Linux OpenGL/Vulkan Renderer:

PlatformRenderer *Win32_InitOpenGL(); - in win32_renderer_opengl.h/.cc
PlatformRenderer *Linux_InitVulkan(); - in linux_renderer_vulkan.h/.cc

The problem is how would I combine them to make one function to initialize an Renderer in regards to the platform & graphics API without exposing the platform specific initialization functions?

One solution I have would be:

  • Add a generic 'PlatformRenderer *CreateRenderer(RendererType renderer_type)' to 'renderer.h'
  • Create 'win32_renderer.h/.cc' & 'linux_renderer.h/.cc' and patch 'PlatformRenderer *CreateRenderer(RendererType renderer_type)' at compile-time based on if WIN32/LINUX macro is defined.

So my 'win32_renderer.cc' would contain:

win32_renderer.cc

PlatformRenderer *CreateRenderer(RendererType renderer_type)
{
	PlatformRenderer *platform_renderer = nullptr;
	switch(renderer_type)
	{
		case RENDERER_TYPE_OPENGL:
		{
			platform_renderer = Win32_InitOpenGL();
			break;
		}
		case RENDERER_TYPE_VULKAN:
		{
			platform_renderer = Win32_InitVulkan();
			break;
		}
	}

	return platform_renderer;
}

And the equivalent for Linux 'linux_renderer.cc'

linux_renderer.cc

PlatformRenderer *CreateRenderer(RendererType renderer_type)
{
	PlatformRenderer *platform_renderer = nullptr;
	switch(renderer_type)
	{
		case RENDERER_TYPE_OPENGL:
		{
			platform_renderer = Linux_InitOpenGL();
			break;
		}
		case RENDERER_TYPE_VULKAN:
		{
			platform_renderer = Linux_InitVulkan();
			break;
		}
	}

	return platform_renderer;
}

But if I use an enum I would have to make switches for every function I guess
like I did for 'PlatformRenderer *CreateRenderer(RendererType renderer_type)' or am I wrong?

I wonder if there is a way to improve the way I did things and if there is a better way in general?
Would be interesting to know/see how developers handle this especially when developing for multiple platforms.

sincerely,
Cryonic

Share this post


Link to post
Share on other sites
Advertisement

You are mixing two topics here in your question and we should first make things clear:

  • Platform Independence is a term of building for different platforms with a unified interface to let code on a higher layer use those functions without knowing the hardware layer
  • Function Awareness is the term of creating a unified interface on runtime interchangeable function abstraction. The clue here is to have the API behind an interface wall that could be initialized at runtime

So grabbing the easier one first: Hardware Abstraction e.g. Platform Independence is the more easier part because you have to decide at compile time what platform you want to build for. You can't make a Windows and a Linux build on the same compilation step or you have a huge mistake in your design. Normally the compiler will complain about missing bindings for the one or the other platform.

#ifdef-ing your code here is the standard approach and I use it for my game engine too. I don't know about an alternative technic to built platform/hardware dependent code without #ifdef. Even loading a library requires to build that library with certain #ifdef tags or you end to have an own project per platform.

Function Awareness on the other hand has hand full of possible implementations. You can decide at compile time here also or have a fixed library per renderer you want to have and load that at runtime from disc but another way is to have both renderer in your application and instanciate a class (the inheritance approach).

The binding model of OpenGL but also of Vulkan requires loading a library from the system and bind function pointers to the GL extensions e.g. to the VK API layer. So the approach I took was to create a unified set of functions that each use a runtime bound proxy in the background. Those proxy functions are bind to the GL or VK API subsetvia a simple assignment

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!