DirectX11的Shader Reflect的几个问题(2012-2-22更新)

CSDN贴地址

http://blog.csdn.net/noslopforever/article/details/7269353

 

很久没有做图形系统的东西了,之前因为家中出了些事情,把精力主要放到了一些杂务和俗务上。最近回过头来看看,离京前的日记里,清楚地标明着,DX11 Shader Reflect的问题还没有解决。

DX9时代带过来的习惯,因为DX9时代的Shader编撰是离不开D3DX库的,因此,感觉上总是习惯把Shader Reflect和ID3DXEffect9理解为同一个层次的东西,而且当时学习DX的时候,也是Effect的文档要远多于Shader Reflect的文档。

因此带来的一个毛病就是,写Shader系统时一直是比较依赖Effect的,组织结构上基于Effect来组织,即便是迁移OpenGL的时候,也会考虑直接使用CG里跟Effect相近的一系列概念。Effect好用、简单、粗暴、有效,围绕其进行组织也没有什么大错,只不过,它确实还不够“本质”,准备写篇“本质论”什么的,如果拿这个堂而皇之地写进去,估计要笑掉围观者的大牙。

因此,当DX11一下子将Effect“扫进垃圾堆”的时候,我多少有些措手不及。编译一个Samples里带的CEffect,总感觉不如自己全新去写一个或者研究一下,到底是怎么个情况,既然您带了CEffect的代码,读读总是无害的。

1.Basic & Misc

1? Shader Parameter以Constant Buffer来组织,不放到某个具体CB的,默认会认为是放到了全局的一个CB中。

2 一次更新一个CBuffer [?],这样比起之前DX9时一刷整个Shader所有的参数要好。如果不用CB,就是DX9那个样子。

3 CB最好按频率来进行组织:PerView、PerDrawCall、PerObject……

4 想到既然CB和Texture一个地位,也就是说Texture和Sampler虽然放到了Global下,但仍然也只是一刷只一个,不会一刷刷一堆的哈,与此同理,UAV哪些东西也是一样的咯?

2.关于Parameter

2.1.基础概念Basic

D3DReflect一个Shader后,交还给用户的是一个ID3D11ShaderReflection接口。这个接口有几个应用是对Parameter分析有关系的:

GetConstantBufferByIndex / GetConstantBufferByName

GetResourceBindingDesc / GetResourceBindingDescByName

Constant Buffer的总数量和Resource Binding的总数量在ID3D11ShaderReflection->GetDesc里面。

那么,这两者什么关系呢?

粗略来说,Resource Binding里包括很多,CBuffer、TBuffer、Texture、Sampler、UAVResource,都在里面,这里面的CBuffer一般就是我们所说的参数部分,这些东西统称为Resource Binding,也就是Resource Binding不仅仅包括CBuffer。

但是Resource Binding Description只有这些资源的一些基本信息:

Name:Resource名称,对于CBuffer来说就是你写的那个"cbuffer xxxx {  ...... } "里的那个xxxx,CBuffer的本名。

Type:此Resource是个啥玩意儿?对应D3D_SHADER_INPUT_TYPE。0(D3D_SIT_CBUFFER)就是CBuffer。

BindPoint / BindCount:绑定到了哪个索引上,总共绑了多少个。对于CBuffer而言,就是XSSetConstantBuffer()里那些个index该怎么填,查这里就对了。

uFlags:Resource Binding的这个uFlags对应D3D_SHADER_INPUT_FLAGS。

其它几个都跟纹理有关,跟CBuffer半毛钱关系没有,暂时无视之。

 

呃,好了,问题来了:这里面一个参数的信息都没有呀?!

没错,因为这里才是对Shader而言真正有用的东西:Shader绑定的资源。

参数是你程序员看了方便的东西,跟Shader本身就半毛钱关系没有,我这绑定资源的接口,凭啥要给你参数信息呢?

不过参数确实是太有用了,没了参数,那不抓瞎了,啥都跟DX文档上那样写,一个cbuffer什么样,我就C++写一个struct也什么样,然后提交CBuffer,我累不累……

所以人微软还是很厚道滴给了另一组信息,就是GetConstantBufferByIndex / GetConstantBufferByName这一组接口。

这组接口里信息就多了,直接一下子返回了一个新的接口:ID3D11ShaderReflectionConstantBuffer,这里面就有一大堆参数信息了,Description里的Variables就是CBuffer里面所有参数的总数量,另一个有用的信息就是Size:指出了这个CBuffer的大小。

使用这个接口,首先通过GetDesc获取Variables的总数量,然后就可以继续foreach,调用GetVariableByIndex,或者直接通过GetVariableByName来获取某个参数的具体信息了。

 

