前言

  • DX12对于初学者来说难度是偏大的,龙书确实写的不错,但笔者认为还是不够清晰,因此本篇将带你了解DX12最为基本的流程,希望能带你快速入门DX12
  • 本篇为上册,下册将讲解渲染管线

Direct3D流程

  • 流程如下:
    1. 创建windows窗口

    2. Direct3D初始化

    3. 重复

    4. 更新(消息循环)

    5. 渲染图形

    6. 销毁。应用程序结束,清除COM对象,程序退出

  • 一图流:Direct3D基本流程

各组件的关系

  • 下图中,显示了各接口的继承关系,我们大部分时候都只使用派生接口img其中,绿色部分是DX12新增接口

初始化的基础知识

COM和ComPtr

COM

  • 什么是COM?

    1. 一种令DirectX不受编程语言限制,使之向后兼容的技术
    2. COM对象是一种接口
  • 不同之处

    1. 获取指向COM接口的指针。使用COM的特定函数,而非new
    2. 释放COM对象。使用Release(),而非delete
  • 特性

    1. COM对象会统计它自己的引用次数
    2. 引用次数为0,会自行释放自己占用的内存

ComPtr

  • 什么是ComPtr?

    1. 用于帮助用户管理COM对象的生命周期
    2. 类似c++智能指针
  • 如何使用ComPtr?

    #include<wrl.h>
    using namespace Microsoft::WRL;
    
  • 常用方法

    1. Get:返回指向COM接口的指针
    2. GetAddressOf:返回指向COM接口指针的地址
    3. Reset:将此ComPtr置为nullptr并释放与之相关的所有引用,还会减少与之相关的COM接口的引用次数
  • COM接口都以大写字母”I“开头。如,表示命令列表的COM接口:ID3D12GraphicsCommandList

资源

  • 什么是资源(resource)?

    1. 资源为管道提供数据,并定义场景中present的内容
    2. 只是简单的内存块,无其他任何点缀
    3. resource包括纹理数据、顶点数据和着色器数据
  • 为什么需要资源?

    1. 资源是场景的基石,包含了 D3D用来解释和渲染场景的大部分数据
    2. 封装了 CPU 和 GPU 对物理内存或堆进行读写的通用能力,更加方便
  • 资源类别

    1. Buffer Resources(缓冲区资源)

    2. 什么是缓冲区资源?

      缓冲区资源是全部类型化数据的集合,且为非结构化。在内部,缓冲区包含元素,一个元素由1到4个组成分别构成。元素数据类型有打包过的数据值(如R8G8B8A8),单个8位整数,4个32位浮点值

    3. 缓冲区类别

      1. Vertex Buffer(顶点缓冲区)

      2. Index Buffer(索引缓冲区)

      3. Constant Buffer(常量缓冲区)

    4. Texture Resources(纹理资源)

    5. 什么是纹理资源?

    用于存储纹理的结构化数据结合。Texel 表示可由管道读取或写入的纹理的最小单位,每个 texel 包含1到4个组件,以一种 DXGI 格式排列
    
    1. 纹理类型

      1. 1D Texture
        2. 2D Texture
        3. 3D Texture
    2. 子资源

    用于指定资源的一部分。缓冲区被定义为单个子资源,纹理有些复杂

    1. read/write buffers and textures(读/写缓冲区和纹理)

    2. structured buffers(结构化缓冲区)

    包含大小相同的元素的缓冲区

    1. byte-address buffers(字节地址缓冲区)

    字节地址缓冲区是其内容可通过字节偏移量寻址的缓冲区

    1. append and consume buffers

    一种特殊类型的无序资源,支持从缓冲区末尾添加和删除值,类似于堆栈

    1. Unordered Access Buffer or Texture(无序访问缓冲区或纹理)

    无序访问资源允许由多个线程同时读/写,不会产生内存冲突

  • 定义

    ID3D12Resource
    

纹理格式

  • 什么是纹理格式?

    资源数据格式,包括全类型和无类型格式

  • 用途

    十分广泛,不局限于存储图像数据,可以表示颜色、透明度、描述顶点索引、mipmap,数据数组、缓冲区等等

  • 注意

    1. 并不是任意类型的数据元素都能用于组成纹理,只可存储DXGI_FORMAT枚举类型中描述的特定格式的数据元素
    2. 无类型格式的纹理仅预留内存

交换链

  • 什么是交换链?

    1. 交换链由两个或两个以上的表面组成。一般由两个缓冲区(双缓冲)构成:前台缓冲区和后台缓冲区,每个缓冲区都有纹理格式表示且都存储图像数据
    2. 前台缓冲区存储当前显示在屏幕上的图像数据,后台缓冲区则存储动画的下一帧,后台缓冲区存储完成则,前后台缓冲区角色互换(呈现):后台缓冲区变为前台缓冲区呈现新一帧的画面,而前台缓冲区则为存储新一帧转为后台缓冲区,如此反复image-20230107232647975
  • 为什么需要交换链?

    让画面可平滑过渡

  • 特点

    1. 每个表面储存2D图形的一个线性数组,其中每个元素都表示屏幕上的一个像素。对于三维物体来说,还需要保存深度信息
  • 常用接口

    1. IDXGISwapChain:表示交换链。存储了前后台缓冲区两种纹理
      1. IDXGISwapChain::ReSizeBuffers:修改缓冲区大小
      2. IDXGISwapChain::Present:呈现缓冲区
  • 注意

    1. 为了尽可能提高向输出呈现数据的速度,交换链几乎总是在显示子系统(通常为显卡)的内存中创建
    2. 当交换链被创建时,交换链被绑定到一个窗口,如此提高了性能并节省了内存
    3. 若调用IDXGIFactory::MakeWindowAssociation(),用户可以按 Alt-Enter 组合键,DXGI 将在窗口模式和全屏模式之间转换
    4. 在调用 IDXGISwapChain: : ResizeBuffers() 之前需要释放它在现有缓冲区上的任何引用
  • 如何创建交换链?

    //The following is create a swap chain chain for window
    /* Describe and create the swap chain */
    DXGI_SWAP_CHAIN_DESC1 swapChainDesc{};
    swapChainDesc.Width = m_width;
    swapChainDesc.Height = m_height;
    swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;	//Resource data formats
    swapChainDesc.SampleDesc.Count = 1;
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;	//Flags for surface and resource creation 
    swapChainDesc.BufferCount = FrameCount;
    swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;	//specify that DXGI discard the contents of the back buffer after be called
    
    ComPtr<IDXGISwapChain1> swapChain;
    ThrowIfFailed(factory->CreateSwapChainForHwnd(
        m_commandQueue.Get(),
        Win32Application::GetHwnd(),
        &swapChainDesc,
        nullptr,
        nullptr,
        &swapChain
    ));
    
  • 如何摧毁交换链?

    我们不能在全屏下摧毁交换链,因为这样做可以会导致线程抢夺。所以,在发布交换链之前,首先切换到窗口模式(IDXGISwapChain: : SetFullscreen State (FALSE,NULL)),再调用IUnknown: : Release

