Dukungan Multi-Adapter di DirectX 12


* Artikel ini adalah hasil kerja sama dengan Intel Developer Zone. Artikel asli bisa dilihat di link ini.

Contoh ini menunjukkan bagaimana mengimplementasikan aplikasi multi-adapter menggunakan DirectX 12. GPU terintegrasi dari Intel (iGPU) dan NVIDIA GPU (dGPU) digunakan untuk berbagi beban kerja dalam ray-tracing sebuah adegan. Penggunaan paralel di antara GPU memungkinkan terjadinya peningkatan performa dan dapat melalkukan beban kinerja yang lebih kompleks.

Contoh ini menggunakan beberapa adapter untuk membuat adegan ray-traced sederhana menggunakan pixel shader. Kedua adapter me-render sebagian dari adegan secara paralel.

Ringkasan Multi-Adapter Eksplisit

Dukungan untuk multi-adapter eksplisit adalah fitur baru pada Directx 12. Fitur ini memungkinkan penggunaan beberapa GPU secara paralel tanpa memperhatikan produsen dan jenis GPU-nya (sebagai contoh, terintegrasi atau tersendiri).

Kemampuan untuk memisahkan pekerjaan di beberapa GPU disediakan oleh sebuah manajemen resource independen dan antrian paralel untuk setiap GPU di tingkat API.

DirectX 12 memperkenalkan dua API fitur utama yang membantu memungkinkan aplikasi multi-adapter

  1. Memori cross-adapter yang dapat dilihat oleh kedua adapter
    DirectX 12 memperkenalkan resource yang cross-adapter-spesific dan heap flags:
    – D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER
    – D3D12_HEAP_FLAG_SHARED_CROSS_ADAPTER
    Resource cross-adapter tersedia pada memori utama adapter dan dapat direferensikan dari adapter lain dengan biaya minimal.
    Dukungan Multi-Adapter di DirectX 12
  2. Parallel queues dan sinkronisasi cross-adapter yang memungkinkan untuk mengeksekusi perintah paralel. Sebuah flag spesial digunakan untuk membuat sinkronisasi:
    D3D12_FENCE_FLAG_SHARED_CROSS_ADAPTER.
    Sebuah pagar cross-adapter memungkinkan antrian pada satu adapter untuk ditandai dengan antrian yang lainnya.
    Dukungan Multi-Adapter di DirectX 12 Diagram di atas menunjukan tiga atrian untuk memfasilitasi penyalinan ke dalam resource cross-adapter. Ini adalah teknik yang digunakan dalam contoh ini dan menampilkan langkah-langkah berikut:1. Queue 1 pada GPU A dan Queue 1 pada GPU B me-render sebagian adegan 3D secara pararel.
    2. Ketika proses render selesai, Queue 1 memberi sinyal, memungkinkan Queue 2 untuk memulai penyalinan.
    3. Queue 2 menyalin adegan yang sudah di-render ke dalam resource dan sinyal cross-adapter.
    4. Queue 1 pada GPU B menunggu untuk Queue 2 pada GPU A ke signal dan Menggabungkan kedua scene yang sudah dirender ke dalam output akhir.

