Adapters and devices

With the DXGI factory now created, it is time to specify the render adapter we wish to use, and to create a device object. The ID3D12Device will serve as our main interface for managing our GPU activities and subobjects related to that purpose.

In order to create the device, we will first need to pick an adapter to run. An adapter can be thought of as a GPU or other rendering subsystem: When multiple GPUs are present in the system, you can explicitly pick which GPU you would prefer to run on, or even create multiple devices on multiple adapters and allow them to co-operate with Direct3D 12's explicit multi-GPU support.

However, another purpose for the adapter system was alluded to earlier: Since DirectX 11, Microsoft has shipped the Windows SDKs with support for the so called “WARP” adapter. WARP stands for “Windows Advanced Rasterization Platform”, and is effectively a highly-optimized software rasterizer, allowing for graphics applications to be written in DirectX 11 and DirectX 12 to be run on the CPU with minimal code differences. This can be useful for two destinct, but vital, purposes:

  • None of the GPUs in your system support the DirectX 12 API yet. In this case, WARP can be used instead. WARP is usually far slower than a hardware implementation provided by a GPU vendor, but allows for the code to be developed and tested without the prescense of such hardware.
  • There is a discrepancy between 2 GPU behaviours. There can be cases where issues occur on an integrated GPU that do not occur in a dedicated GPU, or there is a difference between two different dedicated GPUs. In order to aid investigation in finding the cause of the problem, WARP can be used as a baseline renderer from Microsoft itself. Generally, when an implementation deviates from the results generated by WARP, it can be safely assumed that implementation is at fault. Therefore, this functionality can be useful for tracking down an issue for submission to a hardware vendor.

In order to create a device, we would like to support both WARP and dedicated GPUs in the system. To do this, we enumerate the adapters in the IDXGIFactory4 object as follows:

IDXGIAdapter1* adapters[4] = { NULL };
uint32_t adapterCount = 0;
#if ALLOW_HARDWARE_ADAPTERS
while ( adapterCount < (STATIC_ARRAY_SIZE(adapters)-1) && renderer->dxgiFactory->lpVtbl->EnumAdapters1 ( renderer->dxgiFactory, adapterCount, &adapters[adapterCount] ) != DXGI_ERROR_NOT_FOUND )
{
    adapterCount++;
}
#endif
#if ALLOW_WARP_ADAPTER
uint32_t warpIndex = RVM_MIN(adapterCount,STATIC_ARRAY_SIZE(adapters)-1);
if ( SUCCEEDED ( renderer->dxgiFactory->lpVtbl->EnumWarpAdapter ( renderer->dxgiFactory, &IID_IDXGIAdapter1, &(void*)adapters[warpIndex] ) ) )
    adapterCount = warpIndex + 1;
#endif

The code above comes down to the following:

  1. Zero-initialize a static array of adapters
  2. If we enabled hardware adapters in a preprocessor macro, we fill the list of adapters until either the list is full, or we ran out of adapters to run through
  3. If we enabled warp adapters in a preprocessor macro, we append the warp adapter at the end of the list, assuming there is space left. If there is no space left, instead the implementation will overwrite the last GPU.

With our preferred list of adapters, we want to go by them one-by-one in order to try and create a D3D12 device. In order to do so, we need to loop through all adapters and call D3D12CreateDevice with the adapter as input.

In addition, we want to try to call this function with multiple “feature levels.” A feature level describes the level of functionality to a baseline level in order to indicate what features we require at a bare minimum. A description of the feature levels available is outside of the scope of this article, but can be found here.

for ( uint32_t adapterIndex = 0; adapterIndex < adapterCount; adapterIndex++ )
{
    D3D_FEATURE_LEVEL featureLevels[] = {
        D3D_FEATURE_LEVEL_12_1,
        D3D_FEATURE_LEVEL_12_0,
        D3D_FEATURE_LEVEL_11_1,
        D3D_FEATURE_LEVEL_11_0,
    };

    IDXGIAdapter3* adapter = NULL;
    result = adapters[adapterIndex]->lpVtbl->QueryInterface (
        adapters[adapterIndex],
        &IID_IDXGIAdapter3,
        &adapter
    );
    if ( !SUCCEEDED(result) )
        continue;

    for ( uint32_t i = 0; i < sizeof ( featureLevels ) / sizeof ( featureLevels[0] ); i++ )
    {
        ID3D12Device* device = NULL;

        result = D3D12CreateDevice(
            (IUnknown*)adapter,
            featureLevels[i],
            &IID_ID3D12Device,
            &(void*)device
        );
        if ( SUCCEEDED ( result ) )
        {
            renderer->device = device;
            break;
        }
    }

    if ( renderer->device )
        break;
}

Just below the list of feature levels, there is a strange call to QueryInterface. This is done in order to obtain the IDXGIAdapter3 object from the IDXGIAdapter1 object. The reason for this is related to the added functionality in IDXGIAdapter3 related to memory management, in addition to the fact that you should try to avoid mingling different versions of the object for as much as possible. Therefore, we convert our IDXGIAdapter1 object to IDXGIAdapter3 whereever required.