0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

NVRHI的主要功能及通过编写可移植的渲染代码

星星科技指导员 来源:NVIDIA 作者:Alexey Panteleev 2022-04-20 16:08 次阅读

现代图形 API ,如 Direct3D 12 和 Vulkan ,旨在提供对 GPU 的较低级别访问,并消除与 API 转换相关的 GPU 驱动程序开销。此低级接口允许应用程序对系统进行更多控制,并提供以最适合每个应用程序的方式管理管道、着色器编译、内存分配和资源描述符的能力。

另一方面,这更接近于对 GPU 的硬件访问,这意味着应用程序必须自己管理这些东西,而不是依赖 GPU 驱动程序。使用这些 API 绘制单个三角形的基本“ hello world ”程序可以扩展到 1000 行或更多代码。在复杂的渲染器中,如果不系统地管理 GPU 内存、描述符等,可能会很快变得难以控制。

如果应用程序或引擎必须使用多个图形 API ,可以通过两种方式完成:

复制渲染代码以分别使用每个 API 。这种方法有一个明显的缺点,就是必须开发和维护多个独立的实现。

在图形 API 上实现一个抽象层,在公共接口中提供必要的功能。这在开发和维护抽象层方面有一个不同的缺点。大多数主要的游戏引擎都实现了第二种方法。

NVIDIA 渲染硬件接口( NVRHI )是一个处理这些缺点的库。它定义了一个定制的、更高级的图形 API ,可以很好地映射到三个受支持的本机图形 API : Vulkan 、 D3D12 和 D3D11 。它以安全、自动的方式管理资源、管道、描述符和屏障,必要时可以轻松禁用或绕过这些资源,以减少 CPU 开销。除此之外, NVRHI 还提供了一个验证层,以确保应用程序正确使用 API ,类似于 Direct3D 调试运行时或 Vulkan 验证层的功能,但在更高的级别上。

NVRHI 没有提供一些与便携性相关的功能。首先,它不会在运行时编译着色器或读取着色器反射数据以动态绑定资源。事实上, NVRHI 根本不在运行时处理着色器。该应用程序提供特定于平台的着色器二进制文件,即 DXBC 、 DXIL 或 SPIR-V blob 。 NVRHI 将其直接传递给底层图形 API 。匹配绑定布局由应用程序决定,并由底层图形 API 验证。其次, NVRHI 不创建图形设备或窗口。这也取决于应用程序或其他库,如GLFW。

在本文中,我将介绍 NVRHI 的主要功能,并解释每个功能如何帮助图形工程师提高工作效率和编写更安全的代码。

资源生命周期管理

绑定布局和绑定集

自动资源状态跟踪

上传管理

与图形 API 的交互

着色器置换

资源生命周期管理

在 Vulkan 和 D3D12 中,应用程序必须注意仅销毁 GPU 不再使用的设备资源。如果仔细规划资源使用情况,这可以用很少的开销完成,但问题在于规划。

NVRHI 几乎完全遵循 D3D11 资源生命周期模型。资源(如缓冲区、纹理或管道)具有引用计数。复制资源句柄时,引用计数将递增。当句柄被销毁时,引用计数将递减。当最后一个句柄被销毁并且引用计数达到零时,资源对象被销毁,包括底层图形 API 资源。但 D3D12 也是这么做的,对吗?不完全是。

NVRHI 还保留对命令列表中使用的资源的内部引用。打开命令列表进行录制时,将创建命令列表的新实例。该实例保存对其使用的每个资源的引用。当命令列表关闭并提交以供执行时,实例与围栏或信号量值一起存储在队列中,可用于确定实例是否已在 GPU 上完成执行。之后可以立即重新打开相同的命令列表进行录制,即使之前的实例仍在 GPU 上执行。

应用程序应该偶尔调用nvrhi::IDevice::runGarbageCollection方法,每帧至少调用一次。此方法查看正在运行的命令列表实例队列,并清除已完成执行的实例。清除实例会自动删除对实例中使用的资源的内部引用。如果一个资源没有剩下其他引用,它将在那个时候被销毁。

此行为可通过以下代码示例显示:

  
       // Creates an internal instance of the command list
       commandList->open(); 
  
       // Adds a buffer reference to the instance, which increases reference count to 2
       commandList->clearBufferUInt(buffer, 0); 
  
       commandList->close();
  
       // The local reference to the buffer is released here, decrements reference count to 1
 }
  
 // Puts the command list instance into the queue
 device->executeCommandList(commandList); 
  
 // Likely doesn't do anything with the instance
 // because it's just been submitted and still executing on the GPU
 device->runGarbageCollection();
  
 device->waitForIdle();
  
 // This time, the buffer should be destroyed because
 // waitForIdle ensures that all command list instances
 // have finished executing, so when the finished instance
 // is cleared, the buffer reference count is decremented to zero
 // and it can be safely destroyed
 device->runGarbageCollection(); 

