DirectX* 12 中的多适配器支持

下载代码示例   下载 PDF

简介

本示例展示了如何使用 DirectX 12 实施显式多适配器应用。 英特尔的集成 GPU (iGPU) 和独立 NVIDIA GPU (dGPU) 用于分担场景光线追踪工作负载。 并行使用这两种 GPU 有助于提升性能,并支持更复杂的工作负载。

本示例使用多个适配器渲染采用像素着色器的简单光线追踪场景。 两个适配器并行渲染场景的一部分。

显式多适配器概述

显式多适配器支持是 DirectX 12 的新特性。 该特性支持并行使用多个 GPU,无论其制造商和类型(如集成或独立)是什么。 借助独立的资源管理,以及 API 层上针对每个 GPU 的并行队列,所以能够将工作分配多个 GPU 上。

DirectX 12 主要采用了两种 API 特性来支持多适配器应用:

  • 对两个适配器均可见的跨适配器内存

    DirectX 12 使用了跨适配器特定资源和堆标记:
    • D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER
    • D3D12_HEAP_FLAG_SHARED_CROSS_ADAPTER
    跨适配器资源存储在主适配器的内存中,并且能够以最小的成本从其他适配器中引用。

  • 并行队列和跨适配器同步支持并行执行命令。 当创建同步栅栏时,使用特殊标记: D3D12_FENCE_FLAG_SHARED_CROSS_ADAPTER.

    跨适配器栅栏支持一个适配器的队列响应另一个适配器队列的信号。



    以上图表展示了使用三个队列加速向跨适配器资源复制的操作。 以下是本示例中使用的技巧,并展示了以下步骤:
    1. GPU A 上的队列 1 和 GPU B 上的队列 1 并行渲染部分 3D 场景。
    2. 渲染完成后,队列 1 发出信号,让队列 2 开始复制。
    3. 队列 2 将渲染的场景复制到跨适配器资源和信号中。
    4. GPU B 上的队列 1 等待 GPU A 上的队列 2 发出信号,并将两个渲染场景集成到最终的输出中。

跨适配器实施步骤

在 DirectX 12 应用中整合次要适配器包含以下步骤:

  1. 在主要 GPU 上创建跨适配器资源,并在次要 GPU 上创建一个与这些资源对应的句柄。
    // 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. 为两个适配器间共享的资源创建同步栅栏。
    // 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. 在主要 GPU 上渲染到屏幕外渲染目标,完成后向队列发送信号。
    // 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. 将资源从屏幕外渲染目标复制到跨适配器资源,并在完成后向队列发送信号。
    // 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. 在次要 GPU 上渲染,使用跨适配器资源的句柄将资源作为纹理访问。
    // 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 在屏幕上显示帧。
    mSwapChain->Present(0, 0);
    
    MoveToNextFrame();

注意,为简便起见,上述提供的代码进行了修改,删除了全部错误检查代码。 它无法进行编译。

性能和结果

与依赖单个适配器执行整个渲染工作负载相比,使用多个适配器并行渲染一个场景能够显著提升性能。


图 1. 相比在集成和独立显卡上分配工作,100 帧的帧时间(单位:毫秒)。

在示例光线追踪场景中,结果显示,当同时使用 NVIDIA GeForce* 840M 和英特尔® 高清显卡 5500 分担渲染负载时,时间减少将近 26 毫秒。

与使用单个适配器相比,对工作负载并行处理最多可将所需的帧时间减少近 50%。

注意,本示例中展示的工作负载可以轻松实现并行,可能不会立即转换为真实的游戏应用。

附录: 示例架构概述

本示例可从 Github 上进行下载。 其构建方式如下:

  • WinMain.cpp
    • 应用接入点
    • 创建对象和实例化渲染器
  • DXDevice.cpp
    • 将 ID3D12Device 对象封装到相关对象旁边
    • 其中包含命令队列、分配算符、渲染目标、栅栏和描述符堆
  • DXRenderer.cpp
    • 基本的渲染器类
    • 实施共享功能(如,创建定点缓冲区或更新纹理)
  • DXMultiAdapterRenderer.cpp
    • 执行所有实施特定的核心渲染功能(即,设置管线、加载资产和填充命令列表)
  • DXCrossAdapterResources.cpp
    • 对创建的内容进行抽象化处理并更新多适配器资源
    • 在两个 GPU 间复制资源并处理栅栏

DXMultiAdapterRenderer.cpp 包括以下函数:

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;

该类可实施所有的核心渲染功能。 LoadPipeline() 和 LoadAssets() 函数负责创建所有必要的根签名,编译着色器,创建管线状态对象以及指定和创建所有纹理、常量缓冲区和顶点缓冲区及其相关视图。 所有基本的命令列表也在此时创建。

针对每一帧,调用 PopulateCommandLists() 和 ExecuteCommandLists()。

为了将传统 DirectX 12 渲染功能与使用多适配器的应用区别开来,我们将所有的基本跨适配器功能封装到 DXCrossAdapterResources 类中,其中包含以下功能:

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

初始化时调用 CreateResources()、CreateCommandList() 和 SetupFences() 函数,以创建跨适配器资源并初始化同步对象。

在每一帧中,调用 PopulateCommandList() 函数填充复制命令列表。

DXCrossAdapterResources 类包含一个单独的命令分配符、命令队列和命令列表,用于将资源从主要适配器的渲染目标上复制到跨适配器资源中。

关于作者

本文的作者是 Nicolas Langley,他是英特尔视觉计算软件事业部 DirectX* 12 项目多适配器支持的实习生。

通知:

英特尔技术可能要求激活支持的硬件、特定软件或服务。 实际性能会因您使用的具体系统配置的不同而有所差异。 请咨询您的系统制造商或零售商。

在性能检测过程中涉及的软件及其性能只有在英特尔微处理器的架构下才能得到优化。 诸如 SYSmark* 和 MobileMark* 等测试均系基于特定计算机系统、硬件、软件、操作系统及功能。 上述任何要素的变动都有可能导致测试结果的变化。 请参考其它信息及性能测试(包括结合其它产品使用时的运行性能)以对目标产品进行全面评估。

本文档不代表英特尔公司或其它机构向任何人明确或隐含地授予任何知识产权。

英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。

本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的所有信息可随时更改,恕不另行通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。

本文所述的产品和服务可能包含与宣称的规格不符的缺陷或失误。 英特尔提供最新的勘误表备索。

如欲获取本文提及的带订购编号的文档副本,可致电 1-800-548-4725,或访问 www.intel.com/design/literature.htm

英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。

*其他的名称和品牌可能是其他所有者的资产。

英特尔公司 © 2015 年版权所有。

有关编译器优化的更完整信息,请参阅优化通知