引言

这个寒假学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]
TypeUINT
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]
TypeUINT
Number of buffers to set (ranges from 0 to D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT - StartSlot).
ppConstantBuffers [in, optional]
TypeID3D11Buffer*
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

我很擅长灌了半瓶子水就出发,这次也不例外——在搞明白上一节中说的内容后,我就觉得可以动手写代码了,于是有了这样的鬼畜设计:

  1. 读取待编译的Shader的源代码,手动Parse一下,取得必要的信息(比如,用了哪些ConstantBuffer,用了哪些SamplerState等)。

  2. 为代码中未指明寄存器的资源生成寄存器使用方案,然后根据这个重新生成一份Shader的代码,新代码中所有的资源都显式地指定了通过哪个寄存器访问。

  3. 把这份重新生成的代码用DirectX11提供的方法编译,创建Shader并投入使用。

这样一来,在第二步的时候,应用程序就已经对Shader中的所有信息一清二楚(因为自己Parse了一遍,还给把所有不明确的东西都显式指定了),然后就可以仿照D3DX11Effect方便地设置Shader中的变量值和资源了。

然后我就准备动手了(无知者无畏啊)。我注意到D3D11在编译Shader的时候是只注意使用了的变量的,比如这样写:

Texture2D tex;
Texture2D normalMap;
SamplerState texSampler;
SamplerState normalMapSampler;
//...something elsefloat4 PS(PSInput input) : SV_TARGET
{    return tex.Sample(texSampler, input.texCoord);
}

也就是说我虽然定义了normalMapnormalMapSampler,但是没用上,那么编译过后如果发现这两个东西根本不存在,也不要惊讶。

这样一来,直接解析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 struct ResourcesBinder;    template<> struct ResourcesBinder
    {        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
    {        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::BindSampler(slot_, sampler_);

其中ShaderType可以是VERTEX_SHADERHULL_SHADER等等(当然,它也必须是静态的)。

实现Resource Object

以Sampler State的设置为例,我们期望实现的效果是这样的:

shader->GetPSSamplerState("mySampler")->SetSampler(samplerState);

这里面GetPSSamplerState返回的就应该是个Sampler Object的指针,现在来讨论一下它的实现。之前提到用int类型的模板参数抹掉了不同Shader Stages之间操作的差异,因此这个模板参数也就应该出现在我们的 Sampler Object中(这样Sampler Object才能用这个参数来设置Sampler State)。它应该长这样:

templateclass 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    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::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    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 samplers_;
    };
}

简单地说,就是实现了一个NameSamplerObjectName→SamplerObject的映射,我比较偷懒,这里就直接拿std::map做了。

流程汇总

到现在为止,可以给出一个Shader封装的总体形貌了:

  1. Shader中除了有D3D11的Shader对象外,还有对应的Resource Object Manager,并将其各类Get/Set方法暴露出来以供使用。

  2. 初始化时,通过Shader Reflection取得Shader中各种Shader Object的信息,用这些信息来初始化各种Resource Object Manager,创建对应的Resource Object。

Shader Stage的封装

静态分派again

之前用到的static dispatcher是针对ConstantBuffers、Samplers等的,现在故技重施,做个关于Shader本身的:

namespace ShaderAux
{    template struct ShaderStageSpecialization;    template<> struct ShaderStageSpecialization
    {        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 class _ShaderStage
    {    public:        static constexpr int ShaderType = _ShaderSelector;        using _SPEC = ShaderStageSpecialization;        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
        ConstantBufferObject<_ConstantBufferType, ShaderType, _IsDynamic> *
        GetConstantBufferObject(const std::string &name)
        {
            cbMgr_.GetConstantBufferObject<_ConstantBufferType, _IsDynamic>(name);
        }

        ShaderResourceObject *
        GetShaderResourceObject(const std::string &name)
        {            return srMgr_.GetShaderResourceObject(name);
        }

        SamplerObject *
        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 cbMgr_;
        ShaderResourceObjectManager srMgr_;
        SamplerObjectManager samMgr_;        typename _SPEC::ID3DShaderType *shader_ = nullptr;
    };    using VertexShaderStage = _ShaderStage;    using HullShaderStage = _ShaderStage;    using DomainShaderStage = _ShaderStage;    using GeometryShaderStage = _ShaderStage;
}

注意到代码的最后只利用_ShaderStage定义了Vertex、Hull、Domain、Geometry的Shader Stage,之所以没有PixelShaderStage,是因为它的一些特殊性:Unordered Access View(UAV)是可以被用在Pixel Shader Stage上的(事实上UAV只能被用在PS和CS上,这里说的是渲染管线所以就不考虑CS了)。于是Pixel Shader Stage被做了特殊对待,它继承_ShaderStage,并提供了额外的关于UAV的接口:

namespace ShaderAux
{    class PixelShaderStage : public _ShaderStage
    {    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_;
    };
}