与 D3D12 和 Vulkan 不同,在 NVRHI 中,当应用程序创建资源、使用资源并立即释放资源时,此处显示的“触发并忘记”模式非常好。

如果应用程序执行多个draw调用,并且为每个draw调用绑定了大量资源,那么这种类型的资源跟踪是否会变得昂贵。不是真的。Draw调用和分派不处理单个资源。纹理和缓冲区被分组为不可变的绑定集,这些绑定集被创建,保存对其资源的永久引用,并作为单个对象进行跟踪。

因此,当在命令列表中使用某个绑定集时,命令列表实例仅存储对该绑定集的引用。如果绑定集已绑定,则跳过该存储,以便使用相同绑定重复调用 draw 不会增加跟踪成本。我将在下一节更详细地解释绑定集。

另一个有助于减少资源生存期跟踪带来的 CPU 开销的方法是绑定集和加速结构上的trackLiveness设置。当此参数设置为false时,不会为该特定资源创建内部引用。在这种情况下,应用程序负责保留自己的引用,而不是在资源使用时释放它。

绑定布局和绑定集

NVRHI 具有独特的资源绑定模型,旨在实现安全性和运行效率。如前所述,图形或计算管道使用的各种资源被分组到绑定集中。

简言之,绑定集是绑定到管道中特定插槽的资源视图数组。例如,绑定集可能包含绑定到插槽t1的结构化缓冲区 SRV 、绑定到插槽u0的单个纹理 mip 级别的 UAV 以及绑定到插槽b2的常量缓冲区。集合中的所有绑定共享相同的可见性遮罩(着色器阶段将看到该绑定)和寄存器空间,两者都由绑定布局指定。

绑定布局是 D3D12 根签名和 Vulkan 描述符集布局的 NVRHI 版本。绑定布局类似于绑定集的模板。它声明哪些资源类型绑定到哪些插槽,但不说明使用了哪些特定资源。

与根签名和描述符集布局一样, NVHRI 绑定布局用于创建管道。可以使用多个绑定布局创建单个管道。根据资源的修改频率将资源分为不同的组,或者将不同的资源集绑定到不同的管道阶段,这些都很有用。

以下代码示例显示了如何使用一个绑定布局创建基本计算管道:

auto layoutDesc = nvrhi::BindingLayoutDesc()
     .setVisibility(nvrhi::ShaderType::All)
     .addItem(nvrhi::BindingLayoutItem::Texture_SRV(0))     // texture at t0
     .addItem(nvrhi::BindingLayoutItem::ConstantBuffer(2)); // constants at b2
  
// Create a binding layout.
nvrhi::BindingLayoutHandle bindingLayout = device->createBindingLayout(layoutDesc);
  
auto pipelineDesc = nvrhi::ComputePipelineDesc()
       .setComputeShader(shader)
       .addBindingLayout(bindingLayout);
  
// Use the layout to create a compute pipeline.
nvrhi::ComputePipelineHandle computePipeline = device->createComputePipeline(pipelineDesc); 

只能从匹配的绑定布局创建绑定集。匹配意味着布局必须具有相同数量、相同类型、绑定到相同插槽、顺序相同的项目。这看起来可能是冗余的, D3D12 和 Vulkan API 在其描述符系统中的冗余更少。这种冗余非常有用:它使代码更加明显,并且允许 NVRHI 验证层捕获更多的 bug 。

auto bindingSetDesc = nvrhi::BindingSetDesc()
       // An SRV for two mip levels of myTexture.
       // Subresource specification is optional, default is the entire texture.
     .addItem(nvrhi::BindingSetItem::Texture_SRV(0, myTexture, nvrhi::Format::UNKNOWN,
       nvrhi::TextureSubresourceSet().setBaseMipLevel(2).setNumMipLevels(2)))
     .addItem(nvrhi::BindingSetItem::ConstantBuffer(2, constantBuffer));
  
// Create a binding set using the layout created in the previous code snippet.
nvrhi::BindingSetHandle bindingSet = device->createBindingSet(bindingSetDesc, bindingLayout); 

由于绑定集描述符也包含创建绑定布局所需的几乎所有信息,因此可以通过一个函数调用同时创建这两个信息。这在创建仅需要一个绑定集的某些渲染过程时可能很有用。