参数信息存储于GetVariableByxxxx返回的ID3D11ShaderReflectionVariable中。包括Variable本身的信息:

Name:名字,不解释。

StartOffset:在CB里的起始字节。

Size:占了CB多少个字节。

uFlags:对应D3D_SHADER_VARIABLE_FLAGS,比较有用的是检查是不是D3D_SVF_USED,没有Used的就不需要刷了,参数表也可以无视之。

 

还有就是Type信息,就是这个Variable是int4?float4x4?数组?Struct?这个就不解释了,填值的时候,肯定得对照着这个来填,要不一个int4填成一个float4那就麻烦了。

提醒一下,注意这个uFlags,虽然都是INT uFlags,但在几个不同的地方其对应的实际的enum是不同的。

ID3D11ShaderReflection::GetResourceBindingDesc ——D3D_SHADER_INPUT_FLAGS

ID3D11ShaderReflectionConstantBuffer::GetDesc——D3D_SHADER_CBUFFER_FLAGS

ID3D11ShaderReflectionVariable::GetDesc——D3D_SHADER_VARIABLE_FLAGS

2.2关于组织模式:

基本上要解决的几个问题是:

这个Shader总共引用了多少个CBuffer?这个可以通过Resource Binding的分析得到。

每个CBuffer里的参数是怎么组织的?这个可以通过分析ID3D11ShaderReflectionConstantBuffer得到。

原理介绍到这里,现在介绍几种之前看到过的组织模式:

一种是先分析CBuffers,得到所有的Parameter与CB的对应关系,然后建立出CB放到哪里,然后再分析ResourceBinding,再将这些CB按照RB里的Binding Point来Cache到对应的索引上,最后提交时,调用XSSetConstantBuffer刷入所有CB。KlayGE使用的就根这个模式很像,特别是,印象里,Klay并没有把CB按照RB里的Binding Point重排位置,而是按照CBuffers的获取时传入的Index(就是foreach CBuffers那会儿)直接建立了CB表,这两者是否真正一致?待验证。

另一种是直接上来就分析Resource Binding,如果当前分析到的是CBuffer,那么就根据Resource的名字,通过GetConstantBufferByName来获取Constant Buffer,再继续分析Parameters。CEffect和U3都是这个思路。与前者没有根本的不同,只是习惯怎么写方便就怎么写的区别吧。

最后U3的专门要说明一下,首先,每个Shader参数本身存储参数信息的空间肯定是必须有的,我们称之为Shadow Buffer,这个肯定得有,要不你参数计入进来了,没个参照的,难道要直接刷到CBuffer里去?万一没Dirty你这不白白占了个Cpu-Gpu传输吗,无论哪种方法这里没有不同的。组织上有不同的地方,主要是在于CBuffer。

看到其他的实现更多的都是每个Shader留存自己独有的CB列表,估计每个人写,一开始都会直接想到这里的,这么写直观、方便,也没有什么不适的。

但U3的做法不同,粗略看了一下,它的做法是总共就建立了8个(?)CB,所有Shader共享这8个CB。新的Shader到来后,分析时会瞅瞅自己某个索引所需的CB大小是否会超出此索引现有的CB大小,如果会就重刷这个索引的CB。刷参数的时候也很有意思,会根据参数Dirty与否,尽可能刷少的字节数,也就是整个CB可能256个字节,但发现前32个字节动了,就只刷前32个。理论上,刷少量字节确实会取得好的效果,到底能省多少,这块儿最好试验一下。最大的问题在于,不同Shader的CBuffer是铁定不一样的,您这里省了,新Shader跟旧的Shader不一样的地方,就该刷还得刷,这样做到底有什么好处?难道是为了减少显存的占用?(如果Shader数量较多的话,CB确实也是个不小的开销)目前没有想太明白,当然,也有可能是看的太粗略了,还没有掌握精髓,继续看吧。

——2012-2-22更新——

我现在主要采用的是第二种模式,伪代码可能如下:

ShaderReflectionDesc srd = ID3D11ShaderReflection->GetDesc();

uint _CurrentParsingCBIndex= 0;

std::vector<Parameter > CachedParameters;

std::vector<ConstantBuffer> CachedCBuffers;

foreach (ResourceBinding rb in srd .ResourceBindings)

