DX12 基础篇(上)

前言

DX12对于初学者来说难度是偏大的,龙书确实写的不错,但对于初学者来说并不是很友善。本篇将带你了解DX12最为基本的流程,希望能带你快速入门DX12

什么是DirectX?

DirectX是一系列的图像应用编程接口,正是有了这些接口开发人员便无需和寄存器、显存打交道,大大降低了开发难度和学习难度。这些接口联通了上层应用程序和底层GPU的沟通桥梁,应用程序向这些接口发送渲染命令,而这些接口会依次向显卡驱动发送渲染命令,这些显卡驱动是真正知道如何和 GPU通信的角色,正是它们把DirectX的函数调用翻译成了GPU能够听懂的语言,同时它们也负责把纹理等数据转换成GPU所支持的格式,可以说显卡驱动就是显卡的操作系统

下图是各硬件和应用程序的关系image-20230206151931563

Direct3D流程

  • 流程
    image-20230504143458058

各组件的关系

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

基础知识

COM和ComPtr

COM

  • 什么是COM?

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

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

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

ComPtr

  • 什么是ComPtr?

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

    • 如果包含头文件wrl.h,所有的WRL类型都会被包含在内,这适合于UWP开发

      #include <wrl.h>
      
    • 如果包含头文件wrl/client.h,则仅仅包含ComPtr

      #include <wrl/client.h>
      
    • namespace

      using Microsoft::WRL::ComPtr;
      
  • 常用函数

    • Get():返回指向COM接口的原始指针——将ComPtr类型的指针转换为原始指针Reader Q&A: Why don’t modern smart pointers implicitly convert to *?

      ComPtr<ID3D11RasterizerState> rasterState;
      
      device->RSSetState( rasterState.Get() );
      
    • GetAddressOf():返回指向COM接口指针的地址(**) ,并不会释放引用对象

      如何使用该函数?

      • 当需要传递一个指向数组的指针且该指针是单一的时,通常使用该函数
      • 创建一个新的ComPtr
      context->OMSetRenderTargets(1, m_renderTargetView.GetAddressOf(),
          m_depthStencilView.Get());
      // or
      auto rt = m_renderTargetView.Get();
      context->OMSetRenderTargets(1, &rt, m_depthStencilView.Get());	
      
      ComPtr<ID3D11Device> device;
      
      hr = D3D11CreateDevice( ..., device.GetAddressOf(), ... );
      // or
      hr = D3D11CreateDevice( ..., &device, ... );
      
    • ReleaseAndGetAddressOf () :返回指向COM接口指针的地址(**),并释放引用对象

      // 在调用OMSetRenderTargets前,m_renderTargetView变量会被释放,导致崩溃
      context->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView.Get());
      
      
      ComPtr<ID3D11Device> m_device;
      
      hr = D3D11CreateDevice( ..., m_device.ReleaseAndGetAddressOf(), ... );
      // or
      hr = D3D11CreateDevice( ..., &m_device, ... );
      
    • operator&:与ReleaseAndGetAddressOf ()相同,但ReleaseAndGetAddressOf ()代码更加清楚

    • Reset():将此ComPtr置为nullptr并释放与之相关的所有引用

      ComPtr<ID3D11Debug> d3dDebug;
      
      ...
      // 与"d3dDebug = nullptr;"效果相同
      d3dDebug.Reset();
      
    • 检查是否为空

      if ( !d3dDebug )
          // d3dDebug is currently set to nullptr
      ...
      assert( d3dDebug ); // trigger error in debug builds if d3dDebug is set to null
      
  • 要点

    • COM接口都以大写字母”I“开头。如,表示命令列表的COM接口:ID3D12GraphicsCommandList

Device

  • 什么是Device?

    device与一个特定的GPU相关联,它是一个虚拟适配器,用于创建命令列表、命令队列、命令分配器、fence、resource、描述符、描述符堆、根签名、PSO

  • 方法

  • 要点

    • 由于PC可能不止一个GPU,一般来说我们用使用DXGI factory来枚举GPU设备并找到一个最适合(满足功能级别需求)的GPU
    • DXGI factory:用于创建device

Resource

  • 什么是资源(resource)?

    • 存储几何图形、纹理和shader数据的内存块。只是简单的内存块,无其他任何点缀
  • 为什么需要资源?

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

    • Buffer Resources(缓冲区资源)

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

      • 缓冲区类别(ID3D12Resource)
        • Vertex Buffer(顶点缓冲区)

        • Index Buffer(索引缓冲区)

        • Constant Buffer(常量缓冲区)

    • Texture Resources(纹理资源)

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

    • 纹理类型

    • 子资源

      子资源是指资源的子集。缓冲区被定义为单个子资源,纹理有些复杂

  • 定义和方法

    • 资源类型

      ID3D12Resource
      
    • 创建资源一般有两步

      • 创建资源描述D3D12_RESOURCE_DESC (d3d12.h)

        typedef struct D3D12_RESOURCE_DESC {
            D3D12_RESOURCE_DIMENSION Dimension;
            UINT64                   Alignment;
            UINT64                   Width;
            UINT                     Height;
            UINT16                   DepthOrArraySize;
            UINT16                   MipLevels;
            DXGI_FORMAT              Format;
            DXGI_SAMPLE_DESC         SampleDesc;
            D3D12_TEXTURE_LAYOUT     Layout;
            D3D12_RESOURCE_FLAGS     Flags;
        } D3D12_RESOURCE_DESC;
        
      • 创建需要的堆类型

DXGI

  • 什么是DXGI(DirectX Graphics Infrastructure)?

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

  • 为什么需要DXGI?

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

    用于创建swap chains(交换链)和枚举设备适配器

  • 用途

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

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

适配器、显示输出

  • 什么是适配器?

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

  • 为什么需要适配器?

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

  • 实例

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

  • 接口

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

交换链

  • 什么是交换链?

    1. 交换链是用于向用户显示画面的缓冲区的集合.交换链由两个或两个以上的表面组成,一般由两个缓冲区(双缓冲)构成:前台缓冲区和后台缓冲区,每个缓冲区都有纹理格式表示且都存储图像数据.显卡有一个指向表面(缓冲区)的指针
    2. 前台缓冲区存储当前显示在屏幕上的图像数据,后台缓冲区则存储动画的下一帧,后台缓冲区存储完成则,前后台缓冲区角色互换(呈现):后台缓冲区变为前台缓冲区呈现新一帧的画面,而前台缓冲区则为存储新一帧转为后台缓冲区,如此反复image-20230107232647975
    3. 与计算机的其他部分相比,显示器的刷新速度非常慢。如果应用程序正在更新前端缓冲区,而显示器处于刷新过程的中间,那么显示的图像将与包含旧图像的显示器的上半部分和包含新图像的显示器的下半部分一分为二。这个问题称为画面撕裂
  • 为什么需要交换链?

    让画面可平滑过渡

  • 特点

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

    • IDXGISwapChainIDXGISwapChain (dxgi.h):表示交换链。存储了前后台缓冲区两种纹理
      • IDXGISwapChain::ResizeBuffers (dxgi.h):修改缓冲区大小

      • IDXGISwapChain::Present (dxgi.h) :呈现缓冲区

        HRESULT Present(
          UINT SyncInterval,
          UINT Flags
        );
        
        1. SyncInterval(垂直空白)

          垂直空白:当前帧的最后一列更新时间和下一帧的第一列更新时间的时间差.显示器更新方式是一列一列这样垂直更新像素。SyncInterval可以设为0~4的整数,在第n个垂直空白后进行更新

        2. 为什么需要垂直空白?

          当我们希望用交换链来呈现帧且非全屏时,一个名为DWM(桌面窗口管理器)的系统服务会在每个垂直空白处醒来,并获得在桌面上运行的所有图形应用程序的交换链中最新完成的后台缓冲区,将整个桌面的最终图像合成到它自己的后台缓冲区中,当它成为当前缓冲区时,它将在下一个垂直间隔显示在屏幕上
          Image

  • 注意

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

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

  • 几种交换链用途

    • IDXGISwapChain

      用于在将渲染数据present输出前进行存储

    • IDXGISwapChain1

      继承自IDXGISwapChain,支持脏矩形和滚动矩形来优化present性能

    • IDXGISwapChain2

      继承自IDXGISwapChain1,用于支持交换回缓冲区缩放和低延迟交换链

    • IDXGISwapChain3

      继承自IDXGISwapChain2,用于获取交换链当前后台缓冲区的索引并支持颜色空间

渲染目标

  • 什么是渲染目标Render Targets

    • 渲染目标是一个缓冲区,用于保存屏幕上所绘制的像素值,不将帧缓冲绘制到屏幕而是用在别处(离屏渲染,将需要渲染的场景作为纹理帖到其他地方;保存延迟渲染器所需的诸多缓存;在玩家投掷物体到水塘中时形成各种复杂效果)

    • 本质是个资源,继承自ID2D1RenderTarget ,会存储颜色、法线以及AO等信息

  • 几种渲染方式

    1. 向窗口渲染内容
    2. 渲染至 GDI 设备上下文
    3. 位图渲染目标对象 将内容渲染至屏幕外的位图
    4. DXGI 将目标对象渲染至 DXGI 表面,以便与 D3D 一起使用
  • 创建渲染目标描述符

    步骤如下

    1. 获取交换链中的缓冲区资源IDXGISwapChain::GetBuffer (dxgi.h)

      HRESULT IDXGISwapChain::GetBuffer(
              UINT   Buffer,	//缓冲区索引
        [in]  REFIID riid,
        [out] void   **ppSurface
      );
      
    2. 为获取到的后台缓冲区创建渲染目标描述符ID3D12Device::CreateRenderTargetView (d3d12.h)

      void CreateRenderTargetView(
          [in, optional] ID3D12Resource                      *pResource,	//用作渲染目标的资源
          [in, optional] const D3D12_RENDER_TARGET_VIEW_DESC *pDesc,		//指向RTV描述符.该资源若指定了格式则设为nullptr
          [in]           D3D12_CPU_DESCRIPTOR_HANDLE         DestDescriptor	//引用所创建渲染目标视图的描述符句柄
      );
      

深度、模板、帧缓冲区

  • 什么是深度缓冲区、模板缓冲区、帧缓冲区?

    • 深度缓冲区用于存储每个像素的深度值z
    • 模板缓冲区是一个额外的buffer,通常附加到z缓冲区中(也就是做记号),每个像素对应一个深度缓冲区。如在一个像素的模板缓冲区中存放1,表示该像素对应的空间点处于阴影体中
    • 帧缓冲区用于存放显示输出的数据,帧缓冲区是颜色缓冲、模板缓冲、深度缓冲的集合,储存在内存中
  • 为什么需要深度缓冲区和模板缓冲区?

    • 在交换链我们提到过,前台缓冲区和后台缓冲区的每个表面储存2D图形的一个线性数组,对于三维物体来说,我们需要存储深度信息来区分前后
    • 模板缓冲区做记号可以用于制作特效
  • 模板测试和深度测试

    以下是模板测试和深度测试的流程图。深度测试在模板测试之后
    image-20230206142312949

    • 模板测试:GPU读取模板缓冲区中该片元位置的模板值,再将该值和读取到的参考值进行比较,没有通过测试的会被舍弃。注意!不管一个片元有没有通过模板测试,都可以根据模板测试和深度测试的结果来修改模板缓冲区
    • 深度测试:通过模板测试的片元会进入深度测试,GPU会把该片元的深度和已经存在于深度缓冲区中的深度值进行比较,没通过测试的片元会被舍弃。注意!与模板测试不同,若一个片元没有通过深度测试,就没有权力更改深度缓冲区中的值
  • 深度缓冲区的定义

    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. 使用深度缓冲,便无需关注绘制顺序
    3. 模板缓冲区和深度缓冲区通常在显存中共享同一片区域