#include 
...
nvrhi::BindingLayoutHandle bindingLayout;
nvrhi::BindingSetHandle bindingSet;
nvrhi::utils::CreateBindingSetAndLayout(device, /* visibility = */ nvrhi::ShaderType::All,
       /* registerSpace = */ 0, bindingSetDesc, /* out */ bindingLayout, /* out */ bindingSet);
  
// Now you can create the pipeline using bindingLayout. 

绑定集是不可变的。创建绑定集时, NVRHI 从 D3D12 上的堆中分配描述符,或在 Vulkan 上创建描述符集,并用必要的资源视图填充它。

稍后,当绑定集用于绘制或分派调用时,绑定操作是轻量级的,并转换为相应的图形 API 绑定调用。渲染时不会创建或复制描述符。

自动资源状态跟踪

在 D3D12 和 Vulkan API 中,改变资源状态并在图形管道中引入依赖关系的显式屏障都是一个重要部分。它们允许应用程序最小化管道依赖项和气泡的数量,并优化它们的位置。通过从驱动程序中删除该逻辑,它们同时减少了 CPU 开销。这主要与绘制大量几何体的紧密渲染循环有关。大多数情况下,尤其是在编写新的渲染代码时,处理障碍非常烦人且容易出现错误。

NVHRI 实现了一个系统,该系统跟踪每个资源的状态,以及每个命令列表的子资源(可选)。当命令与资源交互时,资源将转换为该命令所需的状态(如果尚未处于该状态)。例如,writeTexture命令将纹理转换为CopyDest状态,随后从纹理读取的绘制操作将纹理转换为ShaderResources状态。

当两个连续命令的资源处于UnorderedAccess状态时,将应用特殊处理:不涉及转换,但在命令之间插入无人机屏障。如有必要,可以暂时禁用无人机屏障的插入。

我前面说过, NVRHI 会根据每个命令列表跟踪每个资源的状态。应用程序可以以任意顺序或并行方式记录多个命令列表,并在每个命令列表中以不同方式使用相同的资源。因此,您无法全局或每个设备跟踪资源状态,因为在记录命令列表时需要导出屏障。执行命令列表时,全局跟踪可能不会按照与设备命令队列上实际资源使用情况相同的顺序进行。

因此,您可以分别跟踪每个命令列表中的资源状态。在某种意义上,这可以看作是一个微分方程。您知道命令列表中的状态是如何变化的,但不知道边界条件,也就是说,当您按执行顺序进入和退出命令列表时,每个资源都处于哪个状态。

应用程序必须为每个资源提供边界条件。有两种方法可以做到这一点:

Explicit:打开命令列表后使用beginTrackingTextureState和beginTrackingBufferState功能,关闭命令列表前使用setTextureState和setBufferState功能。

Automatic:创建资源时使用TextureDesc和BufferDesc结构的initialState和keepInitialState字段。然后,使用资源的每个命令列表在进入命令列表时都假定它处于初始状态,并在离开命令列表之前将其转换回初始状态。

在这里,您 MIG 想知道如何避免资源状态跟踪的 CPU 开销,或者手动优化屏障放置。好吧,你可以!命令列表具有setEnableAutomaticBarriers功能,可完全禁用自动安全栅。在此模式下,在需要屏障的位置使用setTextureState和setBufferState功能。它仍然使用相同的状态跟踪逻辑,但频率可能更低。

上传管理

NVRHI 自动化了现代图形 API 的另一个方面,这一点通常很烦人。这就是 GPU 对上传缓冲区的管理和对其使用情况的跟踪。

通常,当必须从 CPU 对每帧或每帧多次更新某些纹理或缓冲区时,会分配一个分级缓冲区,其大小比资源内存需求大数倍。这将在 GPU 上启用多个正在运行的帧。或者,大型暂存缓冲区的部分在运行时进行子分配。使用 NVRHI 实现相同的策略是可能的,但是有一个内置的实现可以很好地适用于大多数用例。

每个 NVRHI 命令列表都有自己的上载管理器。调用writeBuffer或writeTexture时,上载管理器会尝试查找 GPU 不再使用的现有缓冲区,该缓冲区可以容纳必要的数据。如果没有可用的缓冲区,将创建一个新的缓冲区并将其添加到上载管理器的池中。将提供的数据复制到该缓冲区中,然后将复制命令添加到命令列表中。 GPU 使用的缓冲区的跟踪是自动执行的。

ConstantBufferStruct myConstants;
myConstants.member = value;
  
// This is all that's necessary to fill the constant buffer with data and have it ready for rendering.
commandList->writeBuffer(constantBuffer, myConstants, sizeof(myConstants)); 

