Vertex data in XNA Game Studio 4.0

Originally posted to Shawn Hargreaves Blog on MSDN, Monday, April 19, 2010

In previous XNA versions, a VertexBuffer was just a loosely typed bag of bytes. A separate VertexDeclaration object specified how to interpret these bytes.

As of Game Studio 4.0, every VertexBuffer now has an associated VertexDeclaration, which is specified at creation time. VertexBuffer thus becomes a strongly typed container, providing all the information necessary to interpret its contents.

 

Why?

Consider this typical 3.1 rendering code:

    // Create
    VertexDeclaration decl = new VertexDeclaration(device, VertexPositionColor.VertexElements);

    int size = VertexPositionColor.SizeInBytes * vertices.Length;
    VertexBuffer vb = new VertexBuffer(device, size, BufferUsage.None);

    vb.SetData(...);

    // Draw
    device.VertexDeclaration = decl;
    device.Vertices[0].SetSource(vb, 0, VertexPositionColor.SizeInBytes);

    device.DrawPrimitives(...);

I highlighted the problem in red. See how many times I had to repeat what vertex format I am dealing with? The more places this is repeated, the more chances to make a mistake. I cannot be the only one who ever forget to set the right VertexDeclaration, or specified the wrong stride when calling Vertices.SetSource!

This loosely typed design also presented challenges for our runtime implementation. Because a VertexBuffer did not specify the format of its contents, the framework was unable to tweak this data for different platforms or hardware (at least not until the actual draw call, at which point it is too late for efficient fixups).

For example, although we generally prefer to perform Xbox endian conversion as late as possible in the Content Pipeline build process, our ContentTypeWriter<VertexBufferContent> did not have enough information to do this correctly. Instead, we had to specify the TargetPlatform when calling VertexContent.CreateVertexBuffer, because the necessary type information was discarded from that point on.

As we add new platforms, we need more flexibility to adjust for the needs of each, and this requires a deeper understanding of the data we are dealing with. Strongly typed vertex buffers simplify the API, which reduces the chance of error, at the same time as increasing framework implementation flexibility. A classic win-win.

 

VertexDeclaration changes

The VertexElement and VertexDeclaration types still exist, but are used somewhat differently:

And the important one:

This is no longer necessary, because whenever you set a VertexBuffer, the device can automatically look up its associated declaration. I find it great fun porting from Game Studio 3.1 to 4.0, because I can simply delete everywhere that used to set GraphicsDevice.VertexDeclaration, and everything still magically "just works" ™.

 

VertexBuffer changes

VertexBuffer has different constructor arguments:

Also:

With 4.0, the code example from the top of this article becomes:

    // Create
    VertexBuffer vb = new VertexBuffer(device, typeof(VertexPositionColor), vertices.Length, BufferUsage.None);

    vb.SetData(...);

    // Draw
    device.SetVertexBuffer(vb);

    device.DrawPrimitives(...);

Note how the vertex format is only specified in one place. Less code, and less potential for error.

These changes mean it is no longer possible to store vertices of different formats at different offsets within a single vertex buffer. This was once a common pattern in graphics programming, because changing vertex buffer used to be incredibly expensive (back in DirectX 7). Developers learned to optimize by merging many models into a single giant vertex buffer, and some have been doing that ever since. But changing vertex buffer is cheap these days, so this is no longer a sensible optimization.

 

IVertexType interface

In the previous code example, notice how I pass typeof(VertexPositionColor) when creating my VertexBuffer? How can the VertexBuffer constructor get from this type to its associated VertexDeclaration?

We added a new interface:

    public interface IVertexType
    {
        VertexDeclaration VertexDeclaration { get; }
    }

This is implemented by all the built-in vertex structures, so anyone who has one of these types can look up its associated VertexDeclaration.

If you try to create a VertexBuffer using a type that does not implement the IVertexType interface, you will get an exception. This is a common situation when loading model data, as you may wish to support arbitrary vertex layouts that do not match any existing .NET types, so your vertex data is loaded as a simple byte array. In such cases you should use the alternative VertexBuffer constructor overload which takes a VertexDeclaration instead of a Type.

 

Custom vertex structures

When creating custom vertex structures, it is a good idea to implement the IVertexType interface, so your custom types can be used the same way as the built-in ones. A simple example:

    struct MyVertexThatHasNothingButPosition : IVertexType
    {
        public Vector3 Position;

        public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration
        (
            new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
        );

        VertexDeclaration IVertexType.VertexDeclaration { get { return VertexDeclaration; } }
    };

 

DrawUserPrimitives changes

DrawUserPrimitives<T> and DrawUserIndexedPrimitives<T> get their vertex data from a managed array, as opposed to a vertex buffer object. With no VertexBuffer, how can they find the necessary VertexDeclaration?

 

Multiple vertex streams

Prior to version 4.0, a single VertexDeclaration could combine inputs from many vertex streams, using the VertexElement.Stream property to specify the source stream for each element. Multiple vertex buffers were then set onto the device like so:

    device.Vertices[0].SetSource(vb1, 0, stride1);
    device.Vertices[1].SetSource(vb2, 0, stride2);

As of 4.0, each VertexDeclaration now describes the format of a single VertexBuffer, so if you are using multiple buffers, there are also multiple declarations. This made the VertexElement.Stream property unnecessary, so it was removed. Multiple buffers are set onto the device with a single atomic call, similar to (and for the same reasons as) the new SetRenderTargets API:

    device.SetVertexBuffers(vb1, vb2);

 

Content Pipeline changes

We made some minor Content Pipeline tweaks to match the new vertex buffer API:

Blog index   -   Back to my homepage