Advanced Data

We have been putting vertex data, indices and constants into buffers since Hello Triangle, nearly always the same way: fill the memory once, at creation, and never touch it again. This chapter looks at the other ways to get data into a buffer and move it around, and at one more way to arrange vertex attributes. LearnOpenGL's chapter on the subject has no program of its own; ours does, a patch of water whose surface is recomputed on the CPU every frame.

A wireframe grid of water, its waves lit brighter toward the camera

What a buffer is

A buffer is a block of memory the GPU can read, and on its own nothing more. In OpenGL a buffer gets its meaning from the target it is bound to: the same buffer object is vertex data while bound to GL_ARRAY_BUFFER and indices while bound to GL_ELEMENT_ARRAY_BUFFER. Direct3D asks the question up front instead. When we create a buffer we say, in its BindFlags, every way it may ever be bound (D3D11_BIND_VERTEX_BUFFER, D3D11_BIND_INDEX_BUFFER, D3D11_BIND_CONSTANT_BUFFER, and a few more), and binding it any other way later is an error.

We also say, in its Usage, who will write to it and how often. This is the part that matters for this chapter. There are four choices:

UsageGPUCPUTypical use
D3D11_USAGE_IMMUTABLEreadsnothing; the data is given at creationmeshes that never change
D3D11_USAGE_DEFAULTreads and writesoccasional updates through UpdateSubresourcedata that changes now and then; render targets
D3D11_USAGE_DYNAMICreadswrites through Mapdata rewritten every frame
D3D11_USAGE_STAGINGnothing; only copies to and from itreads and writes through Mapreading GPU results back

OpenGL's GL_STATIC_DRAW and GL_DYNAMIC_DRAW are hints that a driver is free to ignore. Direct3D's usages are contracts: an immutable buffer cannot be written, a dynamic one cannot be written with UpdateSubresource, and so on. In exchange the driver knows exactly where to put each buffer.

Filling a buffer

So far every vertex buffer has been immutable, filled by passing the data straight to CreateBuffer, which is what glBufferData with a data pointer does. Like glBufferData with a NULL pointer, CreateBuffer also accepts no data at all, and then only reserves the memory. That is not allowed for an immutable buffer (it could never be filled afterwards), but it is how a default or dynamic buffer usually starts out.

OpenGL's next tool is glBufferSubData, which writes part of a buffer at some offset. Its Direct3D counterpart is UpdateSubresource on a default-usage resource. It takes an optional D3D11_BOX; for a buffer, the box's left and right are the byte range to write. Written out (this is not from our lessons):

// write sizeof(data) bytes at byte 24 of a DEFAULT buffer
D3D11_BOX box = { 24, 0, 0, 24 + sizeof(data), 1, 1 };
context->UpdateSubresource(buffer, 0, &box, &data, 0, 0);

Our texture loading has used UpdateSubresource all along, for the top level of each texture before its mipmaps are generated:

ID3D11DeviceContext_UpdateSubresource(context, cast(texture, *ID3D11Resource), 0, null, img.pixels, cast(img.width * 4, UINT), 0);

The null is the box: no box means the whole subresource, here mip level 0.

The third way, and the one this chapter's program uses, is to ask for a pointer to the buffer's memory and write into it ourselves. OpenGL calls it glMapBuffer; Direct3D calls it Map, on a dynamic buffer. We have in fact been doing this every frame since Transformations, for the constant buffers:

// WRITE_DISCARD hands back fresh memory, so the GPU never waits on the CPU.
update_buffer: proc(context: *ID3D11DeviceContext, buffer: *ID3D11Buffer, data: *const void, size: u64) -> void = {
    mapped: D3D11_MAPPED_SUBRESOURCE = {};
    ID3D11DeviceContext_Map(context, cast(buffer, *ID3D11Resource), 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0, mapped&);
    memcpy(mapped.pData, data, size);
    ID3D11DeviceContext_Unmap(context, cast(buffer, *ID3D11Resource), 0);
}

Map fills in pData, a pointer we can write through until we call Unmap, the equivalent of glUnmapBuffer. The interesting argument is the map type. The GPU runs behind the CPU, often by a frame or more, so when we want to write a buffer, the GPU may still be reading the previous contents for a draw we issued earlier. Overwriting that memory would change a draw after the fact; waiting for the GPU to finish would stall us. D3D11_MAP_WRITE_DISCARD avoids both: it tells the driver that we will replace the whole buffer and don't need the old contents, so the driver can hand us a fresh piece of memory and quietly retire the old one once the GPU is done with it. This is called buffer renaming, and it is why one small constant buffer can be rewritten before every draw call in a frame and each draw still sees its own values.