{

    if (rb.Type == CONSTANT_BUFFER)

    {

        // 分析此CBuffer

        ID3D11ShaderReflectionConstantBuffer* srcb = ID3D11ShaderReflection->GetConstantBufferByName(rb.Name);

        foreach (Variable v in srcb.Variables)

        {

            // 没用到的Parameter无视

            if(v .Flags & FLAG_USED == 0)

                continue;

            // 增加一个Parameter

            Parameter param {

                Name = v.Name; Offset = v.Offset; ArraySize = v.ArraySize; ByteSize = v.Size; CBufferIndex = _CurrentParsingCBIndex;

            };

            CachedParameters.pushback(param);

        }

        // 增加一个CBuffer到Cache中

        CachedCBuffers.pushback( ConstantBuffer{ Name == rb.Name; BindingPoint = rb.BindingPoint; } );

        _CurrentParsingCBIndex ++;

        assert(CachedCBuffers.size() == _CurrentParsingCBIndex);

    }

……

}

2.2.1-----2012-2月22日更新----关于U3

又看了看U3 CBuffer这块儿的组织,不是怪异,而是牛逼。

U3因为它整个渲染引擎是具有一整套完整的体系的,而且借助于其优秀的Vertex Factory / Material Template设计,所以可以做到一开始就把所有的Constant全部合理安排的地步,因此它把自己所可能用到的所有CB按照频率和其它因素设置成了8个Buffer。

例如,其Vertex Factory相关的Buffer,在整个Static Mesh的各个SubMesh渲染完之前,是不会Update的,因为没有Update的必要,里面记录的World Matrix信息在这中间是不会改动的。

这样合理的划分即便是相比于使用Effect Pool,也要合理很多,而很多人使用Effect连Effect Pool都不用,性能上的浪费可想而知!因为Effect的实现,是每个Effect自己为自己下属的所有Shader保存一个Cbuffer Cache的,每切换一次Effect,哪怕Shader本身没有变化,CBuffer都会强制重新提交。而且更重要的是,Shader间的CBuffer不共享,明明两个Shader的某个CBuffer的信息完全一样,但只是因为Shader不一样,这个Shader的World Matrix变化还需要在另一个Shader里面设置一次,使得CBuffer重新提交的数量级迅速增加。而U3的组织流程就不存在这个问题。

当然了,整个游戏每次只需要渲染不到100个批次,你用Effect和自己管理Shader代价当然差不了多少了。

但是10000个呢?材质爆炸后呢?单不同Shader的这些Cache Buffer的浪费就是一个恐怖的数量。须知Unreal3就算在材质良好设计大量使用Material Instance的情况下,Shader Pool仍能保持在K这个数量级,还是没开DX11的,K个Shader的Cached Buffer,如果每个Shader独立存储的话,再加上需要临时记录在内存里的Shadow Buffer,是什么概念呢?可想而知。

但是,为什么U3可以这么做呢?是因为它不是一个通用化图形引擎,而是一个体系化的游戏引擎。

通用化图形引擎你不知道别人会怎么使用你的引擎,有些人做《Magicka》那样的小品级游戏(没有任何别的意思,我很喜欢这种小品级游戏),你迫使他花精力去管理各种Buffer组织什么的,人家才不跟你玩那个呢,XNA比你这套方便多了。

但还有些人,整个场景里1000个物体,1000种不同的材质,每个材质都一个独立Shader,这种游戏你如果只支持Effect式的方案就麻烦了,浪费+切换,在这方面可能就会落后一些。

所以,根据实际情况来选择吧。

2.2.2——2012-2-22——关于KlayGE的CBuffer(?)

“印象里,Klay并没有把CB按照RB里的Binding Point重排位置,而是按照CBuffers的获取时传入的Index(就是foreach CBuffers那会儿)直接建立了CB表,这两者是否真正一致?”

手头暂时没有Klay,之前看过,也不敢确认自己到底是不是记得准确。Klay是个优秀的引擎,事实上去年自己用过一段时间,很好用,写的也很不错。这里本文并无意于去讨论Klay,本文还是主要想讨论CB的使用和组织问题,如果这里小生记错了,还望龚大海涵。

这个问题的答案是——不一致。如果在使用CBuffer时,强制通过register(b13)把CBuffer设到靠后的位置,就会很快发现这种不一致。

所以,组织CBuffer时,千万记得按照Resource Binding里的Binding Point信息重排CBuffer的位置,否则在后面Set Constant Buffer时,传入的Index就有可能是错的

例如我建立了1个CBuffer,在register b13,但是按照Desc.ConstantBuffers获取出来的数量是1个,所以如果按照ConstantBuffers来阻止的话,这个CB的索引是0,而不是13,提交时如果也按照这个索引提交,就会把本身应提交到13的CB提交给0。

2.3关于XSSetConstantBuffer

与Effect不同,Shader Reflect还需要解决的一个问题在于:Effect的参数,设置一次,自动就会在应用Pass时决定哪个Shader Update哪些参数。咱们自己Shader Reflect就得自己处理这个工作了。

