3D引擎优化方法整理

来自:http://blog.csdn.net/udking/article/details/6048210

【游戏开发】DirectX 性能优化:(From D3D9 SDK)2009-06-04 15:19性能优化:(From D3D9 SDK) 常规技巧

1 只在必须的时候Clear。
IDirect3DDevice9::Clear函数通常需要花费较多的时间,因此要尽量少调用,而且只清空的确需要清空的缓存。

2 尽量减少状态切换。并且将需要进行的状态切换组合在一起设置。
状态包括RenderState,SamplerState,TextureStageState等

3 纹理尺寸尽可能小

4 从前至后渲染场景中的对象
从前至后渲染可以尽可能早地精选出不需要绘制的对象和象素

5 使用三角条带代替三角列表和三角扇。为了能更有效利用顶点高速缓存(cache),在排列条带时因考虑尽快重用顶点。

6   根所需要据消耗的系统资源来逐步减少特效。

7 经常性地检测程序的性能。
这样可以更容易发现引起性能突变的部分

8 最小化顶点缓存的切换

9 尽可能使用静态顶点缓存

10 对静态对象,对每种FVF使用一个大的静态顶点缓存来保存多个对象的顶点数据,而不是每个对象使用一个顶点缓存。
其目的也是减少顶点缓存的切换

11 如果程序需要随机访问AGP内存中的顶点缓存,顶点格式的大小最好是32bytes的倍数。否则,选择合适的最小的格式。
32bytes 也就是8个float数据或2个vector4。

12 使用顶点索引方式渲染,这样可以更有效利用顶点高速缓存。

13 如果深度缓存格式中包含有模版缓存,总是将两者一起Clear。

14 将计算结果和输出的shader指令合并:
// Rather than doing a multiply and add, and then output the data with
// two instructions:
mad r2, r1, v0, c0
mov oD0, r2

// Combine both in a single instruction, because this eliminates an 
// additional register copy.
mad oD0, r1, v0, c0

建立一个场景对象的数据库,首先使用最低精度的模型,在保证性能的前提下逐步使用更高精度的模型。密切关注渲染的总的三角面数。


http://nvidia.e-works.net.cn/document/200910/article9305_1.htm
将使用相同渲染状态和贴图的图元集中在一起绘制,这样能尽量减少顶点缓存和状态的切换。并且将状态切换操作集中成一组设置。

尽量减少光源数量,使用环境光来提高亮度。方向光源比点光源和聚光灯更高效,因为光的方向是固定的。使用光照范围参数来剔除不受光照影响的物体。镜面高光几乎使光照计算量加倍,因此只在需要时使用,
将D3DRS_SPECULARENABLE设为FALSE,将材质的specular power设为0,将材质的specular color 设为0。

尽量减小纹理尺寸,这样可以增加纹理被缓存的可能性。尽量减少纹理的切换,将使用同一纹理的对象集中绘制。尽量使用正方形纹理。最快的纹理是256×256,将4张128×128的纹理拼接成256×256使用。

连接World-View Matrix, 将ViewMatrix设为Identity减少矩阵乘法运算。

动态纹理。首先要检查D3DCAPS2_DYNAMICTEXTURES来判断硬件是否支持。
其二,动态纹理不能放在MANAGED pool中。动态纹理总是能锁定,甚至是在D3DPOOL_DEFAULT中。D3DLOCK_DISCARD是合法的。
DrawProceduralTexture(pTex)
{
// pTex should not be very small because overhead of
// calling driver every D3DLOCK_DISCARD will not
// justify the performance gain. Experimentation is encouraged.
pTex->Lock(D3DLOCK_DISCARD);

pTex->Unlock();
pDev->SetTexture();
pDev->DrawPrimitive();
}

当需要在每帧里锁定顶点或索引缓存是,应该使用动态缓存(D3DUSAGE_DYNAMIC)。对动态缓存使用D3DLOCK_DISCARD锁定能减少延迟。D3DLOCK_NOOVERWRITE锁定可以用于在缓存空闲处添加新的数据而不修改
已经写入的数据。

使用Effect时,应该根据Effect,然后根据Technique来安排渲染顺序,也就是使用相同Effect和Technique的物体应该集中绘制。这样可以减少状态切换开销。

一般来说, 定位渲染通道瓶颈的方法就是改变渲染通道每个步骤的工作量, 如果吞吐量也改变了, 那个步骤就是瓶颈.。找到了瓶颈就要想办法消除瓶颈, 可以减少该步骤的工作量, 增加其他步骤的工作量。

 

 

 

   一般在光栅化之前的瓶颈称作”transform bound”, 三角形设置处理后的瓶颈称作”fill bound”定位瓶颈的办法:
1.改变帧缓冲或者渲染目标(Render Target)的颜色深度(16 到 32 位), 如果帧速改变了, 那么瓶颈应该在帧缓冲(RenderTarget)的填充率上。

2.否则试试改变贴图大小和贴图过滤设置, 如果帧速变了,那么瓶颈应该是在贴图这里。

3.否则改变分辨率.如果帧速改变了, 那么改变一下pixel shader的指令数量, 如果帧速变了, 那么瓶颈应该就是pixel shader. 否则瓶颈就在光栅化过程中。

4.否则, 改变顶点格式的大小, 如果帧速改变了, 那么瓶颈应该在显卡带宽上。

5.如果以上都不是, 那么瓶颈就在CPU这一边。

优化方法36条:

1.尽量减少无用的顶点数据, 比如贴图坐标, 如果有Object使用2组有的使用1组, 那么不 要将他们放在一个vertex buffer中, 这样可以减少传输的数据量。

2.使用多个streamsource, 比如SkinMesh渲染, 可以把顶点坐标和法线这些每一帧都要修改的数据放在一个动态VB中, 其它不需要修改的(如贴图坐标)放到一个静态VB中, 这样就减少了数据传输量。

3.尽量使用16位的索引缓冲,避免32位的. 一方面浪费带宽, 一方面也不是所有的显卡都支持32位的索引缓冲。

4.可以考虑使用vertex shader来计算静态VB中的数据.比如SkinMesh的顶点可以放到vectex shader中计算, 这样就可以避免每一帧都从AGP内存中向显存传送数据. 这样也可以使用静态VB了。

5.坚决避免使用Draw**UP一族的函数来绘制多边形。

6.在设计程序之前好好规划一下显卡内存的使用, 确保framebuffer, 贴图, 静态VB能够正好放入显卡的本地内存中。

7.尽量使顶点格式大小是32字节的倍数.可以考虑使用压缩过的顶点格式然后用vertex shader去解. 或者留下冗余的部分, 使顶点大小刚好使32字节的倍数。

8.顶点在顶点缓冲中的顺序尽量符合绘制的顺序, 考虑使用strips来代替list。

9.如果可能尽量多的使用static vertex buffer代替dynamic vertex buffer。

10.动态VB使用DISCARD参数来lock更新, 使用NOOVERWRITE来添加.尽量不要使用不带参数的lock调用(0)。

11.尽量减少lock的次数, 有些东西并不一定非要每一帧都更新VB, 比如人物动画一般每秒钟更新30次VB基本上就够了。