深度缓冲区

  • 为什么需要深度缓冲区?

    在交换链我们提到过,前台缓冲区和后台缓冲区的每个表面储存2D图形的一个线性数组,对于三维物体来说,我们需要存储深度信息来区分前后

  • 深度缓冲区的定义

    1. 同样以纹理格式表示,但存储像素的深度信息,深度值为0.0~1.0
    2. 0.0代表观察者在能看到的空间范围中能看到的离自己最近的物体;1.0则是能看到的最远的物体
  • 深度缓冲区可用纹理格式

    1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT
    2. DXGI_FORMAT_D32_FLOAT
    3. DXGI_FORMAT_D24_UNORM_S8_UINT
    4. DXGI_FORMAT_D16_UNORM
  • 注意

    1. 深度缓冲区的元素和后台缓冲区内的像素呈一一对应关系
    2. 使用深度缓冲,便无需关注绘制顺序

描述符

  • 什么是描述符?

    一种轻量级结构且是中间层。描述送往GPU的资源和资源的必要信息

  • 为何需要描述符?

    • GPU资源实质为普通的内存块,由于资源的通用性,它们可以被送往渲染流水线不同阶段供其使用

    • 告知D3D此资源被绑定在渲染流水线哪个阶段

    • 借助描述符指定欲绑定资源中的局部数据

  • 描述符的种类

    1. 渲染目标视图(RTV)

    2. 深度模板视图(DSV)

    3. 着色器资源视图(SRV)

    4. 无序访问视图(UAV)

    5. 常量缓冲区视图(CBV)

    6. 采样器

  • 注意

    1. 可用多个描述符引用同一资源
    2. 创建描述符的最佳实机为初始化期间。因为此时需要执行类型的检测和验证工作
    3. 描述符的大小取决于GPU硬件,可调用 ID3D12Device: : GetDescriptorHandleIncrementSize()查询SRV、 UAV 或 CBV 的大小
    4. 不需要释放描述符对象
    5. 使用描述符的主要方法是将它们的句柄放在描述符堆中,描述符堆是描述符的后备内存

描述符句柄

  • 什么是描述符句柄?

    描述符句柄是描述符的唯一地址,类似于指针,但不透明,因为它的实现是特定于硬件

  • 注意

    1. CPU 句柄可以立即使用。而GPU 句柄不能立即使用ーー它们从命令列表中标识位置,以便在 GPU 执行时使用
    2. 把句柄指针解引用,或者分析句柄中的位,都是不安全的
    3. 句柄的使用必须通过 API

描述符堆

  • 什么是描述符堆?

    描述符堆是描述符的连续分配的集合,存放连续的特定类型描述符句柄的一块内存

  • 为什么需要描述符堆?

    包含存储对象类型的描述符规格所需的大量内存分配,这样在调用时只需从描述符堆的开始位置一个一个按顺序调用即可

  • 常用方法

    1. 创建和设置描述符堆

      创建和设置描述符堆需要选择描述符堆的类型,确定描述符的数量,并设置flags它是CPU可见还是着色器可见

      1. 描述符堆类型

        typedef enum D3D12_DESCRIPTOR_HEAP_TYPE
        {
            D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV,    // Constant buffer/Shader resource/Unordered access views
            D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER,        // Samplers
            D3D12_DESCRIPTOR_HEAP_TYPE_RTV,            // Render target view
            D3D12_DESCRIPTOR_HEAP_TYPE_DSV,            // Depth stencil view
            D3D12_DESCRIPTOR_HEAP_TYPE_NUM_TYPES       // Simply the number of descriptor heap types
        } D3D12_DESCRIPTOR_HEAP_TYPE;
        
      2. 描述符堆的属性设置

        typedef struct D3D12_DESCRIPTOR_HEAP_DESC {
            D3D12_DESCRIPTOR_HEAP_TYPE  Type;
            UINT                        NumDescriptors;
            D3D12_DESCRIPTOR_HEAP_FLAGS Flags;
            UINT                        NodeMask;
        } D3D12_DESCRIPTOR_HEAP_DESC;
        
        1. Flags若赋值为D3D12 _ DESCRIPTOR _ HEAP _ FLAG _ SHADER _ VISIBLE,则让它在命令列表上被绑定给着色器引用
        2. Flags若赋值为D3D12_DESCRIPTOR_HEAP_FLAG_NONE,则允许应用程序在将描述符复制到着色器可见的描述符堆之前在 CPU 内存中暂存描述符
      3. 创建描述符堆

        ID3D12Device::CreateDescriptorHeap()

        HRESULT CreateDescriptorHeap(
            [in]  const D3D12_DESCRIPTOR_HEAP_DESC *pDescriptorHeapDesc,
            REFIID                           riid,
            [out] void                             **ppvHeap
        );
        
    2. 获取堆的首个 CPU 描述符句柄

      ID3D12DescriptorHeap::GetCPUDescriptorHandleForHeapStart()

      D3D12_CPU_DESCRIPTOR_HANDLE GetCPUDescriptorHandleForHeapStart();
      
    3. 获取堆的首个 CPU 描述符句柄

      ID3D12DescriptorHeap::GetGPUDescriptorHandleForHeapStart()

      D3D12_GPU_DESCRIPTOR_HANDLE GetGPUDescriptorHandleForHeapStart();
      
    4. 由于描述符的大小因硬件而异,获得堆中每个描述符句柄之间的增量

      ID3D12Device::GetDescriptorHandleIncrementSize()

      UINT GetDescriptorHandleIncrementSize(
          [in] D3D12_DESCRIPTOR_HEAP_TYPE DescriptorHeapType
      );
      
  • 渲染的几种管理方法

    1. 填充下一个绘制调用所需要的描述符堆。在命令列表提交前,将一个描述符堆指针指向描述符堆的开始处

    2. 优点:无需记录堆中描述符的位置

    3. 缺点:

      1. 有大量重复的描述符。尤其是present相似的场景
        2. 非常消耗描述符堆的空间
    4. 用描述符堆预先填充渲染场景中已知的一部分对象,只需在绘制时设置描述符表

    5. 绘制调用时接受一组常量,这些常量是需要使用的描述符位置的索引

  • 注意

    1. 需为每一种类型的描述符都创建单独的描述符堆
    2. 描述符句柄在描述符堆中是唯一的
    3. 所有堆对于 CPU 都是可见的
    4. 描述符堆只能由 CPU 编辑,GPU 不可以编辑描述符堆
    5. 描述符堆内容可以在记录引用它的命令列表之前、期间和之后更改。但是,当提交命令列表可能引用该位置时,不能更改描述符
    6. 可以使用 ID3D12GraphicsCommandList::SetDescriptorHeaps() 和 ID3D12GraphicsCommandList::Reset() 在同一个命令列表或不同的命令列表中切换堆
    7. 捆绑包只能有一个对ID3D12GraphicsCommandList::SetDescriptorHeaps()的调用,且描述符堆的集合必须与调用 捆绑包的命令列表完全匹配
    8. 捆绑包不更改描述符表,则不需要设置描述符堆

