Vertex Buffers

Introduction

Creating a Vertex Buffer

Filling Vertex Buffers

Rendering With Vertex Buffers

Lesson Downloads

MSDN Links For Functions/Concepts Introduced Here

Introduction

In the last few lessons we've called the DrawPrimitiveUP method for all of our rendering. This works well for examples because it's very easy to use and very little preparation is required to use it. It isn't very efficient though.

Since our vertex array is in system memory, every time we call DrawPrimitiveUP that array has to be copied into device memory so it can be rendered. A better solution is to allocate our vertex array in memory that the graphics card has access to. A Vertex Buffer allows us to allocate memory from a variety of memory pools, device accessible memory among them.

As a code example, I'll convert part of the example used in the Triangle lesson to show how easy it is to switch to using Vertex Buffers. Only the Triangle List portion will be converted. Showing the others would be redundant, but they could also be used as an exercise to see if you understood this lesson.

Creating a Vertex Buffer

To create a Vertex Buffer, you use the CreateVertexBuffer method of your device. I'll show the prototype for the method and then discuss the parameters.

HRESULT CreateVertexBuffer(
   unsigned int Length,
   DWORD Usage,
   DWORD FVF,
   D3DPOOL Pool,
   IDirect3DVertexBuffer9** ppVertexBuffer,
   HANDLE* pHandle
);
Length
The size of the buffer in bytes. Easily calculated by multiplying the number of vertices by the size of your vertex structure.
Usage
These flags declare how the Vertex Buffer will be used. This allows the video driver to optimize the storage of the buffer. Common flags are: D3DUSAGE_DYNAMIC, D3DUSAGE_SOFTWAREPROCESSING and D3DUSAGE_WRITEONLY.
  • D3DUSAGE_DYNAMIC - this vertex buffer is designed to be over-written on a regular (frame-by-frame) basis.
  • D3DUSAGE_WRITEONLY should almost always be set since it allows the video driver to store the vertices in a format designed for rendering without worrying about making it possible to read back.
  • D3DUSAGE_SOFTWAREPROCESSING should be set when you create your device with the software processing flag. If you create the device with hardware processing flag do not set this flag.
If you have no flags to set you may leave this parameter as 0.
FVF
Set this to the FVF flags that describe your vertices.
Pool
This indicates where in memory the buffer should be allocated. The 2 most common choices are D3DPOOL_DEFAULT and D3DPOOL_MANAGED.
  • D3DPOOL_DEFAULT - the buffer is placed in video-accessible memory. Either in local video card memory or AGP memory. This kind of buffer can require extra management on your part. A default pool resource needs to be re-allocated when the device is Reset.
  • D3DPOOL_MANAGED - the buffer is created in system memory and a copy is created in device-accessible memory as needed. Direct3D will take care of all the issue involved in swapping resources in and out so you don't have to worry about managing them. In general you should place your resources the managed pool unless you have a good reason not to. This also means that no special effort is required when calling Reset on your device.
ppVertexBuffer
Pass the address of a Vertex Buffer pointer and it will be set to the vertex buffer if the method succeeds.
pHandle
This is not currently used, set it to NULL.

And here is an example taken from the lesson's code.

IDirect3DVertexBuffer9 *g_list_vb=NULL;
int vert_count;
int byte_count;
HRESULT hr;

   vert_count=sizeof(data)/sizeof(tri_vertex);
   byte_count=vert_count*sizeof(tri_vertex);

   hr=g_d3d_device->CreateVertexBuffer(byte_count,        //Length

                                       D3DUSAGE_WRITEONLY,//Usage

                                       tri_fvf,           //FVF

                                       D3DPOOL_MANAGED,   //Pool

                                       &g_list_vb,        //ppVertexBuffer

                                       NULL);             //Handle


Here we allocate enough room for our data, and specify that it should be in the MANAGED Pool and that it's WRITEONLY. Using (and respecting) the WRITEONLY flag can give nice performance boosts so I recommend using it whenever possible.

As with other Direct3D objects, when we're done with our Vertex Buffer, we call its Release method.

   if(g_list_vb){
      g_list_vb->Release();
      g_list_vb=NULL;
   }

Filling Vertex Buffers

Now that we've created a Vertex Buffer we have to fill it with our data. Vertex Buffers are an "opaque" container. The internal data could be stored in a variety of different ways. This means the video driver can allocate the memory and store the data in a way that makes the most sense for its use. This can mean that it's stored in a way that would be difficult for us to deal with.

What we need is a method to tell the Vertex Buffer that we need to manipulate its data in a manner and format that is convenient to us. Also, we need to signal that we are modifying the data so the video card doesn't try to use the data as we modify it. Both of those goals are achieved by using the Lock method. Calling Lock marks the data as locked by us and hands us a memory buffer in the same format as we initially defined.