12.如果是因为需要绘制的顶点数据太多了可以考虑使用LOD, 但是现在的显卡的绘制能力都很强劲, 所以需要权衡一下LOD是否能够带来相应的好处, 如果过分的强化LOD很可能将瓶颈转移到CPU这边。

13.避免过多的顶点计算,比如过多的光源, 过于复杂的光照计算(复杂的光照模型), 纹理自动生成的开启也会增加顶点的计算量. 如果贴图坐标变换矩阵不是单位矩阵, 也会造成顶点计算量的增加,
所以如果纹理变换已经结束, 记得要将纹理变换矩阵设为单位矩阵同时调整贴图坐标。

14.避免Vertex shader指令数量太多或者分支过多, 尽量减少vertex shader的长度和复杂程度. 尽量使用swizzling代替mov。

15.如果图象质量方面的计算(pixel shader)范围很大, 并且很复杂, 可以考虑试试全屏反走样。说不定更快。

16.尽量按照front – back的顺序来绘制。

17.在shader中判断Z值可以避免绘制不可见的象素, 但是nvidia建议简单的shader不要这么做.(Don't do this in a simple shader)。

18.如果可能, 尽量使用vertex shader来代替pixel shader.将计算从逐象素变成逐顶点。

19.尽量降低贴图的大小.过大的贴图可能造成贴图cache过载, 从而导致贴图cache命中降低.过大的贴图会导致显存过载, 这时候贴图是从系统内存中取的。

20.只要可能就用16位色的贴图, 如环境贴图或者shadow map.它们用32位色的贴图实在是浪费。

21.考虑使用DXT 贴图压缩。

22.如果可能,使用简单的贴图过滤或者mip map, 除非必要否则尽量不要使用三线过滤和各项异性过滤. light map 和 环境贴图基本上都不需要使用它们。

23.只有真正需要修改的贴图才使用Dynamic, 并且使用DISCRAD和WRITEONLY来lock。

24.太多的帧缓冲读写可以考虑关闭Z-Writes如有些多pass的渲染中的后续pass或者粒子系统等半透明几何物体(如果可以)。

25.可能的话尽量使用alpha test代替alpha blending。

26.如果不需要stencil buffer就尽量使用16位的Z buffer。

27.减小RenderTarget 贴图的大小, 如shadow map 环境贴图. 可能根本不需要那么大效果就很好。

28.Stencil 和 Z buffer 尽量一起clear. 他们本来就是一块缓冲。

29.尽量减少渲染状态的切换, 尽量一次画尽可能多的多边形。(根据显卡性能决定最多画多少, 不过一般再多也不会多到哪里去。 除非你根本不需要贴图和渲染状态的切换)。

30.尽量使用shader来代替Fixed Pipeline。

31.尽量使用shader来实现来取代Multipass渲染效果。

32.尽量优先先建立重要的资源, 如Render target, shaders, 贴图, VB, IB等等.以免显存过载的时候它们被创建到系统内存中。

33.坚决不要在渲染循环中调用创建资源。

34.按照shader和贴图分组后再渲染.先按照shaders分组再按贴图。

35.Color Stencil Z buffer尽量在一次Clear调用中清除。

36.一个Vertex buffer 的大小在2M-4M之间最好。

 

 

 


转 深入理解D3D9
文章来源:http://www.cnblogs.com/effulgent/archive/2009/02/10/1387438.html
深入理解D3D9对图形程序员来说意义重大,我把以前的一些学习笔记都汇总起来,希望对朋友们有些所帮助,因为是零散笔记,思路很杂,还请包涵。

其实只要你能完美理解D3DLOCK、D3DUSAGE、D3DPOOL、LOST DEVICE、QUERY、Present()、BeginScene()、EndScene()等概念,就算是理解D3D9了, 不知道大家有没有同感。有如下几个问题,
如果你能圆满回答就算过关:)。
1、       D3DPOOL_DEFAULT、D3DPOOL_MANAGED、D3DPOOL_SYSTEMMEM和D3DPOOL_SCRATCH到底有何本质区别?
2、       D3DUSAGE的具体怎么使用?
3、       什么是Adapter?什么是D3D Device?HAL Device和Ref Device有何区别?Device的类型又和Vertex Processing类型有什么关系?
4、       APP(CPU)、RUNTIME、DRIVER、GPU是如何协同工作的?D3D API是同步函数还是异步函数?
5、       Lost Device到底发生了什么?为什么在设备丢失后D3DPOOL_DEFAULT类型资源需要重新创建?

在D3D中有三大对象,他们是D3D OBJECT、D3D ADAPTER和D3D DEVICE。D3D OBJECT很简单,就是一个使用D3D功能的COM对象,其提供了创建DEVICE和枚举ADAPTER的功能。ADAPTER是对计算机图形硬件
和软件性能的一个抽象,其包含了DEVICE。DEVICE则是D3D的核心,它包装了整个图形流水管线,包括变换、光照和光栅化(着色),根据D3D版本不同,流水线也有区别,比如最新的D3D10就包含了新的
GS几何处理。图形管线的所有功能由DRIVER提供,而DIRVER分两类,一种是GPU硬件DRIVER,另一种是软件DRIVER,这就是为什么在D3D中主要有两类DEVICE, REF和HAL,使用REF DEVICE时,图形管线
的光栅化功能由软件DRIVER在CPU上模拟的,REF DEVICE从名字就可以看出这个给硬件厂商做功能参考用的,所以按常理它应该是全软件实现,具备全部DX标准功能。而使用HAL DEVICE时,RUNTIME则将
使用HAL硬件层控制GPU来完成变换、光照和光栅化,而且只有HAL DEVICE中同时实现了硬件顶点处理和软件顶点处理(REF DEVICE一般不能使用硬件顶点处理,除非自己在驱动上做手脚,比如PERFHUD)。
另外还有个一个不常用的SOFTWARE DEVICE,用户可以使用DDI编写自己的软件图形驱动,然后注册进系统,之后便可在程序中使用。

检查系统软件硬件性能。
在程序的开始我们就要判断目标机的性能,其主要流程是:
确定要用的缓冲格式
GetAdapterCount()
GetAdapterDisplayMode

GetAdapterIdentifier //得到适配器描述
CheckDeviceType //判断指定适配器上的设备是否支持硬件加速
GetDeviceCaps //指定设备的性能,主要判断是否支持硬件顶点处理(T&L)
GetAdapterModeCount //得到适配器上指定缓冲格式所有可用的显示模式
EnumAdapterModes //枚举所有显示模式
CheckDeviceFormat
CheckDeviceMultiSampleType
详细使用请参考DX文档。