检测功能支持

  • 什么是检测功能支持(check feature suppotr)?

    它可以获取有关当前图形驱动程序支持的特性的信息

  • 为什么需要检测功能支持?

    因为不同图形驱动程序支持的特性不同

  • 语法

    HRESULT CheckFeatureSupport(
                D3D12_FEATURE Feature,	//D3D12_FEATURE枚举中的一个常量,描述要查询以获得支持的特性
      [in, out] void          *pFeatureSupportData,	//指向与 Feature 参数值对应的数据结构的指针
                UINT          FeatureSupportDataSize	//PFeatureSupportData 参数指向的结构的大小
    );
    
  • 如何使用检测功能支持?

    inline UINT8 D3D12GetFormatPlaneCount(
        _In_ ID3D12Device* pDevice,
        DXGI_FORMAT Format
        )
    {
        D3D12_FEATURE_DATA_FORMAT_INFO formatInfo = {Format};
        if (FAILED(pDevice->CheckFeatureSupport(D3D12_FEATURE_FORMAT_INFO, &formatInfo, sizeof(formatInfo))))
        {
            return 0;
        }
        return formatInfo.PlaneCount;
    }
    

MSAA反走样

  • 采样:把一个函数离散化的过程

    屏幕中显示的像素不可能为无穷小,因此不是任意一条直线都能在显示器上平滑地呈现出来

  • 走样:光栅化的图形显示器用离散量来表示连续量,因为其中采样的频率不满足Nyquist采样定理引起的信息失真,而造成图片具有锯齿状

  • 基于超采样的反走样方法

    1. SSAA(Super Sample Anti-Aliasing)
    • 原理:在每个像素内取多个子采样点,对子采样点进行颜色计算,再合成此像素最终的颜色

    • 缺点:计算量过大,内存占用,带宽

    1. MSAA(Multisample Anti-Aliasing)
    • 原理:与SSAA一样将每个像素内取多个子采样点,但其子采样点的颜色和中心像素颜色值相同,不单独计算。每个子像素则根据自己的可视性(深度/模板测试)和覆盖性(在多边形内还是外)来看是否接受这个颜色值
  • MSAA描述符

    typedef struct DXGI_SAMPLE_DESC
    {
        UINT Count;		//每个像素采样次数
        UINT Qualitiy;		//图像质量级别
    }
    
  • 查询质量级别

    根据MSAA描述符给定的纹理格式采样数量,运用ID3D12Device::CheckFeatureSupport()查询对应的质量级别

    typedef struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS
    {
        DXGI_FORMAT	   Format;
        UINT	SampleCount;
        D3D12_MULTISAMPLE_QUALITY_LEVEL_FLAGS	Flags;
        UINT	NumQualityLevels;
    }
    
    D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
    //...指定格式、采样数量等
    ThrowIfFailed(md3dDevice->CheckFeatureSupport(
    	D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
        msQualityLevels,	//输出对应的质量级别
        sizeof(msQualityLevels)
    ))
    

功能级别

  • 什么是功能级别(feature level)?

    功能级别为不同级别所支持的功能进行了严格的界定

  • 为什么需要功能级别?

    因为这样使得开发更加快捷,只要了解所支持的功能,便可得知哪些功能可以使用

  • 以枚举类型D3D_FEATURE_LEVEL表示(9到12的版本)

    enum D3D_FEATURE_LEVEL
    {
        D3D_FEATURE_LEVEL_9_1,
        D3D_FEATURE_LEVEL_9_2,
        D3D_FEATURE_LEVEL_9_3,
        D3D_FEATURE_LEVEL_10_0,
        D3D_FEATURE_LEVEL_10_1,
        D3D_FEATURE_LEVEL_11_0,
        D3D_FEATURE_LEVEL_11_1,
        D3D_FEATURE_LEVEL_12_0,
        D3D_FEATURE_LEVEL_12_1,
        D3D_FEATURE_LEVEL_12_2
    }
    
  • 注意

    1. 若用户的硬件不支持某特定功能级别,应用程序应回退至版本更低的功能级别

DXGI

  • 什么是DXGI(DirectX Graphics Infrastructure)?

    一种图像基础架构,用于管理DX的低级别任务(如硬件设备的枚举、控制伽玛、切换全屏模式),而在D3D早期,低级任务包含在 Direct3D 运行期,但现在这些任务在DXGI中实现

  • 为什么需要DXGI?

    因为D3D各个图像的组成部分发展速度并不是相同,某些可能要慢一些。所以需要借助DXGI来使多种图形API共有的底层任务能借助一组通用API进行处理,这样为未来的图形组件提供了一个通用框架

  • 用途

    1. 与内核模式驱动程序和系统硬件进行通信img
    2. 应用程序使用DXGI枚举设备控制如何将数据呈现给输出
  • 调用

    应用程序可以直接访问 DXGI,或者调用Direct3D API。其中,后者可以处理与DXGI的通信

适配器、显示输出

  • 什么是适配器?

    适配器是计算机硬件和软件能力的抽象,计算机上通常有许多适配器。有些以硬件方式实现(显卡),有些以软件方式实现(光栅化器)

  • 为什么需要适配器?

    适配器实现图形应用程序需要使用的功能

  • 实例

    下图实现了一个单台计算机,两个显示适配器(IDXGIAdapter),三个输出显示器(IDXGIOutput)的系统img

  • 接口

    • IDXGIFactory。主要用于创建IDXGISwapChain接口和枚举显示适配器
    • IDXGIAdapter。显示适配器
    • IDXGIOutput。显示输出
      1. 每种显示设备都有一系列它所支持的显示模式,以DXGI_MODE_DESC表示
      2. 确定显示模式的格式后,可以获得某显示输出对当前格式所支持的全部显示模式