Cara Mengimplementasi Cross-Adapter

  1. Membuat resource cross-adapter pada GPU utama serta menangani resource ini pada GPU sekunder.
    // Describe cross-adapter shared resources on primaryDevice adapter
    D3D12_RESOURCE_DESC crossAdapterDesc = mRenderTargets[0]->GetDesc();
    crossAdapterDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER;
    crossAdapterDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
    
    // Create a shader resource and shared handle
    for (int i = 0; i < NumRenderTargets; i++)
    {
        mPrimaryDevice->CreateCommittedResource(
            &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
            D3D12_HEAP_FLAG_SHARED | D3D12_HEAP_FLAG_SHARED_CROSS_ADAPTER,
            &crossAdapterDesc,
            D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
            nullptr,
            IID_PPV_ARGS(&shaderResources[i]));
    
        HANDLE heapHandle = nullptr;
        mPrimaryDevice->CreateSharedHandle(
            mShaderResources[i].Get(),
            nullptr,
            GENERIC_ALL,
            nullptr,
            &heapHandle);
    
        // Open shared handle on secondaryDevice device
        mSecondaryDevice->OpenSharedHandle(heapHandle, IID_PPV_ARGS(&shaderResourceViews[i]));
    
        CloseHandle(heapHandle);
    }
    
    // Create a shader resource view (SRV) for each of the cross adapter resources
    CD3DX12_CPU_DESCRIPTOR_HANDLE secondarySRVHandle(mSecondaryCbvSrvUavHeap->GetCPUDescriptorHandleForHeapStart());
    for (int i = 0; i < NumRenderTargets; i++)
    {
        mSecondaryDevice->CreateShaderResourceView(shaderResourceViews[i].Get(), nullptr, secondarySRVHandle);
        secondarySRVHandle.Offset(mSecondaryCbvSrvUavDescriptorSize);
    }
  2. Membuat sinkronasi untuk resource yang dibagi di antara kedua adapter.
    // Create fence for cross adapter resources
    mPrimaryDevice->CreateFence(mCurrentFenceValue,
        D3D12_FENCE_FLAG_SHARED | D3D12_FENCE_FLAG_SHARED_CROSS_ADAPTER,
        IID_PPV_ARGS(&primaryFence));
    
    // Create a shared handle to the cross adapter fence
    HANDLE fenceHandle = nullptr;
    mPrimaryDevice->CreateSharedHandle(
        primaryFence.Get(),
        nullptr,
        GENERIC_ALL,
        nullptr,
        &fenceHandle));
    
    // Open shared handle to fence on secondaryDevice GPU
    mSecondaryDevice->OpenSharedHandle(fenceHandle, IID_PPV_ARGS(&secondaryFence));
  3. Render di GPU primer ke dalam target render dan beri sinyal antrian ketika selesai
    // Render scene on primary device
    mPrimaryCommandQueue->ExecuteCommandLists(1, primaryCommandList);;
    
    // Signal primary device command queue to indicate render is complete
    mPrimaryCommandQueue->Signal(mPrimaryFence.Get(), currentFenceValue));
    fenceValues[currentFrameIndex] = currentFenceValue;
    mCurrentFenceValue++;
  4. Salin resource dari target render offscreen ke dalam resource cross-adapter, dan beri sinyal antrian ketika selesai.
    // Wait for primary device to finish rendering the frame
    mCopyCommandQueue->Wait(mPrimaryFence.Get(), fenceValues[currentFrameIndex]);
    
    // Copy from off-screen render target to cross-adapter resource
    mCopyCommandQueue->ExecuteCommandLists(1, crossAdapterResources->mCopyCommandLists.Get());
    
    // Signal secondary device to indicate copy is complete
    mCopyCommandQueue->Signal(mPrimaryCrossAdapterFence.Get(), mCurrentCrossAdapterFenceValue));
    mCrossAdapterFenceValues[mCurrentFrameIndex] = mCurrentCrossAdapterFenceValue;
    mCurrentCrossAdapterFenceValue++;
  5. Render pada GPU sekunder menggunakan penanganan untuk resource cross-adapter untuk mengakses resource sebagai tekstur.
    // Wait for primary device to finish copying
    mSecondaryCommandQueue->Wait(mSecondaryCrossAdapterFence.Get(), mCrossAdapterFenceValues[mCurrentFrameIndex]));
    
    // Render cross adapter resources and segmented texture overlay on secondary device
    mSecondaryCommandQueue->ExecuteCommandLists(1, secondaryCommandList);
  6. GPU sekunder menampilkan frame ke layar.
    mSwapChain->Present(0, 0);
    MoveToNextFrame();

Perhatikan bahwa kode yang diberikan di atas telah dimodifikasi demi penyederhanaan dengan semua pengecekan eror telah dihapus. Kode ini tidak diharapkan untuk di-compile.

Performa dan Hasil

Menggunakan beberapa adapter untuk me-render sebuah adegan secara paralel menyebabkan terjadinya peningkatan yang signifikan dalam performa dibandingkan dengan mengandalkan satu adapter untuk melakukan seluruh pekerjaan render.

