I thought this idea might be of interest to DirectX C++ programmers.
Typesafety is perhaps the most critical feature of higher programming languages, and yet so often application programming interfaces introduce non-typesafe constructs. This is frequently the case with low-level APIs. A great example of this is DirectX’s APIs for setting GPU registers for vertex and pixel shaders. They include methods like these:
D3DVOID SetVertexShaderConstantF(
UINT StartRegister,
CONST float *pConstantData,
DWORD Vector4fCount
);
D3DVOID SetPixelShaderConstantF(
UINT StartRegister,
CONST float *pConstantData,
DWORD Vector4fCount
);
StartRegister specifies the base register number, pConstantData should point to the value(s) to be loaded into the registers where each register is four floats and Vector4fCount specifies the number of registers to which the API should write.
This API is intentionally generic. But it lacks most of the type safety C++ offers. Consider the following simple cases representing aberrant uses of the API:
const float fValue = 1.5f;
const D3DVECTOR4 v4Value(1.0f, 2.0f, 3.0f, 1.0f);
// case 1
g_piDevice->SetVertexShaderConstantF(0, &fValue, 1);
// case 2
g_piDevice->SetVertexShaderConstantF(2, (float*)fValue, 4);
In case 1, the y, z and w components of vertex constant register 0 will be loaded with unintended values. In case two, the values in register 3, 4 and 5 will be overwritten with garbage values since the Vector4Count parameter is wrong. In neither of these cases will the compiler provide an error or warning that something’s wrong.
The easiest way to add some type safety is to provide a function that is overloaded on the value to be written. For the simple cases above, we could introduce the following:
inline void SetVertexShaderConstantF(UINT RegisterID, float value)
{
D3DXVECTOR4 vTemp = { value, 0, 0, 0 };
// note: on xbox, use XMVECTOR
g_piDevice->SetVertexShaderConstantF(RegisterID, &vTemp, 1);
}
inline void SetVertexShaderConstantF(UINT RegisterID, const D3DVECTOR4& value)
{
g_piDevice->SetVertexShaderConstantF(RegisterID, (float*)value, 1);
}
Some compilers, like the one on Xbox 360, are able to provide additional optimizations when parameter values to inline functions are literals. To ensure that this optimization is available when using our typesafe versions, we could add a template that passes the RegisterID as a literal instead of as a variable:
template <UINT TRegisterID>
inline void SetVertexShaderConstantF(float value)
{
D3DXVECTOR4 vTemp = { value, 0, 0, 0 };
g_piDevice->SetVertexShaderConstantF(TRegisterID, &vTemp, 1);
}
. . .
// used this way
SetVertexShaderConstantF<0>(value);
If the register traits of the target GPU are known a priori, we can further refine this idea by introducing a compile time constraint on the template parameter TRegisterID. On Xbox 360, this value must be in the range 0…255. To constrain this at compile time on the xbox 360 we can use the _STATIC_ASSERT macro:
template <UINT TRegisterID>
inline void SetVertexShaderConstantF(float value)
{
_STATIC_ASSERT(0<=TRegisterID && TRegisterID<=255);
D3DXVECTOR4 vTemp = { value, 0, 0, 0 };
g_piDevice->SetVertexShaderConstantF(TRegisterID, &vTemp, 1);
}
An error will now be generated at compile time if the programmer uses this function with a register id that is out of range. (NOTE: If static assertions are not available in your environment, you can use or build something like the boost library’s BOOST_STATIC_ASSERT. )
Aside from type safety, it would be nice if our interface provided a simple, typesafe way to declare all the registers needed for a particular shader. Let me show you the basic way I do this for vertex shaders:
class CVertexShader
{
public:
template <class TDataType, UINT TRegisterID>
class CConstant
{
public:
inline void operator=(const TDataType& value)
{
SetVertexShaderConstantF<TRegisterID>(value);
}
};
};
Notice that sizeof(CVertexShader::CConstant) is zero. This is important because we don't want our strategy to impose any additional memory requirements.
Derived classes can then easily describe a typesafe program interface to a vertex shader. For example:
class CSimpleVertexShader : public CVertexShader
{
public:
CConstant< XMMATRIX,0 > mWorld;
CConstant< XMMATRIX,4 > mView;
CConstant< XMMATRIX,8 > mProjection;
CConstant< XMVECTOR, 12 > vEyePositionW;
};
. . .
// used this way
CSimpleVertexShader Simple;
. . .
Simple.mWorld = mWorld;
Essentially, this provides a compile-time name binding of a particular vertex shader’s registers that is both type-safe and convenient to use.
I also enhance class CVertexShader to add run-time binding to a particular instance of a loaded or compiled vertex shader. This looks something like the following (I’ve omitted the runtime assertions and state checking for brevity):
class CVertexShader
{
protected:
CInterfacePtr<IDirect3DVertexShader9> m_piVertexShader;
public:
IDirect3DVertexShader9* operator -> () { return m_piVertexShader; }
operator IDirect3DVertexShader9* () { return m_piVertexShader; }
HRESULT Set() { return g_piDevice->SetVertexShader( m_piVertexShader ); }
HRESULT Load(const char* pszFilename);
HRESULT Load(const wchar_t* pszFilename);
HRESULT Compile(const char* pszCode);
HRESULT Compile(const wchar_t* pszCode);
};
The implementation for the concomitant CPixelShader is nearly identical. For brevity's sake I've left out the details, but I’ll be happy to post the complete code if anyone requests it.