Direct3D* 12 - PC 上的控制台 API 效率和性能





5.00/5 (1投票)
在 GDC 2014 上,微软宣布了 2015 年 PC 游戏的惊人消息——下一代 Direct3D,版本 12。D3D 12 回归低级编程;它赋予游戏开发者更多控制权,并引入了许多令人兴奋的新功能。
英特尔®开发人员专区提供跨平台应用开发的工具和操作方法信息、平台和技术信息、代码示例以及同行专业知识,帮助开发人员创新和成功。加入我们的物联网、Android*、英特尔®实感™技术和Windows*社区,下载工具、访问开发套件、与志同道合的开发人员分享想法,并参与黑客马拉松、竞赛、路演和本地活动。
摘要
Microsoft Direct3D* 12 是 PC 游戏技术的一大飞跃,它赋予开发人员对其游戏更大的控制权,并提高了 CPU 效率和可扩展性。
目录
2.0 – 管线状态对象
3.0 – 资源绑定
3.1 – 资源危险
3.1 – 资源驻留管理
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 上,微软宣布了 2015 年 PC 游戏的惊人消息——下一代 Direct3D,版本 12。D3D 12 回归低级编程;它赋予游戏开发者更多控制权,并引入了许多令人兴奋的新功能。D3D 12 开发团队专注于减少 CPU 开销并提高 CPU 核心的可扩展性。目标是实现主机 API 的效率和性能,主机游戏可以更有效地利用 CPU/GPU,从而取得出色的效果。在 PC 游戏领域,线程 0 通常承担大部分(如果不是全部)工作。其他线程仅处理操作系统或其他系统任务。真正多线程的 PC 游戏很少。微软希望通过 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,以及在稍小程度上驱动程序,已经达到了如此复杂的程度,以至于在渲染帧时它们可能会成为障碍,这会带来性能成本。这就是低级编程发挥作用的地方。
当 MS-DOS* 时代结束,3DFX* 的 3Dglide* 等特定于供应商的 API 为 Direct3D 等 API 让路时,PC 上的低级编程就消失了。PC 为了急需的便利性和灵活性而牺牲了性能。硬件市场变得复杂,选择太多。开发时间增加,因为开发人员希望确保每个购买他们游戏的人都能玩它。不仅软件方面发生了变化,而且 CPU 功耗效率变得比性能更重要。然而,现在,除了关注原始的 GHz 之外,CPU 中的多核和多线程以及现代 GPU 上的并行渲染才是未来性能提升的关键。PC 游戏是时候向游戏主机寻求方向了。是时候更好地、更有效地利用所有这些核心和线程了。是时候让 PC 游戏进入 21 世纪了。
1.1 更接近硬件
为了使游戏“更接近硬件”,我们必须减小 API 和驱动程序的规模和复杂性。硬件和游戏本身之间的层级应该更少。API 和驱动程序花费太多时间转换命令和调用。其中一些(如果不是大部分)控制权将重新交还给游戏开发者。D3D 12 减少的开销将提高性能,游戏和 GPU 硬件之间更少的层级将意味着更好看和更高性能的游戏。当然,这方面的另一面是,一些开发者可能不想控制 API 过去处理的领域,例如 GPU 内存管理。也许这就是游戏引擎开发者发挥作用的地方,但只有时间才能证明。由于 D3D 12 的发布还有一段时间,所以有充足的时间来解决这部分问题。那么,有了所有这些伟大的承诺,将如何实现呢?主要是通过四个新功能:管线状态对象、命令列表、包和堆。
2.0 管线状态对象
为了讨论管线状态对象(PSO),我们首先回顾 D3D 11 渲染上下文,然后介绍 D3D 12 中的更改。图 1 包含了 D3D 11 开发负责人 Max McMullen 在 2014 年 4 月 BUILD 大会上展示的 D3D 11 渲染上下文。
粗大的箭头表示单个管线状态。每个状态都可以根据游戏的需求进行检索或设置。底部的其他状态是固定功能状态,例如视口或裁剪矩形。本文章后面部分将解释此图中其他相关功能。对于 PSO 的讨论,我们只需回顾图的左侧。D3D 11 的小型状态对象确实减少了 D3D 9 的 CPU 开销,但驱动程序在渲染时仍需要额外的工作来获取这些小型状态对象并将它们组合成 GPU 代码。我们称之为硬件不匹配开销。请看图 2 中 BUILD 2014 的另一个图表。
左侧显示了 D3D 9 风格的管线,这是应用程序用来完成其工作的。图 2 右侧的硬件需要编程。状态 1 代表着色器代码。状态 2 是光栅器和连接光栅器到着色器的控制流的组合。状态 3 是混合和像素着色器之间的连接。D3D 顶点着色器影响硬件状态 1 和 2,光栅器状态 2,像素着色器状态 1-3,等等。大多数驱动程序不想与应用程序同时提交调用。它们更喜欢记录并推迟,直到工作完成,以便它们可以看到应用程序实际想要什么。这意味着额外的 CPU 开销,因为旧的和过时的数据被标记为“脏”。驱动程序的控制流在绘制时检查每个对象的状态,并对硬件进行编程以匹配游戏设置的状态。由于额外的工作,资源被耗尽,事情可能会出错。理想情况下,一旦游戏设置了管线状态,驱动程序就知道游戏意图并一次性对硬件进行编程。图 3 显示了 D3D 12 管线,它通过所谓的管线状态对象(PSO)实现了这一点。
图 3 展示了一个流程精简、开销更小的过程。单个 PSO 包含每个着色器的状态信息,可以通过一次复制设置所有硬件状态。请记住,在 D3D 11 渲染上下文中,有些状态被标记为“其他”。D3D 12 团队认识到最小化 PSO 大小并允许游戏更改渲染目标而不影响已编译 PSO 的重要性。视口和裁剪矩形等内容独立出来,并与管线的其余部分正交编程(图 4)。
现在我们有一个单一的设置点,而不是分别设置和读取每个单独的状态;这减少或完全消除了硬件不匹配的开销。应用程序根据需要设置 PSO,而驱动程序获取 API 命令并将其转换为 GPU 代码,无需额外的流控制开销。这种“更接近硬件”的方法意味着绘制命令需要更少的周期,从而提高性能。
3.0 资源绑定
在讨论资源绑定更改之前,我们需要快速回顾 D3D 11 中使用的资源绑定模型。图 5 再次显示了渲染上下文图,左侧是 D3D 12 PSO,右侧是 D3D 11 资源绑定模型。
在图 5 中,每个着色器右侧是显式绑定点。显式绑定模型意味着管线中的每个阶段都有其可以引用的特定资源。这些绑定点引用 GPU 内存中的资源。这些可以是纹理、渲染目标、缓冲区、UAV 等。资源绑定已经存在很长时间了;事实上,它早于 D3D。目标是在幕后处理多个属性,并帮助游戏高效地提交渲染命令。然而,系统需要在三个关键区域执行许多绑定检查。下一节将回顾这些区域以及 D3D 团队如何针对 D3D 12 对它们进行优化。
3.1 资源危险
危险通常是一个转换,比如从渲染目标转变为纹理。一个游戏可能会渲染一个用作场景周围环境贴图的帧。游戏完成环境贴图的渲染后,现在想将其用作纹理。在此过程中,运行时和驱动程序都会跟踪何时将某个对象绑定为渲染目标或纹理。如果运行时或驱动程序发现某个对象同时绑定为两者,它们将解除最旧的设置并遵循最新的设置。这样,游戏可以根据需要进行切换,软件堆栈在幕后管理切换。驱动程序还必须刷新 GPU 管线,以便渲染目标可以用作纹理。否则,像素会在 GPU 中处理完成之前被读取,并且您将无法获得一致的状态。从本质上讲,危险是指任何需要在 GPU 中进行额外工作以确保数据一致性的情况。
与 D3D 12 中的其他功能和增强功能一样,解决方案是将更多控制权交给游戏。当一帧中只有一点时,API 和驱动程序为什么要承担所有工作和跟踪?从一个资源切换到另一个资源大约需要六十分之一秒。通过将控制权交还给游戏,开销会减少,而且成本只需支付一次,即游戏进行资源转换时(图 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 中的资源屏障 API 声明了一个资源及其源和目标用途,然后调用以告知运行时和驱动程序该转换。它变成了一个显式的东西,而不是在帧渲染过程中通过大量条件逻辑进行跟踪的东西,使其在每帧只发生一次,或游戏需要进行转换的任何频率。
3.2 资源驻留管理
D3D 11(及更早版本)的行为就好像调用是排队的。游戏认为 API 立即执行了该调用。事实并非如此。GPU 有一个命令队列,所有命令都延迟执行。虽然这允许 GPU 和 CPU 之间更大的并行性和效率,但它需要大量的引用计数和跟踪。所有这些计数和跟踪都需要 CPU 的努力。
为了解决这个问题,游戏获得了对资源生命周期的显式控制。D3D 12 不再隐藏 GPU 的排队性质。已添加 Fence API 来跟踪 GPU 进度。游戏可以在给定点(可能每帧一次)检查并查看哪些资源不再需要,然后可以释放该内存供其他用途。不再需要使用额外逻辑在帧渲染期间跟踪资源以释放资源和释放内存。
3.3 状态镜像
在上述三个区域得到优化后,又发现了一个可以提高效率的额外元素(尽管性能提升较小)。当设置一个绑定点时,运行时会跟踪该点,以便游戏稍后可以调用 `Get` 来查找绑定到管线的内容。绑定点被镜像或复制。此功能旨在方便中间件,以便组件化软件可以发现渲染上下文的当前状态。一旦资源绑定得到优化,就不再需要状态的镜像副本。除了从前三个区域移除流控制之外,状态镜像的 `Get` 也被移除。
4.0 堆和表
还有一项重要的资源绑定更改需要回顾。在第 4 节末尾,将揭示完整的 D3D 12 渲染上下文。新的 D3D 12 渲染上下文是实现 API 更高 CPU 效率目标的第一大步。
4.1 冗余资源绑定
在分析了几款游戏后,D3D 开发团队发现,游戏通常在帧与帧之间使用相同的命令序列。不仅命令如此,绑定也倾向于在帧与帧之间保持不变。CPU 生成一系列绑定(例如 12 个)以在帧中绘制一个对象。通常,CPU 必须为下一帧再次生成相同的 12 个绑定。为什么不缓存这些绑定,并为开发人员提供一个指向缓存的命令,以便可以重用相同的绑定呢?
在第 3 节中,我们讨论了排队。当发出调用时,游戏认为 API 立即执行该调用。然而事实并非如此。命令被放入一个队列中,所有命令都会被延迟,并在稍后由 GPU 执行。因此,如果你对我们之前谈到的 12 个绑定中的一个进行更改,驱动程序会复制所有 12 个绑定到新位置,编辑副本,然后告诉 GPU 开始使用复制的绑定。通常,12 个绑定中有许多是静态值,只有少数是动态值需要更新。当游戏想要对这些绑定进行部分更改时,它会复制所有 12 个,这对于微小的更改来说是过度的 CPU 开销。
4.2 描述符
什么是描述符?简而言之,它是一段定义资源参数的数据。它本质上是 D3D 11 视图对象背后的一切。它没有操作系统生命周期管理。它只是 GPU 内存中的不透明数据。它包含类型和格式信息、纹理的 mip 计数以及指向像素数据的指针。描述符是新资源绑定模型的核心。
4.3 堆
在 D3D 11 中设置视图时,它会将描述符复制到当前从 GPU 内存中读取描述符的位置。如果您在同一位置设置新视图,D3D 11 会将描述符复制到新的内存位置,并在下一个绘制命令中告诉 GPU 从该新位置读取。D3D 12 在描述符创建、复制等方面赋予游戏或应用程序显式控制权。
堆(图 8)只是一个巨大的描述符数组。您可以重用以前绘制或帧中的描述符。您还可以根据需要流式传输新的描述符。布局由游戏拥有,并且操作堆的开销很小。堆大小取决于 GPU 架构。较旧和低功耗 GPU 的大小可能限制在 65k,而高端 GPU 则受内存限制。对于低功耗 GPU,超出堆是可能的。因此,D3D 12 允许使用多个堆并在一个描述符堆和下一个描述符堆之间切换。但是,在某些 GPU 中切换堆会导致刷新,因此这是一个最好谨慎使用的功能。
现在,我们如何将着色器代码与特定的描述符或描述符集关联起来?答案是:表。
4.4 表
表是堆中的起始索引和大小。它们是上下文点,但它们不是 API 对象。您可以根据需要为每个着色器阶段设置一个或多个表。例如,绘制调用的顶点着色器可以有一个表,指向堆中偏移量 20 到 32 的描述符。当开始下一个绘制工作时,偏移量可能会更改为 32 到 40。
使用当前的硬件 D3D 12 可以在 PSO 中处理每个着色器阶段的多个表。您可以有一个表只包含频繁更改的内容,而第二个表包含从调用到调用、从帧到帧的静态内容。这样做可以避免在每次调用时复制所有描述符。但是,较旧的 GPU 仅限于每个着色器阶段一个表。多个表仅在当前和未来的硬件上才可能实现。
4.5 无绑定和高效
描述符堆和表是 D3D 团队对无绑定渲染的理解,只不过它跨 PC 硬件进行扩展。D3D 12 支持从低端 SoC 到高端独立显卡的一切。这种统一的方法为游戏开发人员提供了多种绑定流的可能性。此外,新模型还包括多频率更新。允许缓存静态绑定的表以供重用,以及一个动态表,其中包含随每次绘制而变化的数据,从而消除了每次新绘制时复制所有绑定的需要。
4.6 渲染上下文回顾
图 10 显示了到目前为止讨论的 D3D 12 更改后的渲染上下文。它还显示了新的 PSO 和 Get 的移除,但它仍然保留了 D3D 11 显式盲点。
让我们移除 D3D 11 渲染上下文的其余部分,并包含描述符表和堆。现在我们为每个着色器阶段都有一个表,或者多个表,如像素着色器所示。
细粒度状态对象已不复存在,取而代之的是管线状态对象。危险跟踪和状态镜像已移除。显式绑定点已被应用程序/游戏管理的内存对象取代。通过减少开销以及移除 API 和驱动程序中的控制流和逻辑,CPU 效率更高。
5.0 包
我们已经完成了 D3D 12 中新渲染上下文的介绍,并看到了 D3D 12 如何将控制权交还给游戏,使其“更接近硬件”。然而,D3D 12 在消除或简化 API 迭代方面做得更多。API 中仍然存在降低性能的开销,并且还有其他有效利用 CPU 的方法。命令序列如何?有多少重复序列,如何才能使它们更高效?
5.1 冗余渲染命令
通过逐帧检查渲染命令,微软 D3D 团队发现只有 5-10% 的命令序列被删除或添加。其余的在帧与帧之间重复使用。因此,CPU 在 90-95% 的时间里都在重复相同的命令序列!
如何才能提高效率呢?为什么 D3D 直到现在才尝试这样做?在 BUILD 2014 上,Max McMullen 说:“很难建立一种既符合规范又可靠的命令记录方法。因此,它在多个不同的 GPU 和多个不同的驱动程序上行为相同,同时又具有高性能。”游戏需要依赖任何记录的命令序列像单个命令一样快速执行。什么改变了?D3D 改变了。有了新的 PSO、描述符堆和表,记录和回放命令所需的状态大大简化了。
5.2 什么是包?
包是一小段命令列表,它们被记录一次,但可以在跨帧或单帧中重复使用——对重用没有限制。包可以在任何线程上创建,并无限次使用。包不与 PSO 状态绑定,这意味着 PSO 可以更新描述符表,然后当包以不同的绑定再次运行时,游戏会得到不同的结果。就像 Excel* 电子表格中的公式一样,数学运算总是相同的,但结果基于源数据。存在某些限制以确保驱动程序可以高效地实现包,其中之一是没有命令会更改渲染目标。但这仍然留下许多可以被记录和回放的命令。
图 12 的左侧是一个渲染上下文示例,一系列由 CPU 生成并传递给 GPU 执行的命令。右侧是两个包,其中包含在不同线程上录制的命令序列以供重用。当 GPU 运行命令时,它最终会到达一个执行包命令。然后它会回放录制的包。完成后,它会返回到命令序列,继续执行,并找到另一个包执行命令。然后读取并回放第二个包,然后继续执行。
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);
// 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);
// 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);
使用包的示例代码
// Create bundle
pDevice->CreateCommandList(D3D12_COMMAND_LIST_TYPE_BUNDLE, pBundleAllocator, pPSO, pDescriptorHeap, &pBundle);
// 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();
现在让我们看看 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 中,团队尝试了命令并行,也称为延迟上下文。然而,由于所需的开销,它未能实现 D3D 团队的性能目标。进一步分析表明,许多地方存在大量串行开销,导致 CPU 核心的扩展性较差。D3D 12 中通过第 2-5 节中回顾的 CPU 效率设计消除了一些串行开销。
6.2 列表和队列
想象两个线程正在生成渲染命令列表。一个序列应该在另一个序列之前运行。如果存在危险,一个线程将资源用作纹理,而另一个线程将相同资源用作渲染目标。驱动程序需要在渲染时查看资源使用情况并解决危险,确保数据一致。危险跟踪是 D3D 11 中串行开销的一个领域。D3D 12 中,游戏(而不是驱动程序)负责危险跟踪。
D3D 11 允许使用多个延迟上下文,但这会带来代价。驱动程序会跟踪每个资源的状态。因此,当您开始记录延迟上下文的命令时,驱动程序需要分配内存来跟踪所用“每个”资源的状态。在生成延迟上下文时会保留此内存。完成后,驱动程序必须从内存中删除所有跟踪对象。这会导致不必要的开销。游戏在 API 级别声明可以并行生成的最大命令列表数量。然后,驱动程序会预先在一个连贯的内存块中安排和分配所有跟踪对象。
在 D3D 11 中,使用动态缓冲区(上下文、顶点等)是很常见的,但在幕后存在多个内存跟踪被丢弃的缓冲区实例。例如,可能会并行生成两个命令列表,并调用 MapDiscard。一旦列表提交,驱动程序必须修补第二个命令列表以纠正被丢弃的缓冲区信息。就像之前的危险示例一样,这需要一些开销。D3D 12 已将重命名控制权交给游戏;动态缓冲区已不复存在。相反,游戏拥有细粒度控制。它构建自己的分配器,并可以根据需要细分缓冲区。然后命令可以指向内存中的显式点。
如第 3.1 节所述,运行时和驱动程序在 D3D 11 中跟踪资源生命周期。这需要大量的资源计数和跟踪,并且所有内容都必须在提交时解决。D3D 12 中,游戏拥有资源生命周期和危险控制权,从而消除了串行开销,提高了 CPU 效率。在优化这四个区域后,D3D 12 中的并行命令生成效率更高,从而提高了 CPU 并行性。此外,D3D 开发团队正在构建新的驱动程序模型 WDDM 2.0,并计划进一步优化以降低命令列表提交成本。
6.3 命令队列流
图 19 显示了第 5.2 节中的捆绑包图,但它是多线程的。左侧的命令队列是提交给 GPU 的事件序列。中间是两个命令列表,右侧是场景开始前录制的两个捆绑包。从命令列表开始,这些命令列表是为场景的不同部分并行生成的,命令列表 1 完成录制,提交到命令队列,然后 GPU 开始执行它。同时,命令队列控制流启动,命令列表 2 在线程 2 上录制。当 GPU 执行命令列表 1 时,线程 2 完成生成命令列表 2 并将其提交到命令队列。当命令队列完成执行命令列表 1 时,它以串行顺序移动到命令列表 2。命令队列是 GPU 需要执行命令的串行顺序。尽管命令列表 2 在 GPU 完成执行命令列表 1 之前已生成并提交到命令队列,但它直到命令列表 1 执行完成后才执行。D3D 12 在整个过程中提供了更高效的并行性。
7.0 动态堆
如前所述,游戏控制资源重命名以实现命令生成的并行性。此外,D3D 12 中简化了资源重命名。D3D 11 具有类型化缓冲区:顶点、常量和索引缓冲区。游戏开发者要求能够根据自己的意愿使用这些保留内存。D3D 团队对此表示同意。D3D 12 缓冲区不再是类型化的。缓冲区只是游戏根据帧(或多帧)所需大小分配的一块内存。甚至可以使用堆分配器并根据需要进行细分,从而创建一个更高效的过程。D3D 12 还具有标准对齐方式。只要游戏使用标准对齐方式,GPU 就能够读取数据。我们标准化得越多,创建在各种 CPU、GPU 和其他硬件上都能良好运行的内容就越容易。内存也是持久映射的,因此 CPU 始终知道地址。这样可以实现更多的 CPU 并行性,因为您可以让一个线程将 CPU 指向该内存,然后让 CPU 确定帧所需的数据。
图 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 上的典型游戏工作负载。应用程序逻辑、D3D 运行时、UMD、DXGKernel、KMD 和呈现使用情况在一个具有四个线程的 CPU 上工作。线程 0 承担了大部分繁重的工作。线程 1-3 除了应用程序逻辑和 D3D 11 运行时生成渲染命令之外,实际上并没有使用。由于 D3D 11 的设计,用户模式驱动程序甚至没有在这些线程上生成命令。
现在我们来看相同的工作负载,但使用 D3D 12(图 22)。同样,应用程序逻辑、D3D 运行时、UMD、DXGKernel、KMD 和呈现使用情况在一个具有四个线程的 CPU 上工作。然而,通过 D3D 12 优化,工作在所有线程上均匀分配。得益于真正的命令生成,D3D 运行时并行运行。通过 WDDM 2.0 中的内核优化,内核开销显著减少。UMD 在所有线程上工作,而不仅仅是线程 0,这显示了真正的命令生成并行性。最后,捆绑包取代了 D3D 11 中冗余的状态更改逻辑,并减少了应用程序逻辑时间。
图 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 处理器。