上载管理器从不释放其缓冲区,也不会与其他命令列表共享缓冲区。也许一个应用程序正在进行大量的上传,例如在场景加载期间,然后切换到上传强度较小的操作模式。在这种情况下,最好为上传活动创建一个单独的命令列表,并在上传完成后释放它。这将释放与命令列表关联的上载缓冲区。

无需等待 GPU 完成从上载缓冲区复制数据。在复制完成之前,前面描述的资源生存期跟踪系统不会释放上载缓冲区。

与图形 API 的交互

有时,有必要避开抽象层,直接使用底层图形 API 进行操作。也许您必须使用 NVRHI 不支持的某些功能,在示例应用程序中演示一些 API 用法,或者使可移植呈现代码与来自其他地方的本机资源一起工作。 NVRHI 使做这些事情相对容易。

每个 NVRHI 对象都有一个getNativeObject函数,该函数返回所需类型的底层 API 资源。预期的类型被传递给该函数,如果该类型可用,它只返回非 NULL 值,以提供某种类型安全性。

支持的类型包括ID3D11Device或ID3D12Resource等接口和vk::Image等句柄。此外, NVRHI 纹理对象具有getNativeView功能,可以创建和返回纹理视图,如 SRV 或 UAV 。

例如,为了在 NVRHI 命令列表的中间发布一些本地的 D3D12 渲染命令,您 MIG HT 使用代码,如下面的示例:

ID3D12GraphicsCommandList* d3dCmdList = nvrhiCommandList->getNativeObject(
       nvrhi::ObjectTypes::D3D12_GraphicsCommandList);
  
D3D12_CPU_DESCRIPTOR_HANDLE d3dTextureRTV = nvrhiTexture->getNativeView(
       nvrhi::ObjectTypes::D3D12_RenderTargetViewDescriptor);
  
const float clearColor[4] = { 0.f, 0.f, 0.f, 0.f };
d3dCmdList->ClearRenderTargetView(d3dTextureRTV, clearColor, 0, nullptr); 

着色器置换

这里要提到的最后一个生产力特性是 NVRHI 附带的批处理着色器编译器。这是一项可选功能,没有它, NVRHI 完全可以正常工作。 NVRHI 接受通过其他方式编译的着色器。尽管如此,它还是一个有用的工具。

通常需要使用多个预处理器定义组合编译同一着色器。但是,例如, VisualStudio 为着色器编译提供的本机工具根本无法轻松完成此任务。

NVRHI 着色器编译器正好解决了这个问题。由列出着色器源文件和编译选项的文本文件驱动,它生成选项排列并调用底层编译器( DXC 或 FXC )生成二进制文件。然后,同一着色器的不同版本的二进制文件被打包成一个自定义块格式的文件,该文件可以使用《nvrhi/common/shader-blob.h》中声明的函数进行处理。

应用程序可以加载包含所有着色器排列的文件,并将其连同预处理器定义及其值的列表一起传递给nvrhi::utils::createShaderPermutation或nvrhi::utils::createShaderLibraryPermutation。如果文件中存在请求的置换,则会创建相应的着色器对象。如果没有,将生成一条错误消息。

除了置换处理之外,着色器编译器还有其他很好的功能。首先,它扫描源文件以构建包含在每个文件中的标题树。它检测是否修改了任何标题,以及是否必须重建特定着色器。其次,它可以使用所有可用的 CPU 内核并行构建所有过时的着色器。

结论

在这篇文章中,我介绍了 NVRHI 的一些最重要的功能,在我看来,使用这些功能是一种乐趣。

关于作者

Alexey Panteleev 是 NVIDIA 开发人员和性能技术团队的杰出工程师,他专注于新渲染技术的优化、产品化和集成。他最近的工作包括地震 II RTX 、带有 RTX 的地雷探测器以及各种技术演示和样品,如 ReSTIR 和小行星。亚历克赛拥有博士学位。莫斯科工程和物理研究所(梅菲州立大学)计算机科学专业。

审核编辑:郭婷

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 接口
    +关注

    关注

    33

    文章

    7629

    浏览量

    148439
  • NVIDIA
    +关注

    关注

    14

    文章

    4585

    浏览量

    101691
  • API
    API
    +关注

    关注

    2

    文章

    1380

    浏览量

    60983