这个取决于具体的组织模式了,如果您的Shader系统也是按照类似Effect的模式来组织的,那么这里倒不会有太多麻烦,按之前说的,把参数表不要存到Shader里,而是存到Effect里,提交的时候,Shader的CBuffer都去Effect里获取Dirty参数信息,然后决定怎么刷自己,怎么提交,问题不大。

但是如果您的Shader系统跟Effect的组织模式完全不一样呢?

那就麻烦了,首先就要确认什么时候刷参数的问题。Set Shader的时候刷?不合适,因为Set Shader后您再改这个Shader的参数,就得等下次Set同一个Shader时才能起作用了。所以一般对使用者不太造成影响的做法可能就是把提交的时机定在Draw Primitive时,判断当前的Shader是不是有参数更新、或者是不是新的Shader,有就刷,没有就过。
确认什么时候刷参数后,就是最头疼的问题了,对于Effect,可以共享参数,一个WorldMatrix,只需要对一个Effect(甚至一个EffectPool)设置一次,所有这个Effect下的Shader、只要这个参数启用了,就自动刷自己的CB并提交。但对于我们自己Reflect的Shader,怎么处理共享参数呢?

别告诉我您希望对每个Shader:VS、GS、DS、HS、PS,各调用一遍SetParamValue("WorldMatrix", m_matWorld);

这样基本上会导致下面的结果:

1,对写Shader的人要求增加了,他必须自己清楚自己的Shader里到底用到了哪些参数,并在需要的时候更新这些参数。写了没用到的参数,就白白浪费了效率。

2,即便是都有用的参数,也不好优化,VS里的WorldMatrix和PS里的WorldMatrix,必须都得手动调用设置一遍,每次都得查询->刷新,Effect只需要查询一次即可。

目前想到的没更好的办法,只能是按这个说的来,自己写Shader,自己维护参数。

2.3.1——2012-2-22——再论U3的组织

上面说到的问题,感觉U3的那个思路也是很不错的,但是就像之前所说的,那是一种体系化游戏引擎的思路,对于做通用化图形引擎来说并没有太多的参考价值。因为实际的使用者很可能并不能像U3那样去把整个应用程序的CBuffer安排出来,而且这样做对于使用者本身的要求就太高了,会把很多创意高手、图形新手给拒之门外。

另外就是也可以考虑一下Effect Pool的那个思路,把一些可能会共享的参数通过一个Pool,来使得若干个从此Pool产生的Shader可以共享。但是Effect Pool本身事实上是通过对Effect脚本语法分析得出来的,如果要做这个,而不想在D3D的基础上引出太多概念,要怎么做确实还是一个新的问题。

引擎要做成什么样,只有做引擎的人最了解,没有一成不变的方案,只有最适合自己的方案。No Silver Bullets,任何时候都别忘记这一点,既然没有银弹,与其花时间去追寻“一揽子解决方案”,不妨更多时候考虑考虑“他想要什么”。

2.4 关于Samplers、Textures的组织

这玩意基本上比前面的CBuffer要简单,因为它不存在着一个CBuffer里好几个Sampler,Texture每个要去算Offset的问题。

所以只需要围绕着各自Resource Binding的BindingPoint、BindingCount建表即可,也是需要注意Binding Point的问题,Texture和Sampler也是可能会通过register给强制设到后面的。

此外,Sampler、Textures还需要注意的一点在于他们是可以“Array化”的。

是的:

Texture2D GMRTs[4];

可以这样。

有什么不同吗?

两点:

第一,他们的Array信息记录在BindingCount里,上面的情况,BindingCount就不再是正常情况下的1了,而是4。这个是废话,因为前面已经强调过这个问题了。

最需注意的在于第二点,你通过Name获得到的这个变量的变量名不是GMRTs,而是GMRTs[4]!所以如果按照经验,直接把这个Name放到变量表里,后面您可就找不到这个变量咯。

解决方案不麻烦:放到变量表前,先看看有木有“[x]”,有的话干掉,然后再放到变量表即可。

还需注意的是,Texture和Sampler跟CB一样,也是可以在不同的Shader间通用的喔~~所以,优化CBuffer的思路也可以跟这些放到一起来做的~

others...

想到再写,继续维护,

至少会想把TBuffer、Texture、Sampler、Array什么的都给写写,DX这套挺好玩,感觉做起来虽然麻烦点,但却深入到很多底层的层面,探索的乐趣才是真正的乐趣。

posted @ 2012-02-22 11:09  天堂里的死神  Views(806)  Comments(1Edit  收藏  举报