(In the interests of disclosure: (1) I have not completed my implementation yet, so I cannot say with certainty how well it worked, (2) I only do graphics programming as a hobby, I write my engine and tools because I enjoy writing engines and tools, I don't know how well any of this would apply to a commerical project.)
I find this question very interesting as I have been working for the last couple of days on the same thing, and it certainly isn't for the feint-hearted (or people who like tidy code and lots of refactoring ;))
It all depends on where you aim; as Patrick Hughes commented so many of the types and concepts are very implementation specific that it is difficult to keep something satisfactorily generic and of use. That said, if too much is done in the plug-in the purpose of the abstraction is negated.
Take for example, a mirror or video screen in the game world, say you implemented this by rendering your scene using a special viewport to a texture on the GPU which was then provided to the shader of your mirror or screen with some post-processing. This I would say is an ideal task for the engine, pertaining to a collection of low level objects with specific configurations, with some knowledge of the scene at large - but if your layer is dealing only with complete game objects such as models, you'd have to implement all that logic once for every API your layer supported.
The very nature of an abstraction means you shall lose functionality the further away from the API you move, how generic your methods and objects depends on the requirements of your application (or probably more accurately, what specific features of your chosen API you want to use and interact with explicitly).
My recent attempt had one purpose, and that was to in some way 'protect' the work I put into an engine, by ensuring it would not be tied to a specific API, so I could go cross-platform, or support different grades of graphics cards. I chose a level slightly below that of XNA, and set some rules for the composition of my wrapper, such as, there shall be no internal caches - all objects shall be passed back through the layer (though they are hidden). I used namespaces and split up the projects so I could force myself to conform to these by omitting references to parts that other parts of the code should not see.
As I wanted to put all the interesting and tricky bits in an engine where they could be reused, I opted for a low level wrapper. I have a 'template' for a 'Renderer' (which abstracts the device) defined by an abstract class, which has methods to create a number of objects representing common paradigms such as Vertex/Index Buffers, Textures, Shader Programs, etc. Each of these implementing an appropriate interface.
The quick and dirty diagram below indicating whereabouts I intended my wrapper to sit:
An example excerpt from my 'template':
public enum ElementFormat
{
Vector4 = 4 * 4,
Vector3 = 3 * 4,
Vector2 = 2 * 4,
Byte4 = 4 * 1,
Int = 1 * 4
}
[Serializable]
public class VertexElement
{
public string Semantic;
public ElementFormat Format;
public int Index;
}
public interface IVertexBuffer
{
VertexElement[] VertexDefinition { get; set; }
void SetData(byte[] data, int destStartPosition);
void GetData(byte[] data, int lengthDataToGet);
}
public abstract class Renderer
{
/// <summary>
/// Creates a new index buffer on the GPU, initialising to the supplied data.
/// </summary>
/// <param name="length">Length in bytes of the new buffer</param>
/// <param name="data">Data to initialise to</param>
/// <param name="dynamic">Buffer is accessible by the CPU. If no data is supplied this is assumed to be true</param>
/// <returns>A handle to the new buffer</returns>
public abstract IVertexBuffer CreateVertexBuffer(byte[] data, bool dynamic);
public abstract IVertexBuffer CreateVertexBuffer(int length);
}
The actual object that is returned by CreateVertexBuffer() is defined within the wrapper, and the engine has no knowledge of it.
public class Dx11VertexBuffer : IVertexBuffer
{
internal SlimDX.Direct3D11.Buffer DxBuffer;
internal SlimDX.Direct3D11.VertexBufferBinding DxBinding;
internal SlimDX.Direct3D11.InputElement[] DxVertexDefinition;
internal int DxVertexStride = 0;
internal DirectX_11_64_Renderer Parent;
public void SetData(byte[] data, int destStartPosition)
{
Parent.UpdateBufferData(DxBuffer, data, destStartPosition);
}
private VertexElement[] vertexdefinitioncopy;
public VertexElement[] VertexDefinition
{
get
{
return vertexdefinitioncopy;
}
set
{
vertexdefinitioncopy = value;
Parent.SetVertexFormat(this, vertexdefinitioncopy);
}
}
}
My wrapper abstracts away some detail, for example DirectX has a single type of Resource Buffer with various bindings for things such as vertices, indices, textures, shader constants, etc, whereas I specify multiple explicit types - the ones I intend to use.
There are of course serious caveats with this design:
- It is closely modelled on DirectX - other implementations will have to bend their APIs to a degree.
- It leaks - if there are some (desired) concepts that some APIs implement and others do not, the only option is to expand the template to accommodate them, and have the others break; the engine can work out what is available and what not and use workarounds if available.
I pursued the design for a number of reasons, that I would say are important to keep in mind when thinking about designing something such as this:
- Unless you work for Pixar, there are really only two APIs - DirectX and OpenGL (and if you work for Pixar, there are still only three)
- And both of these use very similar concepts (makes sense, afterall they operate on the exact same hardware!) - look at the 'SlimDx Pipeline' vs. the 'OpenTK Pipeline'.
- Most people use Windows, so optimizing for DirectX is never going to be a bad call. (I am writing this with a view to three implementations - OpenGL, DirectX 9 and DirectX 11)
In addition to that of course, is that I am building this as a tool for my personal use, designed only for the methods of the engine I build on top of it, meaning backwards compatibility or leaks are not as big a concern as they would be if it were public or operated at the same level as the engine - full vertical control gives you much more flexibility.
I would be interested to know how you get on, good luck!