WINDOWS图形系统的主要分为四层:图形应用程序、D3D RUNTIME、SOFTWARE DRIVER和GPU。此四层是按功能来分的,实际上他们之间界限并不如此明确,比如RUNTIME中其实也包含有USER MODE的
SOFTWARE DRIVER,详细结构这里不再多说。而在RUNTIME里有一个很重要的结构,叫做command buffer,当应用程序调用一个D3D API时,RUNTIME将调用转换成设备无关的命令,然后将命令缓冲
到这个COMMAND BUFFER中,这个BUFFER的大小是根据任务负载动态改变的,当这个BUFFER满员之后,RUNTIME会让所有命令FLUSH到KERNEL模式下的驱动中,而驱动中也是有一个BUFFER的,用来存储
已被转换成的硬件相关的命令,D3D一般只允许其缓冲最多3个帧的图形指令,而且RUNTIME和DRIVER都会被BUFFER中的命令做适当优化,比如我们在程序中连续设置同一个RENDER STATE,我们就会
在调试信息中看到如下信息“Ignoring redundant SetRenderState - X”,这便是RUNTIME自动丢弃无用的状态设置命令。在D3D9中可以使用QUERY机制来与GPU进行异步工作,所谓QUERY就是查
询命令,用来查询RUNTIME、DRIVER或者GPU的状态,D3D9中的QUERY对象有三种状态,SIGNALED、BUILDING和ISSUED,当他们处于空闲状态后会将查询状态置于SIGNALED STATE,查询分开始和结束,
查询开始表示对象开始记录应用程序所需数据,当应用程序指定查询结束后,如果被查询的对象处于空闲状态,则被查询对象会将查询对象置于SIGNALED状态。GetData则是用来取得查询结果,
如果返回的是D3D_OK则结果可用,如果使用D3DGETDATA_FLUSH标志,表示将COMMAND BUFFER中的所有命令都发送到DRIVER。现在我们知道D3D API绝大部分都是同步函数,应用程序调用后,
RUNTIME只是简单的将其加入到COMMAND BUFFER,可能有人会疑惑我们如何测定帧率?又如何分析GPU时间呢?对于第一个问题我们要看当一帧完毕,也就是PRESENT()函数调用是否被阻塞,
答案是可能被阻塞也可能不被阻塞,要看RUNTIME允许缓冲中存在的指令数量,如果超过额度,则PRESENT函数会被阻塞下来,如何PRESENT完全不被阻塞,当GPU执行繁重的绘制任务时,
CPU工作进度会大大超过GPU,导致游戏逻辑快于图形显示,这显然是不行的。测定GPU工作时间是件很麻烦的事,首先我们要解决同步问题,要测量GPU时间,首先我们必须让CPU与GPU异步工作,
在D3D9中可以使用QUERY机制做到这点,让我们看看Accurately Profiling Driect3D API Calls中的例子:
IDirect3DQuery9* pQueryEvent;

//1.创建事件类型的查询事件
m_pD3DDevice->CreateQuery( D3DQUERYTYPE_EVENT, &pQueryEvent);
//2.在COMMAND BUFFER中加入一个查询结束的标记,此查询默认开始于CreateDevice
pQueryEvent->Issue(D3DISSUE_END);
//3.将COMMAND BUFFER中的所有命令清空到DRIVER中去,并循环查询事件对象转换到SIGNALED状态,当GPU完成CB中所有命令后会将查询事件状态进行转换。
while(S_FALSE == pQueryEvent->GetData( NULL, 0, D3DGETDATA_FLUSH) )
     ;
LARGE_INTEGER start, stop;
QueryPerformanceCounter(&start);
SetTexture();
DrawPrimitive();
pQueryEvent->Issue(D3DISSUE_END);
while(S_FALSE == pQueryEvent->GetData( NULL, 0, D3DGETDATA_FLUSH) )
       ;
QueryPerformanceCounter(&stop);

1.第一个GetData调用使用了D3DGETDATA_FLUSH标志,表示要将COMMAND BUFFER中的绘制命令都清空到DRIVER中去,当GPU处理完所有命令后会将这个查询对象状态置SIGNALED。
2.将设备无关的SETTEXTURE命令加入到RUNTIME的COMMAND BUFFER中。
3.将设备无关的DrawPrimitive命令加入到RUNTIME的COMMAND BUFFER中。
4.将设备无关的ISSUE命令加入到RUNTIME的COMMAND BUFFER中。
5.GetData会将BUFFER中的所有命令清空到DRIVER中去,注意这是GETDATA不会等待GPU完成所有命令的执行才返回。这里会有一个从用户模式到核心模式的切换。
6.等待DRIVER将所有命令都转换为硬件相关指令,并填充到DRIVER BUFFER中后,调用从核心模式返回到用户模式。
7.GetData循环查询 查询对象 状态。当GPU完成所有DRIVER BUFFER中的指令后会改变查询对象的状态。

如下情况可能清空RUNTIME COMMAND BUFFER,并引起一个模式切换:
1.Lock method(某些条件下和某些LOCK标志)

2.创建设备、顶点缓冲、索引缓冲和纹理
3.完全释放设备、顶点缓冲、索引缓冲和纹理资源
4.调用ValidateDevice
5.调用Present
6.COMMAND BUFFER已满
7.用D3DGETDATA_FLUSH调用GetData函数

对于D3DQUERYTYPE_EVENT的解释我不能完全理解(Query for any and all asynchronous events that have been issued from API calls)明白的朋友一定告诉我,只知道当GPU处理完
D3DQUERYTYPE_EVENT类型查询在CB中加入的D3DISSUE_END标记后,会将查询对象状态置SIGNALED状态,所以CPU等待查询一定是异步的。为了效率所以尽量少在PRESENT之前使用BEGINSCENE ENDSCENE对,
为什么会影响效率?原因只能猜测,可能EndScene会引发Command buffer flush这样会有一个执行的模式切换,也可能会引发D3D RUNTIME对MANAGED资源的一些操作。而且ENDSCENE不是一个同步方法,
它不会等待DRIVER把所有命令执行完才返回。

D3D RUTIME的内存类型,分为3种,VIDEO MEMORY(VM)、AGP MEMORY(AM)和SYSTEM MEMORY(SM),所有D3D资源都创建在这3种内存之中,在创建资源时,我们可以指定如下存储标志,
D3DPOOL_DEFAULT、D3DPOOL_MANAGED、D3DPOOL_SYSTEMMEM和D3DPOOL_SCRATCH。VM就是位于显卡上的显存,CPU只能通过AGP或PCI-E总线访问到,读写速度都是非常慢的,CPU连续写VM稍微快于读,
因为CPU写VM时会在CACHE中分配32或64个字节(取决于CACHE LINE长度)的写缓冲,当缓冲满后会一次性写入VM;SM就是系统内存,CPU读写都非常快,因为SM是被CACHE到2级缓冲的,
但GPU却不能直接访问到系统缓冲,所以创建在SM中的资源,GPU是不能直接使用的;AM是最麻烦的一个类型,AM实际也存在于系统内存中,但这部分MEM不会被CPU CACHE,意味着CPU读写AM都会写来个
CACHE MISSING然后才通过内存总线访问AM,所以CPU读写AM相比SM会比较慢,但连续的写会稍微快于读,原因就是CPU写AM使用了“write combining”,而且GPU可以直接通过AGP或PCI-E总线访问AM。

