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? Shader Parameter以Constant Buffer来组织,不放到某个具体CB的,默认会认为是放到了全局的一个CB中。
2 一次更新一个CBuffer [?],这样比起之前DX9时一刷整个Shader所有的参数要好。如果不用CB,就是DX9那个样子。
3 CB最好按频率来进行组织:PerView、PerDrawCall、PerObject……
4 想到既然CB和Texture一个地位,也就是说Texture和Sampler虽然放到了Global下,但仍然也只是一刷只一个,不会一刷刷一堆的哈,与此同理,UAV哪些东西也是一样的咯?
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
基本上要解决的几个问题是:
这个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);
}
……
}
又看了看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式的方案就麻烦了,浪费+切换,在这方面可能就会落后一些。
所以,根据实际情况来选择吧。
“印象里,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。
与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,自己维护参数。
上面说到的问题,感觉U3的那个思路也是很不错的,但是就像之前所说的,那是一种体系化游戏引擎的思路,对于做通用化图形引擎来说并没有太多的参考价值。因为实际的使用者很可能并不能像U3那样去把整个应用程序的CBuffer安排出来,而且这样做对于使用者本身的要求就太高了,会把很多创意高手、图形新手给拒之门外。
另外就是也可以考虑一下Effect Pool的那个思路,把一些可能会共享的参数通过一个Pool,来使得若干个从此Pool产生的Shader可以共享。但是Effect Pool本身事实上是通过对Effect脚本语法分析得出来的,如果要做这个,而不想在D3D的基础上引出太多概念,要怎么做确实还是一个新的问题。
引擎要做成什么样,只有做引擎的人最了解,没有一成不变的方案,只有最适合自己的方案。No Silver Bullets,任何时候都别忘记这一点,既然没有银弹,与其花时间去追寻“一揽子解决方案”,不妨更多时候考虑考虑“他想要什么”。
这玩意基本上比前面的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这套挺好玩,感觉做起来虽然麻烦点,但却深入到很多底层的层面,探索的乐趣才是真正的乐趣。