HRESULT Lock(
   unsigned int OffsetToLock,
   unsigned int SizeToLock,
   void **ppbData,
   DWORD Flags
);
OffsetToLock
The offset (in bytes) into the vertex data. This can be used to limit the amount of data you lock. Set it to 0 when you want to lock the entire buffer.
SizeToLock
The amount of data (in bytes) that you wish to lock. Setting this to 0 will lock the entire buffer.
ppbData
The address of a pointer which will be set to point to a memory buffer containing the data.
Flags
Flags to describe how the Vertex Buffer should be locked. Here are some common ones.
  • D3DLOCK_DISCARD - All data will be overwritten. Typically a new memory buffer is allocated behind the scenes and you are handed a pointer to that buffer to fill. This way the video card can finish using the original data while you generate new data. This flag can only be used on a Vertex Buffer created with the D3DUSAGE_DYNAMIC flag.
  • D3DLOCK_NOOVERWRITE - This flag guarantees that you will not overwrite any data in the buffer. If you aren't touching data that the video card is using, then this call can return immediately with your data. Typically this is used when drawing batches of geometry. The first lock in a frame will specify D3DLOCK_DISCARD and some data will be written, then the buffer is locked with D3DLOCK_NOOVERWRITE to add to the data already written earlier in the frame.
  • D3DLOCK_READONLY - If you need to read back some of your data, this lets the device know that you only need a copy of the data and it doesn't need to update its own copy when you unlock the data.

When we use it to initially fill our Vertex Buffer, none of the flags are required and we want to lock the entire buffer. These calls will typically look like this.

HRESULT hr;
void *vb_vertices;

   hr=g_list_vb->Lock(0,            //Offset

                      0,            //SizeToLock

                      &vb_vertices, //Vertices

                      0);           //Flags

vb_vertices now points to an array of vertices. To fill the Vertex Buffer with our data, we'll use memcpy from the standard C library.

   memcpy(vb_vertices, //Destination

          data,        //Source

          byte_count); //Amount of data to copy

After we've copied our data into the buffer, we can unlock the buffer so it can be used for rendering. The Unlock call takes no parameters.

HRESULT Unlock(void);

That's all there is to managing a Vertex Buffer. It's really not much harder than just using a system memory array. Now that you know how to manage the Vertex Buffer, let's look at how to use it for rendering.

Rendering With Vertex Buffers

To render the geometry stored in a Vertex Buffer we use the DrawPrimitive method of the Direct3D device. It's very similar to the DrawPrimitiveUP method we've used in the past. The biggest difference is that you don't pass the geometry data to the method. Instead you first call SetStreamSource to indicate where data should be read from.

Direct3D can render from multiple streams of data. The simplified interface of DrawPrimitiveUP always sets your data as stream 0 and doesn't allow you to set other streams at all. Using SetStreamSource you can set multiple streams and then DrawPrimitive will read from all of them. I will not go into any detail on using multiple streams in this lesson, that will be saved for later lessons. For now, let's look at the prototype for SetStreamSource.

HRESULT SetStreamSource(
   UINT StreamNumber,
   IDirect3DVertexBuffer9 *pStreamData,
   UINT OffsetInBytes,
   UINT Stride
);
StreamNumber
Which stream to bind the data to. Since we only use a single stream, we set this to 0.
pStreamData
This is the Vertex Buffer we wish to bind to the stream.
OffsetInBytes
This sets the initial read location and is given in bytes. Typically this is set to 0.
Stride
The size in bytes of a vertex.

Now let's see the DrawPrimitive prototype.

HRESULT DrawPrimitive(
   D3DPRIMITIVETYPE PrimitiveType,
   unsigned int StartVertex,
   unsigned int PrimitiveCount
);
PrimitiveType
The type of primitive to render.D3DPT_TRIANGLELIST for example.
StartVertex
The first vertex to be read from the Vertex Buffer. Most often this will be 0.
PrimitiveCount
The number of primitives (not vertices) to be rendered.

There's very little here that we haven't seen in previous lessons. Though the methods are slightly different, the concepts are the same as when we used DrawPrimitiveUP. To help pull it all together here is a code sample showing both SetStreamSource and DrawPrimitive.

   g_d3d_device->SetStreamSource(0,                   //StreamNumber

                                 g_list_vb,           //StreamData

                                 0,                   //OffsetInBytes

                                 sizeof(tri_vertex)); //Stride


   g_d3d_device->DrawPrimitive(D3DPT_TRIANGLELIST, //PrimitiveType

                               0,                  //StartVertex

                               g_list_count);      //PrimitiveCount

Lesson Downloads

MSDN Links For Functions/Concepts Introduced Here

Back