收藏 人收藏

    评论

    相关推荐

    SMD电感器的主要功能是什么?

    SMD电感器的主要功能是什么? SMD电感器的主要功能是用于电路中的电感元件,主要用于储存能量、传输信号、滤波和产生磁场等。 一、能量储存和传输: SMD电感器可以储存电能并将其传输到电路的其他部分
    的头像 发表于 02-03 15:07 424次阅读

    传感器的主要功能是什么

    传感器是一种能够感知外界环境并将这些信息转化为可用信号的装置。它们在各行各业和领域中起到关键作用,被广泛应用于科学研究、医疗诊断、工业控制、农业生产等众多领域。传感器的主要功能包括测量和监测、控制和反馈、安全和监控、诊断和检测以及位置和导航,本文将详细介绍传感器的主要功能
    的头像 发表于 01-27 17:10 1122次阅读
    传感器的<b class='flag-5'>主要功能</b>是什么

    滤波器的主要功能和作用科普

    滤波器是一种用于处理信号的电路或系统,其主要功能和作用包括
    的头像 发表于 01-25 18:10 1037次阅读

    bms主要功能有哪些

    、储能系统等新能源领域的关键部件,其主要功能是对电池组进行实时监控、保护和管理,以保证电池组的安全、稳定和高效运行。本文将对BMS的主要功能进行详细介绍。 数据采集:BMS通过传感器对电池组的电压、电流、温度等关键参数进行实时采
    的头像 发表于 01-05 18:09 4376次阅读
    bms<b class='flag-5'>主要功能</b>有哪些

    集成放大电路中输出级的主要功能

    级的主要功能,并探讨其工作原理、应用和性能优化。 一、输出级的主要功能 集成放大电路输出级的主要功能是放大输入信号,并将其输出到外部负载上。具体来说,它需要完成以下几个任务: 放大信号:输出级的核心任务是将输入信号放
    的头像 发表于 12-29 10:34 404次阅读

    电源滤波器的主要功能和作用

    电源滤波器是电子设备中非常重要的一部分,其主要功能是过滤电源中的杂波和干扰信号。
    的头像 发表于 12-25 18:19 734次阅读

    EMI滤波器有哪些应用与主要功能

    EMI滤波器有哪些应用与主要功能?相信不少人是有疑问的,今天深圳市比创达电子科技有限公司就跟大家解答一下!
    的头像 发表于 11-29 10:40 394次阅读
    EMI滤波器有哪些应用与<b class='flag-5'>主要功能</b>?

    AMI网络的主要功能

    电子发烧友网站提供《AMI网络的主要功能.pdf》资料免费下载
    发表于 11-27 11:56 0次下载
    AMI网络的<b class='flag-5'>主要功能</b>

    数字示波器的主要功能和应用介绍

    和更丰富的功能。数字示波器已经成为现代电子测量领域不可或缺的工具,被广泛应用于电子工程、通信、医疗、教育等领域。 数字示波器的主要功能如下: 1. 波形显示:数字示波器能够以图形的形式显示电信号的波形。通过高精度的A/D转换和数
    的头像 发表于 11-07 10:18 1591次阅读

    滤波器的主要功能和作用

    滤波器的主要功能和作用是处理信号,根据特定的频率响应特性对信号进行频率选择、增强或抑制。以下是滤波器的主要功能和作用。
    的头像 发表于 10-27 11:16 1351次阅读

    变压器的主要功能

    变压器的主要功能 变压器是一种用于变换交流电压的电气设备。它可以将高电压变成低电压,或将低电压转换为高电压。由于变压器在电力系统中发挥着至关重要的作用,其主要功能必须详尽、详实、细致地解释。因此
    的头像 发表于 09-04 17:25 1749次阅读

    直线导轨的主要功能

    直线导轨的主要功能
    的头像 发表于 07-26 17:42 723次阅读
    直线导轨的<b class='flag-5'>主要功能</b>

    简述增益模块放大器的主要功能

    增益模块放大器(GainBlockAmplifier)是一种专用的放大器模块,其主要功能是提供高增益和放大输入信号的幅度,而不引入显著的失真或干扰。以下是增益模块放大器的主要功能
    的头像 发表于 07-06 09:41 547次阅读

    电机控制器的主要功能及组成

    电机控制器是一种用于控制电动机运行的设备,主要通过对电机的电流、电压、频率、相序等参数进行调节,实现电机的启动、停止、速度调节、正反转等运动状态的控制。下面将从主要功能和组成两个方面,对电机控制器进行详细介绍。
    的头像 发表于 06-03 10:23 8050次阅读

    UltraEdit主要功能

    HTML Validator Std v19。 主要功能 语法高亮——包括代码折叠 列/块模式编辑 具有多个帐户设置和自动登录和保存功能的FTP 客户端(仅限32 位)
    的头像 发表于 05-26 15:23 866次阅读
    UltraEdit<b class='flag-5'>主要功能</b>