原文:Effulgent的《深入理解Direct3D9》整理版(转)

深入理解Direct3D9

深入理解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 ADAPTERD3D DEVICE

● D3D OBJECT很简单,就是一个使用D3D功能的COM对象,其提供了创建DEVICE和枚举ADAPTER的功能。

● ADAPTER是对计算机图形硬件和软件性能的一个抽象,其包含了DEVICE。

● DEVICE则是D3D的核心,它包装了整个图形流水管线,包括变换、光照和光栅化(着色);

DEVICE

根据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编写自己的软件图形驱动,然后注册进系统,之后便可在程序中使用。

Direct3D System Integration

检查系统软件硬件性能。
在程序的开始我们就要判断目标机的性能,其主要流程是:

  1. 确定要用的缓冲格式
  2. GetAdapterCount()
  3. GetAdapterDisplayMode
  4. GetAdapterIdentifier //得到适配器描述
  5. CheckDeviceType //判断指定适配器上的设备是否支持硬件加速
  6. GetDeviceCaps //指定设备的性能,主要判断是否支持硬件顶点处理(T&L)
  7. GetAdapterModeCount //得到适配器上指定缓冲格式所有可用的显示模式
  8. EnumAdapterModes //枚举所有显示模式
  9. CheckDeviceFormat
  10. 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)

Memory

  • 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。

所有D3D资源都创建在这3种内存之中,在创建资源时,我们可以指定如下存储标志

  • ● D3DPOOL_DEFAULT
  • ● D3DPOOL_MANAGED
  • ● D3DPOOL_SYSTEMMEM
  • ● D3DPOOL_SCRATCH。

如果我们使用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显示开放驱动和硬件架构信息,这些东西就很容易弄明白了。

【转载】深入理解Direct3D9的更多相关文章

  1. [serverlet][转载: 深入理解HTTP Session]

    [serverlet][转载: 深入理解HTTP Session] 标签(空格分隔): 未分类 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任. ...

  2. [转载] 深入理解Android之Java虚拟机Dalvik

    本文转载自: http://blog.csdn.net/innost/article/details/50377905 一.背景 这个选题很大,但并不是一开始就有这么高大上的追求.最初之时,只是源于对 ...

  3. 转载 深入理解JavaScript中的this关键字

    转载原地址: http://www.cnblogs.com/rainman/archive/2009/05/03/1448392.html 深入理解JavaScript中的this关键字   1. 一 ...

  4. [转载] 快速理解Kafka分布式消息队列框架

    转载自http://blog.csdn.net/xiaolang85/article/details/18048631 ==是什么 == 简单的说,Kafka是由Linkedin开发的一个分布式的消息 ...

  5. Linux安装Tomcat-Nginx-FastDFS-Redis-Solr-集群——【第五集之补充-转载“深入理解VMware虚拟网络”】

    郑重声明,此文太好,按耐不住要保存起来好好研究研究,如果侵权,联系我. 转载自王春海的http://blog.51cto.com/wangchunhai/381225,有所更改. 同时可以参考:htt ...

  6. [转载]深入理解JavaScript系列 --汤姆大叔

    深入理解JavaScript系列文章,包括了原创,翻译,转载,整理等各类型文章,如果对你有用,请推荐支持一把,给大叔写作的动力. 深入理解JavaScript系列(1):编写高质量JavaScript ...

  7. 转载:理解OAuth 2.0

    转载地址:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 作者: 阮一峰 日期: 2014年5月12日 OAuth是一个关于授权(autho ...

  8. (转载)理解Spatial Transformer Networks

    理解Spatial Transformer Networks 转载于:知乎-SIGAI 书的购买链接 书的勘误,优化,源代码资源 获取全文PDF请查看:理解Spatial Transformer Ne ...

  9. 转载 如何理解API,API 是如何工作的

    本文转载于https://blog.csdn.net/cumtdeyurenjie/article/details/80211896 感谢作者 仁杰兄 大家可能最近经常听到 API 这个概念,那什么是 ...

随机推荐

  1. D3——Updates, Transitions, and Motion

    <script type="text/javascript"> ; ; ; , , , , , , , , , ,, , , , , , , , , ]; //crea ...

  2. PostgreSQL 连接的问题

    一.在postgresql的安装文件夹\8.3\data\pg_hba.conf里面(或者在开始菜单程序下面的postgresql的配置文档)找到“# IPv4 local connections:” ...

  3. ZOJ-3278 8G Island---二分第k大

    题目链接: https://cn.vjudge.net/problem/ZOJ-3278 题目大意: 给出两个数列A和B,长度分别为N,M (1<=N, M<=10^5, 1<=Ai ...

  4. Input and Output-The input is all the sources of action for your app

    Programs take input and produce output. The output is the result of doing something with the input. ...

  5. P3174 [HAOI2009]毛毛虫

    题目描述 对于一棵树,我们可以将某条链和与该链相连的边抽出来,看上去就象成一个毛毛虫,点数越多,毛毛虫就越大.例如下图左边的树(图 1 )抽出一部分就变成了右边的一个毛毛虫了(图 2 ). 输入输出格 ...

  6. Spark Streaming和Kafka集成深入浅出

    写在前面 本文主要介绍Spark Streaming基本概念.kafka集成.Offset管理 本文主要介绍Spark Streaming基本概念.kafka集成.Offset管理 一.概述 Spar ...

  7. 6、Android---运用手机多媒体(待完成)

    6.1.程序运行在手机上 6.2.使用通知 通知是Android中比较由特色的一个功能 当某个应用程序需要向用户发出一些提示信息时 而该程序由不在前台的显示 就可以借助通知来实现 6.2.1.通知的基 ...

  8. linux SVN添加新用户

    首先找到用户文件:authz.conf; 用vi 编辑authz.conf文件在develps 后面添加你要添加的用户名:如图:  上图:cheny就是我后面添加上去的用户名 按Esc :wq保存au ...

  9. Qt图标自定义

    https://mp.csdn.net/postedit/83449333   参考连接

  10. Django实现支付宝支付(沙箱)

    1.安装SDK 点击右侧沙箱当面付接入指导,之后可以看到一个下载SDK的按钮,点击后,会有python的SDK下载链接,但还是属于公测中,也可以通过在cmd里输入以下代码来安装. pip instal ...