Buffers

To render meshes, we need to expose the vertex and index data of the meshes to the device. In DirectX 12, it is required to create a resource in order for the API to be able to access the data. Resources are limited to buffers and a wide variety of texture types. The differences between buffers and textures we will get into when we cover textures.

ID3D12Resource *vbibRes;
result = renderer->device->lpVtbl->CreateCommittedResource (
    renderer->device,
    &(D3D12_HEAP_PROPERTIES){ .Type = D3D12_HEAP_TYPE_UPLOAD },
    0,
    &(D3D12_RESOURCE_DESC){
        .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER,
        .Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT,
        .Width     = scene->vertexDataSizeInBytes + scene->indexDataSizeInBytes,
        .Height    = 1, .DepthOrArraySize = 1, .MipLevels = 1,
        .Format    = DXGI_FORMAT_UNKNOWN,
        .Layout    = D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
        .SampleDesc.Count = 1,
    },
    D3D12_RESOURCE_STATE_GENERIC_READ,
    NULL,
    &IID_ID3D12Resource,
    &vbibRes
);
if ( !SUCCEEDED(result) )
    RETURN_ERROR(-1, "CreateCommittedResource failed (0x%08X)", result );

For simplicity, we made a couple of notable choices here:

  1. We created one resource containing both the vertex and the index data. This is separated at a later point in time.
  2. We created the resources on the D3D12_HEAP_TYPE_UPLOAD heap. In DirectX 12, this is the only way to pass data from the CPU to the GPU. However, this heap is CPU-local, and might not be efficient to render. Preferably, we would have a device-local heap (D3D12_HEAP_TYPE_DEFAULT) to copy all data to. Since this is not required, we chose not to do this. It is required for textures, which we will cover later. The same steps can be applied here for performance reasons.
  3. We created what's called a “Committed” resource. This means the driver will automatically allocate a block of memory to contain the resource without having to allocate it manually. This is convenient, especially for a tutorial describing how to get a basic renderer up and running. But in actual production scenarios, it may be beneficial to either use CreatePlacedResource or CreateReservedResource. CreatePlacedResource allows you to specify exactly where the resource is within an ID3D12Heap block, while CreateReservedResource allows you to create a tiled resource.

We create the resource with D3D12_RESOURCE_STATE_GENERIC_READ as it is required for an _UPLOAD heap. For other heap types, it would be preferable to use resource states more closely adapted to actual use.

The layout is D3D12_TEXTURE_LAYOUT_ROW_MAJOR. This means the data is layed out in a linear fashion. This is the only layout that makes sense for buffers, but for textures, other modes may be required.

With our buffers created, we just need to fill in the data, which is done as follows:

uint8_t* vbibData;
result = vbibRes->lpVtbl->Map ( vbibRes, 0, NULL, &vbibData );
if ( !SUCCEEDED(result) )
    RETURN_ERROR(-1, "Map failed (0x%08X)", result );

memcpy ( vbibData, scene->vertexData, scene->vertexDataSizeInBytes );
memcpy ( vbibData + scene->vertexDataSizeInBytes, scene->indexData, scene->indexDataSizeInBytes );

vbibRes->lpVtbl->Unmap ( vbibRes, 0, NULL );

The Map function is used to return an address to a memory area for the resource which, if written to, will be applied to the resource. It should be noted this function exclusively works on heaps created with the D3D12_HEAP_TYPE_UPLOAD type, and will fail on any other heap type.

Because we created a single resource containing both the vertex and index data, we must not forget to offset the index buffer data starting offset by vertexDataSizeInBytes. This is done inside the second call to memcpy in this scenario.

With the vertex and index data now present within the resource, we now just need to create the data structures telling where the vertex data and index data is for each mesh. The sample associated with this chapter has all vertex buffers and all index buffers of all meshes together in the same resource, but when rendering individual meshes, we will need to be able to tell the API exactly where all the data is.

For this purpose, DirectX 12 has the concept of a D3D12_VERTEX_BUFFER_VIEW and a D3D12_INDEX_BUFFER_VIEW. These are Plain-Old-Data (POD) structures containing the virtual memory address where the portion of the buffer starts, the size of the portion of the buffer, and one more property required in interpreting the data.

Since this code is highly application-specific, we will merely excerpt the code creating the VBV and IBV rather than the loop they are created in. It should also be noted that vertex data can be laid out as and Array Of Structures (AOS) or Structure Of Arrays (SOA). In this case, we are using an SOA layout where the position and texcoord properties are separated, and we have a separate VBV for the position and the texcoord. If you chose to use to interleave position and texcoord (eg by having a more generic Vertex struct containing both properties) it will not be required to create more than one vertex buffer view in most cases.

renderer->meshes[i].vbv[0] = (D3D12_VERTEX_BUFFER_VIEW){
    .BufferLocation = vbibRes->lpVtbl->GetGPUVirtualAddress ( vbibRes ) + vb->vertexOffset,
    .SizeInBytes    = vb->vertexCount * 3 * sizeof ( float ),
    .StrideInBytes  = 3 * sizeof ( float ),
};

// ...

renderer->meshes[i].ibv[j] = (D3D12_INDEX_BUFFER_VIEW){
    .BufferLocation = vbibRes->lpVtbl->GetGPUVirtualAddress ( vbibRes ) + (scene->vertexDataSizeInBytes + ib->indexOffset),
    .SizeInBytes    = ib->indexCount * ib->indexByteSize,
    .Format         = DXGI_FORMAT_R32_UINT,
};