如果我们使用D3DPOOL_DEFAULT来创建资源,则表示让D3D RUNTIME根据我们指定的资源使用方法来自动使用存储类型,一般是VM或AM,系统不会在其他地方进行额外备份,当设备丢失后,
这些资源内容也会被丢失掉。但系统并不会在创建的时候使用D3DPOOL_SYSTEMMEM或D3DPOOL_MANAGED来替换它,注意他们是完全不同的POOL类型,创建到D3DPOOL_DEFAULT中的纹理是不能被CPU LOCK的,
除非是动态纹理。但创建在D3DPOOL_DEFAULT中的VB IB RENDERTARGET BACK BUFFERS可以被LOCK。当你用D3DPOOL_DEFAULT创建资源时,如果显存已经使用完毕,则托管资源会被换出显存来释放足够的空间。
D3DPOOL_SYSTEMMEM和D3DPOOL_SCRATCH都是位于SM中的,其差别是使用D3DPOOL_SYSTEMMEM时,资源格式受限于Device性能,因为资源很可能会被更新到AM或VM中去供图形系统使用,
但SCRATCH只受RUNTIME限制,所以这种资源无法被图形系统使用。 D3DRUNTIME会优化D3DUSAGE_DYNAMIC 资源,一般将其放置于AM中,但不敢完全保证。另外为什么静态纹理不能被LOCK,动态纹理却可以,
都关系到D3D RUNTIME的设计,在后面D3DLOCK说明中会叙述。

D3DPOOL_MANAGED表示让D3D RUNTIME来管理资源,被创建的资源会有2份拷贝,一份在SM中,一份在VM/AM中,创建的时候被放置L在SM,在GPU需要使用资源时D3D RUNTIME自动将数据拷贝到VM中去,
当资源被GPU修改后,RUNTIME在必要时自动将其更新到SM中来,而在SM中修改后也会被UPDATE到VM去中。所以被CPU或者GPU频发修改的数据,一定不要使用托管类型,这样会产生非常昂贵的同步负担。
当LOST DEVICE发生后,RESET时RUNTIME会自动利用SM中的COPY来恢复VM中的数据,因为备份在SM中的数据并不是全部都会提交到VM中,所以实际备份数据可以远多于VM容量,随着资源的不断增多,
备份数据很可能被交换到硬盘上,这是RESET的过程可能变得异常缓慢,RUNTIME给每个MANAGED资源都保留了一个时间戳,当RUNTIME需要把备份数据拷贝到VM中时,RUNTIME会在VM中分配显存空间,
如果分配失败,表示VM已经没有可用空间,这样RUNTIME会使用LRU算法根据时间戳释放相关资源,SetPriority通过时间戳来设置资源的优先级,最近常用的资源将拥有高的优先级,这样RUNTIME通
过优先级就能合理的释放资源,发生释放后马上又要使用这种情况的几率会比较小,应用程序还可以调用EvictManagedResources强制清空VM中的所有MANAGED资源,这样如果下一帧有用到MANAGED资源,
RUNTIME需要重新载入,这样对性能有很大影响,平时一般不要使用,但在关卡转换的时候,这个函数是非常有用的,可以消除VM中的内存碎片。LRU算法在某些情况下有性能缺陷,比如绘制一帧所需资源
量无法被VM装下的时候(MANAGED),使用LRU算法会带来严重的性能波动,如下例子:

BeginScene();
Draw(Box0);
Draw(Box1);
Draw(Box2);
Draw(Box3);
Draw(Circle0);
Draw(Circle1);
EndScene();
Present();

假设VM只能装下其中5个几何体的数据,那么根据LRU算法,在绘制Box3之前必须清空部分数据,那清空的必然是Circle0……,很显然清空Box2是最合理的,所以这是RUNTIME使用MRU算法处理后续
Draw Call能很好的解决性能波动问题,但资源是否被使用是按FRAME为单位来检测的,并不是每个DRAW CALL都被记录,每个FRAME的标志就是BEGINSCENE/ENDSCENE对,所以在这种情况下合理使用
BEGINSCENE/ENDSCENE对可以很好的提高VM不够情况下的性能。根据DX文档的提示我们还可以使用QUERY机制来获得更多关于RUNTIME MANAGED RESOURCE信息,但好像只在RUNTIME DEBUG模式下有用,
理解RUNTIME如何MANAGE RESOURCE很重要,但编写程序的时候不要将这些细节暴露出来,因为这些东西都是经常会变的。最后还要提醒的是,不光RUNTEIME会MANAGE RESOURCE,DRIVER也很可能也实
现了这些功能,我们可以通过D3DCAPS2_CANMANAGERESOURCE标志取得DRIVER是否实现资源管理功能的信息,而且也可以在CreateDevice的时候指定D3DCREATE_DISABLE_DRIVER_MANAGEMENT来关闭
DRIVER资源管理功能。 

D3DLOCK探索D3D RUNTIME工作

如果LOCK DEFAULT资源会发生什么情况呢?DEFAULT资源可能在VM或AM中,如果在VM中,必须在系统内容中开辟一个临时缓冲返回给数据,当应用程序将数据填充到临时缓冲后,UNLOCK的时候,RUNTIME
会将临时缓冲的数据传回到VM中去,如果资源D3DUSAGE属性不是WRITEONLY的,则系统还需要先从VM里拷贝一份原始数据到临时缓冲区,这就是为什么不指定WRITEONLY会降低程序性能的原因。CPU写AM也
有需要注意的地方,因为CPU写AM一般是WRITE COMBINING,也就是说将写缓冲到一个CACHE LINE上,当CACHE LINE满了之后才FLUSH到AM中去,第一个要注意的就是写数据必须是WEAK ORDER的
(图形数据一般都满足这个要求),据说D3DRUNTIME和NV DIRVER有点小BUG,就是在CPU没有FLUSH到AM时,GPU就开始绘制相关资源产生的错误,这时请使用SFENCE等指令FLUSH CACHE LINE。第二请尽量
一次写满一个CACHE LINE,否则会有额外延迟,因为CPU每次必须FLUSH整个CACHE LINE到目标,但如果我们只写了LINE中部分字节,CPU必须先从AM中读取整个LINE长数据COMBINE后重新FLUSH。第三尽可
能顺序写,随机写会让WRITE COMBINING反而变成累赘,如果是随机写资源,不要使用D3DUSAGE_DYNAMIC创建,请使用D3DPOOL_MANAGED,这样写会完全在SM中完成。

普通纹理(D3DPOOL_DEFAULT)是不能被锁定的,因为其位于VM中,只能通过UPDATESURFACE和UPDATETEXTURE来访问,为什么D3D不让我们锁定静态纹理,却让我们锁定静态VB IB呢?我猜测可能有2个
方面的原因,第一就是纹理矩阵一般十分庞大,且纹理在GPU内部已二维方式存储;第二是纹理在GPU内部是以NATIVE FORMAT方式存储的,并不是明文RGBA格式。动态纹理因为表明这个纹理需要经常修改,
所以D3D会特别存储对待,高频率修改的动态纹理不适合用动态属性创建,在此分两种情况说明,一种是GPU写入的RENDERTARGET,一种是CPU写入的TEXTURE VIDEO,我们知道动态资源一般是放置在AM中的,
GPU访问AM需要经过AGP/PCI-E总线,速度较VM慢许多,而CPU访问AM又较SM慢很多,如果资源为动态属性,意味着GPU和CPU访问资源会持续的延迟,所以此类资源最好以D3DPOOL_DEFAULT和D3DPOOL_SYSTEMMEM
各创建一份,自己手动进行双向更新更好。千万别 RENDERTARGET以D3DPOOL_MANAGED 属性创建,这样效率极低,原因自己分析。而对于改动不太频繁的资源则推荐使用DEFAULT创建,自己手动更新,
因为一次更新的效率损失远比GPU持续访问AM带来的损失要小。

