Abstracting my renderer

Started by
28 comments, last by cozzie 7 years, 4 months ago

@[member='L. Spiro'], how would you go about one build that supported lets say D3D11 and OpenGL4.5? Then how would you approach the problem?

-potential energy is easily made kinetic-

Advertisement

@[member="L. Spiro"], how would you go about one build that supported lets say D3D11 and OpenGL4.5? Then how would you approach the problem?

You'd make two builds, and a launcher/front-end that selects the right one.
...but when was the last time you played a game that let you choose between D3D and GL? Just make your windows build use D3D as it's more stable, and your Linux build use GL because you have to.

Just to add another thing to the 'Virtuals Are Bullshit' in this case argument - Liskov substitution principle.

In short; a type S which is a sub-type of T may replace objects of type T.

Or;
Animal --> Cat, Dog, Cow

Cat, Dog and Cow are subtypes of Animal; if a function expects an 'Animal' type then you can replace it with any of the other types and the program should work correctly.

Now, lets look at this design;

Texture --> OpenGLTexture, D3DTexture, MetalTexture

The same does not hold true here; your API layer will expect a type of 'Texture', but it would fail to work 'Texture' was replaced by a sub-type it wasn't expecting - passing an OpenGLTexture to a D3D based Renderer isn't going to work therefore the virtual cost is a waste of time and your program could be technically malformed.

Even if I won't compile multiple API supported versions into one executable, I still want to have organized an reusable code.
So I can change the API in some parameter and then simply re-compile to the corresponding executable.

As others have suggested, they way you have reusable code is by a compiler time switch, not a run-time switch:

run-time switch being: IRenderer* myRenderer = new OpenGL() or IRenderer* myRenderer = new DirectX()

Instead the way you get re-usable code is by dropping the virtual = 0 in your classes and just make 2 folders called "GL" and "DX" and they simply implement the IRender functions as normal IRender class functions. You can in those cpp files do something like #if COMPILE_WITH_DX for all the cpp files in the DX folder and #if COMPILE_WITH_GL in the other folder. This way its defining the IRender functions one way or the other. Totally re-usable code, because you have 2 implementations of the same exact class (IRender), meaning all your code is interfacing with your renderer the same way. It won't get redefinitions of the functions because its going to only pick one implementation.

Graphics/Interfaces/ITexture.h
class ITexture{ CreateTexture(); };

