Search code examples
c++cgpudirectx-11dxgi

DirectX11 with a multiple video adapter (GPU) PC


Usually the DirectX11 initialization starts from creating a DirectX11 device:

D3D_DRIVER_TYPE driverTypes[] =
{
    D3D_DRIVER_TYPE_HARDWARE,
    D3D_DRIVER_TYPE_WARP,
    D3D_DRIVER_TYPE_REFERENCE,
};

UINT nNumDriverTypes = ARRAYSIZE(driverTypes);

// Feature levels supported
D3D_FEATURE_LEVEL featureLevels[] =
{
    D3D_FEATURE_LEVEL_11_0,
    D3D_FEATURE_LEVEL_10_1,
    D3D_FEATURE_LEVEL_10_0,
    D3D_FEATURE_LEVEL_9_1
};

UINT nNumFeatureLevels = ARRAYSIZE(featureLevels);
D3D_FEATURE_LEVEL featureLevel;

// Create device
for (UINT n = 0; n < nNumDriverTypes; ++n)
{
    hr = D3D11CreateDevice(nullptr,driverTypes[n],nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT,featureLevels,nNumFeatureLevels,
        D3D11_SDK_VERSION,&m_pDevice,&featureLevel,&m_pDeviceContext);

Then you create a swap chain for your window:

IDXGIDevice* pDXGIDevice = nullptr;
HRESULT hr = m_pDevice->QueryInterface(__uuidof(IDXGIDevice),
    reinterpret_cast<void**>(&pDXGIDevice));
if (SUCCEEDED(hr))
{
    IDXGIAdapter* pDXGIAdapter = nullptr;
    hr = pDXGIDevice->GetParent(__uuidof(IDXGIAdapter),
        reinterpret_cast<void**>(&pDXGIAdapter));   
    if (SUCCEEDED(hr))
    {
        IDXGIFactory2* pDXGIFactory = nullptr;
        hr = pDXGIAdapter->GetParent(__uuidof(IDXGIFactory2),
            reinterpret_cast<void**>(&pDXGIFactory));
        if (SUCCEEDED(hr))
        {
            DXGI_SWAP_CHAIN_DESC1 desc = {};
            desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
            desc.BufferCount = 2;
            desc.Width = nWindowWidth;
            desc.Height = nWindowHeight;
            desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
            desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
            desc.SampleDesc.Count = 1;
            desc.SampleDesc.Quality = 0;        

            hr = pDXGIFactory->CreateSwapChainForHwnd(m_pDevice,hWnd,
                &desc,nullptr,nullptr,&m_pSwapChain);

My PC has two video adapters connected to two monitors. Adapter1 is connected to Monitor1. Adapter2 is connected to Monitor2. I know i can enumerate DXGI adapters and use a specific adapter for D3D11CreateDevice to create a DirectX11 device but this does not help me because i do not know which monitor shows my window.

  • How can i find which monitor shows my window? Do I have to use that monitor video adapter or can I use any adapter?
  • What happens if a user moves the window from Monitor1 to Monitor2? Does another adapter start showing the window?
  • Overall, how does DirectX11 handle such an issue?

Solution

  • If you want to create a device that matches the adapter, you need a few changes in the call to :

    D3D11CreateDevice
    

    The idea is to provide the Adapter parameter yourself, so the process is :

    1/Create the DXGI Factory yourself, using CreateDXGIFactory

    2/Enumerate adapters from this factory, for this you use factory->EnumAdapters

    3/For each adapter, you can enumerate which monitor is attached to them, adapter->EnumOutputs

    4/You can then get the monitor description using output->GetDesc

    5/From that description you can access the screen bounds eg :

    output_desc.DesktopCoordinates
    

    Now you can use the bounds of your window and perform the comparison (largest area, contained...) When you found the most suitable monitor, you can use it for the device create function, such as :

    IDXGIAdapter* my_requested_adapter;
    D3D11CreateDevice(my_requested_adapter, D3D_DRIVER_TYPE_UNKNOWN , nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT,featureLevels,nNumFeatureLevels,
        D3D11_SDK_VERSION,&m_pDevice,&featureLevel,&m_pDeviceContext);
    

    So the only 2 differences in the call are that you specify the requested adapter in the first argument of the function, and driver type becomes D3D_DRIVER_TYPE_UNKNOWN (this is the only valid argument if you provide a specific adapter, since the driver type is inferred from it).

    Now what happens if user moves the window to the second monitor (which is connected to the other graphics card)?

    Initially it will "magically work", since the Desktop Window Manager (DWM) will detect that the content from your window needs to be presented on the other graphics card. The major issue is, this will have a large impact on performances (since DWM will need to "download" the swapchain content from adapter1, and upload it to adapter2 then present it there. So while it works, it involves a GPU round trip (you will also notice the DWM process suddenly increasing CPU usage, in a potential drastic way depending on your system).

    Please note that it can (will) also add increased latency (we had a couple setups with multiple windows attached to a different monitors, each of them attached to a different GPU using a single device, it worked but the content was massively out of sync (the monitor attached to the other card was lagging behind), so we had to make sure that we used 2 devices instead, one for each card).

    So if you want to make sure to always use the most relevant adapter, you need to get windows bounds each frame, and if the window is on a different adapter, you need to create a new device using that adapter instead (of course, that involves recreating every resource that was created on the previous one). The bounds check process is really fast so there is basically no performance impact of checking that (you can also optimize and skip that check if only one adapter is present).

    Note:

    There is also the function called : IDXGISwapChain::GetContainingOutput Which should give you the display monitor that contains the most of the window. From that you could be able to perform a GetParent to retrieve the adapter, and check if the adapter is the same as the one currently used by the device. I never tried that solution but that could also work (the issue with it is that you already need a Swapchain, so at creation time this is not usable).