引言
这个寒假学DirectX11的时候用的书是《Introduction to 3D Game Programming with DirectX 11》,里面关于Shader的部分全都是用的Effects框架。用起来当然没什么问题,但我还是想把相关问题搞清楚,也就是这个框架是如何把HLSL中的各种Shader Object与应用程序中的接口联系起来的。比如:
effect->GetVariableByName("WVP")->AsMatrix()->SetMatrix((float*)&WVP);
其中effect
是一个ID3DX11Effect*
。假设对应的HLSL中的代码是
cbuffer cbPerObject
{
float4x4 worldMat;
float4x4 WVP;
};
那么这行C++代码意思就是设置Shader中WVP的值。类似的,通过Effects框架可以方便地设置Shader Resource(Buffer、Texture等)、Sampler State等。如果不使用Effects框架,又该如何设置这些变量呢?
ID3D11DeviceContext::XXSetXX方法
我们注意到ID3D11DeviceContext
有一系列方法:
ID3D11DeviceContext::VSSetConstantBuffers
ID3D11DeviceContext::VSSetShaderResources
ID3D11DeviceContext::VSSetSamplers
ID3D11DeviceContext::PSSetConstantBuffers
......
一般来说,这些方法都要求提供一个StartSlot参数,一个NumXXX参数,以及一个存放了要设置的Shader Object的值的二重指针。比如:
void VSSetConstantBuffers(
[in] UINT StartSlot,
[in] UINT NumBuffers,
[in, optional] ID3D11Buffer *const *ppConstantBuffers
);
StartSlot [in]
Type: UINT
Index into the device's zero-based array to begin setting constant buffers to (ranges from 0 to D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT
- 1).
NumBuffers [in]
Type: UINT
Number of buffers to set (ranges from 0 to D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT
- StartSlot).
ppConstantBuffers [in, optional]
Type: ID3D11Buffer*
Array of constant buffers (see ID3D11Buffer
) being given to the device.
要想高高兴兴地使用这个东西,就必须搞清楚这几个参数是什么意思。ppConstantBuffers显然就是要设置的Constant Buffer了,NumBuffers显然是要设置的Buffer的数量,那StartSlot是什么含义呢?我们知道在写Shader的时候可以手动指定ConstantBuffer的绑定位置:
cbuffer cbPerObject : register(b2)
{
float4x4 WVP;
};
那么要设置上面代码中的cbPerObject,就应该写
DeviceContext->VSSetConstantBuffers(2, 1, &constantBuffer);
这里的StartSlot(传入2)和Shader代码中的register(b2)中的2是对应的。这下知道了:VSSetConstantBuffers
就是把一些buffer绑定到Vertex Shader Stage,而StartSlot就是第一个被绑定到的“槽位”,或者说寄存器(register)。假如我这样调用:
DeviceContext->VSSetConstantBuffers(2, 2, &cbuffers);
那么Vertex Shader中就能通过register b2、register b3所对应的cbuffer来访问我在cbuffers中传入的两块buffer的内容。
如果看过文档中的Shader Model就会知道,Shader Stage中有各种各样的寄存器:t#,s#,u#等等。比如要绑定SamplerState
,就可以在Shader代码中定义SamplerState
的地方写上register(s0),用来指定这个SamplerState
是通过寄存器s0访问的,然后在应用程序中用XXSetSamplers
来设置对应的寄存器的取值即可。即:
DeviceContext->PSSetSamplers(0, 1, mySampler);
Shader中这样写:
SamplerState sampler : register(s0);
使用反射:ID3D11ShaderReflection
我很擅长灌了半瓶子水就出发,这次也不例外——在搞明白上一节中说的内容后,我就觉得可以动手写代码了,于是有了这样的鬼畜设计:
- 读取待编译的Shader的源代码,手动Parse一下,取得必要的信息(比如,用了哪些
ConstantBuffer
,用了哪些SamplerState
等)。 - 为代码中未指明寄存器的资源生成寄存器使用方案,然后根据这个重新生成一份Shader的代码,新代码中所有的资源都显式地指定了通过哪个寄存器访问。
- 把这份重新生成的代码用DirectX11提供的方法编译,创建Shader并投入使用。
这样一来,在第二步的时候,应用程序就已经对Shader中的所有信息一清二楚(因为自己Parse了一遍,还给把所有不明确的东西都显式指定了),然后就可以仿照D3DX11Effect
方便地设置Shader中的变量值和资源了。
然后我就准备动手了(无知者无畏啊)。我注意到D3D11在编译Shader的时候是只注意使用了的变量的,比如这样写:
Texture2D<float4> tex;
Texture2D<float4> normalMap;
SamplerState texSampler;
SamplerState normalMapSampler;
//...something else
float4 PS(PSInput input) : SV_TARGET
{
return tex.Sample(texSampler, input.texCoord);
}
也就是说我虽然定义了normalMap
和normalMapSampler
,但是没用上,那么编译过后如果发现这两个东西根本不存在,也不要惊讶。
这样一来,直接解析Shader源代码来获知有哪些资源的方案就变麻烦了,Parsing过后还要从起始函数开始分析使用了哪些变量。我可没耐心搞这种东西,于是查了一下(早就该这样了),发现还有这么个东西:
HRESULT WINAPI D3DReflect(
in LPCVOID pSrcData,
in SIZE_T SrcDataSize,
in REFIID pInterface,
out void ppReflector
);
调用这个函数取得的是 ID3D11ShaderReflection Interface。当时我就懵了,D3D11的Compiler都给我提供Reflection了,我还在这折腾个啥呢,于是飞快地把相关文档看了一遍。MSDN上给出的获取ID3D11ShaderReflection
的示例是这样的:
pd3dDevice->CreatePixelShader( pPixelShaderBuffer->GetBufferPointer(),
pPixelShaderBuffer->GetBufferSize(),
g_pPSClassLinkage,
&g_pPixelShader );
ID3D11ShaderReflection* pReflector = NULL;
D3DReflect( pPixelShaderBuffer->GetBufferPointer(), pPixelShaderBuffer->GetBufferSize(),
IID_ID3D11ShaderReflection, (void**) &pReflector);
也就是说要取得Shader Reflection,就必须提供编译后的Shader内容。取得了Shader Reflection,就可以根据文档查询所需的各类信息了。比如我想知道所有的Constant Buffer的信息,并打印出其名字和对应的Bind Point(也就是之前说到的绑定位置),可以这样写(假设shaderRef_
是刚刚获得的ID3D11ShaderReflection
:
D3D11_SHADER_DESC shaderDesc;
shaderRef_->GetDesc(&shaderDesc);
for(int i = 0; i != shaderDesc.ConstantBuffers; ++i)
{
ID3D11ShaderReflectionConstantBuffer *cb = shaderRef_->GetConstantBufferByIndex(i);
D3D11_SHADER_BUFFER_DESC cbDesc;
cb->GetDesc(&cbDesc);
for(int j = 0; j != shaderDesc.BoundResources; ++j)
{
D3D11_SHADER_INPUT_BIND_DESC bindDesc;
shaderRef_->GetResourceBindingDesc(j, &bindDesc);
if(strcmp(cbDesc.Name, bindDesc.Name) == 0)
{
cout << cbDesc.Name << " " << bindDesc.BindPoint << endl;
break;
}
}
}
这段代码遍历所有的Constant Buffer,对每一个Constant Buffer,在所有的Bound Resources中通过名字cbDesc.Name
查找它,并由此获知其Bind Point。
Resource Object
ID3D11ShaderReflection
提供了非常丰富的关于Shader的信息,至少现在不需要去Parse它的代码了。接下来就是自己对D3D11 Shader的封装了。我期望做出来的效果是这样的:
ConstantBufferStructure cbStruct;
cbStruct.lightPosition = XMFLOAT3(1.0f, 2.0f, 3.0f);
cbStruct.lightColor = XMFLOAT3(0.0f, 1.0f, 1.0f);
shader->GetPSConstantBufferObject("Light")->SetConstantBuffer(cbStruct);
其中ConstantBufferStructure
是和Shader代码中的cbuffer对应的C++结构体(当然,要注意padding),shader
是自己封装的Shader类指针。
不同Shader Stages操作的静态分派
首先要解决的问题是,不同Shader Stages的许多操作是有细微差别的。比如,设置Vertex Shader的Constant Buffer的时候,我们使用的是ID3D11DeviceContext::VSSetConstantBuffers
;在设置Pixel Shader的时候,使用的则是ID3D11DeviceContext::PSSetConstantBuffers
。为此,先定义一组常量:
namespace ShaderAux
{
constexpr int UNKNOWN_SHADER = 0;
constexpr int VERTEX_SHADER = 1;
constexpr int HULL_SHADER = 2;
constexpr int DOMAIN_SHADER = 3;
constexpr int GEOMETRY_SHADER = 4;
constexpr int PIXEL_SHADER = 5;
}
然后搞个针对Shader类型的分派器(dispatcher),对不同的Shader Stages分别做特化:
namespace ShaderAux
{
template<int ShaderSelector> struct ResourcesBinder;
template<> struct ResourcesBinder<VERTEX_SHADER>
{
static void BindConstantBuffer(int slot, ID3D11Buffer *buf)
{
D3DContext::_GetDeviceContext()->VSSetConstantBuffers(slot, 1, &buf);
}
static void BindShaderResource(int slot, ID3D11ShaderResourceView *SRV)
{
D3DContext::_GetDeviceContext()->VSSetShaderResources(slot, 1, &SRV);
}
static void BindSampler(int slot, ID3D11SamplerState *sampler)
{
D3DContext::_GetDeviceContext()->VSSetSamplers(slot, 1, &sampler);
}
};
template<> struct ResourcesBinder<HULL_SHADER>
{
static void BindConstantBuffer(int slot, ID3D11Buffer *buf)
{
D3DContext::_GetDeviceContext()->HSSetConstantBuffers(slot, 1, &buf);
}
static void BindShaderResource(int slot, ID3D11ShaderResourceView *SRV)
{
D3DContext::_GetDeviceContext()->HSSetShaderResources(slot, 1, &SRV);
}
static void BindSampler(int slot, ID3D11SamplerState *sampler)
{
D3DContext::_GetDeviceContext()->HSSetSamplers(slot, 1, &sampler);
}
};
//其他Shader Stages实现与上面的类似,不一一列举
这样一来,不同Shader Stages间的这些操作的差异就被一个int类型的模板参数抹平了,而且没有簿记成本。比如要设置SamplerState,只需要这样写:
ResourcesBinder<ShaderType>::BindSampler(slot_, sampler_);
其中ShaderType
可以是VERTEX_SHADER
,HULL_SHADER
等等(当然,它也必须是静态的)。
实现Resource Object
以Sampler State的设置为例,我们期望实现的效果是这样的:
shader->GetPSSamplerState("mySampler")->SetSampler(samplerState);
这里面GetPSSamplerState
返回的就应该是个Sampler Object的指针,现在来讨论一下它的实现。之前提到用int类型的模板参数抹掉了不同Shader Stages之间操作的差异,因此这个模板参数也就应该出现在我们的 Sampler Object中(这样Sampler Object才能用这个参数来设置Sampler State)。它应该长这样:
template<int _ShaderSelector>
class SamplerObject
{
//...
}
在我的设计中,这个Object里面缓存了一个ID3D11SamplerState。也就是说:
private:
int slot_;
std::string name_;
ID3D11SamplerState *sampler_;
当然这只是一个粗略的设计,但足够我们说明了。理所当然地,我们的Sampler Object应该有一个设置Sampler的方法,即:
public:
void SetSampler(ID3D11SamplerState *sampler)
{
if(sampler_) sampler_->Release();
if(sampler) sampler->AddRef();
sampler_ = sampler;
}
这里使用了作为COM Interface的ID3D11SamplerState自带的引用计数功能,目的是防止sampler_所保存的ID3D11SamplerState对象因外部Release而被销毁。
当然,还有个关键的东西,就是把持有的Sampler State绑定到渲染管线,这时我们的模板参数就要派上用场了,如下:
public:
void Bind(void)
{
ResourcesBinder<_ShaderSelector>::BindSampler(slot_, sampler_);
}
最后放一下整个Sampler Object的代码:
namespace ShaderAux
{
template<int _ShaderSelector>
class SamplerObject
{
public:
friend class SamplerObjectManager<_ShaderSelector>;
using Type = SamplerObject<_ShaderSelector>;
static constexpr int ShaderType = _ShaderSelector;
void SetSampler(ID3D11SamplerState *sampler)
{
if(sampler_) sampler_->Release();
if(sampler) sampler->AddRef();
sampler_ = sampler;
}
void Bind(void)
{
ResourcesBinder<ShaderType>::BindSampler(slot_, sampler_);
}
ID3D11SamplerState *GetSampler(void)
{
return sampler_;
}
int GetSlot(void) const
{
return slot_;
}
const std::string &GetName(void) const
{
return name_;
}
private:
SamplerObject(int slot, const std::string &name)
:slot_(slot), name_(name), sampler_(nullptr)
{
}
SamplerObject(const Type&) = delete;
Type &operator=(const Type&) = delete;
~SamplerObject(void)
{
if(sampler_) sampler_->Release();
}
int slot_;
std::string name_;
ID3D11SamplerState *sampler_;
};
}
Resource Object Manager
注意到上面Sampler Object的完整实现中其构造函数和析构函数都是private的,而它只有一个友元类SamplerObjectManager<_ShaderSelector>
,现在就来看一下它的实现:
namespace ShaderAux
{
template<int _ShaderSelector>
class SamplerObjectManager
{
public:
using Type = SamplerObjectManager<_ShaderSelector>;
using SamplerObjectType = SamplerObject<_ShaderSelector>;
static constexpr int ShaderType = _ShaderSelector;
SamplerObjectManager(void) = default;
SamplerObjectManager(const Type&) = delete;
Type &operator=(const Type&) = delete;
~SamplerObjectManager(void)
{
Destroy();
}
SamplerObjectType *GetSamplerObject(const std::string &name)
{
auto it = samplers_.find(name);
if(it == samplers_.end())
return nullptr;
assert(it->second.sampler);
return it->second.sampler;
}
void BindAll(void)
{
for(auto it : samplers_)
it.second.sampler->Bind();
}
bool Add(const std::string &name, int slot)
{
if(samplers_.find(name) != samplers_.end())
return false;
samplers_[name] = { slot, new SamplerObjectType(slot, name) };
return true;
}
void Destroy(void)
{
for(auto it : samplers_)
delete it.second.sampler;
samplers_.clear();
}
private:
struct _SamplerRecord
{
int slot;
SamplerObjectType *sampler;
};
std::map<std::string, _SamplerRecord> samplers_;
};
}
简单地说,就是实现了一个\(Name \to Sampler Object\)的映射,我比较偷懒,这里就直接拿std::map做了。
流程汇总
到现在为止,可以给出一个Shader封装的总体形貌了:
- Shader中除了有D3D11的Shader对象外,还有对应的Resource Object Manager,并将其各类Get/Set方法暴露出来以供使用。
- 初始化时,通过Shader Reflection取得Shader中各种Shader Object的信息,用这些信息来初始化各种Resource Object Manager,创建对应的Resource Object。
Shader Stage的封装
静态分派again
之前用到的static dispatcher是针对ConstantBuffers、Samplers等的,现在故技重施,做个关于Shader本身的:
namespace ShaderAux
{
template<int _ShaderSelector> struct ShaderStageSpecialization;
template<> struct ShaderStageSpecialization<VERTEX_SHADER>
{
using ID3DShaderType = ID3D11VertexShader;
static void Bind(ID3DShaderType *shader)
{
D3DContext::_GetDeviceContext()->VSSetShader(shader, nullptr, 0);
}
static ID3DShaderType *InitShader(void *shaderCompiled, int length)
{
ID3DShaderType *result = nullptr;
if(FAILED(D3DContext::_GetDevice()->CreateVertexShader(shaderCompiled, length, nullptr, &result)))
return nullptr;
return result;
}
};
//其他Shader Stages
}
这里有两个操作,分别是把Shader对象绑定到渲染管线,以及根据编译过的ShaderByteCode创建对应的D3D Shader对象。
一般Shader Stage的封装
现在可以给出Shader Stage的代码了。Shader Stage代表了渲染管线中的一个可编程阶段(programmable stage),提供设置Shader中的各种Resources、把Shader绑定到管线等功能。简单地说,我们的Shader Stage就是用一个类把ID3D11XXXShader和其对应的各种Resource Object Manager包装了起来。代码如下:
namespace ShaderAux
{
template<int _ShaderSelector> class _ShaderStage
{
public:
static constexpr int ShaderType = _ShaderSelector;
using _SPEC = ShaderStageSpecialization<ShaderType>;
using Type = _ShaderStage<_ShaderSelector>;
_ShaderStage(void) = default;
_ShaderStage(const Type&) = delete;
Type &operator=(const Type&) = delete;
~_ShaderStage(void)
{
Destroy();
}
bool AddResources(void *shaderCompiled, int length)
{
//使用Shader Reflection初始化那几个Resource Object Manager
}
bool InitializeShader(void *shaderCompiled, int length)
{
shader_ = _SPEC::InitShader(shaderCompiled, length);
return shader_ != nullptr;
}
void Destroy(void)
{
ReleaseD3DObjects(shader_);
cbMgr_.Destroy();
srMgr_.Destroy();
samMgr_.Destroy();
}
template<typename _ConstantBufferType, bool _IsDynamic>
ConstantBufferObject<_ConstantBufferType, ShaderType, _IsDynamic> *
GetConstantBufferObject(const std::string &name)
{
cbMgr_.GetConstantBufferObject<_ConstantBufferType, _IsDynamic>(name);
}
ShaderResourceObject<ShaderType> *
GetShaderResourceObject(const std::string &name)
{
return srMgr_.GetShaderResourceObject(name);
}
SamplerObject<ShaderType> *
GetSamplerObject(const std::string &name)
{
return samMgr_.GetSamplerObject(name);
}
void BindAllConstantBuffers(void)
{
cbMgr_.BindAll();
}
void BindAllShaderResources(void)
{
srMgr_.BindAll();
}
void BindAllSamplers(void)
{
samMgr_.BindAll();
}
void BindAllResources(void)
{
BindAllConstantBuffers();
BindAllShaderResources();
BindAllSamplers();
}
void Bind(void)
{
_SPEC::BindShader(shader_);
}
protected:
ConstantBufferObjectManager<ShaderType> cbMgr_;
ShaderResourceObjectManager<ShaderType> srMgr_;
SamplerObjectManager<ShaderType> samMgr_;
typename _SPEC::ID3DShaderType *shader_ = nullptr;
};
using VertexShaderStage = _ShaderStage<VERTEX_SHADER>;
using HullShaderStage = _ShaderStage<HULL_SHADER>;
using DomainShaderStage = _ShaderStage<DOMAIN_SHADER>;
using GeometryShaderStage = _ShaderStage<GEOMETRY_SHADER>;
}
注意到代码的最后只利用_ShaderStage
定义了Vertex、Hull、Domain、Geometry的Shader Stage,之所以没有PixelShaderStage,是因为它的一些特殊性:Unordered Access View(UAV)是可以被用在Pixel Shader Stage上的(事实上UAV只能被用在PS和CS上,这里说的是渲染管线所以就不考虑CS了)。于是Pixel Shader Stage被做了特殊对待,它继承_ShaderStage<PIXEL_SHADER>
,并提供了额外的关于UAV的接口:
namespace ShaderAux
{
class PixelShaderStage : public _ShaderStage<PIXEL_SHADER>
{
public:
PixelShaderStage(void)
:_ShaderStage()
{
}
PixelShaderStage(const PixelShaderStage&) = delete;
PixelShaderStage &operator=(const PixelShaderStage&) = delete;
~PixelShaderStage(void)
{
Destroy();
}
bool AddResources(void *shaderCompiled, int length)
{
//使用Shader Reflection初始化那几个Resource Object Manager
}
void Destroy(void)
{
_ShaderStage::Destroy();
RWMgr_.Destroy();
}
RWResourceObject *GetRWResourceObject(const std::string &name)
{
return RWMgr_.GetRWResourceObject(name);
}
void BindAllRWResources(void)
{
RWMgr_.BindAll();
}
void BindAllResources(void)
{
_ShaderStage::BindAllResources();
BindAllRWResources();
}
protected:
RWResourceObjectManager RWMgr_;
};
}
这就完成了各个Shader Stages的封装。如果高兴的话可以用一个Shader类把它们都包装起来,这里就不再赘述了。
PS
这些东西和各个成熟的渲染引擎中的Shader框架不可同日而语(代码加起来也就几百行),算是我学习DirectX11时的一点想法。