The other write mode for buffers, D3D11_MAP_WRITE_NO_OVERWRITE, is a promise in the opposite direction: we will only write parts of the buffer that no pending draw is using, so the driver can give us the existing memory without waiting. Together they make the classic pattern for streaming many small pieces of geometry: append with no-overwrite until the buffer is full, then discard and start again at the beginning.

Mapping is handy whenever the data is being produced anyway: decoded straight from a file, or computed in a loop, as we do below. It saves building a copy in our own memory first. In our lessons it is simplest to compute into an array and memcpy it, which is still a single copy.

A buffer rewritten every frame

The program draws a square of water 8 units wide, as a grid of 96 by 96 points. The height of each point is a sum of three sine waves running in different directions at different speeds:

wave_height: proc(x: f32, z: f32, t: f32) -> f32 = {
    return 0.25 * sinf(x * 1.3 + t * 1.7) + 0.15 * sinf(z * 2.1 - t * 1.1) + 0.08 * sinf((x + z) * 3.7 + t * 2.3);
}

A vertex shader could compute this itself. We do it on the CPU on purpose, as a stand-in for all the cases where vertex data really does come from the CPU every frame: cloth or physics simulations, particles, text, debug lines. The positions therefore go into a dynamic buffer, made with no initial data and with CPU write access:

// slot 0: positions, DYNAMIC, rewritten every frame
position_desc: D3D11_BUFFER_DESC = {
    .ByteWidth = cast(points * sizeof(vec3), UINT),
    .Usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC,
    .BindFlags = D3D11_BIND_VERTEX_BUFFER,
    .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE,
};
position_buffer: *ID3D11Buffer = null;
ID3D11Device_CreateBuffer(device, position_desc&, null, position_buffer&);

It is the same recipe as create_constant_buffer, with a different bind flag. Every frame, before drawing, we recompute all 9216 positions into an array in our own memory and copy the lot in with the same update_buffer the constant buffers use:

// update the positions, then copy them into the dynamic buffer
for (z: u32 = 0; z < grid; z += 1) {
    for (x: u32 = 0; x < grid; x += 1) {
        wx: f32 = (cast(x, f32) / cast(grid - 1, f32) - 0.5) * grid_size;
        wz: f32 = (cast(z, f32) / cast(grid - 1, f32) - 0.5) * grid_size;
        positions[z * grid + x] = v3(wx, wave_height(wx, wz, current_frame), wz);
    }
}
update_buffer(context, position_buffer, positions, points * sizeof(vec3));

Since we write every byte, discarding is exactly right. The points are joined into triangles by an index buffer, built once at startup, two triangles per grid square, and that never changes: an immutable buffer. So is the water's colour, and that brings us to the second topic.

Batching vertex attributes

Every vertex buffer so far has been interleaved: all of one vertex's attributes side by side, then all of the next vertex's. Position, normal, uv, position, normal, uv: 123123123. Another option is to keep each attribute in a block of its own, all positions, then all normals, then all uvs: 111222333. Data loaded from files often comes this way, one array per attribute, and keeping it so saves interleaving it ourselves.

LearnOpenGL does it by writing the three arrays one after another into a single buffer with glBufferSubData, and pointing each attribute at its own offset. In Direct3D an attribute does not point at a buffer at all; it names an input slot, and the buffers are bound to slots separately. The input assembler has 32 of them (D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT), and each slot has its own buffer, offset and stride. Splitting attributes is a matter of reading each from a different slot.

For our water there is a good reason to split them beyond convenience: the positions change every frame, and the colours never do. In one interleaved buffer, the unchanging colours would have to be rewritten every frame along with the positions. In two buffers, we only rewrite what changes. The input layout says which slot each attribute comes from:

layout: [2]D3D11_INPUT_ELEMENT_DESC = {
    { .SemanticName = "POSITION", .Format = DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT, .InputSlot = 0 },
    { .SemanticName = "COLOR", .Format = DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT, .InputSlot = 1 },
};

Both attributes start at byte 0 of their vertex, since each slot holds only one attribute. The colours, a blue that brightens toward the near edge of the water, are computed once and go into an immutable buffer like any mesh:

// slot 1: colours, IMMUTABLE, uploaded once
color_buffer: *ID3D11Buffer = create_buffer(device, D3D11_BIND_VERTEX_BUFFER, colors, points * sizeof(vec3));
index_buffer: *ID3D11Buffer = create_buffer(device, D3D11_BIND_INDEX_BUFFER, indices, index_count * 4);