设备

  • 什么是设备(device)?

    设备代表一个显示适配器,一般来说显示适配器是3D图形硬件(如显卡),但也可以用软件显示适配器(如WARP适配器)来模拟图形硬件的功能

  • 为什么需要设备?

    用于创建一系列资源,如命令队列,描述符堆等等,以及枚举显示适配器

  • 定义和方法

    1. 创建显示适配器

      D3D12CreateDevice()

      HRESULT D3D12CreateDevice(
          [in, optional]  IUnknown          *pAdapter,	//指向创建设备时要用的显示适配器
          D3D_FEATURE_LEVEL MinimumFeatureLevel,	//最低功能级别
          [in]            REFIID            riid,	
          [out, optional] void              **ppDevice
      );
      

命令队列、命令列表和捆绑包

  • 什么是命令列表(command queue)、命令队列(command list)和第二级命令列表捆绑包(bundle)

    1. 命令列表用于存放一系列在GPU上执行特定命令的GPU命令
    2. 命令队列用于GPU调用CPU上传的命令列表数组中的命令列表
    3. 捆绑包用于存放少量API命令,以便后续执行
  • 一般来说,API调用构成捆绑包,API调用和捆绑包构成命令列表,命令列表又构成一个框架img

  • 为什么需要命令列表、命令队列、捆绑包?

    • 从DX11到DX12提交工作的方式变化

      • 在DX11中,所有命令的提交都是通过直接上下文(immedate context,一个到GPU的命令流)完成
        • 缺点
          1. 直接上下文会隐式地把提交的命令分成GPU工作项并提交,这几乎不能多次重复使用
          2. 因为在DX11API中允许单独设置这些管道阶段,所以显示驱动程序不能解决管道状态的问题,直到状态最终确定(运行期)
          3. 所有对直接上下文的访问都是单线程
      • 在DX12中,抛弃了直接上下文,而是采用命令列表、命令队列和捆绑包相结合的方式
        • 优点
          1. 驱动程序可以预先计算所有必要的 GPU 命令且是自由线程,因为每个命令列表都是独立的,且不继承任何状态。这减少了运行期的开销
          2. 可以同时记录多个命令列表,这利用了多线程的优势
          3. 命令列表可以多次提交,只要在新的执行前确保之前的执行工作已经完成
          4. 任何线程都可以随时向任何命令队列提交命令列表,运行期将自动序列化命令队列中提交的命令列表并保留提交顺序
          5. 可以在命令列表中重复执行捆绑包里的API命令,可以提升单线程的效率
          6. 命令队列允许开发人员避免意外同步导致的低效率
    • 命令列表和捆绑包的区别

      • 命令列表是完全独立的,且通常情况下,命令列表的生命阶段是构造、提交一次并丢弃

      • 捆绑包则不同,它提供一种允许重用的状态继承形式,且捆绑包不能直接提交到命令队列中

      • 举个例子

        一个游戏想要绘制两个具有不同纹理的人物模型,一种方法是用两组相同的绘制调用记录一个命令列表。但另一种方法是“记录”一个捆绑包,该捆绑包绘制单个字符模型,然后使用不同的资源在命令列表中“回放”该绑定包两次

        显然,后一种情况驱动程序只需计算一次指令,并且创建命令列表实际上相当于两次低成本函数调用

创建

  • 创建命令列表

    1. 两种方式

      1. ID3D12Device::CreateCommandList()创建命令列表和捆绑包,处于打开状态

        1. 语法
        HRESULT CreateCommandList(
            [in]           UINT                    nodeMask,	//若是单GPU操作,设置为0
            [in]           D3D12_COMMAND_LIST_TYPE type,	//要创建的命令列表的类型
            [in]           ID3D12CommandAllocator  *pCommandAllocator,	//指向命令分配器对象的指针
            [in, optional] ID3D12PipelineState     *pInitialState,	//指向包含命令列表的初始管道状态的管道状态对象的可选指针。若是nullptr,则运行时设置一个虚拟初始管道状态,这样驱动程序就不必处理未定义的状态,开销低
            [in]           REFIID                  riid,
            [out]          void                    **ppCommandList	//指向内存块的指针,该内存块接收指向命令列表的接口的指针
        );
        
        1. D3D12_COMMAND_LIST_TYPE列出几种常用的
          1. D3D12_COMMAND_LIST_TYPE_DIRECT

          2. D3D12_COMMAND_LIST_TYPE_BUNDLE

      2. ID3D12Device4::CreateCommandList1()创建一个处于关闭状态的命令列表和捆绑包,当使用分配器和PSO创建命令列表而不使用它时就选这个,这样可以避免低效率

    2. 命令分配器

      1. 什么是命令分配器?

        允许应用程序分配给命令列表的内存

        存储与之相关联的命令列表记录的命令

      2. 创建命令分配器

        1. CreateCommandAllocator()

          HRESULT CreateCommandAllocator(
              [in]  D3D12_COMMAND_LIST_TYPE type,		//要创建的命令列表的类型
              REFIID                  riid,
              [out] void                    **ppCommandAllocator	//指向内存块的指针,该内存块接收指向命令分配器的 ID3D12CommandAllocator 接口的指针
          );
          
        2. 注意

          1. 一个分配器一次只能和一个当前记录的命令列表相关联,但一个命令分配器可以用来创建任意数量的命令列表对象
      3. 回收命令分配器分配的内存

        1. ID3D12CommandAllocator::Reset()

          为新命令列表重用分配器,但不会减少其底层大小

        2. 注意

          1. 必须确保GPU不再执行与当前程序相关的命令列表,否则调用将失败
          2. 这个 API 不是自由线程,因此不能从多个线程同时在同一个分配程序上调用
  • 创建命令队列

    • 描述命令队列

      typedef struct D3D12_COMMAND_QUEUE_DESC {
          D3D12_COMMAND_LIST_TYPE   Type;
          INT                       Priority;
          D3D12_COMMAND_QUEUE_FLAGS Flags;	//设置为默认队列即可D3D12_COMMAND_QUEUE_FLAG_NONE
          UINT                      NodeMask;
      } D3D12_COMMAND_QUEUE_DESC;
      
      1. Priority有如下三个优先级
        1. D3D12_COMMAND_QUEUE_PRIORITY_NORMAL
        2. D3D12_COMMAND_QUEUE_PRIORITY_HIGH
        3. D3D12_COMMAND_QUEUE_PRIORITY_GLOBAL_REALTIME
    • 创建命令队列

      HRESULT CreateCommandQueue(
          const D3D12_COMMAND_QUEUE_DESC *pDesc,
          REFIID                         riid,
          void                           **ppCommandQueue
      );
      

记录命令列表

  • 以下情况,命令列表都处于记录状态

    1. 创建后
    2. 调用 ID3D12GraphicsCommandList::Reset() 重用现有的命令列表
  • 向命令列表添加命令

    • 当命令列表处于记录状态时,调用 ID3D12GraphicsCommandList 接口的方法向命令列表添加命令
  • 注意

    1. 将命令添加命令列表完成后,最好是调用ID3D12GraphicsCommandList::Close()方法来取消记录状态
    2. 可以在命令列表仍在执行时调用 Reset()。如,提交一个命令列表,然后立即重新设置它,以便为另一个命令列表重新分配内存
    3. 一次只有一个与每个命令分配程序关联的命令列表可能处于记录状态

