This seemingly innocuous code:
device.SetVertexBuffer(vb);
device.Draw(...);
device.SetVertexBuffer(null);
vb.SetData(...);
is the sort of thing that makes graphics driver writers wake in the night, heart pounding, drenched in sweat. Verily 'tis the stuff of nightmares.
The problem is that the GPU runs asynchronously later than the CPU. When the CPU reaches the SetData call and wants to change the contents of the vertex buffer (or index buffer, or texture...) the GPU hasn't yet got around to processing the earlier draw call, so it still needs the previous contents of that buffer. What on earth is a poor driver to do?
So which approach does XNA choose? As of version 4.0:
Prior to version 4.0, things worked the same on Windows, but our Xbox implementation was less awesome:
I'm very happy that we finally found time to implement resource renaming on Xbox, so you can SetData any time you like, regardless of whether predicated tiling is in use, and SetDataOptions.Discard works the same as on other platforms.
The Discard flag is a hint to the driver that you no longer care about any of the data in the resource, so it does not need to bother preserving the existing contents. This can make resource renaming more efficient, because it allows the driver to skip the data copy described in the above step 4.b.
You should specify the Discard flag any time you are calling SetData on a dynamic buffer, and are planning on entirely replacing the contents of that buffer. Even if the current SetData call only changes part of the buffer, if you no longer need the data in the rest of the buffer, this is your chance to let the driver know that. It will love you for giving such a useful hint!
Note that Discard only means "the driver is allowed to throw away the current contents of the buffer if it finds that to be useful". The driver does not HAVE to discard the buffer contents if it does not wish to do so! If the buffer is not currently in use by the GPU, the driver will ignore the Discard hint.
The NoOverwrite flag is a hint to the driver that you are not going to change any part of the resource which the GPU might still be using. This is not enforced, but if you do change data while the GPU is using it, you will get incorrect rendering (typically flickering, but almost anything might happen depending on timing).
You can use the NoOverwrite flag when combining multiple independent pieces of data into a single larger buffer. If you SetData one piece of data into one part of the buffer, then draw using this data, and are now about to SetData a different piece of data into a different part of the buffer, this is your chance to tell the driver that even though you are changing a resource which is still in use by the GPU, you happen to know that the region you are changing is not the same as the region the GPU is using, so there is no need for it to bother stalling or renaming the resource.
Used wisely, the NoOverwrite hint can provide dramatic speed gains. But used incorrectly, it can produce incorrect rendering results. Check out our Particle 3D sample for an example of using it correctly (see the giant comment near the top of the ParticleSystem class), or the way I implemented skidmarks in MotoGP.
A common pattern for games that need to generate lots of dynamic geometry is to use a single dynamic buffer as a circular queue. New geometry is appended to the buffer with NoOverwrite, then drawn, then more geometry is appended again using NoOverwrite, and drawn, rinse, lather, repeat. When you reach the end of the buffer, the position is reset back to the start, and this wrapping SetData switches to Discard mode, which signals the driver to perform a rename and give us a fresh copy of the buffer. This scheme allows any amount of geometry to be efficiently drawn using a single relatively small buffer. The driver will internally allocate however many renamed copies are neccessary to avoid stalling.
In code:
// Initialize. const int BufferSize = xxxx; DynamicVertexBuffer vb = new DynamicVertexBuffer(device, typeof(VertexType), BufferSize, 0); int currentBufferPosition = 0; // Add new geometry to the buffer, and return the offset for drawing these vertices. int AddVerticesToDynamicBuffer(VertexType[] vertices) { // Append to the existing buffer. int position = currentBufferPosition; SetDataOptions hint = SetDataOptions.NoOverwrite; // If we reached the end, wrap back to the beginning and Discard the existing buffer contents. if (position + vertices.Length > BufferSize) { position = 0; hint = SetDataOptions.Discard; } // Write the new data into the buffer. vb.SetData(position * sizeof(VertexType), vertices, 0, vertices.Length, sizeof(VertexType), hint); currentBufferPosition = position + vertices.Length; return position; }
Internally, SpriteBatch does pretty much exactly this.