Monday, 13 October 2014

Cross-API Rendering 2: RenderPlatform - the main interface

This is the definition for RenderPlatform, the core of the cross-API rendering interface.

virtual ID3D11Device *AsD3D11Device()=0;
We've forward-declared ID3D11Device as a struct. Non D3D11 implementations return NULL. This is a convenience function for the D3D11 implementation.

virtual void RestoreDeviceObjects(void*);
We call this once, when the 3D graphics device has been initialized, and pass the API-specific device pointer/identifier.

virtual void InvalidateDeviceObjects();

We call this once, when the 3d graphics device object is being shut down.

virtual void RecompileShaders();

This is optional - call this to recompile the standard shaders. RenderPlatform implementations will contain some standard shaders for things like drawing text and putting textured quads to the screen. It's handy to be able to force a recompile when the shader source is modified.

DeviceContext &GetImmediateContext();

This returns an object containing immediate-context API-specific values. Context is a funny thing: some API's have it explicitly, some don't. Commands executed with the immediate context are sent straight away to the GPU.

virtual void PushTexturePath (const char *pathUtf8)=0;
virtual void PopTexturePath ()=0;

For resources, like textures, a stack is a great way of telling the renderer where to find them, because it handles multiple locations, and prioritizes them based on what was added last.

virtual void DispatchCompute (DeviceContext &deviceContext,int w,int l,int d)=0;
Most modern API's support compute shaders, and they pretty much all work like this - you apply the shader, then call "Dispatch" with three integers, representing the width, length, and depth of the compute block.

virtual void Draw  (DeviceContext &deviceContext,int num_verts,int start_vert)=0;
virtual void DrawIndexed (DeviceContext &deviceContext,int num_indices,int start_index=0,int base_vertex=0)=0;

And with the demise of OpenGL's immediate mode, most API's do their drawing this way: either you'll do a draw call with a specified number of vertices from the current buffer, or you'll do an indexed draw call, with a given set of indices.

virtual void DrawTexture (DeviceContext &deviceContext,int x,int y,int dx,int dy,crossplatform::Texture *tex,float mult=1.f,bool blend=false)=0;
virtual void DrawDepth  (DeviceContext &deviceContext,int x,int y,int dx,int dy,crossplatform::Texture *tex,const crossplatform::Viewport *v=NULL)=0;

These are two functions for drawing an onscreen quad. DrawTexture just puts the specified texture to the screen at the specified coordinates. DrawDepth draws a depth buffer: we needed a special function for this because depth values are often stored in a highly non-linear way, so that it's hard to see the detail in them - values are either very close to 1, or very close to zero. Implementations of DrawDepth get around this by using the projection matrix (part of the DeviceContext).

virtual void DrawQuad(DeviceContext &deviceContext)=0;

Here we just issue a draw call with four vertices - and usually, no vertex buffer. Modern shader languages can look at the vertex index instead of needing any actual vertex data. So the vertex shader can infer vertex position from the index - e.g. for fullscreen quads.

virtual void Print  (DeviceContext &deviceContext,int x,int y,const char *text,const float* colr=NULL,const float* bkg=NULL);

It is tremendously useful to be able to put text to the screen. This function doesn't need to be efficient: you probably won't use it for nice-looking in-game text, it's for debugging. We pass four floats (including alpha opacity) for the colour (white if NULL), and four for the background - if NULL, we don't draw the background.

virtual void PrintAt3dPos  (DeviceContext &deviceContext,const float *pos3,const char *text,const float* colr,int offsetx=0,int offsety=0,bool centred=false)  =0;