执行命令列表

  • 记录命令列表并检索默认命令队列或创建新的命令队列之后,可以通过调用 ID3D12CommandQueue:: ExecuteCommandLists 来执行命令列表

  • ID3D12CommandQueue::ExecuteCommandLists()用于执行命令列表数组,执行的是命令分配器里记录的命令

    void ExecuteCommandLists(
        [in] UINT              NumCommandLists,		//命令列表数
        [in] ID3D12CommandList * const *ppCommandLists	//要执行的 ID3D12CommandList 命令列表的数组
    );
    
  • 注意

    1. 应用程序应分批次处理命令列表的执行,以减少与提交给 GPU 的命令相关的固定成本
    2. 命令队列执行命令列表时串行
    3. 对于以下原因,该调用将被中断且删除
      1. 提交的是捆绑包,而不是命令列表
      2. 命令列表没有调用ID3D12GraphicsCommandList::Close就进行提交
      3. 命令队列的围栏指出以前的命令列表尚未执行完成

围栏同步命令列表的执行

  • 什么是围栏?

    1. 围栏(fence)是用于同步 CPU 和一个或多个 GPU 的对象
    2. 在API中由ID3D12Fence 接口表示。围栏是一个无符号整数,表示当前正在处理的工作单元
  • 为什么需要围栏?

    因为DX12支持多个并行命令队列,因此我们需要一种技术去控制GPU和CPU的同步

    比如,一个队列中的命令列表依赖于另一个命令队列正在操作的资源

  • 如何进行同步?image-20230112170421646

    上图中,GPU执行到了命令(large x_{gpu}),而CPU在(large x_{cpu})处调用ID3D12CommandQueue::Signal(fence, n+1)让GPU端设置围栏值,而GPU在处理完命令队列中Signal(fence, n+1)之前的所有命令前,CPU端调用的mFence->GetCompletedValue()会一直返回n

  • 方法

    1. 创建围栏

      //ID3D12Device::CreateFence()
      HRESULT CreateFence(
          UINT64            InitialValue,
          D3D12_FENCE_FLAGS Flags,
          REFIID            riid,
          [out] void              **ppFence
      );
      
      1. Flags。常用的两种
        1. D3D12_FENCE_FLAG_NONE。默认的
        2. D3D12_FENCE_FLAG_SHARED。非独占
    2. 将围栏设置为指定值

      ID3D12Fence::Signal()

      HRESULT Signal(
          UINT64 Value
      );
      
    3. 获取围栏的当前值

      ID3D12Fence::GetCompletedValue()

      UINT64 GetCompletedValue();
      
    4. 指定当围栏达到某个值时引发的事件

      ID3D12Fence::SetEventOnCompletion()

      HRESULT SetEventOnCompletion(
          UINT64 Value,
          HANDLE hEvent
      );
      

资源屏障

  • 什么是资源屏障(resource barrier)?

    资源屏障通知驱动在哪些情况下驱动可能需要同步对存储资源的内存的多个访问

  • 为什么需要资源屏障?

    1. 防止资源冒险

      比如GPU对某资源按顺序进行先写后读的操作,若写操作还未完成却开始读资源

    2. 减少CPU使用量,并支持驱动多线程和预处理

      1. 在DX11中,驱动在后台跟踪资源的状态,但对于CPU来说这是昂贵的,且这让多线程的设计更加复杂
      2. 在DX12中,将资源状态管理的责任从驱动转移给应用程序,大多数资源的状态都是由应用程序通过单一API进行管理
  • 资源屏障有三种类型(我们暂且先使用第一种)

    1. 转换屏障(Transition barrier)

      1. 什么是转换屏障?

        表示一组子资源在不同用途之间的状态转换

      2. 为什么需要转换屏障?

        解决资源冒险问题

      3. 如何实现转换屏障?

         CD3DX12_RESOURCE_BARRIER static inline Transition(
             ID3D12Resource* pResource, 
             D3D12_RESOURCE_STATES stateBefore, 
             D3D12_RESOURCE_STATES stateAfter, 
             UINT subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, 
         	D3D12_RESOURCE_BARRIER_FLAGS flags = D3D12_RESOURCE_BARRIER_FLAG_NONE
         );
        
  • 定义

    注:如下的CD3DX12_RESOURCE_BARRIER不属于DX12 SDK,这是对应的扩展辅助结构体,相较于d3d12.h用起来更方便

    struct CD3DX12_RESOURCE_BARRIER  : public D3D12_RESOURCE_BARRIER{
        CD3DX12_RESOURCE_BARRIER();
        explicit CD3DX12_RESOURCE_BARRIER(const D3D12_RESOURCE_BARRIER &o);
        CD3DX12_RESOURCE_BARRIER static inline Transition(ID3D12Resource* pResource, D3D12_RESOURCE_STATES stateBefore, D3D12_RESOURCE_STATES stateAfter, UINT subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, D3D12_RESOURCE_BARRIER_FLAGS flags = D3D12_RESOURCE_BARRIER_FLAG_NONE);
        CD3DX12_RESOURCE_BARRIER static inline Aliasing(ID3D12Resource* pResourceBefore, ID3D12Resource* pResourceAfter);
        CD3DX12_RESOURCE_BARRIER static inline UAV(ID3D12Resource* pResource);
        operator const D3D12_RESOURCE_BARRIER&() const;
    };
    
    • 一些常用的D3D12_RESOURCE_STATES

      1. D3D12_RESOURCE_STATE_COMMON

        跨不同图形引擎类型访问资源时才应转换到此状态

      2. D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER

        当子资源作为顶点缓冲区或常量缓冲区被 GPU 访问时,它必须处于这种状态。这是一个只读状态

      3. D3D12_RESOURCE_STATE_INDEX_BUFFER

        当3D 管道作为索引缓冲区访问子资源时,它必须处于这种状态。这是一个只读状态

      4. D3D12_RESOURCE_STATE_RENDER_TARGET

        该资源用作呈现目标。子资源呈现时或清除时必须处于此状态。这是一个只写状态

  • 注意

    1. 一次调用中应尽可能多的调用多个资源转换
    2. 用于描述资源状态的资源状态使用位被划分为只读状态和读/写状态
    3. 对于任何资源,最多只能设置一个读/写位。如果设置了写入位,则不能为该资源设置只读位。如果没有设置写入位,则可以设置任意数量的读取位
    4. 一个后台缓冲区被呈现前,它必须是D3D12 _ RESOURCE _ STATE _ COMMON状态。D3D12 _ RESOURCE _ STATE _ PREENT 是 D3D12 _ RESOURCE _ STATE _ COMMON的同义词

