DirectX11中Shader的封装

时间:2022-10-03 08:25:53

引言

​ 这个寒假学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

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

  1. 读取待编译的Shader的源代码,手动Parse一下,取得必要的信息(比如,用了哪些ConstantBuffer,用了哪些SamplerState等)。
  2. 为代码中未指明寄存器的资源生成寄存器使用方案,然后根据这个重新生成一份Shader的代码,新代码中所有的资源都显式地指定了通过哪个寄存器访问。
  3. 把这份重新生成的代码用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);
}

也就是说我虽然定义了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<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_SHADERHULL_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封装的总体形貌了:

  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<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时的一点想法。