Another super-useful debugging function - draw the given text at the specified 3D position (but keep the text itself the same pixel size so it's readable).

virtual void Draw2dLines (DeviceContext &deviceContext,Vertext *lines,int vertex_count,bool strip)  =0;
virtual void DrawLines  (DeviceContext &deviceContext,Vertext *lines,int count,bool strip=false, bool test_depth=false,bool view_centred=false)  =0;
virtual void DrawCircle   (DeviceContext &deviceContext,const float *dir,float rads,const float *colr,bool fill=false)  =0;

Because each API has platform-specific stuff it needs to do for different kinds of rendering object, we define crossplatform base classes for them, and derived platform-specific classes.

virtual Texture *CreateTexture(const char *lFileNameUtf8=NULL) =0;
virtual BaseFramebuffer *CreateFramebuffer()=0;
virtual SamplerState *CreateSamplerState(SamplerStateDesc *) =0;
Effect *CreateEffect(const char *filename_utf8);
virtual Effect *CreateEffect  (const char *filename_utf8,const std::map<std::string,std::string> &defines)=0;
virtual Buffer *CreateBuffer()=0;
virtual Layout *CreateLayout(int num_elements,const LayoutDesc *layoutDesc) =0;
virtual RenderState *CreateRenderState(const RenderStateDesc &desc)=0;
virtual Query *CreateQuery(QueryType q)=0;

Each of these functions creates an instance of the platform-specific derived class, and returns a pointer to it. When you're done with the pointer, you just delete it. Later on we'll need to think about memory allocation - if the RenderPlatform is creating these objects, and we're deleting them in some other class, we need to provide some kind of memory allocator interface. But for now, we'll go with new/delete.

virtual Mesh *CreateMesh() =0;
virtual Light *CreateLight() =0;
virtual Material *CreateMaterial() =0;

Here's where we get a bit high-level. Neither OpenGL, nor DirectX defines things like lights or materials - those are more like game engine objects. But I think it's worthwhile to add them to the interface.

virtual void SetVertexBuffers(DeviceContext &deviceContext,int slot,int num_buffers,Buffer **buffers,const crossplatform::Layout *layout)=0;

Activate the specifided vertex buffers in preparation for rendering.

virtual void SetStreamOutTarget    (DeviceContext &deviceContext,Buffer *buffer)=0;

Graphics hardware can write to vertex buffers using vertex and geometry shaders; we use this function to set the target buffer.

virtual void     ActivateRenderTargets(DeviceContext &deviceContext,int num,Texture **targs,Texture *depth)=0;

Make the specified rendertargets and optional depth target active.

virtual void SetViewports(DeviceContext &deviceContext,int num,Viewport *vps)=0;
virtual Viewport GetViewport(DeviceContext &deviceContext,int index)=0;

Get the viewport at the given index.

   virtual void     SetIndexBuffer     (DeviceContext &deviceContext,Buffer *buffer)=0;

Activate the specified index buffer in preparation for rendering. //! Set the topology for following draw calls, e.g. TRIANGLELIST etc.

 virtual void     SetTopology      (DeviceContext &deviceContext,Topology t)=0;

This function is called to ensure that the named shader is compiled with all the possible combinations of \#define's given in \em options.

 virtual void     EnsureEffectIsBuilt    (const char *filename_utf8,const std::vector<effectdefineoptions> &options);

Called to store the render state - blending, depth check, etc. - for later retrieval with RestoreRenderState. Some platforms may not support this.

virtual void     StoreRenderState    (DeviceContext &deviceContext)=0;
virtual void     RestoreRenderState    (DeviceContext &deviceContext)=0;

Called to restore the render state previously stored with StoreRenderState. There must be exactly one call of RestoreRenderState for each StoreRenderState call, and they can be nested.

virtual void     SetRenderState     (DeviceContext &deviceContext,const RenderState *s)=0;

Apply the RenderState to the device context - e.g. blend state, depth masking etc.

virtual void     SetStandardRenderState   (DeviceContext &deviceContext,StandardRenderState s);

Apply a standard renderstate - e.g. opaque blending. This is a shortcut for the 95% cases that we'll be using a lot.

virtual void     PushRenderTargets(DeviceContext &deviceContext)=0;

Store the current rendertargets and viewports at the top of a stack, must always be followed by:

virtual void     PopRenderTargets(DeviceContext &deviceContext)=0;

This will restore rendertargets and viewports from the top of the stack.