深入理解Direct3D9

原文: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显示开放驱动和硬件架构信息,这些东西就很容易弄明白了。

发表于


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/hziee_/archive/2010/06/22/5687181.aspx

你可能感兴趣的:(cache,command,query,buffer,图形,Direct3D)