深入Managed DirectX9

http://dev.gameres.com/Program/Visual/DirectX/ManagedDirectX9_13.htm

作者:clayman 

 
 

深入Managed DirectX9(十三

 
 
目录:
  第一部分
  第二部分
  第三部分
  第四部分
  第五部分
  第六部分
  第七部分
  第八部分
  第九部分
  第十部分
  第十一部分
  第十二部分
  第十三部分
  第十四部分
  第十五部分
  第十六部分
  第十七部分
  第十八部分
  第十九部分
  第二十部分
  第二十一部分
  第二十二部分
  第二十三部分


第八章 理解资源(Understanding Resoruces)

资源是在Direct3D中渲染复杂对象的重要部分。在这一章的学习中,我们将会讨论关于资源的一些高级特性,包括:
  静态和动态的资源
  Updating the buffers included in meshes
  使用各种锁定标志。
  使用不安全代码最优化性能。

初识Resource类
  Resource是一个很小的类,同时也是Direct3D中其他资源类的基类,因此,先把这个类所有方法及其用途列出来作为本章开始将是一个不错的主意。由于Resource是一个抽象类,你不会直接创建或使用这个类,但你会在其他其他的资源类中使用到这些方法:
Device Read-only property that will return the device this resource was created with.
Type Read-only property that returns the type of resource this is. Valid return values for this property are in the ResourceType enumeration.
Priority Read-write property that returns the priority of this resource. Priorities only matter when dealing with resources that reside in the managed memory pool. They are used to determine when a resource can be removed from memory. The lower the priority, the faster a resource can be removed. By default, all managed resources have a priority of 0, and all non-managed resources will always have a priority of 0. There is also a SetPriority method, which returns the old priority after setting the new one.
PreLoad Use this method to indicate that a managed resource will be needed shortly. This will allow Direct3D to move your resource into video memory before it is actually needed. It's important to note, that if you are already using more video memory than you have, this method will silently do nothing.
PrivateData members There are three members that allow you to get and set private data for each of your resources. This data is never used by Direct3D and can be used by your application as you see fit.

  如你所见,resource类主要用来处理提高托管资源的性能。除了上表和Resource所列出的条目外,还需要注意Direct3D中的每个资源都还包括一个使用状态(Usage)和一个内存池(memorey pool)成员(定义在Usage枚举和Pool枚举中)。这几个属性定义了如何使用资源,以及资源存放在内存的什么区域(可以是系统内存,video memory,或者AGP memory)。

  资源类通过一个方法,允许CPU直接访问他所储存的数据,通常我们把这种机制称为锁定(locking)。迄今为止所举的例子中,只使用过SetDate方法来填充缓冲,并且都是通过文件来创建纹理;但是,通过锁定这些资源,则可以完成恢复数据,或者只填充数据中的一小部分的任务。我们会在这一章详细讨论锁定。先来看看我们所介绍的第一种资源吧。


使用顶点和索引缓冲
  
顶点缓冲是Direct3D中储存顶点的主要数据结构,同样,索引缓冲也是储存索引的主要数据结构。这些类都源自Resource类,所以除了继承基类所有方法外,还添加了一些额外的方法。

  第一个新属性就是对缓冲自身的描述。通过这个熟悉返回的结果可以告诉你关于缓冲是如何创建的所有信息,包括格式(format),使用状态,内存池,大小,以及顶点格式。虽然大多数情况下你是知道了这些信息才创建缓冲,但如果获得了来自外部资源的未知缓冲,那么这些信息还是很有用的。

特别提示:静态及动态缓冲
  在讨论关于缓冲的其他方法前,最好先说说静态缓冲和动态缓冲的区别。这里所讨论的内容对顶点缓冲和索引缓冲都是可行的。

  好了,这里第一个问题就是“静态缓冲和动态缓冲有什么区别呢?”好在我已经准备好了答案。除了使用Usage.Dynamic标志所创建的缓冲是动态的,其他所有缓冲都是静态。静态缓冲是为不经常改变的数据设计的,相反,对于经常改变的数据来说,使用动态缓冲则更方便。这就是两者名字的由来。

  然而,静态缓冲并不意味着不能修改数据。但是,锁定一块已经在使用的静态缓冲时,会带来巨大的性能损失。在修改数据之前,GPU必须完成对当前这组数据的读取,这可是很费时的。如果同一帧做几次这样的操作,情况还会更遭,因为你阻止了驱动器所能做的缓冲,并且强迫GPU闲置下来等待你完成对数据的修改。如今的图形卡已经相当强大了,不应该把它浪费在等待数据上,而是要不停绘图。

  如果数据时要经常改动的,就应该用Usage.Dynamic标志来创建缓冲。这个标志允许Direct3D为了经常性的修改数据而优化管道。另外,创建一个永远不会被修改的动态缓冲,并不会带来性能提升,事实上,它比简单创建一个静态缓冲还要慢。

  总是应该根据每一帧在缓冲中所做的改动来创建缓冲。为了最优化性能,建议你为每一种需要渲染的顶点格式都创建一个大的静态缓冲。只有在静态缓冲不能满足需要的情况下再使用动态缓冲。


锁定缓冲
  
锁存机制也许是Direct3D中被误解最多的技术,特别是在Managed DirectX中。人们经常会问如何锁定以及如何高效的完成锁定。

  那么,究竟什么是“锁定”缓存呢?它其实就是允许CPU直接访问资源中一定范围内数据的操作。你不能直接访问图形硬件,因此需要一种方法来控制程序中的顶点数据,而锁存就是用来完成这个任务的。来看看可用在顶点和索引缓冲上的众多lock方法吧。

public System.Array Lock ( System.Int32 offsetToLock ,Microsoft.DirectX.Direct3D.LockFlags flags )
public System.Array Lock ( System.Int32 offsetToLock , System.Type typeVertex , Microsoft.DirectX.Direct3D.LockFlags flags , params int[] ranks )
public Microsoft.DirectX.Direct3D.GraphicsStream Lock ( System.Int32 offsetToLock , System.Int32 sizeToLock , Microsoft.DirectX.Direct3D.LockFlags)

  如你所见,有3种方法可用来锁定缓冲。我们先从最简单的第一个开始。这个方法对只对通过使用System.Type以及一系列顶点或索引的构造函数创建的缓冲有用。实际上,这个方法只是使用构造函数所传入的数据来再调用第二个重载的方法而已。

  接下来的两个重载就比较有意思了。他们的第一个参数都表示开始锁定的偏移值(以比特为单位)。如果需要锁定整个缓冲,把这个值设置为0就可以了。你可能已经注意到前两个重载的方法都把数据以数组的方式作为返回值。在第二个重载中,第二个参数可以设置所返回的数组的类型。最后一个参数决定了返回数组的大小。

  由于某些原因“Rank”参数总是困扰着开发者,让我们来仔细讨论一下吧。假设有一个只包含了位置数据(Vector3)的顶点缓冲,并且它保存了1200个顶点。如果你想把它锁定为一个包含1200个元素的Vector3数组,那么应该这样调用方法:

Vector3[] data = (Vector3[])vb.Lock(0, typeof(Vector3), LockFlags.None, 1200);

注意到额外的参数没有?Rank参数实际上是一个参数数组。它还可以创建三维数组作为返回值。你可以使用这个参数来指定返回多大的数组。

特别提示:如何高效的锁定缓冲
  
LockFlag应该和最后一个重载放到一起来讨论,但首先,我想指出前两个以数组作为返回值的方法的缺点。第一位,也是最重要的,我们应该讨论一下性能。假设你使用“default”选项创建了一个顶点缓冲,并且没有lock flag,当调用这个方法的时,将发生以下情况:

顶点数据被锁定;保存下数据的内存地址。
根据rank参数指定的大小,在内存中定位一个类型正确的新数组。
数据从被锁定的内存中复制到新的缓冲。
新的缓冲被返回给用户进行修改。
调用Unlock方法的时,新缓冲中的数据再次被复制回锁定的内存中。
最后,解锁顶点数据。

  不难明白为什么这个方法要比你所预计的慢一些。 通过设置可以减少一次复制:创建缓冲时指定Usage.WriteOnly参数(避免第一次复制),或者在锁定缓冲时使用LockFlags.ReadOly标志(避免第二次复制); 但没有方法可以把两次复制都消除。对一块ReadOnly/WriteOnly的缓冲能干些什么呢?
最后一种重载是最强大的。同时,他也有一个其他重载没有的参数,指定了我们想要锁定的数据大小。在其他的重载中,这个值是通过调用(sizeof(type)*NumberRanks)方法计算出来的。如果使用这个方法,则只需要传入需要锁定的数据大小(以比特为单位)就可以了。如果如要锁定整个缓冲,那么把前两个参数设为0就可以了。

  这个方法将返回一个GraphicsStream类来让你直接控制锁定的数据,而不需要为数组分配额外的内存,也不需要额外的内存复制。你可以对这快内存作任意的修改。GraphicsStream对象有很多方法允许你把数据“写入”这块内存,这并不会带来很大的速度提升。但你可以通过直接控制内存来获得速度提升。

  当然,我并不是想告诉你返回数组的方法会慢很多。合适的使用这两个方法是很方便的,差不多和返回数据流的方法一样快。但是,如果你想把系统里每一盎司性能都炸干的话,就应该使用返回数据流的方法。


控制如何锁定缓冲
最后,我们来讨论一下锁定内存时可以使用的标志(flags)。锁定顶点缓冲时,只有以下标志是有效的:

LockFlags.None
LockFlags.Discard
LockFlags.NoOverwrite
LockFlags.NoDirtyUpdate
LockFlags.NoSystemLock
LockFlags.ReadOnly

显然,当不使用锁定标致时,就使用默认的锁定机制。但是,如果你需要对锁定进行更多控制,其他标志就可以完成多种选项。

Discard标志只能用于动态缓冲。对顶点缓冲和索引缓冲来说,整个缓冲都会被丢弃,另外返回一块新的内存,以防还有其他程序在访问旧的数据。这个标志在填充动态的顶点缓冲时特别有用。一旦完成了对顶点缓冲的填充,锁定就结束了。它通常和其他标志一起使用。

NoOverWrite标志(同样也只对动态缓冲有用)告诉Direct3D你不会复写顶点和索引缓冲中的任何数据。时用这个标志,会使调用立即返回,并且继续使用缓冲。如果不使用这个标志,那么锁定调用直到完成了当前所有渲染之后才会返回。既然你不会修改但前缓冲中的任何数据,它只在向缓冲中添加顶点时才有用。

这两个标志使用最多的情况,是在填充大块的动态缓冲时。但持续向缓冲填充数据的时候,保持使用NoOverwrite标志,直到填充完毕。接下来,使用Discard标志刷新缓冲,然后再次开始填充。现在来看看随DirectX SDK一起发布的实例Point Sprites:

if (++numParticlesToRender == flush)
{
    // Done filling this chunk of the vertex buffer. Lets unlock and
    // draw this portion so we can begin filling the next chunk.
    vertexBuffer.Unlock();
    dev.DrawPrimitives(PrimitiveType.PointList, baseParticle,
    numParticlesToRender);
    // Lock the next chunk of the vertex buffer. If we are at the
    // end of the vertex buffer, LockFlags.Discard the vertex buffer and start
    // at the beginning. Otherwise, specify LockFlags.NoOverWrite, so we can
    // continue filling the VB while the previous chunk is drawing.
    baseParticle += flush;
    if (baseParticle >= discard)
        baseParticle = 0;
    vertices = (PointVertex[])vertexBuffer.Lock(baseParticle *DXHelp.GetTypeSize(typeof(PointVertex)), typeof(PointVertex),(baseParticle != 0) ? LockFlags.NoOverwrite : LockFlags.Discard,flush);
    count = 0;
    numParticlesToRender = 0;
}

  在这段代码中,我们检测是否是应该“刷新”数据了。如果需要,我们就解锁并且渲染。接下来,检查是否到达了缓存的最后(baseParticle>=discard),最后再次锁定缓冲。我们使用NoOverwrite标志,从缓冲的尾部开始锁定,假如baseParticle是0,就使用Discard标志,从缓冲头开始锁定。这样就可以不停把新的point sprite添加到场景中,直到缓冲满了,这时候,就丢弃旧的缓冲,使用一块新的。

  讨论完了2个“复杂”的标志之后,来看看剩下的标志。其中最简单的就是ReadOnly标志了,正如它名字所示,它会告诉Direct3D你不会写入缓冲。当使用返还数组的重载方法时,它也表示在最后解锁缓冲时,不需要复制更新过的数据。

  NoDirtyUpdate标志防止对任何处于“dirty”状态的资源进行修改。没有这个标志,锁定资源时会自动对资源添加一个脏区域(dirty region)。

  最后一个标志不太常用。一般来说,当你锁定显存中的资源时,会保留一个系统级别的临界区,在使用这个标志的锁定期间,不允许改变任何显示模式。使用systemLock标志,可以取消这种效果。它只有在你预计锁定时间很长,并且还需要保证很快的系统速度时使用。通常不推荐使用这个选项。

  自然,在调用了lock方法之后,你还必须在某个时刻告诉Direct3D完成了锁定。Unlock就是用来完成这个任务的。这个函数没有其他的参数,而且必须在每锁定之后都调用unlock方法。有任何为解锁的数据都将导致渲染失败。

你可能感兴趣的:(深入Managed DirectX9)