0
\$\begingroup\$

I'm developing a 3D game engine in C# and I was just wondering what are the different ways I can approach designing a graphics API abstraction.

My original design was going to be something like this:

public interface IVertexBuffer : IDisposable
{
    void Bind();
}

#if DRIVER_TYPE_OPENGL_45

public sealed class OpenGLVertexBuffer : IVertexBuffer
{
    public void Bind()
    {
        // Bind buffer...
    }

    public void Dispose()
    {
        // Delete buffer...
    }
}

#endif

#if DRIVER_TYPE_OPENGL_ES30

public sealed class OpenGLESVertexBuffer : IVertexBuffer
{
    public void Bind()
    {
        // Bind buffer...
    }

    public void Dispose()
    {
        // Delete buffer...
    }
}

#endif

In C#, sadly pre-processors don't work the same way they do in C++, so I would have to compile two versions of my project, but that just seems hacky and I would much rather be able to switch between drivers types.

So, I had another idea to do something like this:

public interface IRenderDevice
{
    IVertexBuffer CreateVertexBuffer(float[] vertexData, int sizeInBytes);

    void DestroyVertexBuffer(IVertexBuffer vertexBuffer);
}

public sealed class OpenGLRenderDevice : IRenderDevice
{
    public IVertexBuffer CreateVertexBuffer(float[] vertexData, int sizeInBytes)
    {
        // Create vertex buffer
    }

    public void DestroyVertexBuffer(IVertexBuffer vertexBuffer)
    {
        // Destroy vertex buffer, or should I just call vertexBuffer.Dispose()?
    }
}

and then just use an enum to determine which render device to create, but I know this solution will quickly get out of hand when I begin to introduce other things like vertex arrays, index buffers, frame buffers, etc.

What other options do I have that will still maintain readable code and look native to C#?

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Your second choice is somewhat better, but still requires development.

My advice would be to develop and inheritance tree of base type IRenderer, which defines the common API of any possible renderer object.

Secondly, define a static factory method, which has the pre-processor directives in the body of the method. When you call CreateRenderer(), the factory will return a reference to the renderer most appropriate to the platform, which will be hard coded according to the platform it was compiled for.

Functionally, for all practical purposes pre-processor directives work the same way as C++. They define how the code will be compiled, therefore if you have a bunch of platforms with pre-processor guards, then you will have to compile for each platform whether you code it in C# or C++. The only way to avoid that, would be to code for the Java Virtual Machine, which allows true platform agnostic code.

But your users would not thank you for that.

There is no way to write native code which will work on all platforms. It is simply not possible on a machine code level, as differing CPU's have different instruction sets, and may have differing endianness. An executable compiled for one platform will not work on another(best case), or crash it out completely (worst case).

On a related note. If true platform abstraction is a real concern to you, I would recommend moving away from C# altogether, and coding with C/C++, with the very compelling reason being, a C/C++ compiler exists for all platforms, past and present. The same cannot be said for C#, which at present, is only supported natively on windows, and only recently has the support extended to Linux/Mac. Console manufacturers (with the notable exception of Microsoft, with the XBox360) do not write compilers to support C#, as the vast majority of the games industry codes in C/C++. You will find support and information sorely lacking.

\$\endgroup\$
1
\$\begingroup\$

People have tried in the past to create such abstraction over opengl, they either just complicated the API until using the original was better, or abstracted too much so that a lot of operations that are a bit more advanced than what you had in mind when creating the abstraction became impossible.

For example a buffer in opengl can be used in a lot of different ways, holding vertex data, holding index data, holding uniform data (that may or may not be writable by the shader), holding texture data during upload/download. The same buffer can do all of those things one after the other. So binding a buffer to be strictly vertex data may be a mistake when a user wants to build vertex data in a compute shader.


You should also consider Vulkan and D3D and whether you want to provide abstractions for them as well. And if you will, will it be with the same user-facing API transparently.

\$\endgroup\$
1
  • \$\begingroup\$ An observation: "binding a buffet to be strictly vertex data" - this is an OpenGLism and does not necessarily translate to other APIs; i.e. It's not a portable concept. If you're building an abstraction layer, and if you wish it to be truly portable, you do need to be aware that not all APIs work the way OpenGL does. \$\endgroup\$ Commented Apr 27, 2018 at 20:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .