精华内容
下载资源
问答
  • D3D12-项目 关于 开始学习Direct3D12(D3D12)的项目。 :创建一个用于显示的窗口 :渲染窗口的背景色 发展 平台:Windows 10 32位和64位(Windows API + Direct3D 12) 引擎编程语言:ISO C ++最新草案标准(> C...
  • D3D12挂钩 d3d12挂钩,directx12挂钩,dx12挂钩,d3d12 api挂钩 鸣谢:Rebzzel
  • D3D12内存分配器 易于集成Direct3D 12的内存分配库。 许可证:麻省理工学院。 参见 变更日志:请参阅 产品页面: 生成状态: 视窗: 问题 与较旧的图形API(例如Direct3D 11或OpenGL:registered:)相比,在新的...
  • Novus Engine 2是用于图形实验的引擎,它利用了D3D12功能 NE2的主要目的是利用从NE1汲取的经验教训,将基础架构重建为更实用,更健壮,并创建一个新的渲染系统,以利用D3D12的多线程支持。 建造 Novus Engine 2目前...
  • kiero:适用于D3D9-D3D12,基于OpenGL和Vulkan的游戏的通用图形挂钩
  • DirectX12(D3D12)基础教程(五)——理解和使用捆绑包,加载并使用DDS Cube Map 示例代码中需要的资源: DDSTextureLoader12.cpp DDSTextureLoader12.h sky_cube.dds sphere.txt 金星.jpg
  • PBR:在D3D11,D3D12,Vulkan和OpenGL 4中基于物理阴影和基于图像的照明的实现
  • D3D12将资源状态管理从图形API层移交到应用层,迫使我们自己来管理资源的状态,我们不仅要正确的使用资源状态转换,还要保证转换的性能,这就需要我们深入的了解D3D12资源状态转换的一些规则。让我们先来看看D3D12都...

    D3D12将资源状态管理从图形API层移交到应用层,迫使我们自己来管理资源的状态,我们不仅要正确的使用资源状态转换,还要保证转换的性能,这就需要我们深入的了解D3D12资源状态转换的一些规则。让我们先来看看D3D12都有哪些应用场景。

    • Transition barrier 最常用的资源状态转换,比如RT->SRV。RT是写状态,SRV是读状态,因此我们需要建立一个转换屏障,保证写操作完成后再去读取资源。
    • Aliasing barrier D3D12允许一块堆内存被多个资源占用,这可以提高内存的利用率。因此我们需要一个别名屏障来保证同一时间只有一个资源在使用该内存。
    • Unordered access view (UAV) barrier 有的时候我们希望一个UAV先写完再读,虽然这样很低效,但是能保证结果是正确的。因此我们需要一个UAV屏障来保证写完再读。这个和转换屏障很类似,可以理解为转换屏障的UAV版本。

    Barrier在底层导致做了什么?

    • change state of memory for resource 首先它会改变内存的状态,比如从RT状态改变到SRV状态。写状态和读状态可能要求不同的数据格式,这取决于具体的资源类型及显卡的实现。
    • cache flushes 通常为了数据一致性,需要在转换的时候刷新缓存。
    • pipeline statlls 状态转换经常会造成gpu的空闲等待,比如在等待写完成。gpu内部其实也是分多个模块的,比如传输模块,计算模块。一个资源转换可能只占用传输模块,计算模块此时如果也被强制等待,就会造成gpu的资源浪费。再比如D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE和D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE。为什么将读状态拆分成像素着色器使用还是非像素着色器使用呢?一部分原因就是告诉显卡,如果发生资源状体转换,那么请只阻塞相应的模块,而没必要将整个gpu都阻塞。
    • 还有一些其他工作比如对内存的解压等等,为什么会有内存的解压,我没做过驱动层,只能感性理解,比如写内存的时候,不需要随机读取,因此我们可以压缩内存,减少带宽的使用,当读的时候就需要把它解压缩加速读取速度。这还取决于硬件是否支持。

    综上所述Barrier是一个很昂贵的api调用。如何优化Barrier的使用是一个复杂的问题,有几个基本的准则如下:

    尽量减少Barrier的使用

    尽量减少Barrier的使用,这貌似是一句废话,但是里面的细节却很重要。对于资源的状态转换,有些时候必须显示转换,有些时候不需显示转换,如果你显示转换了,那么就可能造成不必要的flushes和stalls。换句话说有些资源状态的转换是顺理成章的,应用层,驱动层,都不需要进行任何操作,硬件会很自然的正确使用资源,如果你此时强制使用了Barrier就可能强迫GPU进行不必要的flushes和stalls。那么问题来了,哪些资源转换是可以省略的?D3D12将这种省略的状态转换称为Implicit state transitions:"Resources can only be "promoted" out of D3D12_RESOURCE_STATE_COMMON. Similarly, resources will only "decay" to D3D12_RESOURCE_STATE_COMMON."对于隐式提升和衰减都是围绕D3D12_RESOURCE_STATE_COMMON状态进行的。为了方便理解隐式状态转换,D3D12将资源分成两类,一类是BufferSimultaneous-Access Textures,这类资源可以从common状态提升到任何状态,除了Depth-stencil resources,因为它必须是non-simultaneous-access textures。这类资源可以连续进行隐式资源转换,比如先转换成读,再转换成写,这些转换都不需要显式Barrier。另一类是Non-Simultaneous-Access Textures,这类资源只能隐式转换到某些状态,具体参考D3D12文档,而且一旦提升到写状态后,就不能继续隐式转换了,但是提升到读后是可以继续隐式转换的。当一个ExecuteCommandLists执行完毕后,以下情况的资源会自动退回到common状态(注意当ExecuteCommandLists调用多个CLS时是无法进行自动回退的。这里就有一个鱼和熊掌的问题了,D3D12要求我们尽可能的合并ExecuteCommandLists的调用,因为可以降低DDI层的消耗,因为调用这个API,操作系统会从用户态切换到系统态,另外驱动层也可以进行一些优化,比如去掉冗余的命令,让显卡异步调用起来。但是合并的ExecuteCommandLists的调用会阻止common状态的回退,导致我们可能需要显式状态转换)

    • 任何资源被拷贝队列使用
    • buffer资源或被标记为D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS状态的贴图资源在任何队列中使用
    • 任何资源隐式提升到只读状态

    其实可以简单理解Common状态的资源,这类资源需要满足以下两个条件:

    1) Have no pending write operations, cache flushes or layout changes.

    2) Have a layout that is intrinsically readable by any GPU operation.

    因为D3D12标准要求ExecuteCommandLists执行后不允许有任何未完成的操作包括cache flushes and layout changes。因此ExecuteCommandLists之后的资源只需要满足第2条就可以回退到common状态。

    因为Buffers and Simultaneous-Access Textures必须保证所有gpu操作都可以读,而且写操作不允许改变layout,因此只要它们没有被使用就会退回common状态。正是因为这个原因,这类资源可以连续进行隐式转换。

    总结能隐式转换的时候不要显式转换

    除非必须,不要转换到D3D12_RESOURCE_STATE_GENERIC_READ和D3D12_RESOURCE_STATE_COMMON状态

    转换到D3D12_RESOURCE_STATE_GENERIC_READ将会导致者整个gpu流水线的flushes和stalls。因为D3D12_RESOURCE_STATE_GENERIC_READ代表了整个流水线都可以Read。只有上传堆资源可以使用D3D12_RESOURCE_STATE_GENERIC_READ状态,因为上传只涉及到cpu的拷贝,占用的是cpu timeline。

    不允许进行read to read 的转换,这会增加转换的开销,可以将其转换到combine read state。

    我们希望D3D12_RESOURCE_STATE_COMMON状态可以隐式转换,因此不建议显示转换到common状态,除非必要的时候。一种情况是在copy queue中使用的资源,如果之前是在graphic or compute engine使用的资源,在copy queue使用之前需要将其转换到common state,反之亦然。另一种情况是cpu需要访问的贴图资源,需要将资源放入到 CPU-visible heap中,并且资源的状态需要是common状态。

    尽量batch Barrier

    可以减少DDI的调用,驱动层也可以尝试进行优化,比如去掉冗余的状态转换

    使用Split Barriers

    如果一个转换过程不需要立即转换,我们可以把这个转换过程的实际时间拉长,通过start和end告诉驱动,在start和end期间可以安排异步执行的任务。比如D3D12_RESOURCE_STATE_DEPTH_WRITE状态转换到D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE。深度写转换到pixel读的时候,占用大量的带宽资源,但是ALU相对空闲,如果不使用split barriers,gpu资源就会浪费ALU部分。如果使用split barriers,驱动程序就有可能会安排一些ALU的任务同步执行。

    我们可以利用D3D12的debug layer帮组我们正确的使用Barriers,比如AssertResourceState API。

    理解了以上内容,也就理解了AMD和Nivida对于barrier的使用建议,最后附上它们的使用建议。

    AMD Barriers

    Barriers are how dependencies between operations are conveyed to the API and driver. Barriers open up a whole new world of operations by allowing the application to decide if the GPU can overlap work. They are also an easy way to slow down the rendering by adding too many barriers or can cause corruptions from not having correct resource transitions. The validation layers can often help with identifying missing barriers.

    • Minimize the number of barriers used per frame.
      • Barriers can drain the GPU of work.
      • Don’t issue read to read barriers. Transition the resource into the correct state the first time.
    • Batch groups of barriers into a single call to reduce overhead of barriers.
      • This creates less calls into the driver and allows the driver to remove redundant operations.
    • Avoid GENERAL / COMMON layouts unless required.
      • Always use the optimized state for your usage.

    Nivida

    • Minimize the use of barriers and fences
      • We have seen redundant barriers and associated wait for idle operations as a major performance problem for DX11 to DX12 ports
        • The DX11 driver is doing a great job of reducing barriers – now under DX12 you need to do it
      • Any barrier or fence can limit parallelism
    • Make sure to always use the minimum set of resource usage flags
      • Stay away from using D3D12_RESOURCE_USAGE_GENERIC_READ unless you really need every single flag that is set in this combination of flags
      • Redundant flags may trigger redundant flushes and stalls and slow down your game unnecessarily
      • To reiterate: We have seen redundant and/or overly conservative barrier flags and their associated wait for idle operations as a major performance problem for DX11 to DX12 ports.
    • Specify the minimum set of targets in ID3D12CommandList::ResourceBarrier
      • Adding false dependencies adds redundancy
    • Group barriers in one call to ID3D12CommandList::ResourceBarrier
      • This way the worst case can be picked instead of sequentially going through all barriers
    • Use split barriers when possible
      • Use the _BEGIN_ONLY/_END_ONLY flags
      • This helps the driver doing a more efficient job
    • Do use fences to signal events/advance across calls to ExecuteCommandLists

    Dont's

    • Don’t insert redundant barriers
      • This limits parallelism
      • A transition from D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE to D3D12_RESOURCE_STATE_RENDER_TARGET and back without any draw calls in-between is redundant
      • Avoid read-to-read barriers
        • Get the resource in the right state for all subsequent reads
    • Don’t use D3D12_RESOURCE_USAGE_GENERIC_READ without good reason.
      • For transitions from write-to-read states, ensure the transition target is inclusive of all required read states needed before the next transition to write. This is done from the API by combining read state flags– and is preferred over transitioning from read-to-read in subsequent ResourceBarrier calls.
    • Don’t sequentially call ID3D12CommandList::ResourceBarrier with just one barrier
      • This doesn’t allow the driver to pick the worst case of a set of barriers
    • Don’t expect fences to trigger signals/advance at a finer granularity then once per ExecuteCommandLists call.
    展开全文
  • D3D12渲染技术之初始化流程

    千次阅读 2018-08-23 14:12:47
    也就是我们通常说的初始化D3D12D3D12的初始化工作与以前的D3D9是完全不一样的,D3D12做了大幅的升级。但是每个D3D图形API都有自己的初始化流程,大家只要记住这个流程,学习起来就比较容易,就跟生产车架的流水线...

    掌握了矩阵向量运算后,接下来我们要做的事情利用D3D12图形库API编程,也就是我们通常说的初始化D3D12,D3D12的初始化工作与以前的D3D9是完全不一样的,D3D12做了大幅的升级。但是每个D3D图形API都有自己的初始化流程,大家只要记住这个流程,学习起来就比较容易,就跟生产车架的流水线作业一样的原理,先做什么后做什么。其他的内容向里面添加就可以了,我们先聊聊D3D12的初始化流程,然后再编程实现。

    D3D初始化

    1、创建窗体
    2、创建ID3D12Device设备
    3、创建 ID3D12Fence用于查询descriptor 大小
    4、检查设备是否支持4X MSAA
    5、创建指令队列,指令列表和主指令列表。
    6、创建交换链
    7、创建描述符堆(descriptor heaps)
    8、创建渲染目标视图。
    9、创建深度/模板缓冲区及其关联的深度/模板视图。
    10、设置视口

    共分为10步,严格来说,第一步不属于D3D12初始化里面的内容,因为我们要显示D3D12中的内容,必须要要有窗体作为载体,在这里就把他们加进来了,我们下面就按照这10步去编写程序实现我们的D3D12的初始化工作。

    • 创建窗体
      窗体是一个载体,用于显示D3D12内容的,首先需要注册窗体类,把它们里面的参数填满,代码如下所示:
    bool D3DApp::InitMainWindow()
    {
        WNDCLASS wc;
        wc.style         = CS_HREDRAW | CS_VREDRAW;
        wc.lpfnWndProc   = MainWndProc; 
        wc.cbClsExtra    = 0;
        wc.cbWndExtra    = 0;
        wc.hInstance     = mhAppInst;
        wc.hIcon         = LoadIcon(0, IDI_APPLICATION);
        wc.hCursor       = LoadCursor(0, IDC_ARROW);
        wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
        wc.lpszMenuName  = 0;
        wc.lpszClassName = L"MainWnd";
    
        if( !RegisterClass(&wc) )
        {
            MessageBox(0, L"RegisterClass Failed.", 0, 0);
            return false;
        }
    
        // Compute window rectangle dimensions based on requested client area dimensions.
        RECT R = { 0, 0, mClientWidth, mClientHeight };
        AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
        int width  = R.right - R.left;
        int height = R.bottom - R.top;
    
        mhMainWnd = CreateWindow(L"MainWnd", mMainWndCaption.c_str(), 
            WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, mhAppInst, 0); 
        if( !mhMainWnd )
        {
            MessageBox(0, L"CreateWindow Failed.", 0, 0);
            return false;
        }
    
        ShowWindow(mhMainWnd, SW_SHOW);
        UpdateWindow(mhMainWnd);
    
        return true;
    }

    这个在DX9或者MFC中都会用到,这里就不介绍了,接下来开始进的入D3D12编程的正题了。

    • 创建ID3D12Device设备
      创建D3D12的设备,自然想到函数:D3D12CreateDevice,在D3D12中我们还需要做一件事情就是调用函数:CreateDXGIFactory1在这个函数中有DXGI字样,DXGI它是DirectX Graphics Infrastructure的缩写,DXGI 可以操控一些普通的图形功能,这其中包括全屏转换、枚举图形系统信息(比如显示适配器、显示器、被支持的显示模式(分辨率,刷新率等等)),它还定义了各种纹理格式(DXGI_FORMAT)。CreateDXGIFactory1就是DXGI下面的一个接口函数,可以枚举显示适配器。
      函数调用代码如下:
    CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory))

    其中 IID_PPV_ARGS 宏用来返回 COM 接口的 ID
    接下来调用D3D12CreateDevice函数,代码实现如下:

    HRESULT hardwareResult = D3D12CreateDevice(
            nullptr,             // default adapter
            D3D_FEATURE_LEVEL_11_0,
            IID_PPV_ARGS(&md3dDevice));
    
    
        if(FAILED(hardwareResult))
        {
            ComPtr<IDXGIAdapter> pWarpAdapter;
            ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
    
            ThrowIfFailed(D3D12CreateDevice(
                pWarpAdapter.Get(),
                D3D_FEATURE_LEVEL_11_0,
                IID_PPV_ARGS(&md3dDevice)));
        }

    if条件语句内部就是枚举适配器的代码,设备创建好了以后,下面就是创建Fence了。

    • 创建 ID3D12Fence
      接下来我们需要创建Fence对象用于CPU/GPU同步,另外,一旦我们开始使用描述符,我们将需要知道它们的大小, 描述符大小可能因GPU而异,因此我们需要查询此信息, 我们缓存描述符的大小,以便在我们需要它时可用于各种描述符类型,代码如下所示:
        ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
            IID_PPV_ARGS(&mFence)));
    
        mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
        mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
        mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
    • 检查设备是否支持4X MSAA
      我们之所以选择支持4X MSAA,在这里还是要求电脑硬件配置相对高一些比较好,因为所有支持Direct3D 11的设备都支持所有渲染目标格式的4X MSAA, 因此,保证在Direct3D 11硬件上可用,我们无需验证对它的支持。 但是,我们必须检查支持的质量等级,可以使用以下方法完成:
    D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
        msQualityLevels.Format = mBackBufferFormat;
        msQualityLevels.SampleCount = 4;
        msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
        msQualityLevels.NumQualityLevels = 0;
        ThrowIfFailed(md3dDevice->CheckFeatureSupport(
            D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
            &msQualityLevels,
            sizeof(msQualityLevels)));
    
        m4xMsaaQuality = msQualityLevels.NumQualityLevels;
        assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
    • 创建指令队列,指令列表和主指令列表
      指令队列由ID3D12CommandQueue接口表示,指令分配器由ID3D12CommandAllocator接口表示,指令列表由ID3D12GraphicsCommandList接口表示,如下图所示:
      这里写图片描述
      CPU 通过 Direct3D API 的指令列表(Command Lists)向指令队列提交一个命令后,GPU并不会将这些命令立即执行,而是让它们置于队列之中等待处理,因为 GPU 此时可能正在处理队列中其它的命令。
      一旦指令队列为空,则 GPU 不再工作,因为没有指令可以处理。反过来,如果队列已满,则 CPU 不再工作,而是等待 GPU 处理队列中的指令。当然这两种情况哪个都是不可取的,理想情况是存在一种动态的平衡,让指令队列中始终存在可以被处理的指令却不能处于满格状态,这样 GPU 和 CPU 都能够被充分利用,这个需要用到我们上文提到的Fence。以下函数显示了我们如何创建命令队列,指令分配器和命令列表:
    void D3DApp::CreateCommandObjects()
    {
        D3D12_COMMAND_QUEUE_DESC queueDesc = {};
        queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
        queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
        ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
    
        ThrowIfFailed(md3dDevice->CreateCommandAllocator(
            D3D12_COMMAND_LIST_TYPE_DIRECT,
            IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
    
        ThrowIfFailed(md3dDevice->CreateCommandList(
            0,
            D3D12_COMMAND_LIST_TYPE_DIRECT,
            mDirectCmdListAlloc.Get(), // Associated command allocator
            nullptr,                   // Initial PipelineStateObject
            IID_PPV_ARGS(mCommandList.GetAddressOf())));
    
        // Start off in a closed state.  This is because the first time we refer 
        // to the command list we will Reset it, and it needs to be closed before
        // calling Reset.
        mCommandList->Close();
    }

    注意,对于CreateCommandList,我们为管道状态对象参数指定null。 在本篇博客的示例程序中,我们不发出任何绘图命令,因此我们不需要有效的管道状态对象。

    • 创建交换链
      初始化过程的下一步是创建交换链, 这是通过填写DXGI_SWAP_CHAIN_DESC结构的实例来完成的,该结构描述了我们要创建的交换链的特征, 该结构定义如下:
     mSwapChain.Reset();
    
        DXGI_SWAP_CHAIN_DESC sd;
        sd.BufferDesc.Width = mClientWidth;
        sd.BufferDesc.Height = mClientHeight;
        sd.BufferDesc.RefreshRate.Numerator = 60;
        sd.BufferDesc.RefreshRate.Denominator = 1;
        sd.BufferDesc.Format = mBackBufferFormat;
        sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
        sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
        sd.SampleDesc.Count = m4xMsaaState ? 4 : 1;
        sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
        sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        sd.BufferCount = SwapChainBufferCount;
        sd.OutputWindow = mhMainWnd;
        sd.Windowed = true;
        sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
        sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

    在D3D12中的实现效果如下所示:
    这里写图片描述
    利用两个缓冲区(前后缓冲)组成交换链的方式叫做双重缓冲(double buffering),也就是说,交换链不是必须要用两个缓冲区才可以,也可以用两个数量以上的缓冲,但是通常情况下两个就已经绰绰有余了。
    下面我们介绍这个结构体中的一些常用的属性:
    BufferDesc,此结构描述了我们要创建的后台缓冲区的属性, 我们关注的主要属性是宽度和高度,以及像素格式; 其他可以查看帮助文档。
    SampleDesc,多重采样的数量和质量等级; 对于单次采样,请指定样本计数为1,质量等级为0。
    BufferUsage,指定DXGI_USAGE_RENDER_TARGET_OUTPUT,因为我们将渲染到后台缓冲区(即,将其用作渲染目标)。
    BufferCount,交换链中使用的缓冲区数量; 为双缓冲指定两个。
    OutputWindow,我们正在渲染的窗口的句柄。
    Windowed,指定true以在窗口模式下运行,或指定为全屏模式时为false。
    SwapEffect,指定DXGI_SWAP_EFFECT_FLIP_DISCARD。
    Flags,可选标志, 如果指定DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,则当应用程序切换到全屏模式时,它将选择与当前应用程序窗口尺寸最匹配的显示模式。 如果未指定此标志,则当应用程序切换到全屏模式时,它将使用当前桌面显示模式。

    在我们描述了交换链之后,我们可以使用IDXGIFactory :: CreateSwapChain方法创建它:

    ThrowIfFailed(mdxgiFactory->CreateSwapChain(
            mCommandQueue.Get(),
            &sd, 
            mSwapChain.GetAddressOf()));
    • 创建描述符堆
      我们需要创建描述符堆来存储我们的应用程序需要的描述符/视图, 描述符堆由ID3D12DescriptorHeap接口表示, 使用ID3D12Device :: CreateDescriptorHeap方法创建堆。 在本篇博客的示例程序中,我们需要SwapChainBufferCount许多渲染目标视图(RTV)来描述我们将渲染到的交换链中的缓冲区资源,以及一个深度/模板视图(DSV)来描述深度/模板缓冲区资源以进行深度测试。 因此,我们需要一个堆来存储SwapChainBufferCount RTV,需要一个堆来存储一个DSV, 这些堆使用以下代码创建:
    void D3DApp::CreateRtvAndDsvDescriptorHeaps()
    {
        D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
        rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
        rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
        rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
        rtvHeapDesc.NodeMask = 0;
        ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
            &rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));
    
    
        D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
        dsvHeapDesc.NumDescriptors = 1;
        dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
        dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
        dsvHeapDesc.NodeMask = 0;
        ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
            &dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
    }
    

    在我们的实现中,我们定义了两个变量:

        static const int SwapChainBufferCount = 2;
        int mCurrBackBuffer = 0;

    我们使用mCurrBackBuffer跟踪当前的后台缓冲区索引(回想一下前页和后台缓冲区在页面翻转中交换,所以我们需要跟踪哪个缓冲区是当前的后台缓冲区,以便我们知道要渲染哪个缓冲区)。

    在我们创建堆之后,我们需要能够访问它们存储的描述符,通过句柄引用描述符, 使用ID3D12DescriptorHeap :: GetCPUDescriptorHandleForHeapStart方法获取堆中第一个描述符的句柄, 以下函数分别获取当前后台缓冲区RTV和DSV:

    D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::CurrentBackBufferView()const
    {
        return CD3DX12_CPU_DESCRIPTOR_HANDLE(
            mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
            mCurrBackBuffer,
            mRtvDescriptorSize);
    }
    D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::DepthStencilView()const
    {
        return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
    }
    • 创建渲染目标视图
      我们不直接将资源绑定到管道; 相反,我们必须为资源创建资源视图(描述符)并将视图绑定到管道, 特别是,为了将后台缓冲区绑定到管道的输出合并(因此Direct3D可以渲染到它上面),我们需要为后台缓冲区创建一个渲染目标视图, 第一步是获取存储在交换链中的缓冲区资源:
    HRESULT IDXGISwapChain::GetBuffer(
      UINT Buffer,
      REFIID riid,
      void **ppSurface);

    Buffer,标识我们想要获取的特定后台缓冲区的索引(如果有多个)。
    riid,我们想要获取指针的ID3D12Resource接口的COM ID。
    ppSurface,返回指向表示后台缓冲区的ID3D12Resource的指针。

    对IDXGISwapChain :: GetBuffer的调用会将COM引用计数增加到后台缓冲区,因此我们必须在完成后释放它, 如果使用ComPtr,则会自动完成此操作。
    要创建渲染目标视图,我们使用ID3D12Device :: CreateRenderTargetView方法:

    void ID3D12Device::CreateRenderTargetView(
      ID3D12Resource *pResource,
      const D3D12_RENDER_TARGET_VIEW_DESC *pDesc,
      D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor);

    pResource,指定将用作渲染目标的资源,在上面的示例中,它是后台缓冲区(即,我们正在为后台缓冲区创建渲染目标视图)。
    pDesc,指向D3D12_RENDER_TARGET_VIEW_DESC的指针, 除此之外,该结构描述了资源中元素的数据类型(格式), 如果资源是使用类型化格式(即非无类型)创建的,则此参数可以为null,这表示创建此资源的第一个mipmap级别的视图(后台缓冲区只有一个mipmap级别),格式为 资源创建的, 因为我们指定了后台缓冲区的类型,所以我们为这个参数指定了null。
    DestDescriptor,处理将存储创建的渲染目标视图的描述符。

    下面是调用这两种方法的示例,其中我们为交换链中的每个缓冲区创建一个RTV:

    ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
      mRtvHeap->GetCPUDescriptorHandleForHeapStart());
    for (UINT i = 0; i < SwapChainBufferCount; i++)
    {
      // Get the ith buffer in the swap chain.
      ThrowIfFailed(mSwapChain->GetBuffer(
        i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
    
      // Create an RTV to it.
      md3dDevice->CreateRenderTargetView(
        mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
         // Next entry in heap.
      rtvHeapHandle.Offset(1, mRtvDescriptorSize);
    } 
    • 创建深度/模板缓冲和视图
      我们现在需要创建深度/模板缓冲区,深度缓冲区只是一个2D纹理,它存储最近的可见对象的深度信息(如果使用模板,则存储模板信息)。 纹理是一种GPU资源,因此我们通过填写描述纹理资源的D3D12_RESOURCE_DESC结构来创建一个,然后调用ID3D12Device :: CreateCommittedResource方法。 D3D12_RESOURCE_DESC结构定义如下:
    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_MISC_FLAG MiscFlags;
      } D3D12_RESOURCE_DESC;

    Dimension,资源的维度,它是以下枚举类型之一:

    enum D3D12_RESOURCE_DIMENSION
      {
        D3D12_RESOURCE_DIMENSION_UNKNOWN = 0,
        D3D12_RESOURCE_DIMENSION_BUFFER = 1,
        D3D12_RESOURCE_DIMENSION_TEXTURE1D = 2,
        D3D12_RESOURCE_DIMENSION_TEXTURE2D = 3,
        D3D12_RESOURCE_DIMENSION_TEXTURE3D = 4
      } D3D12_RESOURCE_DIMENSION;

    Width,纹理的宽度,以纹素为单位。 对于缓冲区资源,这是缓冲区中的字节数。
    Height,纹理的高度,以纹素为单位。
    DepthOrArraySize,纹理的纹理深度,或纹理数组大小(对于1D和2D纹理)。
    MipLevels,mipmap等级的数量,为了创建深度/模板缓冲,我们的纹理只需要一个mipmap级别。
    Format,指定文本格式的DXGI_FORMAT枚举类型的成员,深度/模板缓冲区也需要一个格式;
    SampleDesc,多重采样数和质量等级, 回想一下,4X MSAA使用比屏幕分辨率大4倍的后缓冲和深度缓冲,以便存储每个子像素的颜色和深度/模板信息。 因此,用于深度/模板缓冲区的多重采样设置必须与用于渲染目标的设置相匹配。
    Layout,D3D12_TEXTURE_LAYOUT枚举类型的成员,指定纹理布局。 目前,我们不必担心布局,可以指定D3D12_TEXTURE_LAYOUT_UNKNOWN。
    MiscFlags,其它资源标志, 对于深度/模板缓冲区资源,请指定D3D12_RESOURCE_MISC_DEPTH_STENCIL。

    GPU资源存在于堆中,这些GPU本质上是具有某些属性的GPU内存块。 ID3D12Device :: CreateCommittedResource方法使用我们指定的属性创建并向特定堆提交资源。

    HRESULT ID3D12Device::CreateCommittedResource(
      const D3D12_HEAP_PROPERTIES *pHeapProperties,
      D3D12_HEAP_MISC_FLAG HeapMiscFlags,
      const D3D12_RESOURCE_DESC *pResourceDesc,
      D3D12_RESOURCE_USAGE InitialResourceState,
      const D3D12_CLEAR_VALUE *pOptimizedClearValue,
      REFIID riidResource,
      void **ppvResource);
    
    typedef struct D3D12_HEAP_PROPERTIES {
     D3D12_HEAP_TYPE     Type;
     D3D12_CPU_PAGE_PROPERTIES CPUPageProperties;
     D3D12_MEMORY_POOL    MemoryPoolPreference;
     UINT CreationNodeMask;
     UINT VisibleNodeMask;
    } D3D12_HEAP_PROPERTIES;

    pHeapProperties,我们要将资源提交到的堆的属性, 其中一些属性用于高级用法, 目前,我们需要担心的主要属性是D3D12_HEAP_TYPE,它可以是D3D12_HEAP_PROPERTIES枚举类型的以下成员之一:

    • D3D12_HEAP_TYPE_DEFAULT,默认堆, 这是我们提交将由GPU单独访问的资源地方,
      以深度/模板缓冲区为例:GPU读取和写入深度/模板缓冲区, CPU永远不需要访问它,因此深度/模板缓冲区将被放置在默认堆中。
    • D3D12_HEAP_TYPE_UPLOAD,上传堆, 这是我们提交资源的地方,我们需要将数据从CPU上传到GPU资源。
    • D3D12_HEAP_TYPE_READBACK,回读堆,这是我们提交需要由CPU读取的资源的地方。
    • D3D12_HEAP_TYPE_CUSTOM,有关高级使用方案,请参阅MSDN文档以获取更多信息。
      HeapMiscFlags,有关我们要将资源提交到的堆的其他标志, 它通常是D3D12_HEAP_MISC_NONE。
      pResourceDesc,指向描述我们要创建的资源的D3D12_RESOURCE_DESC实例的指针。
      InitialResourceState, 使用此参数可在创建资源时设置资源的初始状态,对于深度/模板缓冲区,初始状态将为D3D12_RESOURCE_USAGE_INITIAL,然后我们将其转换为D3D12_RESOURCE_USAGE_DEPTH,以便它可以作为深度/模板缓冲区绑定到管道。
      pOptimizedClearValue,指向D3D12_CLEAR_VALUE对象的指针,该对象描述用于清除资源的优化值, 可以为此值指定Null,以不指定优化的清除值。
    struct D3D12_CLEAR_VALUE
      {
        DXGI_FORMAT Format;
        union 
        {
            FLOAT Color[ 4 ];
          D3D12_DEPTH_STENCIL_VALUE DepthStencil;
        };
      }    D3D12_CLEAR_VALUE;

    riidResource,我们想要获取指针的ID3D12Resource接口的COM ID。
    ppvResource,返回指向ID3D12Resource的指针,该ID3D12Resource表示新创建的资源。

    此外,在使用深度/模板缓冲区之前,我们必须创建一个关联的深度/模板视图以绑定到管道, 这与创建渲染目标视图类似。 下面的代码示例显示了我们如何创建深度/模板纹理及其相应的深度/模板视图:

    D3D12_RESOURCE_DESC depthStencilDesc;
    depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
    depthStencilDesc.Alignment = 0;
    depthStencilDesc.Width = mClientWidth;
    depthStencilDesc.Height = mClientHeight;
    depthStencilDesc.DepthOrArraySize = 1;
    depthStencilDesc.MipLevels = 1;
    depthStencilDesc.Format = mDepthStencilFormat;
    depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
    depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
    depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
    depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
    
    D3D12_CLEAR_VALUE optClear;
    optClear.Format = mDepthStencilFormat;
    optClear.DepthStencil.Depth = 1.0f;
    optClear.DepthStencil.Stencil = 0;
    ThrowIfFailed(md3dDevice->CreateCommittedResource(
      &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
      D3D12_HEAP_FLAG_NONE,
      &depthStencilDesc,
      D3D12_RESOURCE_STATE_COMMON,
      &optClear,
      IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));
    
    // Create descriptor to mip level 0 of entire resource using the
    // format of the resource.
    md3dDevice->CreateDepthStencilView(
      mDepthStencilBuffer.Get(),
      nullptr, 
      DepthStencilView());
    
    // Transition the resource from its initial state to be used as a depth buffer.
    mCommandList->ResourceBarrier(
      1, 
      &CD3DX12_RESOURCE_BARRIER::Transition(
        mDepthStencilBuffer.Get(),
        D3D12_RESOURCE_STATE_COMMON,
         D3D12_RESOURCE_STATE_DEPTH_WRITE));

    请注意,我们使用CD3DX12_HEAP_PROPERTIES辅助构造函数来创建堆属性结构,其实现方式如下:

    explicit CD3DX12_HEAP_PROPERTIES( 
        D3D12_HEAP_TYPE type, 
        UINT creationNodeMask = 1, 
        UINT nodeMask = 1 )
    {
      Type = type;
      CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
      MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
      CreationNodeMask = creationNodeMask;
      VisibleNodeMask = nodeMask;
    }

    CreateDepthStencilView的第二个参数是指向D3D12_DEPTH_STENCIL_VIEW_DESC的指针,除此之外,该结构描述了资源中元素的数据类型(格式), 如果资源是使用类型化格式创建的,则此参数可以为null,这表示创建此资源的第一个mipmap级别的视图(深度/模板缓冲区仅使用一个mipmap级别创建 )使用创建资源的格式, 因为我们指定了深度/模板缓冲区的类型,所以我们为此参数指定了null。

    • 设置视口
      通常我们喜欢将3D场景绘制到整个后台缓冲区,其中后台缓冲区大小对应于整个屏幕(全屏模式)或窗口的整个客户区域。 但是,有时我们只想将3D场景绘制到后缓冲区的子矩形中;
      这里写图片描述
      将3D场景绘制到后缓冲区的子矩形中, 然后将后缓冲区呈现给窗口的客户区域。我们绘制的后缓冲区的子矩形称为视口,它由以下结构描述:
    typedef struct D3D12_VIEWPORT {
      FLOAT TopLeftX;
      FLOAT TopLeftY;
      FLOAT Width;
      FLOAT Height;
      FLOAT MinDepth;
      FLOAT MaxDepth;
    } D3D12_VIEWPORT;

    前四个数据成员定义相对于后缓冲区的视口矩形(指定小数像素坐标,因为数据成员的类型为float)。 在Direct3D中,深度值以0到1的标准化范围存储在深度缓冲区中,MinDepth和MaxDepth成员用于将深度间隔[0,1]转换为深度间隔[MinDepth,MaxDepth]。 能够变换深度范围可以用于实现某些效果; 例如,您可以设置MinDepth = 0和MaxDepth = 0,以便使用此视口绘制的所有对象的深度值均为0,并显示在场景中所有其他对象的前面。 但是,通常将MinDepth设置为0并将MaxDepth设置为1,以便不修改深度值。这个通过Unity的相机视口就可以体验一下。

    一旦我们填写了D3D12_VIEWPORT结构,我们使用ID3D12CommandList :: RSSetViewports方法设置带有Direct3D的视口。 以下示例创建并设置一个绘制到整个后台缓冲区的视口:

    D3D12_VIEWPORT vp;
    vp.TopLeftX = 0.0f;
    vp.TopLeftY = 0.0f;
    vp.Width  = static_cast<float>(mClientWidth);
    vp.Height  = static_cast<float>(mClientHeight);
    vp.MinDepth = 0.0f;
    vp.MaxDepth = 1.0f;
    
    mCommandList->RSSetViewports(1, &vp);

    最后再给读者介绍一个技术点,我们可以定义相对于后缓冲区的裁剪矩形,使得该矩形外的像素被剔除(即,不被光栅化到后缓冲区)。 这可以用于优化。 例如,如果我们知道屏幕的某个区域将包含一个矩形UI元素,我们就不需要处理UI元素将模糊的3D世界的像素。
    裁剪矩形由D3D12_RECT结构定义,该结构的类型定义为以下结构:

    typedef struct tagRECT
    {
      LONG  left;
      LONG  top;
      LONG  right;
      LONG  bottom;
    } RECT;

    我们使用ID3D12CommandList :: RSSetScissorRects方法设置带有Direct3D的裁剪矩形,以下示例创建并设置一个覆盖后缓冲区左上象限的裁剪矩形:

    mScissorRect = { 0, 0, mClientWidth/2, mClientHeight/2 };
    mCommandList->RSSetScissorRects(1, &mScissorRect);

    与RSSetViewports类似,第一个参数是要绑定的裁剪矩形的数量(使用多个用于高级效果),第二个参数是指向矩形数组的指针。

    关于D3D12初始化工作就已经完成了,下篇博客我们基于这个流程搭建一个小小的框架用于实现我们的D3D12初始化。。。。。。

    展开全文
  • D3D12_简单 Direct3D 12 简单示例
  • 描述 该存储库包含技术手册 7 中分发的“DirectX 12 编程第 2 卷”一书的示例程序。 问题等 如果您有任何错误或问题,请通过此存储库中的问题与我们联系。我想尽可能地支持。 关于模型和动画数据 ...
  • 文章目录 1、 前言 2、D2D、DWrite简介 3、添加D2D、DWrite基础支持文件 4、D2D、DWrite基本编程步骤 5、基于D3D11On12设备创建D2D渲染目标 6、创建DWrite字体用D2D显示文字 7、D2D、D3D11on12与D3D12同步 1、 前言 ...

    1、 前言

      在经过了前面一系列章节的“狂轰滥炸”式的学习之后,如果你现在跟进到了这里,那么请为自己点个赞先!

      从本章开始,教程内容都会开始使用Markdown方式进行发布。一方面主要是为了练习Markdown编辑器的使用;另一方面Markdown目前已经成为比较流程的程序文档的编辑方式,我也来跟跟潮流,也是让大家习惯Markdown。还有就是Markdown的公式编辑比较简洁,后续的文章中可能会出现很多公式,这样编辑、保存、发布、显示都会比较方便了,所以权衡所有的利弊问题后,我还是选择使用Markdown来编辑和发布教程内容,请大家理解和支持!

      这一章中主要的任务就是把不愠不火的D2D和DWrite捡起来,并且和D3D12深度结合起来,为示例程序添加基本的文字支持。

      说到文字支持其实这是一个游戏引擎或者其它3D应用程序必须具备的基本功能之一。当然具体的实现方式有很多种,基本的文字渲染有两大个方向,一种是基于光栅化字体的,也就是把字体提前做出图片,然后按照像素位置索引每个字按照UI显示的一般方式进行字体显示;另一种就是使用矢量字体,进行适当的变换后,按照显示一般曲线的方法进行显示;这两种方法各有优缺点,一般的引擎里都会选择最合适的方式进行字体渲染。

      而在我们的教程中为了做到显示文字内容的目的,还是选择使用D2D+DWrite的方式。这种方式主要跟传统的Windows下的HDC显示和渲染文字的方式方法在编程接口(API)上比较类似,也是我比较熟悉的领域,同时这种方式可以方便的跟D3D12或D3D11无缝融合,在编程方式上也比较类似,学习的成本相对也不是太高,最终这种方式在显示文字信息时因为DWrite的封装,做一般的效果也足够了。所以基于这些考虑最终我还是决定使用D2D和DWrite来显示文字信息。当然这样做的也有弊端,那就是首先这种方式没法跨平台,当然因为本系列教程就是为了说清楚D3D12的,本就是Win平台下的,这样选择也没什么。

      另外对于DWrite和D2D的性能,说法不一,有些说性能差强人意,有些又说性能还行,众说纷纭,我也没时间去细究到底性能怎么样,如果各位有兴趣可以自己想办法测试下看看,毕竟实践出真知!最后呢,因为这一章为了消除大家对其性能的顾虑,所以,我依然使用了多线程+多显卡的渲染框架方式,并且果断的将D2D+DWrite显示文字信息的功能放到了辅助显卡上,这样它就不占用主显卡的资源了,也算是给大家提供一种思路。而且最后实际的例子运行中,我发现显示简单的文字信息也不会占用辅助显卡的性能,因此我想如果大家都掌握了多显卡渲染的基本编程框架,那么完全可以用DWrite+D2D在辅助显卡上显示文本信息,从而解决显示文字信息的问题,至少浪费点辅助显卡的性能还不至于影响3D渲染的大局。

      至于D2D和DWrite究竟是什么,后面我会详细介绍,这里只是说清楚目标和使用它们的根本原因。

      本章示例代码运行后的效果如下:

      全部的例子代码可以到GitHub上下载查看:
    12-D2DWriteOnD3D12

    2、D2D、DWrite简介

      D2D顾名思义,就是Direct2D的缩写,因此他就是DirectX中的2D图形显示API接口,与D3D相对应,D2D和D3D合起来就组成了Win平台下完整的Graphic组件。基本就可以支持所有有性能要求的图形应用程序的图形功能,比如3D游戏等。从API角度讲,其实D2D与传统的Win GDI(DC)API很类似,但在性能上因为直接利用了图形硬件的能力,所以要远比Win GDI高的多的多。这样使用D2D,即使的我们过去学习的Win GDI API的相关知识得以保留,同时又得到了性能方面的显著提升,综合下来是一种在Win平台下处理2D图形显示的不错选择。当然从功能上来讲,D2D也比Win GDI API要扩展和高级了很多。

      同理DWrite就是DirectWrite的缩写,Write直译就是写的意思,顾名思义就是直接显示文本的组件名称。它也是Win GDI API中文字部分的功能升级与扩展。

      以上两个组件都采用了基本COM接口的封装方式,都作为了DirectX中的一部分。因此在调用方式上与DirectX中的其它组件比较类似,同时得益于DXGI功能接口的独立性,因此它们可以天然的与D3D接口进行互操作,从而实现在3D场景渲染结果的基础上进行复杂高效的2D图形或文字的渲染。本章我们就使用DWrite来显示文字。

    3、添加D2D、DWrite基础支持文件

      要使用D2D和DWrite组件,即使它们是采用了COM形式封装的接口,但也是根据一般的C++中引用组件的一般方式来添加引用的。所以首先我们就要像下面这样添加头文件和Lib库文件的引用:

    //--------------------------------------------------------------
    #include <d3d11_4.h>
    #include <d3d11on12.h>
    #include <d2d1_3.h>
    #include <dwrite.h>
    //--------------------------------------------------------------
    //......
    //--------------------------------------------------------------
    #pragma comment(lib, "d2d1.lib")
    #pragma comment(lib, "dwrite.lib")
    #pragma comment(lib, "d3d11.lib")
    //--------------------------------------------------------------
    

      上面的代码稀松平常,唯一可能要引起我们注意的就是我们还包含了D3D11的组件,这是为什么呢?具体接下来就开始具体介绍。

    4、D2D、DWrite基本编程步骤

      刚才已经提到过D2D和DWrite在编程概念上与Win GDI API很类似,其实更确切的说它更像是一个在概念上混合了D3D与Win GDI API的产物,所以它的编程概念首先就是类似D3D的渲染目标、然后就是类似GDI的字体、画笔、画刷、位图、线、线条、矩形、圆、扇形、圆弧等等。当然更高级的在D2D和DWrite中也可以使用Shader来渲染2D的画面或字体,当然目前例子教程中还用不到这个高级的功能。

      如果你对Win GDI API以及D3D编程的基本概念都很熟悉的话,D2D和DWrite的学习曲线就是很平坦的。或者说在概念上就没有什么复杂的东西,瞬间就可以理解了。最后需要强调的就是,这些编程的基本概念其实也是大多数2D图形编程包中的一般概念,或者用一种更易理解的方式来说,比如你要实现一套基于D3D UI渲染技术的2D图形模块出来,那么这些概念以及对应的基本API等,就是你需要参考实现的最基本的一组API及对应功能。

      基于这样的认知,各位就不难理解为什么在教程中要特别讲解一下如何将D2D和DWrite整合到D3D12中来,本章的重点在于怎么把它们的功能整合到D3D12中来,优先解决的是能不能显示文字的问题。相对高级的功能就先不去浪费时间了,一方面这些东西概念上并不复杂,基本的使用会了的话,剩下的就是查看MSDN的手册了;另一方面D2D、DWrite已经有很多大牛编写了很多优秀的教程,这里就不再去班门弄斧了,各位需要深入了解的就在此基础上搜素进行深度学习即可。

      有了上面的概念,再来看看具体怎么样使用D2D和DWrite。从编程模型上来说D2D和DWrite的关系很紧密,或者可以直接认为DWrite就是D2D的一个子功能接口,如果不显示文字信息的话可以单独使用D2D,而如果要显示文字信息,那就要使用DWrite组件。使用DWrite就必须要用D2D来作为支撑。

      从基本编程的过程上来说,使用D2D和DWrite的大致流程如下:

    1、创建D2D和DWrite工厂(与创建DXGI的工厂接口类似,就是个原初接口的创建,没什么特殊的,就不过多详细描述了,大家可以直接看代码);

    2、创建D2D设备对象及接口(D2D的设备接口,在形式上与D3D11的类似,都有一个Device接口以及一个Device Context接口,后者则主要是D2D的各种绘制功能函数的集合接口);

    2、创建D2D渲染目标;

    3、创建DWrite的字体、D2D的画刷、画笔、设置背景色等对象;

    4、利用这些对象,使用D2D各种Draw函数绘制需要的2D几何体或者显示文字信息;

    5、使用完毕、销毁对象、清理资源;

      以上过程是一个基本的过程,其实跟Win GDI的API的调用过程也是大同小异的,只是使用了D2D渲染目标的概念代替了GDI中的Device Context(简称DC)概念而已,其它也就是使用COM接口函数,代替了GDI中的分散的各种Brush、Pen、Font等等相关API。所以从这点上来说,如果之前深入学习过GDI编程的相关知识的话,现在这些知识就在D2D和DWrite中得到了保值增值(传说中,目前流行的一些非IE浏览器内部的底层2D图形支持之类的也都移植到基于D2D和DWrite上来)。这就好像如果你懂的虚拟机,那么现在再把存储和网络等等能虚拟化的设备统统都给你虚拟化了,就成了云计算。

    5、基于D3D11On12设备创建D2D渲染目标

      根据前面的描述,D2D和DWrite的基本使用方法也不是很复杂,那么究竟怎样将它们和D3D12混合在一起使用呢?其实全部的秘密就在如何创建基于D3D12渲染目标的D2D渲染目标上,或者以更容易理解的方式来说,就是让D2D和D3D都绘制到同一张屏幕后缓冲中(或者同一张离屏纹理/表面上)。

      当然这说起来容易做起来稍微有点复杂,因为最早D2D、DWrite能跟D3D混用是D3D11推出不久之后,彼时微软的想法就是用一套比较新的2D图形接口来取代年代久远的DirectDraw并且在一些性能和效果要求较高的应用场景中取代传统的Win GDI 的API,因此D2D和DWrite应运而生,并且通过从D3D中拆出来的DXGI接口,实现了与D3D11的无缝集成,从而整体上形成了Win平台中的Graphic组件,用以通吃2D、3D渲染。

      后来到了D3D12的年代,D2D和DWrite貌似也就没有跟着一起升级了,看上去像两兄弟分家了,而D3D12依然是比较活跃的领域,D2D和DWrite则相对消沉一些。因此现在在D3D12中要使用D2D和DWrite的话,就需要请D3D11来牵线搭桥了,具体的方法就是使用D3D12的设备创建一个D3D11on12的向下兼容接口来过渡。详细的方法过程如下图所示:

      图中左边的调用链路就是参与实现D3D12和D2D渲染到同一张纹理的各种组件的核心设备对象以及接口,主要有D3D12设备对象、D3D11设备对象、及D2D设备对象以及它们各自被用到的接口;右边的链路虽然别分开表示为貌似不同的几种资源对象,其实它们最终都表示的是同一张纹理,或者更直接的理解为它们本身就是同一块显存,这块显存先从它是D3D12 Resource的样子开始被一路”整容“最终变成了D2D Render Target的样子(如果你了解过韩国整形术的话,这里实质上是进行着差不多相同概念的过程!)。这个过程中关键的函数都已经标注在图上了,一目了然。理解了这个过程之后,大家就可以直接去阅读本章代码中相关部分的内容了。代码中也已经标注的很清楚了,就不在这里过多浪费篇幅粘贴代码了。只是需要提醒大家的是,代码中我都尽量使用了较高版本的相关接口,这样做是方便大家可以在此基础上进一步掌握这些高版本中扩展出来的相应组件的一些高级功能。

    6、创建DWrite字体用D2D显示文字

      根据前面的描述,如果解决了D2D与D3D12渲染到同一个纹理的问题,剩下的其实就是各自去按照渲染逻辑组织代码了。它们之间也就没有过多的交互了,无非就是控制下绘制的顺序,一般都是3D场景整个渲染完了之后,再去调用D2D显示2D的图形或文本信息。

      具体的例子中主要是为了显示一段文本信息,说明下当前渲染状态中各种后处理的开关状态。根据D2D绘制文字的一般框架,那么首先就需要创建字体及格式信息对象,因为DWrite支持很多字体及相应格式,所以就必须明确程序要使用的字符集、字体、字号、风格(粗体?斜体?下划线?)等信息,而这些在DWrite中就使用一个IDWriteTextFormat接口来表示。具体代码如下:

    GRS_THROW_IF_FAILED(pIDWriteFactory->CreateTextFormat(
    	L"微软楷体",
    	NULL,
    	DWRITE_FONT_WEIGHT_NORMAL,
    	DWRITE_FONT_STYLE_NORMAL,
    	DWRITE_FONT_STRETCH_NORMAL,
    	20,
    	L"zh-cn", //中文字库
    	&pIDWriteTextFormat
    ));
    // 水平方向左对齐、垂直方向居中
    GRS_THROW_IF_FAILED(pIDWriteTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_JUSTIFIED));
    GRS_THROW_IF_FAILED(pIDWriteTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER));
    

      从代码后面两行还可以看出,我们还可以设置字体水平与垂直对齐方式,这其实是相当于稍后需要将文字显示其中的矩形框而言的。由此也可以看出用DWrite显示文字信息其本身的功能已经是很强悍了。这里要提醒大家注意的就是其中的字库参数,现在搜到的官方示例中都是英文字库,所以要显示中文,就要像这里这样使用中文字库和字体名。否则中文信息显示可能会混乱。

      为了支持显示文字信息,那么还需要创建一个D2D的画刷对象接口。这主要是因为在D2D及DWrite中支持的是矢量型字体字库,所以理论上可以实现任意大小、任意字色、任意填充方式甚至阴影效果等字体(这些其实Win GDI中也可以做到)。而在D2D中最终这些都没有提供默认值,所以就需要我们额外的创建一些对象或提供一些参数值,来完成文本信息的显示。当然这样虽然在文本信息的显示上做到了几乎最大的灵活性,但是在编码方面就给我们带来了一定的复杂性的挑战。最终如果你是一个用惯了很多“简洁即王道”软件包的程序员,很容易对这种方式产生厌恶。不过幸运的是,我们是用C++的开发人员,那么这些就自己稍微封装一下即可。当然具体怎么封装就是各位的事情了,本系列教程要做的就是把最原始最直接的编程方法、概念、模式、模型、原理等等分享给各位,而不会讲怎么封装!

      OK,多的就不啰嗦了。为了成功显示文本,那么接着我们就需要创建一个D2D的画刷对象接口,用于填充最终显示的文字,代码如下:

    // 创建一个画刷,字体输出时即使用该颜色画刷
    GRS_THROW_IF_FAILED(pID2D1DeviceContext6->CreateSolidColorBrush(
    	D2D1::ColorF(D2D1::ColorF::Gold)
    	, &pID2D1SolidColorBrush));
    

      上述基本的辅助对象接口创建完毕后,终于可以显示文本了。在3D渲染包括后处理渲染全部结束后,就可以像下面代码这样准备并显示文本了:

    if ( 1 == g_nFunNO )
    {
    	StringCchPrintfW(pszUIString, MAX_PATH, _T("水彩画效果渲染:开启;"));
    }
    else
    {
    	StringCchPrintfW(pszUIString, MAX_PATH, _T("水彩画效果渲染:关闭;"));
    }
    
    if (1 == g_nUsePSID)
    {
    	StringCchPrintfW(pszUIString, MAX_PATH, _T("%s高斯模糊后处理渲染:开启。"),pszUIString);
    }
    else
    {
    	StringCchPrintfW(pszUIString, MAX_PATH, _T("%s高斯模糊后处理渲染:关闭。"), pszUIString);
    }
    
    pID3D11On12Device1->AcquireWrappedResources(pID3D11WrappedBackBuffers[nCurrentFrameIndex].GetAddressOf(), 1);
    
    pID2D1DeviceContext6->SetTarget(pID2DRenderTargets[nCurrentFrameIndex].Get());
    pID2D1DeviceContext6->BeginDraw();
    pID2D1DeviceContext6->SetTransform(D2D1::Matrix3x2F::Identity());
    pID2D1DeviceContext6->DrawTextW(
    	pszUIString,
    	_countof(pszUIString) - 1,
    	pIDWriteTextFormat.Get(),
    	&stD2DTextRect,
    	pID2D1SolidColorBrush.Get()
    );
    GRS_THROW_IF_FAILED(pID2D1DeviceContext6->EndDraw());
    
    pID3D11On12Device1->ReleaseWrappedResources(pID3D11WrappedBackBuffers[nCurrentFrameIndex].GetAddressOf(), 1);
    
    pID3D11DeviceContext4->Flush();
    

      上述代码中核心的显示文本信息的调用就是DrawTextW,其名称中W表示显示的UNICODE字符,也就是宽字节字符集。这对显示中文信息是最大的福音!

    7、D2D、D3D11on12与D3D12同步

      经过前面的步骤,最终文本信息也成功的显示了。到这里,我想细心的各位一定想到了一个问题,那就是根据我们前面各种教程中反复甚至啰嗦的强调一个概念——D3D12天生就是异步执行的情况下,那么之前代码中又是调用D3D11on12、接着又是调用D2D的情况下,它们究竟是怎么同步的?或者直白的说,D2D最终怎么知道D3D12已经画完了,它可以接着画了呢?

      其实全部的秘密就在前面代码的几个API中,首先AcquireWrappedResources就相当于在D3D12执行完毕之后,在D3D11on12设备开始工作之前,在渲染目标上设置了一个资源屏障,而接着的D2D的相关调用就被D3D11on12理解为像在D3D12中一样录制命令列表而已,最终ReleaseWrappedResources就相当于又放置了一个将渲染状态的纹理切换为可以提交状态的资源屏障。整体上所有这些调用都是在录制命令列表!

      最后D3D11设备对象的Flush()函数就相当于D3D12中的ExecuteCommandLists函数,将所有渲染的命令,包括D2D显示文本的命令提交到显卡去执行。

      这样在编程模式和运行模式上D3D12、中间过渡的D3D11、以及D2D就得到了高度的统一,只要理解了D3D12的异步运行模式,那么理解D2D的异步模式就没什么问题了。

      最终其实基于这样对几个组件间同步控制的理解,那么其实显示文本的D2D过程(含D3D11部分)就可以插入在任意的D3D12渲染过程中的位置,只是需要搞清楚Flush之后渲染目标其实变成了可提交状态,这时如果需要继续D3D12渲染,那么就需要额外插入一个将状态变成可渲染状态的资源屏障!

    展开全文
  • D3D12渲染技术之常量缓冲区

    千次阅读 2018-09-16 13:45:56
    第一个参数是标识要映射的子资源的子资源索引, 对于缓冲区,唯一的子资源是缓冲区本身,因此我们将其设置为0,第二个参数是指向D3D12_RANGE结构的可选指针,该结构描述要映射的内存范围;指定null映射整个资源。 第...

    常量缓冲区是GPU资源(ID3D12Resource),其数据内容可以在着色器程序中引用,正如我们将在博客中学到的,纹理和其他类型的缓冲区资源也可以在着色器程序中引用。顶点着色器具有以下代码:

    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorldViewProj; 
    };

    此代码引用名为cbPerObject的cbuffer对象(常量缓冲区),在此案例中,常量缓冲区存储一个名为gWorldViewProj的4×4矩阵,表示用于将点从局部空间转换为齐次裁剪空间的组合世界:视图和投影矩阵。 在HLSL中,内置的float4x4类型声明了一个4×4矩阵; 例如,要声明一个3×4矩阵和2×4矩阵,我们将分别使用float3x4和float2x2类型。
    与顶点和索引缓冲区不同,常量缓冲区通常由CPU每帧更新一次, 例如,如果摄像机每帧移动,则需要使用每帧的新视图矩阵更新常量缓冲区。 因此,我们在上传堆而不是默认堆中创建常量缓冲区,以便我们可以从CPU更新内容。
    常量缓冲区还具有特殊的硬件要求,即它们的大小必须是最小硬件分配大小(256字节)的倍数。
    通常我们需要多个相同类型的常量缓冲区, 例如,上面的常量缓冲区cbPerObject存储每个对象不同的常量,因此如果我们有n个对象,那么我们将需要n个这种类型的常量缓冲区。 以下代码显示了我们如何创建一个缓冲区来存储NumElements许多常量缓冲区:

    struct ObjectConstants
    {
      DirectX::XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
    };
    UINT elementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
    
    ComPtr<ID3D12Resource> mUploadCBuffer;
    device->CreateCommittedResource(
      &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
      D3D12_HEAP_FLAG_NONE,
      &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize * NumElements),
      D3D12_RESOURCE_STATE_GENERIC_READ,
      nullptr,
      IID_PPV_ARGS(&mUploadCBuffer));

    我们可以将mUploadCBuffer视为存储ObjectConstants类型的常量缓冲区数组(使用填充来生成256字节的倍数)。 当绘制对象时,我们只需将常量缓冲区视图(CBV)绑定到缓冲区的子区域,该区域存储该对象的常量。 请注意,我们经常将缓冲区mUploadCBuffer称为常量缓冲区,因为它存储了一个常量缓冲区数组。
    程序函数d3dUtil :: CalcConstantBufferByteSize执行算术以将缓冲区的字节大小舍入为最小硬件分配大小的倍数(256字节):

    UINT d3dUtil::CalcConstantBufferByteSize(UINT byteSize)
    {
      // Constant buffers must be a multiple of the minimum hardware
      // allocation size (usually 256 bytes). So round up to nearest
      // multiple of 256. We do this by adding 255 and then masking off
      // the lower 2 bytes which store all bits < 256.
      // Example: Suppose byteSize = 300.
      // (300 + 255) & ˜255
      // 555 & ˜255
      // 0x022B & ˜0x00ff
      // 0x022B & 0xff00
      // 0x0200
      // 512
      return (byteSize + 255) & ˜255;
    }

    注意,即使我们以256的倍数分配常量数据,也没有必要在HLSL结构中显式填充相应的常量数据,因为它是隐式完成的:

    // Implicitly padded to 256 bytes.
    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorldViewProj; 
    };
    
    // Explicitly padded to 256 bytes.
    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorldViewProj; 
      float4x4 Pad0;
      float4x4 Pad1;
      float4x4 Pad1;
    };

    为了避免将常量缓冲区元素舍入为256字节的倍数,可以显式填充所有常量缓冲区结构,使其始终为256字节的倍数。
    Direct3D12引入了着色器模型5.1。 Shader模型5.1引入了另一种HLSL语法,用于定义如下所示的常量缓冲区:

    struct ObjectConstants
    {
      float4x4 gWorldViewProj;
      uint matIndex; 
    };
    ConstantBuffer<ObjectConstants> gObjConstants : register(b0);

    这里,常量缓冲区的数据元素只是在一个单独的结构中定义,然后从该结构创建一个常量缓冲区。 然后使用数据成员语法在着色器中访问常量缓冲区的字段:

    uint index = gObjConstants.matIndex;

    因为使用堆类型D3D12_HEAP_TYPE_UPLOAD创建了常量缓冲区,所以我们可以将数据从CPU上传到常量缓冲区资源。 要做到这一点,我们首先必须获得一个指向资源数据的指针,这可以使用Map方法完成:

    ComPtr<ID3D12Resource> mUploadBuffer;
    BYTE* mMappedData = nullptr;
    mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));

    第一个参数是标识要映射的子资源的子资源索引, 对于缓冲区,唯一的子资源是缓冲区本身,因此我们将其设置为0,第二个参数是指向D3D12_RANGE结构的可选指针,该结构描述要映射的内存范围;指定null映射整个资源。 第二个参数返回指向映射数据的指针,要将数据从系统内存复制到常量缓冲区,我们可以执行memcpy:

    memcpy(mMappedData, &data, dataSizeInBytes);

    当我们完成一个常量缓冲区时,我们应该在释放内存之前取消映射它:

    if(mUploadBuffer != nullptr)
      mUploadBuffer->Unmap(0, nullptr);
    
    mMappedData = nullptr;

    Unmap的第一个参数是一个子资源索引,用于标识要映射的子资源,缓冲区为0。 Unmap的第二个参数是指向D3D12_RANGE结构的可选指针,该结构描述了要取消映射的内存范围; 指定null取消映射整个资源。

    我们在UploadBuffer.h中定义了以下类,以便更轻松地使用上传缓冲区, 它为我们处理上传缓冲区资源的构造和销毁,处理映射和取消映射资源,并提供CopyData方法来更新缓冲区中的特定元素。 当我们需要从CPU更改上传缓冲区的内容时(例如,当视图矩阵改变时),我们使用CopyData方法。 请注意,此类可用于任何上传缓冲区,不一定是常量缓冲区。 但是,如果我们将它用于常量缓冲区,我们需要通过isConstantBuffer构造函数参数来指示。 如果它存储一个常量缓冲区,那么它将自动填充内存,使每个常量缓冲区成为256字节的倍数。

    template<typename T>
    class UploadBuffer
    {
    public:
      UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) : 
        mIsConstantBuffer(isConstantBuffer)
      {
        mElementByteSize = sizeof(T);
    
        // Constant buffer elements need to be multiples of 256 bytes.
        // This is because the hardware can only view constant data 
        // at m*256 byte offsets and of n*256 byte lengths. 
        // typedef struct D3D12_CONSTANT_BUFFER_VIEW_DESC {
        // UINT64 OffsetInBytes; // multiple of 256
        // UINT  SizeInBytes;  // multiple of 256
        // } D3D12_CONSTANT_BUFFER_VIEW_DESC;
        if(isConstantBuffer)
        mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));
    
        ThrowIfFailed(device->CreateCommittedResource(
          &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
          D3D12_HEAP_FLAG_NONE,
          &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*elementCount),
             D3D12_RESOURCE_STATE_GENERIC_READ,
          nullptr,
          IID_PPV_ARGS(&mUploadBuffer)));
    
        ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)));
    
        // We do not need to unmap until we are done with the resource.
        // However, we must not write to the resource while it is in use by
        // the GPU (so we must use synchronization techniques).
      }
       UploadBuffer(const UploadBuffer& rhs) = delete;
      UploadBuffer& operator=(const UploadBuffer& rhs) = delete;
      ˜UploadBuffer()
      {
        if(mUploadBuffer != nullptr)
          mUploadBuffer->Unmap(0, nullptr);
    
        mMappedData = nullptr;
      }
    
      ID3D12Resource* Resource()const
      {
        return mUploadBuffer.Get();
      }
    
      void CopyData(int elementIndex, const T& data)
      {
        memcpy(&mMappedData[elementIndex*mElementByteSize], &data, sizeof(T));
      }
    
    private:
      Microsoft::WRL::ComPtr<ID3D12Resource> mUploadBuffer;
      BYTE* mMappedData = nullptr;
    
      UINT mElementByteSize = 0;
      bool mIsConstantBuffer = false;
    };

    通常,对象的世界矩阵在移动/旋转/缩放时将改变,当摄像机移动/旋转时视图矩阵改变,并且当窗口调整大小时投影矩阵改变。 在本篇的案例中,我们允许用户使用鼠标旋转和移动摄像机,并且我们在Update功能的每一帧中使用新视图矩阵更新组合的世界视图投影矩阵:

    void BoxApp::OnMouseMove(WPARAM btnState, int x, int y)
    {
      if((btnState & MK_LBUTTON) != 0)
      {
        // Make each pixel correspond to a quarter of a degree.
        float dx = XMConvertToRadians(0.25f*static_cast<float> (x - mLastMousePos.x));
        float dy = XMConvertToRadians(0.25f*static_cast<float> (y - mLastMousePos.y));
    
        // Update angles based on input to orbit camera around box.
        mTheta += dx;
        mPhi += dy;
         // Restrict the angle mPhi.
        mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi - 0.1f);
      }
      else if((btnState & MK_RBUTTON) != 0)
      {
        // Make each pixel correspond to 0.005 unit in the scene.
        float dx = 0.005f*static_cast<float>(x - mLastMousePos.x);
        float dy = 0.005f*static_cast<float>(y - mLastMousePos.y);
    
        // Update the camera radius based on input.
        mRadius += dx - dy;
    
        // Restrict the radius.
        mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
      }
    
      mLastMousePos.x = x;
      mLastMousePos.y = y;
      }
    
    void BoxApp::Update(const GameTimer& gt)
    {
      // Convert Spherical to Cartesian coordinates.
      float x = mRadius*sinf(mPhi)*cosf(mTheta);
      float z = mRadius*sinf(mPhi)*sinf(mTheta);
      float y = mRadius*cosf(mPhi);
    
      // Build the view matrix.
      XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
      XMVECTOR target = XMVectorZero();
      XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
    
      XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
      XMStoreFloat4x4(&mView, view);
    
      XMMATRIX world = XMLoadFloat4x4(&mWorld);
      XMMATRIX proj = XMLoadFloat4x4(&mProj);
       XMMATRIX worldViewProj = world*view*proj;
    
      // Update the constant buffer with the latest worldViewProj matrix.
      ObjectConstants objConstants;
      XMStoreFloat4x4(&objConstants.WorldViewProj,  XMMatrixTranspose(worldViewProj));
      mObjectCB->CopyData(0, objConstants);
    }

    回想一下,我们通过描述符对象将资源绑定到渲染管道, 到目前为止,我们已经使用描述符/视图来渲染目标,深度/模板缓冲区以及顶点和索引缓冲区。 我们还需要描述符来将常量缓冲区绑定到管道, 常量缓冲区描述符位于D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV类型的描述符堆中。 这样的堆可以存储常量缓冲区,着色器资源和无序访问描述符的混合。 要存储这些新类型的描述符,我们需要创建一个这种类型的新描述符堆:

    D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
    cbvHeapDesc.NumDescriptors = 1;
    cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
    cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
    cbvHeapDesc.NodeMask = 0;
    
    ComPtr<ID3D12DescriptorHeap> mCbvHeap
    md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
      IID_PPV_ARGS(&mCbvHeap));

    此代码类似于我们创建渲染目标和深度/模板缓冲区描述符堆的方式, 但是,一个重要的区别是我们指定D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE标志以指示着色器程序将访问这些描述符。 在演示中,我们没有SRV或UAV描述符,我们只会绘制一个对象; 因此,我们在这个堆中只需要1个描述符来存储1个CBV。
    通过填写D3D12_CONSTANT_BUFFER_VIEW_DESC实例并调用来创建常量缓冲区视图

    ID3D12Device::CreateConstantBufferView:
    // Constant data per-object.
    struct ObjectConstants
    {
      XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
    };
    // Constant buffer to store the constants of n object.
    std::unique_ptr<UploadBuffer<ObjectConstants>> mObjectCB = nullptr;
    mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(
      md3dDevice.Get(), n, true);
    
    UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
    
    // Address to start of the buffer (0th constant buffer).
    D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
    
    // Offset to the ith object constant buffer in the buffer.
    int boxCBufIndex = i;
    cbAddress += boxCBufIndex*objCBByteSize;
    
    D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
    cbvDesc.BufferLocation = cbAddress;
    cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
    
    md3dDevice->CreateConstantBufferView(
      &cbvDesc,
      mCbvHeap->GetCPUDescriptorHandleForHeapStart());
    // Texture resource bound to texture register slot 0.
    Texture2D  gDiffuseMap : register(t0);
    
    // Sampler resources bound to sampler register slots 0-5.
    SamplerState gsamPointWrap        : register(s0);
    SamplerState gsamPointClamp       : register(s1);
    SamplerState gsamLinearWrap       : register(s2);
    SamplerState gsamLinearClamp      : register(s3);
    SamplerState gsamAnisotropicWrap  : register(s4);
    SamplerState gsamAnisotropicClamp : register(s5);
    
    // cbuffer resource bound to cbuffer register slots 0-2
    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorld;
      float4x4 gTexTransform;
    };
    
    // Constant data that varies per material.
    cbuffer cbPass : register(b1)
    {
      float4x4 gView;
      float4x4 gProj;
      […] // Other fields omitted for brevity.
    };
    
    cbuffer cbMaterial : register(b2)
    {
      float4  gDiffuseAlbedo;
      float3  gFresnelR0;
      float  gRoughness;
      float4x4 gMatTransform;
    };
    
    // Root parameter can be a table, root descriptor or root constants.
    CD3DX12_ROOT_PARAMETER slotRootParameter[1];
    
    // Create a single descriptor table of CBVs.
    CD3DX12_DESCRIPTOR_RANGE cbvTable;
    cbvTable.Init(
      D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 
      1, // Number of descriptors in table
      0);// base shader register arguments are bound to for this root parameter
    
    slotRootParameter[0].InitAsDescriptorTable(
      1,     // Number of ranges
      &cbvTable); // Pointer to array of ranges
    
    // A root signature is an array of root parameters.
    CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, 
      D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
    
    // create a root signature with a single slot which points to a 
    // descriptor range consisting of a single constant buffer.
    ComPtr<ID3DBlob> serializedRootSig = nullptr;
    ComPtr<ID3DBlob> errorBlob = nullptr;
    HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, 
      D3D_ROOT_SIGNATURE_VERSION_1,
      serializedRootSig.GetAddressOf(), 
      errorBlob.GetAddressOf());
    
    ThrowIfFailed(md3dDevice->CreateRootSignature(
      0,
      serializedRootSig->GetBufferPointer(),
      serializedRootSig->GetBufferSize(),
      IID_PPV_ARGS(&mRootSignature)));
    CD3DX12_ROOT_PARAMETER slotRootParameter[1];
    
    CD3DX12_DESCRIPTOR_RANGE cbvTable;
    cbvTable.Init(
      D3D12_DESCRIPTOR_RANGE_TYPE_CBV, // table type
      1, // Number of descriptors in table
      0);// base shader register arguments are bound to for this root parameter
      cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
    
    md3dDevice->CreateConstantBufferView(
      &cbvDesc,
      mCbvHeap->GetCPUDescriptorHandleForHeapStart());

    D3D12_CONSTANT_BUFFER_VIEW_DESC结构描述了要绑定到HLSL常量缓冲区结构的常量缓冲区资源的子集。 如上所述,通常一个常量缓冲区存储n个对象的每个对象常量数组,但是我们可以通过使用BufferLocation和SizeInBytes来获取第i个对象常量数据的视图。 由于硬件要求,D3D12_CONSTANT_BUFFER_VIEW_DESC :: SizeInBytes和D3D12_CONSTANT_BUFFER_VIEW_DESC :: OffsetInBytes成员必须是256字节的倍数。 例如,如果指定了64,那么将收到以下调试错误:
    D3D12 ERROR: ID3D12Device::CreateConstantBufferView: SizeInBytes of 64 is invalid. Device requires SizeInBytes be a multiple of 256.

    D3D12 ERROR: ID3D12Device:: CreateConstantBufferView: OffsetInBytes of 64 is invalid.
    设备要求OffsetInBytes是256的倍数。
    通常,不同的着色器程序期望在执行绘制调用之前将不同的资源绑定到呈现管道, 资源绑定到特定的寄存器槽,可以通过着色器程序访问它们。 例如,前一个顶点和像素着色器只需要一个常量缓冲区绑定到寄存器b0。 我们会在后面使用的一组更高级的顶点和像素着色器需要将几个常量缓冲区,纹理和采样器绑定到各种寄存器槽:

    // Texture resource bound to texture register slot 0.
    Texture2D  gDiffuseMap : register(t0);
    
    // Sampler resources bound to sampler register slots 0-5.
    SamplerState gsamPointWrap        : register(s0);
    SamplerState gsamPointClamp       : register(s1);
    SamplerState gsamLinearWrap       : register(s2);
    SamplerState gsamLinearClamp      : register(s3);
    SamplerState gsamAnisotropicWrap  : register(s4);
    SamplerState gsamAnisotropicClamp : register(s5);
    
    // cbuffer resource bound to cbuffer register slots 0-2
    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorld;
      float4x4 gTexTransform;
    };
    
    // Constant data that varies per material.
    cbuffer cbPass : register(b1)
    {
      float4x4 gView;
      float4x4 gProj;
      […] // Other fields omitted for brevity.
    };
    
    cbuffer cbMaterial : register(b2)
    {
      float4  gDiffuseAlbedo;
      float3  gFresnelR0;
      float  gRoughness;
      float4x4 gMatTransform;
    };

    根签名定义了在执行绘制调用之前应用程序将绑定到呈现管道的资源以及这些资源被映射到着色器输入寄存器的位置。 根签名必须与它将使用的着色器兼容(即,根签名必须提供着色器期望在执行绘制调用之前绑定到渲染管道的所有资源); 这将在创建管道状态对象时验证后面介绍。 不同的绘制调用可以使用不同的着色器程序集,这将需要不同的根签名。
    注意,如果我们将着色器程序视为一个函数,并将着色器期望的输入资源视为函数参数,那么根签名可以被认为是定义函数签名(因此名称为root签名)。 通过将不同的资源绑定为参数,着色器输出将是不同的。 因此,例如,顶点着色器将取决于输入到着色器的实际顶点,以及绑定的资源。
    ID3D12RootSignature接口在Direct3D中表示根签名,它由一组根参数定义,这些参数描述着色器对绘制调用所期望的资源。 根参数可以是根常量,根描述符或描述符表。 我们将在下一篇博客讨论根常量和根描述符; 在本篇中,我们将只使用描述符表, 描述符表指定描述符堆中的连续描述符范围。
    下面的代码创建了一个根签名,它有一个根参数,它是一个足以存储一个CBV(常量缓冲区视图)的描述符表:

    // Root parameter can be a table, root descriptor or root constants.
    CD3DX12_ROOT_PARAMETER slotRootParameter[1];
    
    // Create a single descriptor table of CBVs.
    CD3DX12_DESCRIPTOR_RANGE cbvTable;
    cbvTable.Init(
      D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 
      1, // Number of descriptors in table
      0);// base shader register arguments are bound to for this root parameter
    
    slotRootParameter[0].InitAsDescriptorTable(
      1,     // Number of ranges
      &cbvTable); // Pointer to array of ranges
    
    // A root signature is an array of root parameters.
    CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, 
      D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
    
    // create a root signature with a single slot which points to a 
    // descriptor range consisting of a single constant buffer.
    ComPtr<ID3DBlob> serializedRootSig = nullptr;
    ComPtr<ID3DBlob> errorBlob = nullptr;
    HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, 
      D3D_ROOT_SIGNATURE_VERSION_1,
      serializedRootSig.GetAddressOf(), 
      errorBlob.GetAddressOf());
    
    ThrowIfFailed(md3dDevice->CreateRootSignature(
      0,
      serializedRootSig->GetBufferPointer(),
      serializedRootSig->GetBufferSize(),
      IID_PPV_ARGS(&mRootSignature)));

    我们将描述
    CD3DX12_ROOT_PARAMETER和CD3DX12_DESCRIPTOR_RANGE在下一篇中有更多内容,但现在只需了解代码:

    CD3DX12_ROOT_PARAMETER slotRootParameter[1];
    
    CD3DX12_DESCRIPTOR_RANGE cbvTable;
    cbvTable.Init(
      D3D12_DESCRIPTOR_RANGE_TYPE_CBV, // table type
      1, // Number of descriptors in table
      0);// base shader register arguments are bound to for this root parameter
    
    slotRootParameter[0].InitAsDescriptorTable(
      1,     // Number of ranges
      &cbvTable); // Pointer to array of ranges

    创建一个根参数,该参数期望1 CBV的描述符表被绑定到常量缓冲寄存器0(即HLSL代码中的寄存器(b0))
    我们在本篇博客中的根签名示例非常简单, 我们将在本系列博客中看到许多根签名的例子,并且它们将根据需要增加复杂性。
    根签名仅定义应用程序将绑定到呈现管道的资源; 它实际上没有做任何资源绑定。 使用命令列表设置根签名后,我们使用ID3D12GraphicsCommandList :: SetGraphicsRootDescriptorTable将描述符表绑定到管道:

    void ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable( 
      UINT RootParameterIndex,
      D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor);

    RootParameterIndex:我们正在设置的根参数的索引。
    BaseDescriptor:处理堆中的描述符,该描述符指定正在设置的表中的第一个描述符。 例如,如果根签名指定此表有五个描述符,则堆中的BaseDescriptor和接下来的四个描述符将被设置为此根表。
    以下代码将根签名和CBV堆设置为命令列表,并设置描述符表以标识要绑定到管道的资源:

    mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
    ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
    mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); 
    
    // Offset the CBV we want to use for this draw call.
    CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap ->GetGPUDescriptorHandleForHeapStart());
    cbv.Offset(cbvIndex, mCbvSrvUavDescriptorSize);
    
    mCommandList->SetGraphicsRootDescriptorTable(0, cbv);

    注意,为了提高性能,请使根签名尽可能小,并尝试最小化每个渲染帧更改根签名的次数。
    当内容的任何部分在绘制/调度调用之间发生变化时,应用程序绑定的根签名(描述符表,根常量和根描述符)的内容会自动获得D3D12驱动程序的版本控制。 因此,每个绘制/分派都会获得一组唯一的Root Signature状态。
    如果更改根签名,则会丢失所有现有绑定。 也就是说,您需要将所有资源重新绑定到新根签名所需的管道。

    展开全文
  • D3D12渲染技术之绘制网格

    千次阅读 2018-09-13 13:48:25
    有关D3D12_RESOURCE_DESC结构的所有成员的说明,请查看前面的博客, Direct3D 12提供了一个C ++包装类CD3DX12_RESOURCE_DESC,它源自D3D12_RESOURCE_DESC并提供了方便的构造函数和方法。 特别是,它提供了以下方法...
  • d3d12(Frank D Luna ).rar

    2020-06-04 19:35:49
    Introduction.to.3D.Game.Programming.with DirectX.12
  • D3D12翻译层 D3D12转换层是一个帮助程序库,用于将图形概念和命令从D3D11样式的域转换为D3D12样式的域。 D3D11样式的应用程序通常: 以单线程方式记录图形命令。 将CPU和GPU时间线视为一个时间线。尽管在某些地方...
  • C语言 D3D12 Hook

    2018-06-28 11:47:22
    注意 64位 Hook需要还原代码,因为64位Jmp 是14字节 C语言 D3D12 Hook,
  • utm_source=distribute.pc_relevant.none-task#2%E3%80%81%E8%B0%83%E7%94%A8D3D12%E7%9A%84...
  • D3D12渲染技术之根签名

    千次阅读 2018-09-22 14:27:58
    初始化为描述符表的槽参数采用D3D12_DESCRIPTOR_RANGE实例的数组,因为我们可以在一个表中混合各种类型的描述符, 假设我们按顺序通过以下三个范围定义了六个描述符的表:两个CBV,三个SRV和一个UAV, 这个表的定义...
  • TerraFX.Interop.D3D12MemoryAllocator D3D12MemoryAllocator的互操作绑定。 程序包位于: : 或通过NuGet Feed URL: : 目录 行为守则 TerraFX和所有提供帮助的人员(包括问题,拉取请求,Wiki等)必须遵守 。 可...
  • D3D12龙书入手指南

    2021-09-18 22:54:06
    E://d3d12book 然后直接在这里建立VS2019工程: 选择桌面向导 命名与路径 点创建,弹出的界面选择: 拷贝 添加代码与修改符合模式 添加我们刚刚的BoxApp.cpp与common文件夹的内容。 接着在属性页将符合模式选...
  • D3D12渲染技术概述

    千次阅读 2018-08-22 09:27:03
    从D3D9到D3D12逐步提升,现在很多以前的引擎还是停留在D3D9或者D3D11,D3D12用的比较少,相信不久的将来,引擎使用的D3D API都会更新到D3D12,为了普及D3D API的开发,会做一个系列文章用于介绍D3D12的特色和应用。...
  • D3D12给我总体的感觉用一句话来概括就是——D3D12是一个“显卡操作系统!”。 得益于我对Windows内核编程的深入了解和掌握,突然发现掌握起D3D12多线程渲染时居然可以无障碍学习,看来并不是学过的东西都会过时,这...
  • DirectX12(D3D12)基础教程(六)——多线程渲染

    千次阅读 多人点赞 2019-01-14 21:39:37
    现在看来所谓的困难只怪自己没有好好看D3D12多线程渲染的官方示例代码。其实所谓的问题只是个稍微开下脑洞的问题,后面我会详细介绍思路和方法,防止大家也在这里因为思路问题而卡壳。 OK,闲言碎语不多讲,...
  • D3D12渲染技术之创建和启用纹理

    千次阅读 2018-10-04 09:11:49
    纹理在任何引擎都有使用,它其实就是为模型提供的纹理材质,既然我们介绍的是D3D12,那就从dds文件介绍说起。 加载DDS文件 Microsoft提供了轻量级源代码来加载DDS文件: ...但是,代码仅支持DirectX 11,我们修改...
  • D3D12Renderer-源码

    2021-04-01 10:02:00
    D3D12渲染器 该项目使用Direct3D 12在C ++中从头开始实现一个自定义渲染引擎。它支持一些“新”功能,例如光线跟踪,网格着色器等。 不完整的功能列表(许多工作正在进行中) 正向渲染 基于物理的渲染 动态灯光和...
  • D3D12 来说,概念也是类似的,就是我们利用多个 CPU 线程(部门经理)来带领不同部门的员工( GPU 线程集群,或者直接理解为前面提到的命令队列对象)执行不同的任务,同时 CPU 线程(部门经理)只是开会(录制)...
  • 本文接上篇:DirectX12(D3D12)基础教程(十)——DXR(DirectX Raytracing)基础教程(上) 目录 5、C/C++代码中的其它准备工作 6、DXR编程的基本框架 7、枚举高性能适配器(显卡)和创建DXR设备 7.1、枚举高...
  • D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(CmdListAlloc.GetAddressOf()))); PassCB = std::make_unique<UploadBuffer<PassConstants>>(device, passCount, true); ObjectCB = std::make_unique...
  • D3D12渲染技术之渲染

    2018-09-20 14:39:53
    D3D12_PRIMITIVE_TOPOLOGY PrimitiveType = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST; // DrawIndexedInstanced parameters. UINT IndexCount = 0; UINT StartIndexLocation = 0; int BaseVertexLocation = 0; }...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,057
精华内容 2,422
关键字:

d3d12