设置视口

  • 什么是视口?

    视口也就是渲染生成后的2d图片

  • 为什么需要视口?

    因为有时候我们希望把3D场景渲染到后台缓冲区的某个矩形子区域

  • 定义和方法

    1. 设置视口属性

      typedef struct D3D12_VIEWPORT {
          FLOAT TopLeftX;		//左侧x位置
          FLOAT TopLeftY;		//顶部y位置
          FLOAT Width;		//宽
          FLOAT Height;		//高
          FLOAT MinDepth;		//最小深度
          FLOAT MaxDepth;		//最大深度
      } D3D12_VIEWPORT;
      
    2. 设置视口

      将视图数组与管线的光栅化阶段绑定

      void RSSetViewports(
          [in] UINT                 NumViewports,		//绑定的视口数
          [in] const D3D12_VIEWPORT *pViewports	// D3D12_VIEWPORT的数组
      );
      

裁剪矩阵

  • 什么是裁剪矩形?

    定义一个矩形,在此矩阵外的像素将不会被光栅化至后台缓冲区

  • 为什么需要裁剪矩阵?

    用于优化性能

  • 定于和方法

    1. 设置裁剪矩形属性

      typedef struct tagRECT {
          LONG left;
          LONG top;
          LONG right;
          LONG bottom;
      } RECT, *PRECT, *NPRECT, *LPRECT;
      
      typedef RECT D3D12_RECT;
      
    2. 将裁剪矩形数组与管线的光栅化阶段绑定

      void RSSetScissorRects(
          [in] UINT             NumRects,		//绑定的裁剪矩形的个数
          [in] const D3D12_RECT *pRects	//裁剪矩形数组
      );
      
  • 注意

    1. 数组中的每个裁剪矩形对应于视口数组中的一个视口