Dukungan Multi-Adapter di DirectX 12

Gambar 1. Frametime dari 100 frame dalam milidetik dibandingkan perpecahan kinerja antara kartu terintegrasi dan tersendiri.

Dalam contoh adegan ray-traced, terjadi penurunan sekitar 25 milidetik ketika kedua NVIDIA GeForce 840M dan Intel HD Graphics 5500 digunakan untuk melakukan proses render bersama-sama.

Dengan beban kerja yang paralel, ini memungkinkan untuk mengurangi frame time yang dibutuhkan hingga sekitar 50% dibandingkan dengan menggunakan satu adapter.

Dukungan Multi-Adapter di DirectX 12

Lampiran: Ringkasan Arsitektur Contoh

Contoh ini dapat diunduh di Github. Dengan arsitektur sebagai berikut:

  • WinMain.cpp
    > Titik masuk ke dalam aplikasi
    > Membuat onjek dan memberikan contoh render
  • DXDevice.cpp
    > Mengenkapsulasi objek ID3D12Device disamping objek terkait
    > Berisi perintah antrian, alokasi, target render, fence, dan tumpukan descriptop
  • DXRenderer.cpp
    > Class render dasar
    > Mengimplementasi fungsi berbagi (misalnya, membuat titik penyangga atau mempebarui tekstur)
  • DXMultiAdapterRenderer.cpp
    Melakukan semua fungisonalitas render inti yang spesifik implementasiny (yaitu menyediakan pipeline, beban aset, dan mengisi daftar perintah)
  • DXCrossAdapterResources.cpp
    > Menciptakan abstraksi dan memperbarui sumber daya multi-adapter
    > Menangani penyalinan resource dan memagari kedua GPU

DX multi AdapterRenderer.cpu terdiri dari fungsi-fungsi berikut

public:
    DXMultiAdapterRenderer(std::vector<DXDevice*> devices, MS::ComPtr<IDXGIFactory4> dxgiFactory, UINT width, UINT height, HWND hwnd);
    virtual void OnUpdate() override;
    float GetSharePercentage();
    void IncrementSharePercentage();
    void DecrementSharePercentage();
protected:
    virtual void CreateRootSignatures() override;
    virtual void LoadPipeline() override;
    virtual void LoadAssets() override;
    virtual void CreateCommandLists() override;
    virtual void PopulateCommandLists() override;
    virtual void ExecuteCommandLists() override;
    virtual void MoveToNextFrame() override;

Class ini mengimplementasi semua fungsi render inti. LoadPipeline() dan LoadAssets() berfungsi untuk menciptakan semua yang signature root yang diperlukan, kompilasi shader, dan menciptakan objek pipeline serta menentukan dan membuat semua tekstur, buffer konstan, dan vertex buffers dan view terkait mereka. Semua daftar perintah dibuat pada saat tersebut juga.

Untuk setiap frame, PopulateCommandList() dan ExecuteCommandList() dipanggil.

Untuk memisahkan fungsionalitas render DirectX 12 tradisional dengan yang diperlukan untuk menggunakan multiple-adapter, semua fungsi cross-adapter di enkapsulasi dalam class AXCrossAdapterResources yang berisi fungsi-fungsi sebagai berikut:

public:
    DXCrossAdapterResources(DXDevice* primaryDevice, DXDevice* secondaryDevice);
    void CreateResources();
    void CreateCommandList();
    void PopulateCommandList(int currentFrameIndex);
    void SetupFences();

Fungsi CreateResources(), CreateCommandList(), and SetupFences() dipanggil inisialisasinya untuk membuat resource cross-adapter dan menginisialisasi objek sinkronasi.

Pada setiap frame, fungsi PopulateCommandList() dipanggil untuk menyalin daftar perintah.

Class DXCrossAdapterResources berisi daftar perintah alokasi yang terpisah, antrian perintah, dan perintah yang digunakan untuk menyalin resource dari target render dalam adapter primer ke dalam resource cross-adapter.

* Artikel ini adalah hasil kerja sama dengan Intel Developer Zone. Artikel asli bisa dilihat di link ini.