Here's how my engine handles this, I haven't been able to come up with anything else that's any more flexible or simple.
The classes which abstract the main interface to the graphics API:
- DeviceManager - Enumerates all available 'devices' i.e. OpenGL, DirectX 10, DirectX 11, etc
- GraphicsDevice - implements a graphics API interface (i.e. OpenGL), creates a context for the particular device.
- GraphicsContext - main interface which handles GPU object creation (textures, buffers, shaders, etc), as well as submitting draw calls/state changes to the API. Also handles window system interaction. I have an OpenGLContext subclass and will soon have a DirectX10Context, and later others.
- Call context->createTexture2D( image, texture format...) and like methods for other types to create context GPU objects from generic data.
- Call context->draw( vertex buffer, index buffer, shader program ) to draw stuff.
These interfaces provide a way to get access to an underlying API in a uniform manner. Since all APIs wrap hardware functionality for buffers, textures, and shaders, there ends up being a common set of abstractions for different hardware data types:
- Texture - virtual interface for interactions with the texture system. Handles 1D, 2D, 3D, and cube textures and any other common texture parameters. Implemented by OpenGLTexture which provides the OpenGL implementation for the texture, including any API-specific data (GLint texture ID, for example).
- HardwareBuffer - virtual interface for vertex buffers, index buffers, and constant buffers. Provides buffer memory mapping, read/write access. Implemented by OpenGLHardwareBuffer.
- Shader - virtual interface for shader creation. Takes source code and shader type/language (vertex/fragment, glsl/cg/hlsl), then compiles it. Implemented by OpenGLShader.
- ShaderProgram - virtual interface for shader programs. User attaches Shader objects to the program and then link them. Allows users to access the input variables for the linked shader program. Implemented by OpenGLShaderProgram.
In practice it is a little tricky to define good interfaces for these classes, but it can be done. All of these classes are wrapped in reference-counted smart pointers.