检测功能支持

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

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

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

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

  • 函数
    ID3D12Device::CheckFeatureSupport (d3d12.h)

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

MSAA反走样

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

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

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

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

    • SSAA(Super Sample Anti-Aliasing)

      • 原理:在每个像素内取多个子采样点,对子采样点进行颜色计算,再合成此像素最终的颜色

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

    • MSAA(Multisample Anti-Aliasing)

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

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

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

    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
    }
    
  • 要点

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

渲染管线状态对象

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

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

  • 为什么需要PSO?

    • DX11的PSO

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

    • DX12的PSO

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

  • 定义和方法

    • 描述管线状态D3D12_GRAPHICS_PIPELINE_STATE_DESC

      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;
      
    • 创建PSOCreateGraphicsPipelineState()

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

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

      void SetPipelineState(
          [in] ID3D12PipelineState *pPipelineState	//指向ID3D12PipelineState
      );
      
  • 要点

命令提交

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

  • 命令列表(command queue)、命令队列(command list)和第二级命令列表捆绑包(bundle)ID3D12CommandList (d3d12.h)ID3D12CommandAllocatorID3D12CommandQueue

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

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

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

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

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

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

      • 举个例子

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

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

创建

  • 创建命令列表

    • 两种方式

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

        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	//指向内存块的指针,该内存块接收指向命令列表的接口的指针
        );
        
      • ID3D12Device4::CreateCommandList1创建一个处于关闭状态的命令列表和捆绑包,当使用分配器和PSO创建命令列表而不使用它时就选这个,这样可以避免低效率

        HRESULT CreateCommandList1(
          [in]  UINT                     nodeMask,
          [in]  D3D12_COMMAND_LIST_TYPE  type,
                D3D12_COMMAND_LIST_FLAGS flags,
          [in]  REFIID                   riid,
          [out] void                     **ppCommandList
        );
        
    • 命令分配器

      • 什么是命令分配器?

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

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

      • 创建命令分配器

        • CreateCommandAllocator ()

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

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

        • reset()

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

        • 要点

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

    • 描述命令队列D3D12_COMMAND_QUEUE_DESC (d3d12.h)

      typedef struct D3D12_COMMAND_QUEUE_DESC {
          D3D12_COMMAND_LIST_TYPE   Type;
          INT                       Priority;
          D3D12_COMMAND_QUEUE_FLAGS Flags;
          UINT                      NodeMask;
      } D3D12_COMMAND_QUEUE_DESC;
      
      • Priority有如下三个优先级

        • D3D12_COMMAND_QUEUE_PRIORITY_NORMAL
        • D3D12_COMMAND_QUEUE_PRIORITY_HIGH
        • D3D12_COMMAND_QUEUE_PRIORITY_GLOBAL_REALTIME
      • D3D12_COMMAND_QUEUE_FLAGS

        • D3D12_PIPELINE_STATE_FLAG_NONE

        • D3D12_PIPELINE_STATE_FLAG_TOOL_DEBUG

          使用附加信息编译管道状态以协助调试,只能在 WARP上设置

    • 创建命令队列CreateCommandQueue()

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

记录命令列表

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

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

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

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

执行命令列表

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

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

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

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

从多个命令队列访问资源

对于多个命令队列访问资源是有一定限制的,规则如下

  • 无法同时从多个命令队列入同一资源

    当一个资源在其中一个队列转到至可写状态时,这时认为该资源为该队列所独有。若另一个队列需对它进行访问,只有从可写状态转换为可读状态或COMMON状态

  • 当资源处于可读状态时,可以同时从多个命令队列读取资源

同步命令列表

  • 什么是围栏?

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

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

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

  • 如何进行同步?

    当app推进围栏时,会调用ID3D12CommandQueue: : Signal()来更新整数,随后app检查围栏值以确定工作是否完成
    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

  • 方法