At draw time, IASetVertexBuffers binds a run of slots in one call. It takes arrays: one buffer, one stride and one offset per slot, here two of each, each stride being one vec3, 12 bytes:

buffers: [2]*ID3D11Buffer = { position_buffer, color_buffer };
strides: [2]UINT = { 12, 12 };
offsets: [2]UINT = { 0, 0 };
ID3D11DeviceContext_IASetPrimitiveTopology(context, D3D11_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
ID3D11DeviceContext_IASetVertexBuffers(context, 0, 2, buffers[0]&, strides[0]&, offsets[0]&);
ID3D11DeviceContext_IASetIndexBuffer(context, index_buffer, DXGI_FORMAT.DXGI_FORMAT_R32_UINT, 0);
ID3D11DeviceContext_RSSetState(context, wire_state);
ID3D11DeviceContext_DrawIndexed(context, cast(index_count, UINT), 0, 0);
ID3D11DeviceContext_RSSetState(context, raster_state);

An index of 517 fetches position 517 from slot 0 and colour 517 from slot 1. The vertex shader cannot tell that its inputs came from two places; the input layout put them back together:

// POSITION comes from vertex buffer slot 0 and COLOR from slot 1; the
// shader cannot tell, the input layout joins them.
water_vertex vs_main(float3 position : POSITION, float3 color : COLOR)
{
    water_vertex o;
    o.position = mul(projection, mul(view, float4(position, 1.0)));
    // Crests a little brighter, so the motion reads even in wireframe.
    o.color = color * (0.8 + position.y);
    return o;
}

The water is drawn in wireframe, with the rasterizer state from Hello Triangle, because a flat-coloured surface with no lighting would hide the waves; the grid lines show them. For the same reason the vertex shader scales the colour by the height, brighter on the crests and darker in the troughs.

LearnOpenGL's layout, three blocks in one buffer, also works in Direct3D: bind the same buffer to three slots, with offsets of 0, the size of the positions, and the size of the positions plus normals, and give each slot its own stride. Which arrangement is faster depends on the hardware; interleaved data is the usual default, since a vertex shader invocation then finds all its inputs next to each other in memory. Split buffers win when, as here, some attributes change and others do not, or when some passes need only some attributes: a depth-only pass that reads only positions touches only the position buffer.

Copying buffers

Once data is on the GPU, we may want to copy it into another buffer without bringing it back to the CPU. OpenGL's glCopyBufferSubData copies between the buffers bound to two targets, and because both of them may be vertex buffers, it introduced two extra targets, GL_COPY_READ_BUFFER and GL_COPY_WRITE_BUFFER, purely to have somewhere to bind them. Direct3D names resources directly in its calls, so no binding is involved:

// copy 8 floats from the start of vb1 to the start of vb2
D3D11_BOX box = { 0, 0, 0, 8 * sizeof(float), 1, 1 };
context->CopySubresourceRegion(vb2, 0, 0, 0, 0, vb1, 0, &box);

Copies run on the GPU, in order with the draws around them, so a copy issued after a draw sees what that draw left behind. The destination may not be immutable, and the two resources must be compatible: a buffer to a buffer, a texture to a texture of the same format family. Copying into a staging buffer and mapping it for reading is how data computed on the GPU is brought back to the CPU, though the map then has to wait for the GPU to catch up.

With these, buffers become more than places to park meshes. The next chapter puts a constant buffer to work across many shaders at once, and Instancing uses a second vertex slot for data that advances once per object instead of once per vertex.

The full program is here, and its shader here.

Additional resources

Exercises

  1. Change the program to keep positions and colours interleaved in one dynamic buffer, 24 bytes per vertex. What does the input layout become, and how much more data is copied per frame?
  2. Make the colour buffer D3D11_USAGE_DEFAULT and, when a key is pressed, give the water a new colour with UpdateSubresource.
  3. Move the wave computation into the vertex shader: send a flat grid once, pass the time in a constant buffer, and compute the height from position.x and position.z. The dynamic buffer is then no longer needed at all.
Credit: this chapter follows LearnOpenGL's Advanced Data by Joey de Vries, rewritten for Direct3D 11 and rin. LearnOpenGL's text and code are licensed CC BY-NC 4.0, and so is this page; the wording, code and screenshots here are new, and diagrams marked as LearnOpenGL's are its own.