Graphics/DX/ITexture.cpp
#if DX....
ITexture::CreateTexture(){// do DX stuff}


Graphics/GL/ITexture.cpp
#if GL....
ITexture::CreateTexture(){ // do GL stuff}


.... of course if you want to do something like Battlefield 1 where you can choose between DX11 and DX12 then having a virtual interface run-time switch is one way to go. I'd really have to look at performance though, because you don't have 1000 instances of a renderer, the virtual table and prediction shouldn't be too bad I would think.

NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

Does that mean that in the CIndexBuffer you have to include the headers for all APi specific IndexBuffers? (to be able to create the needed object)

It looks like this:
#include "../LSGGraphicsLib.h"

#ifdef LSG_DX9
#include "../DirectX9IndexBuffer/LSGDirectX9IndexBuffer.h"
#elif defined( LSG_DX11 )
#include "../DirectX11IndexBuffer/LSGDirectX11IndexBuffer.h"
#elif defined( LSG_METAL )
#include "../MetalIndexBuffer/LSGMetalIndexBuffer.h"
#elif defined( LSG_OGL )
#include "../OpenGLIndexBuffer/LSGOpenGlIndexBuffer.h"
#elif defined( LSG_OGLES )
#include "../OpenGLESIndexBuffer/LSGOpenGlEsIndexBuffer.h"
#elif defined( LSG_VKN )
#include "../VulkanIndexBuffer/LSGVulkanIndexBuffer.h"
#endif	// #ifdef LSG_DX9

#include "../VertexBuffer/LSGVertexBuffer.h"
You don’t really need the macro mess since the headers are empty (after preprocessing) if they are not the current API, so if you wanted to shrink it down with virtually no compile-time cost it could be:
#include "../LSGGraphicsLib.h"

#include "../DirectX9IndexBuffer/LSGDirectX9IndexBuffer.h"
#include "../DirectX11IndexBuffer/LSGDirectX11IndexBuffer.h"
#include "../MetalIndexBuffer/LSGMetalIndexBuffer.h"
#include "../OpenGLIndexBuffer/LSGOpenGlIndexBuffer.h"
#include "../OpenGLESIndexBuffer/LSGOpenGlEsIndexBuffer.h"
#include "../VulkanIndexBuffer/LSGVulkanIndexBuffer.h"

#include "../VertexBuffer/LSGVertexBuffer.h"

Hey Spiro, how are you implementing your call to "CDirectX11::GetDirectX11Context()" Is that a... singleton?

No but it is global state, for reasons I considered carefully before making it so.
	__inline ID3D11DeviceContext * CDirectX11::GetDirectX11Context() {
		return m_pdcContext;
	}
Initialization of all global state in my engine is controlled and in a specific order with specific guarantees. I chose to go this route because I know about the pitfalls of global state (and singletons)—so those are not issues—and wanted to save myself some development headache.
It is generally best to avoid global state and especially singletons. It is not advised that anyone follow my example without good experience in controlling the standard pitfalls of global state and singletons, and without good justifiable reasons for doing so.


@L. Spiro, how would you go about one build that supported lets say D3D11 and OpenGL4.5? Then how would you approach the problem?

As correctly mentioned by Hodgman, don’t.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

@[member="L. Spiro"], how would you go about one build that supported lets say D3D11 and OpenGL4.5? Then how would you approach the problem?

You'd make two builds, and a launcher/front-end that selects the right one.
...but when was the last time you played a game that let you choose between D3D and GL? Just make your windows build use D3D as it's more stable, and your Linux build use GL because you have to.

This doesn't work that easy.

Some markets are dominated by Windows XP(e.g. Asia) and brand new video cards which would mean you running dx9 but could use opengl4.5 with the whole feature set.

The engine I were using in a company dropped D3D at all and used OpenGL 2.1, 3.5, WebGL and OpenGL ES to cover Mobile, Desktop and Web(they dropped console).

I looked at the no longer used XBox and D3D9 code and they have different flavour they have to write nearly the same thing twice(in some cases).

It's like PS3 with it's customized OpenGL API(which they dropped later on) and you have to use a fully custom graphic API.

An old colleague also showed me his Nintendo Wii code which is also a customized OpenGL flavour.

Which mean you have a chunk of code which is totally different and then you have a hand full of commands which are a little bit different and the rest is the same.

Blizzard's Warcraft 3 and World of Warcraft, Diablo3 Engine are using runtime switches by adding -gl as parameter it maps the d3d commands to opengl.

Valve's Source Engine supported D3D at the beginning then they added OpenGL by choosing ingame and on long run they want to drop d3d because they want to reduce the workload of supporting multiple backends.

Cryengine also added OpenGL support because it's licensed like crazy on Asia MMO market and they could easily add Linux support.

Unity Engine have a couple of backends like D3D9-11, OpenGL, WebGL and you can switch between them.

Unreal Engine did the same.

It's not always the question D3D and OpenGL but D3D9 and D3D11(like Grinding Gear Games did lately).

What I want to say is the major engines and also some less used engines are supporting multiple backends on the same binary.

I choose "service locator" pattern for my new private project and the render class use virtual methods.

The renderer instance can create models, shader and so on but I provide only handles(uint32) to work with and that way you can use a ModelGL45 class without virtual but can use inheritance with ModelGLShared and ModelShared.

Using the virtual methods of the renderer instance are 3 per frame(in my case StartFrame, EndFrame, Draw) and on demand to load/unload ressource every couple of frames.

The major advantage over the pre processor compile time version is understandable and clean code which is seperated in it's own source and header files and it's the same performance and programming style as you learn it at school, in books and upper level code. The impact of the virtual function calls is so little that you have to call them a couple of thousand times per frame to think about the overhead and as already told the hot loop is in the virtual function and not around it.

I was working in the past on an engine with compile time switches and I was the lucky one to add OpenGL3.5 support and the first thing I notices was the mess of code called render backend because you have a couple of hundred lines of code with tons of pre processor branches because you already have D3D9,D3D11,OpenGL1.2, 2.1, WebGL and GL ES there to.

If the API shares the same functions you put them together with pre processor branches and they blow up and you forget the first line when you read the last one.

At this time I read a book about API design https://www.amazon.de/API-Design-C-Martin-Reddy/dp/0123850037 and compile time switch with pre processor did a couple of thinks the opposite way they defined as good api.

I seperated my new OpenGL3.5 code in own sources, header files, created classes and only called the functions from the original function call(the way L. Spiro showed in the post before mine).

This way I had a readable and understandable code with an additional function call with the pre processor branches.

A couple of coder wrote over a decade in the same bad style because one started and the other followed with their tight time constraints and they copy,paste, adjust it.

It's hard to maintain but easy to extend.

This was the reason to decide to go with service locator pattern and design it in a way I have no performance penalty and enforce a good code style.

Valve's Source Engine supported D3D at the beginning then they added OpenGL by choosing ingame and on long run they want to drop d3d because they want to reduce the workload of supporting multiple backends.

Unity Engine have a couple of backends like D3D9-11, OpenGL, WebGL and you can switch between them.
Unreal Engine did the same.

What I want to say is the major engines and also some less used engines are supporting multiple backends on the same binary.


Firstly, Source is just an updated Gold Engine which is an updated Quake engine.
Valve's games originally had GL support, indeed back in the day using D3D to play things like CS was considered crazy; D3D came later and when GL wasn't going anywhere it was dropped - so really they added it back in and have continued to add Vulkan; however there are also crazy political reasons going on too here.

Unity's situation is hard for me to comment on, as I work there so NDAs and all that, but much of it's position is one of legacy and needing to move fast and support platform. I'm willing to bet, given the chance to clean up the code, they wouldn't do it the same way as it causes multiple problems.

UE, again, stands on the platform of legacy as much as anything else.

My point being that just because some major engines have done something one way, and others have copied them because they are major engines, doesn't mean it is the right way to do it or even a sensible way.

Legacy and business demands rarely, if ever, result in good engineering practises over time.

Seems there are more than 1 ways to solve this, all with their pro's and cons.

I'll give it some thought and decide which path to choose.

@LSpiro: clear, regarding the headers. If you also have the #ifdef's in the CPP files, the headers won't need them because they're effectively empty, based on the project configuration.

When I summarize it, I believe I can choose between these options:

1. D3D11 only, frontend 'knows' about D3D11

2. D3D11 only, but the backend with D3D11 is not exposed to the frontend (frontend basically the same as with 'full abstraction')

(let's call this 'semi abstracted', from the end-user/frontend point of view; using virtual functions mainly)

3. Abstracted using Ixxx (base) classes and per API an inherited class with the API specific code.

All API's code will be in compilation, possibility to switch during runtime (no project configuration/ macro's for the API selection)

4. Like 3, but with the addition of the defines, so a macro in the project config determines which API, thus which code has to be compiled and doesn't have to be compiled (#ifdef's)

5. ....

In general, there's one thing I don't fully understand yet.

Let's conside these classes:

class IBuffer

{

public:

virtual IBuffer();

// etc

virtual bool Create(API independent params) = 0;

};

class IDirect3D11Buffer : public IBuffer

{

public:

IDirect3D11Buffer();

bool Create(API independent params);

private:

CComPtr<ID3D11Buffer> mBuffer;

}

Then somewhere you create a buffer:

IBuffer *myLocalBuffer;

if(useD3D11)

{

myLocalBuffer = new IDirect3D11Buffer;

}

Would this mean that myLocalBuffer only 'has' the members from IBuffer?

If so, I'm not able to use the ID3D11Buffer, in the hidden implementation in ID3D11Buffer.

But chances are that I'm overseeing something here (LH is another type then RH, in the pointer).

Crealysm game & engine development: http://www.crealysm.com

Looking for a passionate, disciplined and structured producer? PM me

Would this mean that myLocalBuffer only 'has' the members from IBuffer?

Thats normal c++ and the idea of an "interface" Does the user of your class need to know about the private variable? They shouldn't care. You should interface with the class via public interface listed in the base class. The only code that should care about your private mBuffer, is IDirect3D11Buffer class/functions.

If for whatever reason you want access to that variable you could have

CComPtr<ID3D11Buffer> IDirect3D11Buffer::GetMyLocalBuffer()
{
return mBuffer;
}


if(useD3D11)

{
CComPtr<ID3D11Buffer> myLocalBuffer = static_cast<ID3D11Buffer*> (myLocalBuffer)->GetMyLocalBuffer();
}




NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

All clear

Crealysm game & engine development: http://www.crealysm.com

Looking for a passionate, disciplined and structured producer? PM me

This topic is closed to new replies.

Advertisement