资源屏障

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

    资源屏障用于帮助同步多个线程之间的资源使用

  • 为什么需要资源屏障?

    • 防止资源冒险

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

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

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

    1. Transition Barrier:用于将资源或子资源的状态从一个状态转换到另一个状态
    2. Aliasing Barrier:change the usages of two different resources that have mappings into the same tile pool(这个暂时不懂)
    3. Unordered Access View (UAV) Barrier:在该屏障被调用时确保所有读/写完成
  • 注意

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

资源绑定

描述符

  • 什么是描述符?

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

  • 为何需要描述符?

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

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

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

  • 描述符的种类

    1. Constant buffer view (CBV)

    2. Render Target View (RTV)

    3. Depth Stencil View (DSV)

    4. Vertex Buffer View (VBV)

    5. Index Buffer View (IBV)

    6. Shader resource view (SRV)

    7. Samplers

    8. Unordered access view (UAV)

    9. Stream Output View (SOV)

  • 要点

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

描述符句柄

  • 什么是描述符句柄?

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

  • 要点

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

描述符堆

  • 什么是描述符堆?

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

  • 为什么需要描述符堆?

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

  • 常用方法

    • 创建和设置描述符堆

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

      • 描述符堆类型D3D12_DESCRIPTOR_HEAP_TYPE (d3d12.h)

        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;
        
      • 描述符堆的属性设置D3D12_DESCRIPTOR_HEAP_DESC (d3d12.h)

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

        ID3D12Device::CreateDescriptorHeap (d3d12.h)

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

      D3D12_CPU_DESCRIPTOR_HANDLE GetCPUDescriptorHandleForHeapStart();
      
    • 获取堆的首个 GPU 描述符句柄

      D3D12_GPU_DESCRIPTOR_HANDLE GetGPUDescriptorHandleForHeapStart();
      
  • 几种管理方法

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

      • 优点:无需记录堆中描述符的位置
      • 缺点:
        • 有大量重复的描述符。尤其是present相似的场景
        • 非常消耗描述符堆的空间
    • 用描述符堆预先填充渲染场景中已知的一部分对象,只需在绘制时设置描述符表

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

  • 要点

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

根签名

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

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

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

  • 根参数(root parameter

    • 什么是根参数?

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

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

  • 定义和方法

    • 根签名描述布局(D3D12_ROOT_SIGNATURE_DESC)

      此结构体由 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;
      
    • 检测根签名版本支持(D3D12_FEATURE_DATA_ROOT_SIGNATURE)

      typedef struct D3D12_FEATURE_DATA_ROOT_SIGNATURE {
          D3D_ROOT_SIGNATURE_VERSION HighestVersion;	//要检查的最高版本
      } D3D12_FEATURE_DATA_ROOT_SIGNATURE;
      
      //根签名版本
      typedef enum D3D_ROOT_SIGNATURE_VERSION {
          D3D_ROOT_SIGNATURE_VERSION_1 = 0x1,
          D3D_ROOT_SIGNATURE_VERSION_1_0 = 0x1,
          D3D_ROOT_SIGNATURE_VERSION_1_1 = 0x2
      } ;
      

      定义D3D12_FEATURE_DATA_ROOT_SIGNATURE后,只需调用CheckFeatureSupport()检测即可

    • 定义根参数(D3D12_ROOT_PARAMETER)

      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;
      
      • D3D12_ROOT_PARAMETER_TYPE(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
        } ;
        
      • D3D12_SHADER_VISIBILITY(D3D12_SHADER_VISIBILITY (d3d12.h))

        设置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	//网格着色器
        } ;
        
    • 创建根签名(ID3D12Device::CreateRootSignature)

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

        HRESULT D3D12SerializeRootSignature(
            [in]            const D3D12_ROOT_SIGNATURE_DESC *pRootSignature,
            [in]            D3D_ROOT_SIGNATURE_VERSION      Version,	//根签名的版本
            [out]           ID3DBlob                        **ppBlob,
            [out, optional] ID3DBlob                        **ppErrorBlob
        );
        
  • 注意事项

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

