Direct3D* 12 概述
作者: Michael Coppock
下载 PDF
摘要
Microsoft Direct3D* 12 是 PC 游戏技术令人兴奋的跨越式发展,可帮助开发人员更好地控制游戏,提高 CPU 的效率和可扩展性。
目录
简介
1.0 – 突破过去
1.1 – 接近完美
2.0 – 管线状态对象
3.0 – 资源绑定
3.1 – 资源 Hazards
3.2 – 资源存放管理
3.3 – 状态镜像
4.0 – 堆和表
4.1 – 冗余资源绑定
4.2 – 描述符
4.3 – 堆
4.4 – 表
4.5 – 无绑定和高效性
4.6 – 渲染环境概述
5.0 – 捆绑包
5.1 – 冗余渲染指令
5.2 – 什么是捆绑包?
5.3 – 代码效率
6.0 – 命令列表
6.1 – 命令创建并行性
6.2 – 列表和队列
6.3 – 命令队列流
7.0 – 动态堆
8.0 – CPU 并行性
9.0 – 总结
参考资料和相关链接
声明和免责条款
简介
在 GDC 2014 上,Microsoft 发布了一条震惊整个 PC 游戏界的消息 — 2015 年将推出下一代 Direct3D,即 Direct3D 第12 版。 D3D 12 回归低级别编程,可支持游戏人员更好地进行控制,并推出许多令人兴奋的新特性。 D3D 12 开发团队主要着力于降低 CPU 开销,提高各 CPU 内核间的可扩展性。 其目标是提高控制台 API 效率和性能,以便控制台游戏能够更高效地使用 CPU/GPU 并获得更出色的结果。 在 PC 游戏领域,线程 0 通常执行大部分,甚至全部的任务。 其他的线程仅处理操作系统或其他系统任务。 真正的多线程 PC 游戏很少。 Microsoft 希望通过 D3D 12 改变这一现状。D3D 12 是 D3D 11 渲染功能的超集, 这表示现代 GPU 能够运行 D3D 12,因为它能够更高效地利用当下的多核 CPU 和 GPU。 用户无需额外花钱购买新的 GPU 即可采用 D3D 12。 基于英特尔® 处理器系统的 PC 游戏拥有光明的未来!
1.0 突破过去
低级别编程在控制台行业非常常见,因为每个控制台的规格是固定的。 游戏开发人员将有时间对游戏进行调整,并尽可能地利用 Xbox One* 或 PlayStation* 4 的全部功能和性能。 PC 是一个天生的灵活平台,包含无数个选件。 在开发新的 PC 游戏时,可以制定许多种计划。 OpenGL* 和 Direct3D* 等高级别 API 可以帮助简化开发。 它们能够极大地减轻负担,以便开发人员将更多地精力放在开发游戏上。 问题是,API 以及(从更小一点地范围来说)驱动程序已经达到非常复杂的地步,以至于影响了帧的渲染,从而降低了性能。 因此,大家将目光投向低级别编程。
基于 PC 的低级别编程随着 MS-DOS* 时代的结束而淡出,特定厂商 API (如 3DFX* 提供的 3Dglide*)让位于 Direct3D 等 API。 PC 在便捷性和灵活性方面的性能不断落后。 硬件市场日趋复杂,能够提供大量选项。 开发时间增加,因为开发人员希望确保购买他们游戏的玩家都能够很好地玩游戏。 不仅软件端的情况发生了变化,而且 CPU 能效也变得比性能更重要。 相比原始频率,CPU 中的多核和多线程以及现代 GPU 的并行渲染才是影响未来性能的主要方面。 我们需要将 PC 游戏迁移至游戏控制台。 我们需要更好、更有效地利用所有内核和线程。 我们需要将 PC 游戏发展为 21 世纪的现代游戏。
1.1 接近完美
为了使游戏“接近完美”,我们必须降低 API 和驱动程序的尺寸和复杂性。 我们应该减少硬件和游戏本身之间的层。 API 和驱动程序花费了太多时间转换命令和调用。 游戏开发人员将重获部分(甚至大部分)的控制。 D3D 12 降低的开销将可提高性能,游戏和 GPU 硬件之间更少的层意味着提供外观和性能更出色的游戏。 另一方面是,某些开发人员可能不想控制 API 处理的领域,如 GPU 内存管理, 也可能这是游戏引擎开发人员想要进入的领域,只有时间能够告诉我们结果。 鉴于 D3D 12 的发布还需要一段时间,我们还有时间去弄清楚这件事情。 既然可能拥有如此光明的前景,那么如何实现呢? 主要通过四种新特性,管线状态对象 (Pipeline State Object)、命令列表、捆绑包和堆。
2.0 管线状态对象
为了更好地讨论管线状态对象 (PSO),我们先来回顾一下 D3D 11 渲染环境,然后再了解一下 D3D 12 中的变化。 图 1 中包含 D3D 11 渲染环境,该环境是 D3D 开发领袖 Max McMullen 于四月份在 BUILD 2014 上展示的内容。
图 1: D3D 11 渲染环境。 [转载获得 Microsoft 许可。]
大号的粗体箭头表示每条管线的状态。 每个状态可根据游戏的需求进行检索或设置。 底部的其他状态是固定函数状态,如视口或裁剪矩形。 本土中的其他相关特性将在本文的后续章节中进行介绍。 对于 PSO 讨论,我们只需要查看图表的左侧。 D3D 11 的小型状态对象相比 D3D 9 的 CPU 开销更低,但是驱动程序采用这些小型状态对象并在渲染时将其融入 GPU 代码还需要其他的操作。 我们将称其为硬件失配开销。 接下来我们通过图 2 中了解一下 BUILD 2014 的其他图表。
图 2: 采用小型对象的 D3D 11 管线会频繁导致产生硬件失配开销。
左侧展示了 D3D 9 风格的管线,应用使用该管线执行任务。 图 2 右侧的硬件需要进行编程。 状态 1 代表着色器代码。 状态 2 是光栅化单元以及将光栅化单元与着色器链接的控制流的结合。 状态 3 是混合着色器和像素着色器之间的链接。 D3D 顶点着色器会影响硬件状态 1 & 2、光栅化程序状态 2、像素着色器状态 1-3 等。 多数驱动程序不希望与应用同时提交调用。 它们更希望进行记录,直至任务完成,以便了解应用真正想要什么。 这表示,其他 CPU 开销(如旧数据和过期数据)将标记为“脏数据”。 驱动程序的控制流将在绘制时检查每个对象的状态,并编写硬件以匹配游戏设置的状态。 另外执行这些任务后,资源将会耗尽并可能会出现问题。 理想状态下,游戏设置管线状态后,驱动程序将会了解游戏的目的并对硬件执行一次编写。 图 3 展示了 D3D 12 管线,该管线在管线状态对象 (PSO) 中执行该操作。
图 3: D3D 12 管线状态优化简化了流程。
图 3 展示了开销降低的简化流程。 包含每个着色器的状态信息的一个 PSO 可以在一个文本中设置所有硬件状态。 请记住,一些状态在 D3D 11 渲染器环境中标记为“其他”。 D3D 12 团队意识到应该减小 PSO 的尺寸,并允许游戏在不影响已编译 PSO 的前提下更改渲染器目标。 视口和裁剪矩形等特性独立出来,与其他的管线在编程上无关联(图 4)。
图 4: 左侧展示了新的 D3D 12 PSO,其包含更大的状态,能够提供更高的效率。
相比以前单独设置和读取每个状态,我们现在采用一个视角,减少或完全消除了硬件失配开销。 应用在需要时设置 PSO,驱动程序采用 API 命令并将其转换为 GPU 代码,同时不增加流控制开销。 这一“接近完美”的方法代表绘制命令将需要更少的周期,从而可提高性能。
3.0 资源绑定
在讨论资源绑定变更之前,我们需要快速回顾 D3D 11 中的资源绑定模型。 图 5 再次展示了渲染环境图形,其中左侧是 D3D 12 PSO,右侧是 D3D 11 资源绑定模型。
图 5: 左侧为 D3D 12 PSO,右侧为 D3D 11 资源绑定模型的渲染器环境。
在图 5 中,Explicit 绑定点位于每个着色器的右侧。 Explicit 模型表示管线中的每个阶段都有其要指称的具体资源。 它们与 GPU 内存中的点参考资源绑定。 这些资源可能是纹理、渲染目标、缓冲、UAV 等。资源绑定很久之前就已经发布,事实上,它在 D3D 发布之前便已发布。 其目标是处理场景是处理场景背后的多种属性,并有效帮助游戏提交渲染命令。 但是,系统需要在三个关键区域运行许多绑定检查。 下一部分将会介绍这些区域,以及 D3D 团队如何针对 D3D 12 对其进行优化。
3.1 资源 Hazard
通常,Hazard 是转移,如从渲染目标迁移至纹理。 游戏可能需要渲染一帧,将其用作场景的环境贴图。 游戏完成环境贴图的渲染,现在想要将其用作纹理。 在这一过程中,运行时和驱动程序将会追踪某一对象何时绑定为渲染目标或纹理。 如果运行时和驱动程序发现有绑定为二者的对象,将会解除绑定以前的设置,并遵循最新的设置。 通过这种方式,游戏能够按照需求进行切换,软件堆栈能够在后台管理切换。 驱动程序也需要刷新 GPU 管线才能够将渲染目标用作纹理。 否则,在 GPU 中被检索到之前,像素便会被读取,您将无法获得一致的状态。 本质上,Hazard 是需要在 GPU 进行额外处理以确保数据一致的对象。
与 D3D 12 中的其他特性和增强一样,解决办法提供更多的游戏控制。 API 和驱动程序为什么要费力追踪一帧中某一点的时间? 从一个资源切换为另一资源大约需 1/60 秒的时间。 通过重新支持游戏进行控制,将降低开销,而且当游戏转换资源时,仅需支付一次成本(图 6)。
D3D12_RESOURCE_BARRIER_DESC Desc;
Desc.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
Desc.Transition.pResource = pRTTexture;
Desc.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
Desc.Transition.StateBefore = D3D12_RESOURCE_USAGE_RENDER_TARGET;
Desc.Transition.StateAfter = D3D12_RESOURCE_USAGE_PIXEL_SHADER_RESOURCE;
pContext->ResourceBarrier( 1, &Desc );
图 6: D3D 12 中添加的资源屏障 API
图 6 中的资源屏障 API 可声明一个资源及其源和目标用途,然后调用函数以告知运行时和驱动程序该转换。 它成为明确的对象,而非需要使用大量条件逻辑跨帧渲染器进行追踪的对象,从而使其成为每帧的单一时间或游戏需要进行转换的频率。
3.2 资源存放管理
D3D 11(及其更低版本)以调用在排队为前提运行。 游戏相信 API 能够立即执行调用。 然而事实并非如此。 GPU 的命令都无法即时调用和执行。 虽然这能够提高 GPU 和 CPU 之间的并行性和效率,但是需要大量的引用计数和追踪。 这些计数和追踪都需要占用 CPU。
为了修复这一点,我们在游戏中添加了对资源生命周期的显性控制。 D3D 12 不再隐藏 GPU 的排队特性。 游戏中添加了围栏 API,以追踪 GPU 进程。 游戏可以在特定点(每帧一次)进行检查,并确认不再需要哪些资源,然后将其内存释放,以作他用。 将不再需要使用额外逻辑追踪帧渲染器的时长,以释放资源和内存。
3.3 状态镜像
对上述三个区域进行优化后,发现了另一个可能会对效率进行提升的元素(尽管性能的提升较小)。 设置绑定点后,运行时将会追踪该点,以便游戏以后调用 Get 以了解绑定至管线的点。 将绑定点镜像或复制。 游戏中添加了一个特性,旨在帮助中间件更轻松地操作,以便组件化软件发现渲染环境的当前状态。 资源绑定进行优化后,将不再需要镜像状态副本。 除了将流控制从上述三个区域中删除之外,还删除了用于状态镜像的 Gets。
4.0 堆和表
还有一个重要的资源变更需要介绍一下。 第四部分末将会展示整个 D3D 12 渲染环境。 全新 D3D 12 渲染环境是提高 API CPU 效率的第一步。
4.1 冗余资源绑定
对几个游戏进行分析后,D3D 开发团队发现,一般情况下,游戏在不同帧中使用相同的命令序列。 不仅是命令,绑定在不同的帧间也是相同的。 CPU 生成一系列绑定,比如 12 个,在一帧上绘制对象。 通常,CPU 需要为下一帧再次生成这 12 个绑定。 为什么不将这些绑定缓存,并为开发人员提供一个指向缓存的命令,以便再次使用相同的绑定?
在 第三部分中,我们介绍了队列。 当发起调用时,游戏认为 API 将立即执行调用。 但是事实并非如此。 命令被放入队列,在队列中所有命令都将延迟,并由 GPU 延后执行。 因此,如果您对我们之前说过的 12 个绑定中的一个绑定做出变更,驱动程序将会把 12 个绑定全部复制到新的位置,对副本进行编辑,然后通知 GPU 开始使用复制的绑定。 通常 12 个绑定中多数仅包含静态值,只有少数几个捆绑包含需要更新的动态值。 当游戏想要对这些绑定进行部分变更,它需要复制全部绑定,这样,较小的变更花费了大量的 CPU 成本。
4.2 描述符
什么描述符? 简言之,它是定义资源参数的数据。 本质上,它是 D3D 11 视图对象背后的数据, 没有操作系统生命周期管理。 它只是 GPU 内存中的不透明数据。 它包含类型和格式信息、mip 纹理计数以及一个指向像素数据的指针。 描述符是新资源绑定模型的核心。
图 7: D3D 12 描述符,定义资源参数的小型数据。
4.3 堆
当在 D3D 11 中设置视图时,它将会把描述符复制到 GPU 内存中将读取描述符的位置。 如果您在同一个位置设置新视图,D3D 11 将会把描述符复制到新的内存位置,并告知 GPU 在下一个绘制命令中从新位置读取。 当对描述符执行创建、复制等操作时,D3D 12 将提供对游戏或应用的明确控制。
图 8
堆(图 8)只是大型的描述符阵列。 您可以重新使用以前的绘制或帧中的描述符。 您还可以根据需要传输新的描述符。 布局归游戏所有,因此操作堆无需太多的开销。 堆尺寸取决于 GPU 架构。 较老的低功耗 GPU 的尺寸限制在 65k,而高端 GPU 的尺寸受限于内存。 较低功耗的 GPU 有可能超过堆。 因此,D3D 12 允许使用多个堆,从一个描述符堆切换至下一个。 但是,在某些 GPU 的堆之间切换会导致出现刷新,因此必须谨慎使用该特性。
我们如何将着色器代码与特定描述符或描述符集相关联? 解决方案? 表。
4.4 表
表是堆的起始索引和尺寸。 它们是环境点 (context points),但不是 API 对象。 您可以根据需要在每个着色器阶段部署一个或多个表。 例如,绘制调用的定点着色器可以部署一个表,指向堆中偏移量在 20 至 30 的描述符。 当下一次绘制开始时,偏移量将更改为 32 至 40。
图 9
使用当前硬件,D3D 12 在 PSO 可以在每个着色器阶段处理多个表。 您可以部署一个表,在该表中仅添加在不同调用之间频繁变化的数据;然后再部署一个表,在该表中仅添加在不同调用、帧之间处于静态的数据。 这样做可以避免将所有描述符从一个调用复制到下一个。 但是,较旧的 GPU 仅可在每个着色器阶段部署一个表。 多表仅支持在当前和以后的硬件上使用。
4.5 无绑定和高效性
描述符堆和表是 D3D 团队对无绑定渲染的采用,只是无法在 PC 硬件间扩展。 D3D 12 支持所有系统,从低端的系统芯片到高端的独立显卡。 此统一方式可为游戏开发人员提供许多绑定流可能性。 此外,新模型包括多个频率的更新。 支持包含静态绑定的高速缓存表重新使用,同时支持包含在每次绘制时不断变化的数据的动态表,从而在每次执行新绘制任务时无需复制全部绑定。
4.6 渲染环境概述
图 10 展示了目前讨论的 D3D 12 变化的渲染环境。 它还展示了新 PSO 以及 Gets 的删除,但是仍然包含 D3D 11 显性绑定点。
图 10: 本文中目前介绍的 D3D 12 变化的渲染环境。
接下来,我们将 D3D 11 渲染环境的最后一部分删除,并添加描述符表和堆。 现在,我们在每个着色器阶段部署了一个表或多个表,如像素着色器所示。
图 11: D3D 12 的整个渲染环境。
精细的状态对象消失,取而代之的是管线状态对象。 Hazard 追踪和状态镜像删除。 显性绑定点更换为应用/游戏管理的内存对象。 通过减少开支,删除 API 和驱动程序中的控制流和逻辑,CPU 效率得到提升。
5.0 捆绑包
我们结束在 D3D 12 中添加新的渲染器环境,观察 D3D 12 如何将控制归还游戏,使其“接近完美”。 但是,D3D 12 删除或简化 API churn(混乱)需要执行更多的操作。 API 中仍然有开销降低性能,因此我们需要其他方式充分使用 CPU。 命令序列如何? 重复序列有多少,如何使它们更高效?
5.1 冗余渲染命令
通过逐帧检查渲染命令,Microsoft D3D 团队发现,只有 5-10% 命令序列删除或添加。 其余的命令都是在不同的帧间重复使用。 因此,90-95% 的时间,CPU 都在重复相同的命令序列。
如何使其更高效? D3D 为何目前尚未尝试它? 在 BUILD 2014 上,Max McMullen 表示:“很难构建一种方法,以一致、可靠的方式来记录命令。 在不同驱动程序的不同 GPU 上以相同的方式运行,并保持同步可以确保一致。” 游戏需要所记录的命令序列与单独的命令的运行速度一样快。 什么改变了现状? D3D 改变了现状。 借助全新的 PSO、描述符堆和表,需要记录的状态以及播放命令得到极大简化。
5.2 什么是捆绑包?
捆绑包是一个小型命令列表,它记录了一次,但可在不同的帧和同一帧上重复使用,重复使用没有任何限制。 捆绑包可以在任何线程上创建,并可无数次使用。 捆绑包没有和 PSO 状态绑定,这表示 PSO 可以更新描述符表,然后当捆绑包使用不同的绑定再次运行时,游戏将得到其他的结果。 如同 Excel* 电子数据表中的公式,算法是一样的,只是结果取决于源数据。 我们添加了一些限制,以确保驱动程序能够有效执行捆绑包,其中一个是确保任何命令都无法更改渲染目标。 但是,我们仍然保留了许多能够记录和回放的命令。
图 12: 捆绑包是能够根据需要进行记录和回放的多次重复命令。
图 12 左侧是渲染环境示例,CPU 生成一系列命令,然后传递至 GPU 执行。 右侧是两个捆绑包,其中包含一个记录在不同线程上以便重复使用的命令。 当 GPU 运行命令时,最终会运行一个执行包命令 (execute bundle command)。 然后,它将回放所记录的捆绑包。 完成后,它将返回命令序列,继续查找其他捆绑包执行命令。 然后读取并回放第二个捆绑包,然后再继续。
5.3 代码效率
我们已经介绍了 GPU 中的控制流。 现在我们来看一下捆绑包如何简化代码。
不包含捆绑包的示例代码
我们在下图展示了一个设置管线状态和描述符表的设置阶段。 我们在接下来的图表中展示了两个对象绘制。 它们两个使用了相同的命令序列,只有常量不同。 这是典型的 D3D 11 和更旧的代码。
// Setup
pContext->SetPipelineState(pPSO);
pContext->SetRenderTargetViewTable(0, 1, FALSE, 0);
pContext->SetVertexBufferTable(0, 1);
pContext->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
图 14: 典型 D3D 11 代码中的设置阶段
// Draw 1
pContext->SetConstantBufferViewTable(D3D12_SHADER_STAGE_PIXEL, 0, 1);
pContext->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 0, 1);
pContext->DrawInstanced(6, 1, 0, 0);
pContext->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 1, 1);
pContext->DrawInstanced(6, 1, 6, 0);
图 15: 典型 D3D 11 代码中的绘制
// Draw 2
pContext->SetConstantBufferViewTable(D3D12_SHADER_STAGE_PIXEL, 1, 1);
pContext->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 0, 1);
pContext->DrawInstanced(6, 1, 0, 0);
pContext->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 1, 1);
pContext->DrawInstanced(6, 1, 6, 0);
图 16: 典型 D3D 11 代码中的绘制
包含捆绑包的示例
// Create bundle
pDevice->CreateCommandList(D3D12_COMMAND_LIST_TYPE_BUNDLE, pBundleAllocator, pPSO, pDescriptorHeap, &pBundle);
图 17: 捆绑包创建代码示例
// Record commands
pBundle->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
pBundle->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 0, 1);
pBundle->DrawInstanced(6, 1, 0, 0);
pBundle->SetShaderResourceViewTable(D3D12_SHADER_STAGE_PIXEL, 1, 1);
pBundle->DrawInstanced(6, 1, 6, 0);
pBundle->Close();
图 18: 捆绑包记录代码示例
接下来,我们了解一下与 D3D 12 中的捆绑包相同的命令序列。 以下第一个调用可以创建一个捆绑包。 该操作可以发生在任何线程上。 在下一个阶段,将记录命令序列。 这些命令与我们在之前的示例中看到的命令相同。
图 17 和 18 中的代码示例与图 14-16 中的非捆绑包代码执行的任务相同。 它们展示了捆绑包如何减少所需的调用数,执行相同的任务。 GPU 仍然执行相同的命令并得出相同的结果,只是效率更高。
6.0 命令列表
通过捆绑包、PSO、描述符堆和表,您可以了解 D3D 12 如何提高 CPU 效率,并为开发人员提供更多控制。 PSO 和描述符模型支持使用捆绑包,它们依次用于常见命令和重复命令。 这种更简单的“接近完美”的方法降低了开销,并允许更高效地利用 CPU 实现“控制台 API 效率和性能”。 之前我们讨论过,PC 游戏使用线程 0 执行大部分,甚至全部任务,而其他线程处理其他操作系统或系统任务。 充分利用 PC 游戏中的多核或线程非常困难。 实现游戏多线程通常需要大量的人力和资源。 D3D 开发团队希望借助 D3D 12 改变这一点。
6.1 命令创建并行性
本文中已多次提及,延迟执行的命令看似立即执行,实际上却需要排队并延迟执行。 该函数在 D3D 12 中予以保留,但是对游戏透明。 没有立即执行的环境,因为所有数据都需要延迟执行。 线程可以并行生成一个命令,以完成馈送至 API 对象的一列命令(即命令队列)。 GPU 在命令通过命令队列提交之前将不会执行。 队列是命令的排序,命令列表是上述命令的记录。 如何区别命令列表和捆绑包? 命令列表专门进行设计并优化,因此多条线程可以同时生成命令。 命令列表使用一次后将会从内存中删除,然后该位置将会记录一个新的列表。 捆绑包专门针对一帧或多帧中常使用的渲染命令的多次使用而设计。
在 D3D 11 中,该团队尝试了命令并行性,又称延迟环境 (deferred context)。 但是,由于所需的开销,它无法达到 D3D 团队的性能目标。 进一步的分析显示,许多位置需要大量串行开销,这导致不同的 CPU 内核之间的扩展性较差。 借助 CPU 高效性设计(见第 2-5 章),D3D 12 中消除了某些串行开销。
6.2 列表和队列
想象一下,两条线程生成一个渲染命令列表。 一个序列需要在另一个序列之前运行。 如果有 hazard,一条线程可以使用一个资源作为纹理,其他的线程使用同一个资源作为渲染目标。 驱动程序需要了解渲染时的资源利用率,并解决 hazard,确保提供一致的数据。 Hazard 追踪是 D3D 11 中序列化开销之一。 在 D3D 12 中,游戏(而非驱动程序)负责 hazard 追踪。
D3D 11 允许使用多个延迟环境,但是它们需要消耗成本。 驱动程序按资源追踪状态。 因此当您开始为延迟环境记录命令时,驱动程序需要分配内存,追踪所使用的每个资源的状态。 当生成延迟环境时,将会保留该内存。 完成后,驱动程序需要将所有追踪对象从内存中删除。 这将导致不必要的开销。 游戏可以声明 API 级别能够并行生成的最大命令列表数量。 然后,驱动程序将在同一部分内存中提前安排和分配所有追踪对象。
D3D 11 中常使用动态缓冲区(环境、顶点等),但是后台有许多丢弃的内存追踪实例缓冲区。 例如,可能会有两个并行生成的命令列表,且调用了 MapDiscard。 提交列表后,驱动程序必须在第二个命令列表打补丁,以纠正丢弃的缓冲区的信息。 与之前的 hazard 一样,这也需要一些开销。 D3D 12 将重命名控制交给游戏;动态缓冲区消失。 与此同时,游戏获得精细控制。 它可以构建自己的分配程序,并可以根据需要再次划分缓冲区。 然后命令可以指向内存中明确的点。
如 第 3.1 部分的讨论,在 D3D 11 中,运行时和驱动程序追踪资源生命周期。 这需要大量的资源进行计数和追踪,所有内容必须在提交时解决。 在 D3D 12 中,游戏拥有资源生命周期以及对 hazard 的控制,这消除了串行开销,从而提高了 CPU 的效率。 对这四个区域进行优化后,D3D 12 的并行命令生成更加高效,从而改进了 CPU 的并行性。 另外,D3D 开发团队目标正在构建新的驱动程序模型 WDDM 2.0,计划进一步进行优化,降低名称列表提交成本。
6.3 命令队列流
图 19: 命令队列,其中包含并行生成的两个命令列表以及两个重复使用命令捆绑包。
图 19 展示了 第 5.2 部分的捆绑包图表,但是它为多线程。 左侧的命令队列是提交至 GPU 的活动的序列。 中间是两个命令列表,右侧是场景开始前记录的两个捆绑包。 首先介绍一下命令列表,它们是针对不同部分的场景并行生成,命令列表 1 完成记录,提交至命令队列,然后 GPU 开始运行它。 同时,命令队列控制流开始,命令列表 2 记录在线程 2 上。 当 GPU 运行命令列表 1 时,线程 2 完成命令列表 2 的生成,并将其提交至命令队列。 当命令队列完成命令列表 1 的运行时,将按照顺序迁移至命令列表 2. 命令队列按照 GPU 需要执行命令的顺序排序。 虽然命令列表 2 在 GPU 完成命令列表 1 的运行前生成并提交至命令队列,但是在命令列表 1 的运行完成前不会执行命令列表 2。 D3D 12 支持在整个流程中更高效地并行。
7.0 动态堆
正如之前的讨论,游戏控制资源重命名,以便能够并行生成命令。 此外,D3D 12 中的资源重命名进行了简化。 D3D 11 对缓冲区进行了分类,将其分为顶点、常量和索引缓冲区。 游戏开发人员需要借助该功能按照自己希望的方式使用保留的内存。 D3D 团队遵从了这一需求。 D3D 12 缓冲区不再分类。 缓冲区只是游戏根据需要为帧(或多个帧)分配的必要尺寸的一块内存。 我们甚至可以根据需要使用并再次划分堆分配程序,从而创建了更高效的流程。 D3D 12 还有一个标准基准。 只要游戏使用标准基准,GPU 就能够读取数据。 标准化程度越高,就越容易创建能够在 CPU、GPU 和其他硬件等变量间流畅运行的内容。 此外,内存也是持续映射,这样,CPU 便能够一直知道地址。 它支持更出色的 CPU 并行性,因为您可以部署一个线程,将 CPU 指向该内存,然后让 CPU 决定某一帧需要什么数据。
图 20: 缓冲区分配与再次分配
图 20 顶部是包含分类缓冲区的 D3D 11 风格。 其下方是由游戏控制堆的全新 D3D 12 模型。 呈现出来的是一块连续的内存,而非位于其他内存位置的不同类型的缓冲区。 此外,游戏还将根据当前或未来数帧的渲染需求来调整缓冲区的尺寸。
8.0 CPU 并行性
现在,我们将综合介绍 D3D 12 的新特性如何在 PC 上创建真正的多线程游戏。 D3D 12 支持多个任务并行。 命令列表和绑定包可提供并行命令生成和执行。 绑定包可以记录重复使用的命令,并可在一帧或多帧内在多个命令列表中多次运行它们。 命令列表可跨多条线程生成,然后馈送至命令队列以便 GPU 执行。 最后,持续映射的缓冲区将并行生成动态数据。 D3D 12 和 WDDM 2.0 都针对并行性而设计。 D3D 12 删除了过去的 D3D 版本中的限制,支持开发人员以其理想方式实现其游戏或引擎的并行性。
图 21: 典型的 D3D 11 并行性,线程 0 执行大部分的任务,其他线程较少使用。
图 21 中的图表展示了 D3D 11 中典型游戏工作负载。 应用逻辑、D3D 运行时、UMD、DXGKernel、KMD 以及当前用途在一个包含四条线程的 CPU 上工作。 线程 0 执行大部分的任务, 线程 1-3 很少使用,仅在处理应用逻辑以及 D3D 11 运行时生成渲染命令时使用。 由于 D3D 11 的设计,用户模式驱动程序甚至不会在这些线程上生成命令。
图 22: 与图 21 的工作负载相同,但使用的是 D3D 12。 任务在 4 条线程上平均分配,而且借助 D3D 12 的其他效率,完成时间显著缩短。
接下来,我们看一下使用 D3D 12 处理相同的工作负载(图 22)。 同样,应用逻辑、D3D 运行时、UMD、DXGKernel、KMD 以及当前用途在一个包含四条线程的 CPU 上工作。 但是,该工作在所有线程间平均分配,并采用了 D3D 12 的优化特性。 得益于真正的命令生成,D3D 运行时能够并行运行。 借助 WDDM 2.0 中的内核优化,内核开销显著降低。 UMD 在所有线程上运行,而不仅是线程0,从而带来真正的命令生成并行性。 最后,捆绑包取代了 D3D 11 的冗余状态更改逻辑,并降低了应用逻辑时间。
图 23: D3D 11 与 D3D 12 并行性并排比较
图 23 展示了两个版本的并排比较。 借助真正的并行性,我们看到线程 0 和线程 1-3 之间 CPU 的利用率相对平均。 线程 1-3 执行更多的任务,因此“仅 GFX”显示提升。 此外,由于线程 0 上的工作负载降低,以及新的运行时和驱动程序效率,总体 CPU 使用约减少 50%。 再来看一下应用加 GFX,在各线程之间的分配更均匀,CPU 的使用约减少 32%。
9.0 总结
D3D 12 借助降低精细度的 PSO 提供了更高的 CPU 效率。 开发人员现在无需设置和读取每个状态,而是通过一个点操作,从而降低或全面减少了硬件失配开销。 应用设置 PSO,而驱动程序处理 API 命令,并将其转换为 GPU 码。 资源绑定的新模型消除了以前由于必须使用控制流逻辑带来的混乱。
借助堆、表和捆绑包,D3D 12 在 CPU 效率和扩展性方面都得到显著提升。 线性绑定点替换为应用/游戏管理的内存对象。 频繁使用的命令可以通过捆绑包进行记录并在一帧或多帧中多次播放。 命令列表和命令队列支持在多个 CPU 线程间并行创建命令列表。 现在,大部分的任务能够在 CPU 的所有线程间平均分配,从而充分释放了第四代和第五代智能英特尔® 酷睿™ 处理器的强大潜力和性能。
Direct3D 12 是 PC 游戏技术的跨越式发展。 借助更简单的 API 以及层级更少的驱动程序,游戏开发人员能够“接近完美”。 这提高了效率和性能。 D3D 开发团队通过齐心协力的合作,创建了一个支持开发人员控制的全新 API 和驱动程序模型,该模型支持他们创建更符合他们理想并具备出色图形和性能的游戏。
参考资料和相关链接
相关英特尔链接:
企业品牌识别 http://intelbrandcenter.tagworldwide.com/frames.cfm
英特尔® 产品名称 http://www.intel.com/products/processor_number/
声明和免责条款
请参见: http://legal.intel.com/Marketing/notices+and+disclaimers.htm
关于作者
Michael Coppock致力于 PC 游戏性能和显卡领域,自 1994 年以来一直任职于英特尔。 他负责帮助游戏公司充分发掘英特尔 GPU 和 CPU。 他主要关注硬件和软件,曾负责过许多英特尔产品,最早可追溯到 486DX4 Overdrive 处理器。