不合理的LOCK会严重影响程序性能,因为一般LOCK需要等待COMMAND BUFFER前面的绘制指令全部执行完毕才能返回,否则很可能修改正在使用的资源,从LOCK返回到修改完毕UNLOCK这段时间GPU全部处
于空闲状态,没有合理使用GPU和CPU的并行性,DX8.0引进了一个新的LOCK标志D3DLOCK_DISCARD,表示不会读取资源,只会全写资源,这样驱动和RUNTIME配合来了个瞒天过海,立即返回给应用程序另
外块VM地址指针,而原指针在本次UNLOCK之后被丢弃不再使用,这样CPU LOCK无需等待GPU使用资源完毕,能继续操作图形资源(顶点缓冲和索引缓冲),这技术叫VB IB换名(renaming)。

很多困惑来源于底层资料的不足,相信要是MS开放D3D源码,开放驱动接口规范,NV / ATI显示开放驱动和硬件架构信息,这些东西就很容易弄明白了。

顺便做个书的广告 《人工智能:一种现代方法》中文版 卓越网已经有货,AI巨作,不过阅读需要相当的基础,对思维非常有启迪,想买的朋友不要错过。后面我会将学习重点从图形转到AI上来,对AI有
兴趣的朋友一起交流。
 

 


[原创] 浅谈三维场景的渲染优化

                         浅谈三维场景的渲染优化初探
                               作者:rhett                                  

          
(一) 有效的性能评测
                         
  对于任何一个3D应用程序来说,追求场景画面真实感是一个无止尽的目标,其结果就是让我们的场景越来越复杂,模型更加精细,这必然给图形硬件带来极大的负荷以致于无法达到实时绘制帧率。因此,
渲染优化是必不可少的。在渲染优化之前,我们需要对应用程序性能进行系统的评测,找出瓶颈,对症下药。对于3D应用程序来说,影响性能的十分多,同时不同的硬件配置条件下,瓶径也会有所不同。因此,
对应用程序进行有效的性能评测,不仅需要对整个渲染管线原理有深入地了解,此外借助一些评测工具能让我们的工作事倍功半。
  我们知道渲染流水线的速度是由最慢的阶段决定首先,因此对一个3D应用程序进行评测,首先要分析影响渲染性能的瓶颈是在CPU端还是GPU端,由此来绝对我们优化的对象。由于目前的图形加速硬件都