描述符表

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

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

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

  • 大小

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

  • 描述符范围(descriptor range)

    • 什么是描述符范围?

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

    • 为什么需要描述符范围?

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

  • 定义和方法

    • 定义描述符表D3D12_ROOT_DESCRIPTOR_TABLE (d3d12.h)

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

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

        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;
        
      • BaseShaderRegister

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

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

        可以在不同的寄存器空间中指定着色器寄存器,因为寄存器中包含同名的寄存器槽。如,以下看起来像是重复使用寄存器槽t0,但并非如此

        Texture2D gDiffuseMap : register(t0, space0);
        Texture2D gNormalMap : register(t0, space1);
        
    • 将描述符表与管线绑定ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable (d3d12.h)

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

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

    • 描述符表没有数量限制
    • 从描述符表访问资源时,有两个间接的开销:从描述符表指针到存储在堆中的描述符 和 从描述符堆到实际资源

根描述符

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

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

  • 为什么需要根描述符?

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

  • 大小

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

  • 定义和方法

  • 要点

    • 仅限于 CBV 、结构化的 SRV 、 UAV;不能使用 Texture2D SRV 之类的复杂类型
    • 根描述符不包括大小限制,不可进行越界检查。而描述符堆中的描述符包含大小
    • 从着色器引用根描述符时,有重定向的成本

根常量

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

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

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

  • 定义和方法

    • 描述常量缓冲区出现在着色器中的根签名中的内联常量(D3D12_ROOT_CONSTANTS)

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

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

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

    • 只存储shader访问的经常更改的常量值
    • 从着色器访问这些变量没有成本(重定向),因此访问很快

内存管理

这部分建议看实例,更好理解Suballocation Within Buffers

  • 什么是堆?

    其本质是有特定属性的GPU显存块

  • 为什么需要堆?

    用于存放GPU资源,因为CPU和GPU的内存块类型是不同的,CPU和GPU不可互相访问对方的内存块

  • 三种常用的堆

    typedef enum D3D12_HEAP_TYPE {
      D3D12_HEAP_TYPE_DEFAULT = 1,
      D3D12_HEAP_TYPE_UPLOAD = 2,
      D3D12_HEAP_TYPE_READBACK = 3,
      D3D12_HEAP_TYPE_CUSTOM = 4
    } ;
    
    • D3D12_HEAP_TYPE_DEFAULT(默认堆)

      默认堆为GPU提供最大的带宽(GPU访问这个堆速率是最快的),但CPU不能访问

    • D3D12_HEAP_TYPE_UPLOAD(上传堆)

      上传堆CPU可以访问,此堆提交的都是需经CPU上传至GPU的资源.适合于CPU写一次,GPU读一次的数据

      两种常用用途

      • 使用来自 CPU 的数据初始化默认堆中的资源
      • 将动态数据上传到每个顶点或像素重复读取的常数缓冲区中
    • D3D12_HEAP_TYPE_READBACK(回读堆)

      回读堆可以从GPU读取数据,CPU可以访问,此堆提交的都是需要有CPU读取的资源

  • 要点

    • 纹理不能是上传堆或回读堆
    • 尽量使用默认堆,这个性能最佳
    • 上传堆不要用于每帧重新初始化资源内容上传常量数据

reference

渲染目标(Render Target) | 虚幻引擎文档 (unrealengine.com)

渲染概念:3.RenderTarget-渲染目标 - 知乎 (zhihu.com)

ComPtr · microsoft/DirectXTK Wiki (github.com)

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

https://paminerva.github.io/docs/LearnDirectX/01.F-Hello-Frame-Buffering.html

Directx12 3D 游戏开发实战

Unity Shader入门精要

GPU编程与CG语言之阳春白雪下里巴人

posted @ 2023-01-19 17:59  爱莉希雅  阅读(2017)  评论(1编辑  收藏  举报