根签名

  • 什么是根签名(root signature)?

    1. 根签名定义了绑定到管线的资源,这些资源将被映射至着色器的对应输入寄存器
    2. 根签名和函数签名类似,确定着色器期望的数据类型,但不包含实际的内存。可以看着色器看作一函数,输入资源看作着色器的函数参数,根签名即定义了函数签名
  • 为什么需要根签名?

    在之前我们了解到描述符堆存储一系列渲染时所需要的资源,这些资源大部分将提供给特定的着色器使用,这时我们就需要根签名来将这些资源映射给着色器的特定的寄存器槽

  • 根参数(root parameter)

    • 什么是根参数?

      1. 根签名以一组描述绘制调用过程中着色器所需资源的根参数定义而成
      2. 根参数时根签名中的一个槽(slot)
    • 类型

      根参数分为三类,分别是根常量、根描述符、描述符表

  • 定义和方法

    1. 根签名描述布局

      此结构体由 D3D12SerializeRootSignature() 使用,并由 ID3D12RootSignatureSerializer: : GetRootSignatureDesc() 返回

      typedef struct D3D12_ROOT_SIGNATURE_DESC {
          UINT                            NumParameters;	//根签名中的槽个数
          const D3D12_ROOT_PARAMETER      *pParameters;	//根参数数组
          UINT                            NumStaticSamplers;	//静态采样器的数量
          const D3D12_STATIC_SAMPLER_DESC *pStaticSamplers;	//静态采样器描述符数组
          D3D12_ROOT_SIGNATURE_FLAGS      Flags;
      } D3D12_ROOT_SIGNATURE_DESC;
      
    2. 定义根参数

      typedef struct D3D12_ROOT_PARAMETER {
          D3D12_ROOT_PARAMETER_TYPE ParameterType;	//根参数类型
          union {
              D3D12_ROOT_DESCRIPTOR_TABLE DescriptorTable;
              D3D12_ROOT_CONSTANTS        Constants;
              D3D12_ROOT_DESCRIPTOR       Descriptor;
          };
          D3D12_SHADER_VISIBILITY   ShaderVisibility;	//根参数在着色器中的可见性
      } D3D12_ROOT_PARAMETER;
      
      struct CD3DX12_ROOT_PARAMETER  : public D3D12_ROOT_PARAMETER{
          CD3DX12_ROOT_PARAMETER();
          explicit CD3DX12_ROOT_PARAMETER(const D3D12_ROOT_PARAMETER &o);
          void static inline InitAsDescriptorTable(D3D12_ROOT_PARAMETER &rootParam, UINT numDescriptorRanges, const D3D12_DESCRIPTOR_RANGE* pDescriptorRanges, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
          void static inline InitAsConstants(D3D12_ROOT_PARAMETER &rootParam, UINT num32BitValues, UINT shaderRegister, UINT registerSpace = 0, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
          void static inline InitAsConstantBufferView(D3D12_ROOT_PARAMETER &rootParam, UINT shaderRegister, UINT registerSpace = 0, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
          void static inline InitAsShaderResourceView(D3D12_ROOT_PARAMETER &rootParam, UINT shaderRegister, UINT registerSpace = 0, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
          void static inline InitAsUnorderedAccessView(D3D12_ROOT_PARAMETER &rootParam, UINT shaderRegister, UINT registerSpace = 0, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
          void inline InitAsDescriptorTable(UINT numDescriptorRanges, const D3D12_DESCRIPTOR_RANGE* pDescriptorRanges, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
          void inline InitAsConstants(UINT num32BitValues, UINT shaderRegister, UINT registerSpace = 0, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
          void inline InitAsConstantBufferView(UINT shaderRegister, UINT registerSpace = 0, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
          void inline InitAsShaderResourceView(UINT shaderRegister, UINT registerSpace = 0, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
          void inline InitAsUnorderedAccessView(UINT shaderRegister, UINT registerSpace = 0, D3D12_SHADER_VISIBILITY visibility = D3D12_SHADER_VISIBILITY_ALL);
      };
      
      1. D3D12_ROOT_PARAMETER_TYPE

        typedef enum D3D12_ROOT_PARAMETER_TYPE {
            D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE = 0,
            D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS,
            D3D12_ROOT_PARAMETER_TYPE_CBV,
            D3D12_ROOT_PARAMETER_TYPE_SRV,
            D3D12_ROOT_PARAMETER_TYPE_UAV
        } ;
        
      2. D3D12_SHADER_VISIBILITY

        设置D3D12_SHADER_VISIBILITY的枚举成员来确定此根参数在着色器中的可见性

        限制可见性的目的:有概率优化性能

        typedef enum D3D12_SHADER_VISIBILITY {
            D3D12_SHADER_VISIBILITY_ALL = 0,	//所有着色器都可以访问此根参数内容
            D3D12_SHADER_VISIBILITY_VERTEX = 1,	//顶点着色器
            D3D12_SHADER_VISIBILITY_HULL = 2,	//外壳着色器
            D3D12_SHADER_VISIBILITY_DOMAIN = 3,	//域着色器
            D3D12_SHADER_VISIBILITY_GEOMETRY = 4,	//几何着色器
            D3D12_SHADER_VISIBILITY_PIXEL = 5,	//像素着色器
            D3D12_SHADER_VISIBILITY_AMPLIFICATION = 6,	//放大着色器
            D3D12_SHADER_VISIBILITY_MESH = 7	//网格着色器
        } ;
        
    3. 创建根签名

      HRESULT CreateRootSignature(
          [in]  UINT       nodeMask,	//单GPU设为0
          [in]  const void *pBlobWithRootSignature,	//指向用于序列化签名的源数据
          [in]  SIZE_T     blobLengthInBytes,		//PBlobWithRootSignature指向的内存块的大小
          REFIID     riid,
          [out] void       **ppvRootSignature	
      );
      
      1. DX12规定,必须先将根签名的描述布局进行序列化处理,转换为用ID3DBlob接口表示的序列化数据格式后,才能将其传给CreateRootSignature()

        HRESULT D3D12SerializeRootSignature(
            [in]            const D3D12_ROOT_SIGNATURE_DESC *pRootSignature,
            [in]            D3D_ROOT_SIGNATURE_VERSION      Version,	//根签名的版本
            [out]           ID3DBlob                        **ppBlob,
            [out, optional] ID3DBlob                        **ppErrorBlob
        );
        
        //例子
        D3D12_ROOT_SIGNATURE_DESC rootSigDesc(...);
        
        ComPtr<ID3DBlob> serializedRootSig = nullptr;
        ComPtr<ID3DBlob> errorBlob = nullptr;
        HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc,
        	D3D_ROOT_SIGNATURE_VERSION_1,
            serializedRootSig.GetAddressof(),
            errorBlob.GetAddressof()                                     
        );
        
  • 注意事项

    1. 尽量使根签名尽可能小,以获得最大性能
    2. 可以创建任意组合的根签名,但不可超过64DWORD,这是它的上限。选择此最大大小是为了防止滥用根签名作为存储大容量数据的方式
    3. 虽然根常量使用方便,但它的空间消耗增长迅速。因此,最好是混用三种根参数
    4. 理想情况下,几组管线状态对象共享相同的根签名
    5. 管线上设置根签名后,这些根签名定义的所有绑定都可以单独设置或更改
    6. 根签名必须和使用它的着色器相兼容,也就是在绘制开始前,根签名必须要为着色器提供其执行时需要绑定导渲染管线的所有资源
    7. 不支持根签名空间中的动态索引

描述符表

  • 什么是描述符表(descriptor table)?

    1. 可以视为一个数组,但它不存储描述符句柄
    2. 描述符表引用描述符堆中的一块子连续范围,描述的是在堆中的偏移量长度
    3. 描述符表条目包含描述符、HLSL着色器绑定名称和可见性标志img
  • 为什么需要描述符表?

    当我们填充了描述符堆,还需要一种手段去调用它,即根签名。根签名的参数描述了需要绑定至流水线的资源,而这个参数即是根参数,描述符表即属于根参数

  • 大小

    每个描述符表占用1DWORD(32位无符号整数)

  • 描述符范围(descriptor range)

    1. 什么是描述符范围?

      表示在描述符表中一段连续的描述符集合

    2. 为什么需要描述符范围?

      因为描述符表中的描述符类型可能有多种

  • 定义和方法

    1. 定义描述符表

      typedef struct D3D12_ROOT_DESCRIPTOR_TABLE {
          UINT                         NumDescriptorRanges;	//描述符范围的个数
          const D3D12_DESCRIPTOR_RANGE *pDescriptorRanges;	//描述符范围类型的数组
      } D3D12_ROOT_DESCRIPTOR_TABLE;
      
    2. 描述符范围

      typedef struct D3D12_DESCRIPTOR_RANGE {
          D3D12_DESCRIPTOR_RANGE_TYPE RangeType;
          UINT                        NumDescriptors;		//范围内描述符个数
          UINT                        BaseShaderRegister;	//基准着色器寄存器
          UINT                        RegisterSpace;		//寄存器空间
          UINT                        OffsetInDescriptorsFromTableStart;	//此描述符范围距离描述符表开始处的偏移量
      } D3D12_DESCRIPTOR_RANGE;
      
      1. D3D12_DESCRIPTOR_RANGE_TYPE

        typedef enum D3D12_DESCRIPTOR_RANGE_TYPE
        {
            D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
            D3D12_DESCRIPTOR_RANGE_TYPE_UAV,
            D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
            D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER
        } D3D12_DESCRIPTOR_RANGE_TYPE;
        
      2. BaseShaderRegister

        如,若把NumDescriptors设为3,BaseShaderRegister设为1,令RangeType为D3D12_DESCRIPTOR_RANGE_TYPE_CBV。结果如下:

        cbuffer cbA : register(b1) {};
        cbuffer cbB : register(b2) {};
        cbuffer cbC : register(b3) {};
        
      3. RegisterSpace

        1. 可以在不同的寄存器空间中指定着色器寄存器,因为寄存器中包含同名的寄存器槽
        2. 如,以下看起来像是重复使用寄存器槽t0,但并非如此
        Texture2D gDiffuseMap : register(t0, space0);
        Texture2D gNormalMap : register(t0, space1);
        
    3. 将描述符表与管线绑定

      根签名只是定义要绑定到管线的资源,还需要通过命令列表设置根签名让其和管线绑定

      void SetGraphicsRootDescriptorTable(
          [in] UINT                        RootParameterIndex,
          [in] D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor
      );
      

根描述符

  • 什么是根描述符(root descriptor)?

    根描述符是内联在根参数中的描述符

  • 为什么需要根描述符?

    通过直接设置根描述符可以直接指示要绑定的资源,而无需将它存于描述符堆中

  • 大小

    每个根描述符(64位GPU虚拟地址)占用2DWORD(32位无符号整数)

  • 定义和方法

    1. 在根签名中定义描述符

      typedef struct D3D12_ROOT_DESCRIPTOR {
          UINT ShaderRegister;	//着色器寄存器
          UINT RegisterSpace;		//寄存器空间
      } D3D12_ROOT_DESCRIPTOR;
      
    2. 将描述符和管线绑定

      void SetGraphicsRootConstantBufferView(
          [in] UINT                      RootParameterIndex,
          [in] D3D12_GPU_VIRTUAL_ADDRESS BufferLocation
      );
      
      void SetGraphicsRootShaderResourceView(
          [in] UINT                      RootParameterIndex,
          [in] D3D12_GPU_VIRTUAL_ADDRESS BufferLocation
      );
      
      void SetGraphicsRootUnorderedAccessView(
          [in] UINT                      RootParameterIndex,
          [in] D3D12_GPU_VIRTUAL_ADDRESS BufferLocation
      );
      
  • 注意事项

    1. 只有CBV,缓冲区的SRV、UAV才能根据描述符的身份进行绑定
    2. 根描述符不包括大小限制,不可进行越界检查。而描述符堆中的描述符包含大小
    3. 为避免经过描述符堆,可以将描述符直接放入根签名中,但这样描述符在根签名中将占用大量空间,因此建议尽量少用

根常量

  • 什么是根常量(root constant)?

    1. 根常量是内联在根参数中的常量
    2. 借助根常量可以直接绑定一系列32位的常量值
  • 大小

    每个根常量占用1DWORD(32位无符号整数)

  • 定义和方法

    1. 描述常量缓冲区出现在着色器中的根签名中的内联常量

      D3D12_ROOT_CONSTANS

      typedef struct D3D12_ROOT_CONSTANTS {
          UINT ShaderRegister;	//着色器寄存器
          UINT RegisterSpace;		//寄存器空间
          UINT Num32BitValues;	//根参数所需的32位常量的个数,占据单个着色器槽的常量数
      } D3D12_ROOT_CONSTANTS;
      
    2. 将根常量和管线绑定

      因为不支持根签名空间中的动态索引,所以在映射到根常量的常量缓冲区中不允许使用数组.但可以设置部分常量

      void SetGraphicsRoot32BitConstants(
          [in] UINT       RootParameterIndex,		//根参数索引
          [in] UINT       Num32BitValuesToSet,	//设置的32位常量的个数
          [in] const void *pSrcData,		//指向要设置的32位常量的数组
          [in] UINT       DestOffsetIn32BitValues	//在32-bit values组中首个常量的偏移量
      );
      

渲染管线状态对象

  • 什么是渲染管线状态对象(pipeline state object, PSO)

    PSO用于存储流水线组件的状态。当要被绘制的几何图形提交要GPU时,有一系列硬件设置来决定如何解释和渲染输入数据,这些设置也就被称作绘图管线状态,如光栅化状态、混合状态和深度模板状态、拓扑类型和用于渲染的着色器,而在DX12中,大多数绘图管线状态由PSO来设置

  • 为什么需要PSO?

    1. DX11的PSO

      在DX11中,这些状态都是分开配置的,但各个状态间都有一定联系,以致于如果其中一个状态发生改变,驱动可能就要为了另一个相关的状态而对硬件重新编程。为了避免这些冗余的操作,驱动会推迟针对硬件状态的编程改变,直到明确整条管线的状态发起绘制调用后,才正式生成对应的本地指令状态,但这种延迟操作需要驱动在运行期进行额外的记录工作,即追踪状态的变化

    2. DX12的PSO

      PSO绘总了大量管线状态信息,D3D便可确定所有的状态是否彼此兼容,驱动便能依此提前生成硬件本地指令及其状态,这样在初始化期间便可以生成对管线状态编程的全部代码

  • 定义和方法

    1. 描述管线状态

      typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC {
          ID3D12RootSignature                *pRootSignature;		//与此PSO相绑定的根签名指针
          D3D12_SHADER_BYTECODE              VS;	//待绑定的顶点着色器
          D3D12_SHADER_BYTECODE              PS;	//待绑定的像素着色器
          D3D12_SHADER_BYTECODE              DS;	//待绑定的域着色器
          D3D12_SHADER_BYTECODE              HS;	//待绑定的外壳着色器
          D3D12_SHADER_BYTECODE              GS;	//待绑定的几何着色器
          D3D12_STREAM_OUTPUT_DESC           StreamOutput;	//用于实现流输出
          D3D12_BLEND_DESC                   BlendState;	//混合所用的混合状态
          UINT                               SampleMask;	//每个采样点是否采集
          D3D12_RASTERIZER_DESC              RasterizerState;	//光栅化状态
          D3D12_DEPTH_STENCIL_DESC           DepthStencilState;	//深度模板状态
          D3D12_INPUT_LAYOUT_DESC            InputLayout;	//输入布局描述
          D3D12_INDEX_BUFFER_STRIP_CUT_VALUE IBStripCutValue;	//D3D12 _ INDEX _ BUFFER _ STRIP _ CUT _ VALUE 结构中索引缓冲区的属性
          D3D12_PRIMITIVE_TOPOLOGY_TYPE      PrimitiveTopologyType;	//图元拓扑类型
          UINT                               NumRenderTargets;	//RTVFormats的个数
          DXGI_FORMAT                        RTVFormats[8];	//渲染目标的格式
          DXGI_FORMAT                        DSVFormat;	//深度模板缓冲区的格式
          DXGI_SAMPLE_DESC                   SampleDesc;	//多重采样的数量和级别的描述符
          UINT                               NodeMask;	
          D3D12_CACHED_PIPELINE_STATE        CachedPSO;	//缓存的管道状态对象
          D3D12_PIPELINE_STATE_FLAGS         Flags;	
      } D3D12_GRAPHICS_PIPELINE_STATE_DESC;
      
    2. 创建PSO

      ID3D12Device::CreateGraphicsPipelineState

      HRESULT CreateGraphicsPipelineState(
          [in]  const D3D12_GRAPHICS_PIPELINE_STATE_DESC *pDesc,	//指向描述PSO结构
          REFIID                                   riid,
          [out] void                                     **ppPipelineState
      );
      
    3. 设置PSO

      ID3D12GraphicsCommandList::SetPipelineState()

      PSO实质上是状态机,里面的对象都会保持它们各自的状态,直到我们改变它们。因此,我们可以通过调用SetPipelineState()来切换PSO

      void SetPipelineState(
          [in] ID3D12PipelineState *pPipelineState	//指向ID3D12PipelineState
      );
      
  • 注意

    1. 尽量在初始化期间生成PSO,除非有特殊需求。因为PSO的验证和创建十分耗时
    2. 尽可能少的改变PSO状态
    3. 并非所有的渲染状态都封装在PSO内,某些状态的设置和其他管线状态分开会更有效,比如viewpore和scissor rectangle

reference

DXGI overview - Win32 apps | Microsoft Learn

Immediate and Deferred Rendering - Win32 apps | Microsoft Learn

Resource Types (Direct3D 10) - Win32 apps | Microsoft Learn

New Resource Types - Win32 apps | Microsoft Learn

Directx12 3D 游戏开发实战

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/chenglixue/p/17061919.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!