具有强大的,这个瓶径往往出现在CPU端,我们可以通过一些工具获得这个信息,如Nvidia的NVPerfHUD。在评测选项中,我们可以查看CPU和GPU繁忙度这项,当CPU繁忙度是100%时,GPU还不是时,我们知
道性能的瓶颈在CPU端,我们必须CPU端的操作,同时尽量的“喂饱”GPU,把一些费事的计算移值到GPU上,例如硬件骨骼蒙皮。当GPU端是瓶颈时,说明GPU超荷负载,有可能是因为有过多的渲染填充,也就
是多边形数量太多(当前强大的GPU使得这种情况并不多见。
    CPU上的瓶颈产生有两个方面,一是因为复杂AI计算或低效的代码,二是由于不好的渲染批处理或资源管理。对于第一种情况,我们可以利用VTurn这类的工具,把应用程序中所有函数调用时间从大到小
的排列出来,我们就很容易知道问题所在。对第二种情况来说,同样利用NVPerfHUD,我们可以查看每帧的DP数目,看看批的数量是否过多(有一个具体的换算公式),查看纹理内存的数目,是否消耗了过
多的显存。利用这些工具,我们基本上能够定位应用程序的瓶颈。在应用程序内部,编写一个内嵌的profiler功能,能更加便利的进行评测,此外利用Lua这样的脚本程序,让我们运行时调试,也能提高评测的效率。

(二) 静态场景优化
   
    静态场景包括了地形、植被、建筑物等一般不改变位置的实体集合,对它的优化是场景优化中最重主要的内容。本文就静态场景优化的常见问题进行了探讨。

1 批的优化
    批是场景优化中的最重要的概念之一,它指的是一次渲染调用(DP),批的尺寸是这次渲染调用所能渲染的多边形数量。每个批的调用都会消耗一定的CPU时间,对于显卡来说,一个批里的多边形数量远
达不到最大绘制数量。因此尽可能将更多的多边形放在一个批里渲染,以此来减少批的数目,最终降低CPU时间,是批的优化基本原则。然而事情往往不尽如人意,有些情况下原有的批会被打破,造成额外的
开销,如纹理的改变或不同的矩阵状态。针对这些问题,我们可以采用一些方法来尽量避免它,已达到批尺寸的最大化。
(1)合并多个小纹理为一张大纹理    
    在某个场景中,地面上有十多种不同的植被,它们除了纹理不同外,渲染状态都是一样。我们就可以把它们的纹理打包成一个大纹理,再为每个植被模型指定UV,这样我们就可以用一个渲染调用来渲染
所有的物体,批的数量就从十多个降为一个。这种方法比较适合对纹理精度要求不高,面数不会太多的物体。
(2)利用顶点shader 来统一不同矩阵的情况  
    即使场景中的所有物体材质都一样,如果它们的矩阵状态不同(特别是场景图管理的引擎),也会打碎原有的批。利用顶点shader技术可以避免这种情况,因为可以把要乘的变换矩阵通过常量寄存器传
到shader程序中,这样统一了物体的矩阵状态,可以放在一个批里渲染。

2 渲染状态管理
    渲染状态是用来控制渲染器的渲染行为,在D3D中是setRenderState,通过改变渲染状态,我们可以设置纹理状态、深度写入等等。改变渲染状态对显卡来说,是个比较耗时的工作,因为显卡执行API必
须严格按照渲染路径,当渲染状态变化时,显卡就必须执行浮点运算来改变渲染路径,因此给CPU和GPU带来时间消耗(CPU必须等待),渲染状态变化越大,所要进行的浮点运算越多。因此将渲染状态进行
有效的管理,尽可能 减少其变化,对渲染性能影响巨大。(新六代的显卡Geforce8系列中将一些常见的状态参数集存储在显卡核心中,当渲染状态状态发生变化,可以直接读取保存的参数集,以消除不必要的开销)。绝大部分的3D引擎都会按照渲染状态对PASS进行分组渲染。

3 LOD
    LOD这个已经被人讨论烂掉的技术我就不多废话了,简单谈谈一些实际应用。地形的LOD我就不多说了,方法太多了,不过感觉目前情况下最实用的还是连锁分片的方法。对于模型LOD,自动减面的算法,
如VDPM(渐近网格子)并不少见,但是效果都很一般。常规的做法还是让美工做低模进行替换,对于复杂场景来说,模型LOD的效果还是比较明显的。材质LOD就需要一些技巧,例如可以将雾后的物体,
包括地形等统一成一种材质,采用雾的颜色,这样就统一了渲染状态,至于是否要打包成一个DP就要看具体情况了(这个统一的材质最好把光照影响关掉,这也是比较费时的)。至于角色模型的LOD和普通
模型LOD相类似,低模减少了顶点数,自然减少了蒙皮计算量。个人认为骨骼LOD不是特别的必要,看具体的情况。

4 场景管理的优化
    场景管理的优化包括场景分割,可见性剔除等,有很多的参考文章,这里就不多说了,谈些个人的心得。现在的室外场景一般采用quadtree或octree,当我们在性能评测时发现遍历树的过程比较慢时,
有可能有两个原因。一是树的深度设置的不合理,我们可以很容易寻找到一个最佳的深度。另一个原因可能是我们为太多数量 众多,但体积很小的物体分配了结点,造成结点数量的冗余。解决方法是把这些
小物体划分到他们所在的大的结点中。
    可见性剔除是最常见优化方法,我们常用的是视锥裁减,这也是非常有效的。视锥裁减也是许多优化方法,这里就不详说了。遮挡裁减也是经常被用到的方法,常见的有地平线裁减。但是在有些情况下,
遮挡裁减的效果并不明显,如当CPU使用率已经是100%时,CPU端是瓶颈,这时进行遮挡裁减计算消耗CPU时间,效果就不明显。但是有些情况下利用一些预生成信息的方法,降低遮挡裁减计算的复杂度,提高
遮挡裁减计算的效率,对场景性能会一定的改善。

 


Shader Model 4.0 全新架构

Shader Model4.0统一渲染架构 
微软的DirectX 9.0c距今离它的诞生已经有2年的光景,DX9.0c给我们带来了全新的Shader Model3.0技术,也使得3D画面较以往了有了质的突破,DirectX 9.0c是截至至今微软历史上寿命最长的一代API,
而图形技术的发展是不会停下脚步的,2006年微软发布了全新的DirectX 10.0,仅从版本上看比9.0c相差一级,但是DirectX 10.0带给我们的又将是一个全新的概念。
 

  在微软发布DX10.0后,NVIDIA积极响应,发布了完全符合DirectX 10.0的通用Shader架构图形处理器G80,也标志着DX9.0c将会逐步被DX10.0替代。相对DirectX 9.0c中的SM3.0,在Shader Model 4.0
中微软引入了统一着色架构,这才是DX10最大的改变。我们都知道,微软在DirectX 9中引入的了2.0/2.X/3.0三个版本的Vertex Shader(顶点着色引擎)以及Pixel Shader(像素着色引擎)。其中支持2.0版的
着色引擎是DirectX 9的GPU的最低标准,而当前主流的显卡已经都硬件支持加入了拥有更多高级处理功能的3.0版本着色引擎。


    不过,即便是DirectX 9.0c,对于功能相仿Vertex Shader、Pixel Shader来说,目前图形芯片厂商仍需要在GPU中划分两个区域来存放Vertex Shader阵列和Pixel Shader贴图流水线。
这无疑是一种资源冗余,而且这也加重GPU的设计难度及成本。当DirectX 10把渲染流程更细分为Vertex Shader、Geometry Shader及Pixel Shader,这个情况将会更为明显。而DX10.0的诞生
就将这2种渲染整合在了一起!

 

SM4.0较SM3.0的改进
 而在DirectX 10中引入了统一渲染架,通过一个整合Vertex Shader、 Pixel Shader的可编程整合光影处理器来完成目前Vertex Shader、Pixel Shader所有的工作。所谓统一渲染架构,最容易
的理解方式就是Shader单元不再分离,显示核心不再为Shader类型不同而配置不同类型的Shader单元,对于主流的显示核心,Pixel Shader单元以及vertex Shader单元的概念都应该已经非常熟悉了,
而在统一渲染架构中这两种Shader单元将不再分离,转而所有的Shader单元都可以为需要处理的数据进行处理,不管和是Pixel Shader数据还是Vertex Shader数据。

     而调配哪几组Shader单元负责处理什么数据或者进行什么样子类型的计算,则由一个被称为small sets of instructions(SSI)的部分来控制。这样在硬件上,设计者就无需为不同的着色引擎
设计不同的执行单元,只要按照所对应的接口以及操作方式全部融为一体,仅设置一种独立的Shader执行单元。这意味着GPU厂家可以用更小的核心来实现现在需要用8000万甚至更多晶体管才能实现的功能!


  相比原先的Shader Model 3.0,Shader Model 4.0最大指令数从512条增加到了64000条;临时暂存器数量也从原先的32个增加到惊人的4096个;允许同时对128个Texture进行操作
(Shader Model 3.0只允许16个);材质texture格式变为硬件支持的RGBE格式,其中的"E"是Exponent的省略,是RGB共同的说明,这在HDR的处理上有很大的作用,摒弃了以往需要专门decoding
处理HDR渲染的流程。 另外,对于纹理的尺寸Shader Model4.0也有惊人的提升,8192x8192的最高纹理分辩率比原先最高2048x2048的分辩率要高出4倍。G80图形核心对以上规格都给予了完整的硬件支持。

Shader Model4.0新特性
 Shader Model 4.0另一个重大变化就是在VS和PS之间引入了一个新的可编程图形层----几何着色器(Geometry Shader)。原来的Vertex Shader和Pixel Shader只是对逐个顶点或像素进行处理,
而新的Geometry Shader可以批量进行几何处理,快速的把模型类似的顶点结合起来进行运算。虽然其操作不会象Vertex Shader那样完整,只是处理器单个顶点的相关函数操作,但是这种操作却可
以确定整个模型的物理形状。这将大大加速处理器速度,因为其它Shader单元将不再去需要判定数据所存在的位置,而只是需要简单的为特定区域进行操作就可以了。

 

  Geometry Shader可以把点、线、三角等多边形联系起来快速处理、同时创造新的多边形,在很短时间内直接分配给其他Shader和显存而无需经过CPU,烟雾、爆炸等复杂图象不再需要CPU来处理。
从而极大的提高了CPU速度和显卡速度。游戏图象中可以出现许多精细场景,如不锈钢茶壶上清楚的反射出周围物体、超精细的人物皮肤等。


  为了最大程度的发挥Geometry Shader的威力,DX10硬件还专门设置了一个名为流输出层(Stream Output State)的部件来配合它使用。这个层的功能是将Vertex Shader和Pixel Shader处理
完成的数据输出给用户,由用户进行处理后再反馈给流水线继续处理。我们可以通过Stream Out把GPU拆成两段,只利用前面的一段几何运算单元。对某些科学研究,也许可以通过stream out来利用
GPU的数学运算能力,等于在CPU之外又平白多得了一个数学协处理器。举个例子,Doom3常用的Stencil shadow,因为CPU负担很重,广受批评。但是因为GS可以计算轮廓线, 还可以动态插入新的多边形,
有了Stream out之后,Shadow volume的生成就可以放到GPU端进行,实现Stencil shadow的硬件化,这将大大降低CPU占用。

统一着色架构
在以前的DirectX版本中,像素着色器因为受到常量寄存器、可用指令和总体流程可的限制总是运行在顶点着色器之后,因此程序员必须学会怎样分别去利用好顶点和像素着色器的权限。Shader model 4.0
则带来了与以往不同的统一着色架构,在DirectX 10基础上进行游戏开发,程序员不需要在避免着色冲突限制上花费时间,所有的统一架构着色器都能够使用GPU可以用的全部资源。

 

  Shader model 4.0在着色器程序可用资源的提升方面让人激动,在以往的DirectX下,开发者不得不仔细计算可用的寄存器资源,而在DirectX 10中,这些问题都不复存在,如上表所示,
总体上DirectX 10提供了超过10倍的DirectX 9可用资源。

更多的纹理和渲染
Shader Model 4.0支持纹理队列集,把开发者从繁重的拼接纹理图集的工作中解放出来,并能够在每个着色器上使用更多的特殊纹理实现更好的视觉效果。


  在Shader Model 4.0之前,过高的开销使在一个着色器操作上使用多个特殊纹理的操作基本无法实现。为了解决这个问题,开发把许多小的分散的纹理拼接成一个大的纹理;在运行层中,
着色器也需要进行额外的地址运算以便在拼接纹理图集中找到特定的纹理。纹理图集方式存在两个明显的缺点:首先小纹理之间的分界线回导致过滤操作错误;然后,DirectX 9的4096*4096
纹理尺寸限制也是纹理图集的总体规模受到局限。纹理队列集能够解决所有问题,它能够使用队列格式存储纹理,每个队列能存储512同尺寸个纹理,最大的可用纹理尺寸也提升到8192*8192。
为了促进这种应用,每个着色器可以操作的最大纹理数也提高到了128个,8倍于DirectX 9。


  更多的渲染对象
  多重渲染对象是DirectX 9时代的一个流行特性,它允许每个像素着色周期输出4个不同的渲染结果,从而高效率的在一个周期内渲染一个场景的4遍。在DirectX 10中,渲染对象的数目提高到8,
着极大的提高了着色器能实现的场景复杂程度,延迟渲染和其它一些图像空间优化算法将广泛的从中受益。

两种新的HDR格式
 两种新的HDR格式
  HDR(High dynamic range rendering)从支持浮点色彩格式的DirectX 9时代开始流行。不幸的是浮点格式比整数格式占用更多的寄存器空间而限制了其性能的发挥。如典型的FP16格式的每个
色彩数据需要占用16bits,这两倍于整数格式的空间占用。

 

 

  DirectX 10的新HDR格式能够在和FP16实现同样动态范围的前提下只占用50%的存储空间。第一种格式为R11G11B10,它使用11-bits的红色和绿色以及10-bits的蓝色来优化存储空间;第二种格
式是使用一个5-bits共享首位存储所有色彩然后每个色彩拥有9-bits尾址,这些简化的方法在HDR品质上和标准的FP16几乎没有差别。在最高级别的HDR方面,DirectX 10支持FP32的HDR,这可以用
于科学计算等对计算精度较高的应用程序。


  很显然,DirectX 10.0全新的Shader Model4.0对于消费者来说是一场全新的视觉革命,更逼真的3D游戏画面、流畅的高清视频回放是微软、显卡厂商推动技术发展的动力之源,在不远的将来
我们就会体会到全新的DX10、SM4.0给我们带来的饕餮大餐。

 


MX440 核心是NV17,和GF2类似,硬件只支持DX7,这个是没法用软件patch的。别说3DMark05,3DMark03都不能支持。Mark03的GT1只用了fix function texture stage,MX440可以支持,
GT2和GT3是PS 1.x,GT3是PS 2.0,都不行。Mark05三个GT全是PS 2.0以上的,更没戏。
DX8以上的特效,Vertex Shader可以通过软件模拟,Pixel Shader无法支持

 


关于3D引擎中的多线程渲染(Multithreaded-Rendering)的一些总结
[align=center][size=6][b]关于3D引擎中的多线程渲染(Multithreaded-Rendering) [/b][/size][/align][size=3]


困扰了一个多月的问题,今天终于有个阶段性的了结了,虽然不知道算不算真正的了结.

多核的cpu现在是大势所趋,渲染是一个很费时的活,所以应该考虑考虑能不能利用多核来提升这部分的性能.引擎一开始没有在多线程方面作任何的考虑,因为我从来就不喜欢多线程,这方面的思考能力不强,而且一开始写个单线程的engine已经够费事了,要加入多线程的设计对我来说实在是太难了.但是在积累了这么多时间的经验以后,我开始考虑加入多线程的支持,这玩意对架构的影响很大,所以加入设计还是宜早不宜迟.一开始先加入了多线程载入资源的模块,也顺便恢复一下早已荒废多年的多线程编程技能.接下来就该考虑将渲染部分放入一个单独的线程中去了.

首先,为什么呢?为什么要把渲染部分放到一个单独的线程中去呢?有什么好处呢?我的理解是这样的:显卡可以看成是一个外设,渲染的过程就是cpu不停的给gpu发各种命令,根据d3d的文档上说,d3d内部有一个command buffer,cpu在调用各种d3d的api时,其实是往这个command buffer里添加命令,这个command buffer是有一定大小的,当这个buffer满了以后,会有一个flush过程:这个buffer里的命令会被一齐扔给gpu去执行,然后这个buffer会被清空,以接受新的命令.按照d3d的文档上说,这个flush过程是很慢的,它必须要等待这个command buffer里的命令全部被gpu执行完,才会返回,不过我自己测了测,似乎不完全是这样,会有等待,但等待的时间好像并不是全部的执行完这些命令的时间,具体为什么,我也搞不清楚,也许写显卡驱动的人会更了解一些吧,d3d对我们这些写应用程序的人来说就是黑盒子.不过阻塞是一定会有的.所以我认为可以把gpu连同D3D看成一个类似硬盘的io设备.cpu通过调用d3d的api来给这个设备发命令,大多数情况下,这些api立即就返回了(这些命令被cache在d3d内部的一个command buffer里),但是偶尔的,当一个api调用试图在一个已经满了的command buffer里再加入命令时,就会触发一次d3d内部的flush过程,而这个时候,这个api调用就必须等待这个io设备了,也就是cpu这个时候是空闲的,计算能力在这里被浪费了.很多关于d3d的教材都提到不要调用太多的draw call,应该把多个draw call合并在同一个batch里,我以前以为,可能是因为d3d api的调用本身有开销,我现在的理解是,draw call太多,会导致太多的d3d api调用,太多的命令,而这样会导致d3d内部的command buffer频繁溢出,从而导致d3d内部频繁的flush,加剧了cpu等待gpu的情况. 如果d3d的内部运作模式真是我上面描述的那样的话,把渲染部分放到一个单独的线程中去就显得有必要了,因为可以把cpu在等待gpu的时间充分利用起来,或者至少不会让这些等待耽误到主线程的运行.

接下来,就要考虑怎么来做了.显然,这是一个典型的producer-consumer的结构,主线程(procucer)将要执行的渲染命令不停的加到一个队列里,再开一个渲染线程(consumer)不停的从这个个队列里读取命令,并执行.一开始我主要考虑要减少这些命令的个数,这样实现可以简单些,效率可以高些,所以打算把我自己实现的整个渲染系统放到一个单独的线程中去,把原来的接口全部转换为可以被队列化的一个个命令.干了一段时间后,发现实现难度还不小,接口数量还是太多了,所以又打算把接口提升到更高的层次中去,也就是把整个场景的渲染模块放到一个单独的线程中去,这个渲染模块主要包含了渲染对象的场景管理以及渲染分类.这样做的确减少了接口数量,所以虽然比较艰辛,我终于几乎还是把它给实现了,不过就在接近实现的时候,我又把整个问题重新考虑了一下,发现似乎又走了弯路.我的想法是这样:[/size]
[size=3][/size]

[list][*][size=3]对于目前大多数的游戏,每一帧的运算主要包括两部分:渲染和逻辑.[/size][/list][size=3][/size]
[list][*][size=3]对于3D游戏,gpu完成渲染部分,cpu完成逻辑部分.[/size][/list]
[list][*][size=3]渲染部分是非常耗时的,一般来说比逻辑部分的消耗的时间要多[/size][/list][size=3][/size]
[list][*][size=3]发展的趋势是cpu朝多核方向发展,逻辑部分可以被分到多个线程去做.[/size][/list][size=3][/size]
[list][*][size=3]XBOX360已经有6个核了.[/size][/list][size=3][/size]
[list][*][size=3]gpu也会发展,但是目前的即时渲染的水平离电影级的渲染水平仍然有很大的差距.[/size][/list][size=3][/size]
[list][*][size=3]所以,相当长的时间内,渲染的耗时仍然会是瓶颈[/size][/list][size=3][/size]
[list][*][size=3]而目前的D3D的架构只允许使用一个单独的线程去驱动gpu[/size][/list][size=3][/size]
[list][*][size=3]所以不应该让这个单独的线程做额外的工作了,也就是渲染线程应该越单纯越好,
[/size][*][size=3]而最最单纯的就是在这个渲染线程里只做一件事情,就是给D3D发命令.


[/size][/list][size=3]所以最后我采取了这样的方法,我把所用到的D3D接口函数全部重新写了一下,在这些函数里,我把原来对d3d的调用全部转化成一个个的命令,加到一个队列里,而由一个单独的渲染线程从这个队列里读取命令,再把它们传递给D3D.这样做还有一个附加的好处就是,它对整个引擎的架构的影响很小,所有的功能被局限在一个集中的模块里,并且可以很方便的enable/disable,很方便的在多线程/单线程渲染这两者之间切换,便于对比调试.而且如果这个模块写得完美,甚至可以开放给公众使用.

Threading the OGRE3D Render System(by Jeff Andrews),这篇文章也提到过这种方法,它提出了三种线程化的方案,主要也是按照渲染线程所处的层级来分的,和我上面说的比较类似,并且讨论了不少实现细节.不过它似乎认为在最高层次上进行线程化可以最大限度的发挥多核的威力,我想他可能是站在cpu计算瓶颈的角度上来看的吧.作者也为在最低层次上线程化的方案(重写D3D接口)作了实现(有源代码下载,我参考了一下),并做了测试,但效果似乎并不那么显著.不过他说it's doable.

实现这套接口对我来说并不是件容易的事,先是磨磨蹭蹭花了一周才写完,测了一下,结果很糟糕,在同事的双核电脑上比单线程版本要慢很多,以我贫乏的多核应用经验,一开始我以为是cpu的问题,后来在vista上测试的时候,发现了不错的结果,多线程版本比单线程版本几乎快了一倍(说实话,我第一次对vista有了好感),但是xp下就不行,进而怀疑操作系统的问题,下载了一个多核性能的bench mark,结果一切正常,问题看来还是在自己身上.最终还是找出了问题所在.目前的这个测试用例在双核的电脑上的有不小的性能提升,执行时间大概是单线程版本的60%左右,在单核的电脑上性能有微小的下降.还没有用更多的例子测过,所以不一定能说明问题,但至少说明是有前途的,是doable的.

实现过程中有一个地方值得注意,就是vertex buffer的Lock()/Unlock()的处理.上面说的这篇文章里也重点提到了这个问题,并提出了两种解决方法,Partially Buffered Locks和Fully Buffered Locks.我的方法有所不同,我将vb的lock分为三种情况:[/size]
[size=3][color=#ffffff].[/color][/size] [list=1][*][size=3]static的vertex buffer,通常这种vb用来存储不会发生变化的vertex 数据,一般只在初始化的时候需要lock()/unlock(),对性能的影响不是很大,所以在lock时,会对command buffer进行一次flush,也就是说主线程等待渲染线程将当前command buffer里的所有命令全处理完后,才进行lock()/unlock()
[color=#ffffff].[/color][/size][*][size=3]dynamic的vertex buffer,no overwrite的lock(),所谓no overwrite的lock,就是lock vb后,使用者可以保证不去覆写那些已经被用到的vertex数据(这些vertex的数据可能正在被gpu用来绘制),在这种情况下,可以比较简单的处理,只要暂时冻结渲染线程的处理,然后进行lock就行了,然后在unlock后,再恢复渲染线程的处理.
[color=white].[/color][/size][*][size=3]dynamic的vertex buffer,discard的lock(),这种lock将会丢弃原来vb中的所有内容,这时候显然不能简单的对这个vb使用discard标志进行lock,因为使用这个vb进行绘制的命令可能还在command buffer里,等待渲染线程的处理.我一开始的解决方法是:先冻结住渲染线程的执行,然后添加一条release这个vb的命令到command buffer中去,然后再创建一个新的dynamic的vertex buffer,并对它进行lock,返回lock的数据指针,当unlock后,再恢复渲染线程的执行.后来做了些优化,因为在nVidia的卡上,创建一个vertex buffer似乎对性能有很大的影响,所以我改为使用一个vertex buffer的pool,vb不会被真正release掉,而是扔到这个pool里,当需要新的vb时,再从这个pool里分配.这样可以使性能开销降到最低.
在实际应用中,情况2应该是最常使用的,它的性能开销也是最小的.只需要要锁一下渲染线程.
对于texture/surface的lock()/unlock(),似乎没有很好的方法,必须flush一下command buffer.好在这种情况并不会太频繁的出现.而偶尔flush一下command buffer也不是那么的不可忍受.

[/size][/list][size=3]顺便提一下多线程编程的一个注意点,A线程访问变量a,同时B线程访问变量b,如果a和b这两个变量在地址空间上离的很近的话,是会降低性能的,不能做到真正的同步访问,所谓false sharing.要避免这种情况,这两个变量要分配在不同的内存段上面,内存段的长度可能和硬件有关系吧,一般比如说128个字节,不放心的话,再远一点.[/size]

你可能感兴趣的:(3DEngine,3D引擎)