By Jack Hoxley
This short article gives a brief overview of how a simple, yet typical, vertex buffer creation process differs between Direct3D 9 and Direct3D 10. The aim is to show some simple examples of how the API has changed and what these changes mean to you.
A vertex buffer, as you are probably aware, contains the raw geometric data (a 1D array of vertices) that builds up a scene. Vertex buffers can be arranged in a variety of different ways – depending on how you want the final object to be displayed. If used in conjunction with index buffers then there is no requirement for specific ordering of data in a vertex buffer.
The first step is to define a way of describing the data for each vertex that you wish to store in a vertex buffer. With C/C++ this is typically done using a struct:
Direct3D 9 and Direct3D 10:
struct SimpleVertex
{
D3DXVECTOR3 Pos; // Position
DWORD Color; // Color
};
As you can see, the definition remains the same between 9 and 10. Because there is no fixed-function pipeline in Direct3D 10 the vertex format is implicitly more flexible - although you can mimic much of this via Direct3D 9's programmable route.
The next step is the basic definition of a vertex buffer object. Once created it’s this variable that you use whenever you want to reference/use the data contained within it.
Direct3D 9:
IDirect3DVertexBuffer9 *pVertexBuffer = NULL;
Direct3D 10:
ID3D10Buffer *pVertexBuffer = NULL;
It’s worth noting that there isn’t a specific vertex buffer object in Direct3D 10 – it’s just a different configuration of a generic ‘buffer’ object.
The first difference to note is that Direct3D 9 has an additional, optional, declaration: The Flexible Vertex Format (FVF) which is important mainly for the legacy fixed function pipeline. Direct3D 10 has no concept of a fixed function pipeline, so you’re not going to see any FVF’s. The previously defined SimpleVertex would look like either of these (depending on your coding style):
Direct3D 9:
static const DWORD FVF_SIMPLEVERTEX = D3DFVF_XYZ | D3DFVF_DIFFUSE;
#define FVF_SIMPLEVERTEX ( D3DFVF_XYZ | D3DFVF_DIFFUSE )
Now that we have a method by which we can describe the vertex data that will be stored in our vertex buffer object, we should define some data to use.
Direct3D 10, as we shall see shortly, can accept the initial data as an input into the creation process. If we provide the data at create time then as soon as the call returns we would have an already filled buffer. With Direct3D 9 you must allocate the necessary storage space for the vertex data before you copy any data into it. If you prefer the Direct3D 9 method, then you can still use it with Direct3D 10.
Direct3D 9 and Direct3D 10:
SimpleVertex VertexArray[] =
{
// SimpleVertex::Pos, SimpleVertex::Color
// (x,y,z ) (r,g,b )
{ D3DXVECTOR3( 1.0f, 1.0f, 1.0f ), D3DCOLOR_XRGB( 255, 255, 255 ) },
{ D3DXVECTOR3( 1.0f, 1.0f, -1.0f ), D3DCOLOR_XRGB( 255, 255, 0 ) },
{ D3DXVECTOR3( 1.0f, -1.0f, 1.0f ), D3DCOLOR_XRGB( 255, 0, 255 ) },
{ D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DCOLOR_XRGB( 255, 0, 0 ) },
{ D3DXVECTOR3( -1.0f, 1.0f, 1.0f ), D3DCOLOR_XRGB( 0, 255, 255 ) },
{ D3DXVECTOR3( -1.0f, 1.0f, -1.0f ), D3DCOLOR_XRGB( 0, 255, 0 ) },
{ D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DCOLOR_XRGB( 0, 0, 255 ) },
{ D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DCOLOR_XRGB( 0, 0, 0 ) }
};
At this point we have the necessary components (for either API) to get started. The next step is where things start to get noticeably different. As hinted at earlier, Direct3D10 doesn’t have a specific vertex buffer object – it’s just a specialization of a generic buffer (basically a block of binary data). This means that we must describe a vertex buffer for Direct3D. In Direct3D 9 there is a specific method used to create a vertex buffer (and only a vertex buffer).
The following two fragments allocate a vertex buffer that will contain a simple cube using 8 vertices (one for each corner!).
Direct3D 9:
if( FAILED( device->CreateVertexBuffer(
8 * sizeof( SimpleVertex ),
0,
FVF_SIMPLEVERTEX,
D3DPOOL_DEFAULT,
&pVertexBuffer,
NULL
) ) )
{
// Handle the error condition here
}
Direct3D 10:
D3D10_BUFFER_DESC BufferDescription;
BufferDescription.Usage = D3D10_USAGE_DEFAULT;
BufferDescription.ByteWidth = sizeof( SimpleVertex ) * 8;
BufferDescription.BindFlags = D3D10_BIND_VERTEX_BUFFER;
BufferDescription.CPUAccessFlags = 0;
BufferDescription.MiscFlags = 0;
D3D10_SUBRESOURCE_UP InitialData;
InitialData.pSysMem = VertexArray;
InitialData.SysMemPitch = sizeof( VertexArray );
InitialData.SysMemSlicePitch = sizeof( VertexArray );
if( FAILED( device->CreateBuffer(
&BufferDescription,
&InitialData,
&pVertexBuffer
) ) )
{
// Handle the error condition here
}
There are three things of interest in the Direct3D 9 code.
- There is a “usage” parameter (set to 0/default in this case) that is very critical to set correctly should you wish to manipulate your vertex buffer. Many applications modify the data by reading and/or writing once it’s been created, which can have severe performance penalties if done without the correct usage flag.
- The “pool” that the resource is created in (D3DPOOL_DEFAULT in this example). This has various implications many of which are connected with the previous point.
- More a curiosity – the final parameter (NULL in the above example) is the pSharedHandle. This, according to the documentation, should always be NULL. If you look up the Direct3D 9 revisions for Windows Vista you’ll see references to shared resources and shared handles – which is obviously what this (currently useless) parameter was included for.
With regards to the Direct3D 10 fragment the following points are worth noting:
- The definition obviously has to precede the act of creating the resource. In doing so it also requires that the vertex data already exists. In the above fragment this is represented by the VertexArray variable which will be discussed in the next paragraph.
- To define the usage patterns (previously via the D3DUSAGE enumeration) is a little more involved. D3D10_BUFFER_DESC::Usage is the obvious one to look at, and its options should be fairly recognizable (with the addition of D3D10_USAGE_IMMUTABLE, which will be useful). Additionally there is the D3D10_BUFFER_DESC::CPUAccessFlags field that you can use to restrict how your application code will be able to interact with the buffer (none, read, write, read and write).
- The only part of the code that implies what the contents of the buffer is, and how they can be used is the D3D10_BUFFER_DESC::BindFlags field. If you study the new Direct3D 10 pipeline, there are many more places where a generic resource can be either an input or an output to a given stage. By specifying D3D10_BIND_VERTEX_BUFFER you are effectively telling Direct3D 10 that you can only ‘attach’ it to the pipeline in places that vertex data is a valid input or a valid destination for output.
Now, provided that the respective Create*( ) calls didn’t fail then we should have a vertex buffer ready to use. This is where a big difference from Direct3D 9 can be found. If you chose to provide the VertexArray as initial data then you’re ready to go. If you didn’t (or you’re using Direct3D 9) then you will have to modify the resource and specify the data that belongs in the allocated memory:
Direct3D 9:
SimpleVertex *pData = NULL;
if( SUCCEEDED( pVertexBuffer->Lock( 0, 0, reinterpret_cast< void** >( &pData ), 0 ) ) )
{
memcpy( pData, VertexArray, sizeof( SimpleVertex ) * 8 );
pVertexBuffer->Unlock( );
}
Direct3D 10:
SimpleVertex *pData = NULL;
if( SUCCEEDED( pVertexBuffer->Map( D3D10_MAP_WRITE, 0, reinterpret_cast< void** >( &pData ) ) ) )
{
memcpy( pData, VertexArray, sizeof( SimpleVertex ) * 8 );
g_pVertexBuffer->Unmap( );
}
Both of the above fragments are pretty much the same – substituting the terms “lock” with “map” and “Unlock” with “Unmap”. However, what is not immediately obvious is that you must be more specific about how you wish to map the data in Direct3D 10 – the first parameter (set to D3D10_MAP_WRITE in the above example) is used to indicate this. Whilst it’s not relevant in the fragments shown in this example, the choices made when creating the resource (e.g. the D3D10_BUFFER_DESC::Usage and D3D10_BUFFER_DESC::CPUAccessUsage fields) can restrict what operations you’re allowed to perform later on.
Both pieces of code are about as concise as it gets – many examples will actually fill the pData pointer in a loop whilst the resource is locked (usually not good for performance!). To try and keep the Direct3D 9 and Direct3D 10 code synchronized the concise form has been such that they can both share the VertexArray declaration.
Direct3D 10:
pVertexBuffer->UpdateSubresource( 0,
NULL,
VertexArray,
sizeof( VertexArray ),
sizeof( VertexArray )
);
The above fragment is a convenient way of doing the same map/unmap operation in a single call. The obvious difference is that it will be write-only access as you don’t get given a pointer to read from. It’s worth noting that the last three parameters in the function call are essentially the same as the three fields in the D3D10_SUBRESOURCE_UP struct.
At this point in the article we have covered two of the three main parts when it comes to rendering with vertex buffers. Firstly we covered how to define the data that will exist inside the buffer, and secondly we covered how to actually create the buffer and get the data from our application into the buffer.
The third part is concerned with actually using the data stored in the vertex buffer – this will primarily be the process of rendering the geometry to the screen. It can be conveniently broken down into two sub-parts which will allow us to look at some more differences between Direct3D 9 and Direct3D 10.
Firstly it is necessary to configure Direct3D to use the data stored in the buffer. In a real-world application it is very likely that a large number of different vertex buffers will be used – many of them with data stored in different formats. When you tell Direct3D you want to render from a vertex buffer it has to know how to interpret the raw binary data that it finds in the vertex buffer you provide it.
In Direct3D 9 this could be done quite simply by specifying the Flexible Vertex Format (FVF) discussed earlier, pointing Direct3D to the correct vertex buffer object and issuing a draw call. If this method was chosen then you would be using the fixed-function pipeline – a feature that has now been superseded by the programmable pipeline. The advantages of this transition are huge and consequently far beyond the scope of this short article; however it is worth noting that because of the programmable pipeline the equivalent code has changed quite significantly.
Direct3D 9:
device->SetStreamSource( 0, pVertexBuffer, 0, sizeof( SimpleVertex ) );
device->SetFVF( FVF_SIMPLEVERTEX );
device->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 12 );
With Direct3D 10 it is necessary to configure the Input Assembler (IA) stage of the pipeline before you can actually render anything. The IA stage is a refinement on various pieces that existed for the programmable pipeline found in Direct3D 9, and is effectively the first part of the geometry pipeline. Quite simply the IA will take all of the data that the application provides and merge it together into a stream of primitives and vertices that the Vertex Shader (VS) and Geometry Shader (GS) units can use directly.
A simple example of this is taking the vertex buffer and “chopping” it directly into triangles, an extension would be to use an index buffer to format the vertex buffer into a series of primitives. When appropriate hardware was available, Direct3D 9 allowed applications to use a feature called “Geometry Instancing” as well as pulling in data from multiple vertex buffers – this advanced feature now resides in the IA stage.
Direct3D 10:
D3D10_INPUT_ELEMENT_DESC layout[] =
{
{ L"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 },
{ L"COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 },
};
The above fragment should be immediately familiar to anyone who has done any work with the Direct3D 9 programmable pipeline. A D3D10_INPUT_ELEMENT_DESC struct is used to fully describe the contents of a single element within the vertex buffer – what format a range of bytes are to be interpreted as (via the DXGI_FORMAT value) and the semantic used to map it to the desired input of a vertex shader. There are two slightly subtle differences to the Direct3D 10 declaration:
- The semantic name is now a text string that the application provides. Under Direct3D 9 it had to be an API-defined constant. This allows for much more flexibility, and the potential for some clever shader-reuse.
- The format of the individual elements is now one of the common DXGI_FORMAT values. Whilst it might look odd to describe the position in terms of a 96bit RGB float it is functionally saying that the first 12 bytes contain 3x32bit floating point elements.
Because of the fixed-caps nature of Direct3D 10, there are a lot of possible formats that you can use in a vertex – especially when supported by the more flexible semantic naming.
For reference purposes, the above fragment in Direct3D 9 form would look like this:
Direct3D 9:
D3DVERTEXELEMENT9 decl[] =
{
{ 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },
{ 0, 12, D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0 },
D3DDECL_END( )
};
The D3DDECLTYPE and D3DDECLUSAGE enumerations being the equivalent of the more free-form constructs described above.
Once the initial declaration/layout is defined, it is necessary to create a usable ‘input layout’ – An ID3D10InputLayout for Direct3D 10 or an IDirect3DVertexDeclaration9 for Direct3D 9. This object is used when you need to tell the pipeline how to interpret the data that you’re about to send it. A key conceptual difference here is that under Direct3D 9 you created an internal representation of the D3DVERTEXELEMENT9 array that was just created. There is no verification involved. If you then configure your device to render using the returned IDirect3DVertexDeclaration9 and it isn’t compatible with the current Vertex Shader then you’ll get a silent error (nothing rendered, and a lot of debug output!). This changes under Direct3D 10 because it verifies the layout you provide against the layout of the vertex shader. If they don’t match, the call will fail. This validation step is pretty much essential given that we now have much more freedom in how we can describe individual vertex elements – and it should also allow the runtime to avoid some checks.
Direct3D 9:
if( FAILED( device->CreateVertexDeclaration( decl, &vertexDeclaration ) ) )
{
// Handle the error condition here.
}
Direct3D 10:
D3D10_PASS_DESC PassDesc;
pTechnique->GetPassByIndex( 0 )->GetDesc( &PassDesc );
if( FAILED( device->CreateInputLayout( layout, 2, PassDesc.pIAInputSignature, &pVertexLayout ) ) )
{
// Handle the error condition here..
}
It is worth noting that, because of the programmable pipeline, that the effect framework is more extensively (although, if desired, direct access to a shader is possible) used. The finer details of the new effects framework for Direct3D 10 are beyond the scope of this article - the above fragment assumes that an effect has been loaded and that a technique has been extracted. It also works on the assumption that there is only one pass in the current technique – which is not a safe assumption for a data-driven architecture. It would make for better code to build a layout for each pass in the effect:
Direct3D 10:
D3D10_TECHNIQUE_DESC pDesc;
pTechnique->GetDesc( &pDesc );
ID3D10InputLayout **pVertexLayout = new ID3D10InputLayout*[ pDesc.Passes ];
for( UINT idx = 0; idx < pDesc.Passes; idx ++ )
{
D3D10_PASS_DESC PassDesc;
pTechnique->GetPassByIndex( idx )->GetDesc( &PassDesc );
if( FAILED( device->CreateInputLayout( layout, 2, PassDesc.pIAInputSignature, &pVertexLayout[idx] ) ) )
{
// Handle the error condition here...
}
}
At this point the application will have the two main sources of information that are required in order to render from a vertex buffer: The vertex buffer itself, and the vertex layout. Typically these two objects will be stored in a suitable data structure and when it comes to rendering them they’ll be assigned to the relevant part of the pipeline.
Direct3D 9:
device->SetVertexDeclaration( vertexDeclaration );
device->SetStreamSource( 0, pVertexBuffer, 0, sizeof( SimpleVertex ) );
Direct3D 10:
device->IASetInputLayout( pVertexLayout );
UINT stride = sizeof( SimpleVertex );
UINT offset = 0;
device->IASetVertexBuffers( 0, 1, &pVertexBuffer, &stride, &offset );
device->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
With Direct3D 10 a single IASetVertexBuffers call can assign many vertex buffers to the device (hinted at by the name!). This fits in with the previously mentioned concept that the Input Assembler now takes care of reading data from various buffers and formatting any streams based on any instancing-related rendering. Additionally it’s worth noting that the Input Assembler also needs to know the topology of the data its being given – for the most part this is going to be the same as the first parameter for a typical Direct3D 9 Draw*( ) call. The only obvious exception from the D3D10_PRIMITIVE_TOPOLOGY enumeration is for triangle fans (which was D3DPT_TRIANGLEFAN).
Once the Input Assembler has been configured with the vertex buffer, layout and topology the device should be ready. There are several other factors that need to be setup (such as the current effect technique and effect parameters) but these are beyond the scope of this article.
Direct3D 9:
device->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 12 );
Direct3D 10:
device->Draw( 12, 0 );
That concludes the comparison of vertex buffers in Direct3D 10 and Direct3D 9. There are many more aspects to both the Direct3D 9 and 10 uses for vertex buffers, the majority of these were dropped so as to keep this article short and to the point. The following is a brief overview of some features that weren't covered:
Instancing and multi-stream inputs are common to both versions and allow for some very clever implementations. For example, it is possible to store different parts of a vertex in different vertex buffers and have them "composed" into a single vertex that is fed into the Vertex Shader.
Advanced features of the Direct3D 10 pipeline that don't have an equivalent in Direct3D 9. This article is primarily a comparison, so aspects such as Geometry Shader usage has been ignored.
Summary
Much of what has been covered here will be familiar to any experienced Direct3D 9 programmer, and any bigger changes should be a manageable step-up from Direct3D 9. As a summary of some of the key points, specifically with regards to geometry storage - Direct3D 10…
- has no fixed function pipeline so you won't be seeing any FVF-related code in Direct3D 10. You'll also see a lot more application code that is similar to the programmable route under Direct3D9.
- has no specific vertex buffer object meaning that a Direct3D 10 "vertex buffer" is simply a specialization of a generic data-buffer.
- is more strict about creating the input layout as it requires the layout to be created with reference to a set of vertex shader input semantics.
- is more verbose than Direct3D 9. Using the classic fixed-function pipeline, the code presented in this article takes approximately 40 lines. Using the programmable pipeline in Direct3D 9 results in 50 lines. The Direct3D 10 pipeline uses 60 lines.