現(xiàn)代圖形 API ,如 Direct3D 12 和 Vulkan ,旨在提供對(duì) GPU 的較低級(jí)別訪問,并消除與 API 轉(zhuǎn)換相關(guān)的 GPU 驅(qū)動(dòng)程序開銷。此低級(jí)接口允許應(yīng)用程序?qū)ο到y(tǒng)進(jìn)行更多控制,并提供以最適合每個(gè)應(yīng)用程序的方式管理管道、著色器編譯、內(nèi)存分配和資源描述符的能力。
另一方面,這更接近于對(duì) GPU 的硬件訪問,這意味著應(yīng)用程序必須自己管理這些東西,而不是依賴 GPU 驅(qū)動(dòng)程序。使用這些 API 繪制單個(gè)三角形的基本“ hello world ”程序可以擴(kuò)展到 1000 行或更多代碼。在復(fù)雜的渲染器中,如果不系統(tǒng)地管理 GPU 內(nèi)存、描述符等,可能會(huì)很快變得難以控制。
如果應(yīng)用程序或引擎必須使用多個(gè)圖形 API ,可以通過兩種方式完成:
復(fù)制渲染代碼以分別使用每個(gè) API 。這種方法有一個(gè)明顯的缺點(diǎn),就是必須開發(fā)和維護(hù)多個(gè)獨(dú)立的實(shí)現(xiàn)。
在圖形 API 上實(shí)現(xiàn)一個(gè)抽象層,在公共接口中提供必要的功能。這在開發(fā)和維護(hù)抽象層方面有一個(gè)不同的缺點(diǎn)。大多數(shù)主要的游戲引擎都實(shí)現(xiàn)了第二種方法。
NVIDIA 渲染硬件接口( NVRHI )是一個(gè)處理這些缺點(diǎn)的庫。它定義了一個(gè)定制的、更高級(jí)的圖形 API ,可以很好地映射到三個(gè)受支持的本機(jī)圖形 API : Vulkan 、 D3D12 和 D3D11 。它以安全、自動(dòng)的方式管理資源、管道、描述符和屏障,必要時(shí)可以輕松禁用或繞過這些資源,以減少 CPU 開銷。除此之外, NVRHI 還提供了一個(gè)驗(yàn)證層,以確保應(yīng)用程序正確使用 API ,類似于 Direct3D 調(diào)試運(yùn)行時(shí)或 Vulkan 驗(yàn)證層的功能,但在更高的級(jí)別上。
NVRHI 沒有提供一些與便攜性相關(guān)的功能。首先,它不會(huì)在運(yùn)行時(shí)編譯著色器或讀取著色器反射數(shù)據(jù)以動(dòng)態(tài)綁定資源。事實(shí)上, NVRHI 根本不在運(yùn)行時(shí)處理著色器。該應(yīng)用程序提供特定于平臺(tái)的著色器二進(jìn)制文件,即 DXBC 、 DXIL 或 SPIR-V blob 。 NVRHI 將其直接傳遞給底層圖形 API 。匹配綁定布局由應(yīng)用程序決定,并由底層圖形 API 驗(yàn)證。其次, NVRHI 不創(chuàng)建圖形設(shè)備或窗口。這也取決于應(yīng)用程序或其他庫,如GLFW。
在本文中,我將介紹 NVRHI 的主要功能,并解釋每個(gè)功能如何幫助圖形工程師提高工作效率和編寫更安全的代碼。
資源生命周期管理
綁定布局和綁定集
自動(dòng)資源狀態(tài)跟蹤
上傳管理
與圖形 API 的交互
著色器置換
資源生命周期管理
在 Vulkan 和 D3D12 中,應(yīng)用程序必須注意僅銷毀 GPU 不再使用的設(shè)備資源。如果仔細(xì)規(guī)劃資源使用情況,這可以用很少的開銷完成,但問題在于規(guī)劃。
NVRHI 幾乎完全遵循 D3D11 資源生命周期模型。資源(如緩沖區(qū)、紋理或管道)具有引用計(jì)數(shù)。復(fù)制資源句柄時(shí),引用計(jì)數(shù)將遞增。當(dāng)句柄被銷毀時(shí),引用計(jì)數(shù)將遞減。當(dāng)最后一個(gè)句柄被銷毀并且引用計(jì)數(shù)達(dá)到零時(shí),資源對(duì)象被銷毀,包括底層圖形 API 資源。但 D3D12 也是這么做的,對(duì)嗎?不完全是。
NVRHI 還保留對(duì)命令列表中使用的資源的內(nèi)部引用。打開命令列表進(jìn)行錄制時(shí),將創(chuàng)建命令列表的新實(shí)例。該實(shí)例保存對(duì)其使用的每個(gè)資源的引用。當(dāng)命令列表關(guān)閉并提交以供執(zhí)行時(shí),實(shí)例與圍欄或信號(hào)量值一起存儲(chǔ)在隊(duì)列中,可用于確定實(shí)例是否已在 GPU 上完成執(zhí)行。之后可以立即重新打開相同的命令列表進(jìn)行錄制,即使之前的實(shí)例仍在 GPU 上執(zhí)行。
應(yīng)用程序應(yīng)該偶爾調(diào)用nvrhi::IDevice::runGarbageCollection方法,每幀至少調(diào)用一次。此方法查看正在運(yùn)行的命令列表實(shí)例隊(duì)列,并清除已完成執(zhí)行的實(shí)例。清除實(shí)例會(huì)自動(dòng)刪除對(duì)實(shí)例中使用的資源的內(nèi)部引用。如果一個(gè)資源沒有剩下其他引用,它將在那個(gè)時(shí)候被銷毀。
此行為可通過以下代碼示例顯示:
// 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 中,當(dāng)應(yīng)用程序創(chuàng)建資源、使用資源并立即釋放資源時(shí),此處顯示的“觸發(fā)并忘記”模式非常好。
如果應(yīng)用程序執(zhí)行多個(gè)draw調(diào)用,并且為每個(gè)draw調(diào)用綁定了大量資源,那么這種類型的資源跟蹤是否會(huì)變得昂貴。不是真的。Draw調(diào)用和分派不處理單個(gè)資源。紋理和緩沖區(qū)被分組為不可變的綁定集,這些綁定集被創(chuàng)建,保存對(duì)其資源的永久引用,并作為單個(gè)對(duì)象進(jìn)行跟蹤。
因此,當(dāng)在命令列表中使用某個(gè)綁定集時(shí),命令列表實(shí)例僅存儲(chǔ)對(duì)該綁定集的引用。如果綁定集已綁定,則跳過該存儲(chǔ),以便使用相同綁定重復(fù)調(diào)用 draw 不會(huì)增加跟蹤成本。我將在下一節(jié)更詳細(xì)地解釋綁定集。
另一個(gè)有助于減少資源生存期跟蹤帶來的 CPU 開銷的方法是綁定集和加速結(jié)構(gòu)上的trackLiveness設(shè)置。當(dāng)此參數(shù)設(shè)置為false時(shí),不會(huì)為該特定資源創(chuàng)建內(nèi)部引用。在這種情況下,應(yīng)用程序負(fù)責(zé)保留自己的引用,而不是在資源使用時(shí)釋放它。
綁定布局和綁定集
NVRHI 具有獨(dú)特的資源綁定模型,旨在實(shí)現(xiàn)安全性和運(yùn)行效率。如前所述,圖形或計(jì)算管道使用的各種資源被分組到綁定集中。
簡言之,綁定集是綁定到管道中特定插槽的資源視圖數(shù)組。例如,綁定集可能包含綁定到插槽t1的結(jié)構(gòu)化緩沖區(qū) SRV 、綁定到插槽u0的單個(gè)紋理 mip 級(jí)別的 UAV 以及綁定到插槽b2的常量緩沖區(qū)。集合中的所有綁定共享相同的可見性遮罩(著色器階段將看到該綁定)和寄存器空間,兩者都由綁定布局指定。
綁定布局是 D3D12 根簽名和 Vulkan 描述符集布局的 NVRHI 版本。綁定布局類似于綁定集的模板。它聲明哪些資源類型綁定到哪些插槽,但不說明使用了哪些特定資源。
與根簽名和描述符集布局一樣, NVHRI 綁定布局用于創(chuàng)建管道??梢允褂枚鄠€(gè)綁定布局創(chuàng)建單個(gè)管道。根據(jù)資源的修改頻率將資源分為不同的組,或者將不同的資源集綁定到不同的管道階段,這些都很有用。
以下代碼示例顯示了如何使用一個(gè)綁定布局創(chuàng)建基本計(jì)算管道:
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);
只能從匹配的綁定布局創(chuàng)建綁定集。匹配意味著布局必須具有相同數(shù)量、相同類型、綁定到相同插槽、順序相同的項(xiàng)目。這看起來可能是冗余的, D3D12 和 Vulkan API 在其描述符系統(tǒng)中的冗余更少。這種冗余非常有用:它使代碼更加明顯,并且允許 NVRHI 驗(yàn)證層捕獲更多的 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);
由于綁定集描述符也包含創(chuàng)建綁定布局所需的幾乎所有信息,因此可以通過一個(gè)函數(shù)調(diào)用同時(shí)創(chuàng)建這兩個(gè)信息。這在創(chuàng)建僅需要一個(gè)綁定集的某些渲染過程時(shí)可能很有用。
#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.
綁定集是不可變的。創(chuàng)建綁定集時(shí), NVRHI 從 D3D12 上的堆中分配描述符,或在 Vulkan 上創(chuàng)建描述符集,并用必要的資源視圖填充它。
稍后,當(dāng)綁定集用于繪制或分派調(diào)用時(shí),綁定操作是輕量級(jí)的,并轉(zhuǎn)換為相應(yīng)的圖形 API 綁定調(diào)用。渲染時(shí)不會(huì)創(chuàng)建或復(fù)制描述符。
自動(dòng)資源狀態(tài)跟蹤
在 D3D12 和 Vulkan API 中,改變資源狀態(tài)并在圖形管道中引入依賴關(guān)系的顯式屏障都是一個(gè)重要部分。它們?cè)试S應(yīng)用程序最小化管道依賴項(xiàng)和氣泡的數(shù)量,并優(yōu)化它們的位置。通過從驅(qū)動(dòng)程序中刪除該邏輯,它們同時(shí)減少了 CPU 開銷。這主要與繪制大量幾何體的緊密渲染循環(huán)有關(guān)。大多數(shù)情況下,尤其是在編寫新的渲染代碼時(shí),處理障礙非常煩人且容易出現(xiàn)錯(cuò)誤。
NVHRI 實(shí)現(xiàn)了一個(gè)系統(tǒng),該系統(tǒng)跟蹤每個(gè)資源的狀態(tài),以及每個(gè)命令列表的子資源(可選)。當(dāng)命令與資源交互時(shí),資源將轉(zhuǎn)換為該命令所需的狀態(tài)(如果尚未處于該狀態(tài))。例如,writeTexture命令將紋理轉(zhuǎn)換為CopyDest狀態(tài),隨后從紋理讀取的繪制操作將紋理轉(zhuǎn)換為ShaderResources狀態(tài)。
當(dāng)兩個(gè)連續(xù)命令的資源處于UnorderedAccess狀態(tài)時(shí),將應(yīng)用特殊處理:不涉及轉(zhuǎn)換,但在命令之間插入無人機(jī)屏障。如有必要,可以暫時(shí)禁用無人機(jī)屏障的插入。
我前面說過, NVRHI 會(huì)根據(jù)每個(gè)命令列表跟蹤每個(gè)資源的狀態(tài)。應(yīng)用程序可以以任意順序或并行方式記錄多個(gè)命令列表,并在每個(gè)命令列表中以不同方式使用相同的資源。因此,您無法全局或每個(gè)設(shè)備跟蹤資源狀態(tài),因?yàn)樵谟涗浢盍斜頃r(shí)需要導(dǎo)出屏障。執(zhí)行命令列表時(shí),全局跟蹤可能不會(huì)按照與設(shè)備命令隊(duì)列上實(shí)際資源使用情況相同的順序進(jìn)行。
因此,您可以分別跟蹤每個(gè)命令列表中的資源狀態(tài)。在某種意義上,這可以看作是一個(gè)微分方程。您知道命令列表中的狀態(tài)是如何變化的,但不知道邊界條件,也就是說,當(dāng)您按執(zhí)行順序進(jìn)入和退出命令列表時(shí),每個(gè)資源都處于哪個(gè)狀態(tài)。
應(yīng)用程序必須為每個(gè)資源提供邊界條件。有兩種方法可以做到這一點(diǎn):
Explicit:打開命令列表后使用beginTrackingTextureState和beginTrackingBufferState功能,關(guān)閉命令列表前使用setTextureState和setBufferState功能。
Automatic:創(chuàng)建資源時(shí)使用TextureDesc和BufferDesc結(jié)構(gòu)的initialState和keepInitialState字段。然后,使用資源的每個(gè)命令列表在進(jìn)入命令列表時(shí)都假定它處于初始狀態(tài),并在離開命令列表之前將其轉(zhuǎn)換回初始狀態(tài)。
在這里,您 MIG 想知道如何避免資源狀態(tài)跟蹤的 CPU 開銷,或者手動(dòng)優(yōu)化屏障放置。好吧,你可以!命令列表具有setEnableAutomaticBarriers功能,可完全禁用自動(dòng)安全柵。在此模式下,在需要屏障的位置使用setTextureState和setBufferState功能。它仍然使用相同的狀態(tài)跟蹤邏輯,但頻率可能更低。
上傳管理
NVRHI 自動(dòng)化了現(xiàn)代圖形 API 的另一個(gè)方面,這一點(diǎn)通常很煩人。這就是 GPU 對(duì)上傳緩沖區(qū)的管理和對(duì)其使用情況的跟蹤。
通常,當(dāng)必須從 CPU 對(duì)每幀或每幀多次更新某些紋理或緩沖區(qū)時(shí),會(huì)分配一個(gè)分級(jí)緩沖區(qū),其大小比資源內(nèi)存需求大數(shù)倍。這將在 GPU 上啟用多個(gè)正在運(yùn)行的幀?;蛘?,大型暫存緩沖區(qū)的部分在運(yùn)行時(shí)進(jìn)行子分配。使用 NVRHI 實(shí)現(xiàn)相同的策略是可能的,但是有一個(gè)內(nèi)置的實(shí)現(xiàn)可以很好地適用于大多數(shù)用例。
每個(gè) NVRHI 命令列表都有自己的上載管理器。調(diào)用writeBuffer或writeTexture時(shí),上載管理器會(huì)嘗試查找 GPU 不再使用的現(xiàn)有緩沖區(qū),該緩沖區(qū)可以容納必要的數(shù)據(jù)。如果沒有可用的緩沖區(qū),將創(chuàng)建一個(gè)新的緩沖區(qū)并將其添加到上載管理器的池中。將提供的數(shù)據(jù)復(fù)制到該緩沖區(qū)中,然后將復(fù)制命令添加到命令列表中。 GPU 使用的緩沖區(qū)的跟蹤是自動(dòng)執(zhí)行的。
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));
上載管理器從不釋放其緩沖區(qū),也不會(huì)與其他命令列表共享緩沖區(qū)。也許一個(gè)應(yīng)用程序正在進(jìn)行大量的上傳,例如在場(chǎng)景加載期間,然后切換到上傳強(qiáng)度較小的操作模式。在這種情況下,最好為上傳活動(dòng)創(chuàng)建一個(gè)單獨(dú)的命令列表,并在上傳完成后釋放它。這將釋放與命令列表關(guān)聯(lián)的上載緩沖區(qū)。
無需等待 GPU 完成從上載緩沖區(qū)復(fù)制數(shù)據(jù)。在復(fù)制完成之前,前面描述的資源生存期跟蹤系統(tǒng)不會(huì)釋放上載緩沖區(qū)。
與圖形 API 的交互
有時(shí),有必要避開抽象層,直接使用底層圖形 API 進(jìn)行操作。也許您必須使用 NVRHI 不支持的某些功能,在示例應(yīng)用程序中演示一些 API 用法,或者使可移植呈現(xiàn)代碼與來自其他地方的本機(jī)資源一起工作。 NVRHI 使做這些事情相對(duì)容易。
每個(gè) NVRHI 對(duì)象都有一個(gè)getNativeObject函數(shù),該函數(shù)返回所需類型的底層 API 資源。預(yù)期的類型被傳遞給該函數(shù),如果該類型可用,它只返回非 NULL 值,以提供某種類型安全性。
支持的類型包括ID3D11Device或ID3D12Resource等接口和vk::Image等句柄。此外, NVRHI 紋理對(duì)象具有g(shù)etNativeView功能,可以創(chuàng)建和返回紋理視圖,如 SRV 或 UAV 。
例如,為了在 NVRHI 命令列表的中間發(fā)布一些本地的 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);
著色器置換
這里要提到的最后一個(gè)生產(chǎn)力特性是 NVRHI 附帶的批處理著色器編譯器。這是一項(xiàng)可選功能,沒有它, NVRHI 完全可以正常工作。 NVRHI 接受通過其他方式編譯的著色器。盡管如此,它還是一個(gè)有用的工具。
通常需要使用多個(gè)預(yù)處理器定義組合編譯同一著色器。但是,例如, VisualStudio 為著色器編譯提供的本機(jī)工具根本無法輕松完成此任務(wù)。
NVRHI 著色器編譯器正好解決了這個(gè)問題。由列出著色器源文件和編譯選項(xiàng)的文本文件驅(qū)動(dòng),它生成選項(xiàng)排列并調(diào)用底層編譯器( DXC 或 FXC )生成二進(jìn)制文件。然后,同一著色器的不同版本的二進(jìn)制文件被打包成一個(gè)自定義塊格式的文件,該文件可以使用《nvrhi/common/shader-blob.h》中聲明的函數(shù)進(jìn)行處理。
應(yīng)用程序可以加載包含所有著色器排列的文件,并將其連同預(yù)處理器定義及其值的列表一起傳遞給nvrhi::utils::createShaderPermutation或nvrhi::utils::createShaderLibraryPermutation。如果文件中存在請(qǐng)求的置換,則會(huì)創(chuàng)建相應(yīng)的著色器對(duì)象。如果沒有,將生成一條錯(cuò)誤消息。
除了置換處理之外,著色器編譯器還有其他很好的功能。首先,它掃描源文件以構(gòu)建包含在每個(gè)文件中的標(biāo)題樹。它檢測(cè)是否修改了任何標(biāo)題,以及是否必須重建特定著色器。其次,它可以使用所有可用的 CPU 內(nèi)核并行構(gòu)建所有過時(shí)的著色器。
結(jié)論
在這篇文章中,我介紹了 NVRHI 的一些最重要的功能,在我看來,使用這些功能是一種樂趣。
關(guān)于作者
Alexey Panteleev 是 NVIDIA 開發(fā)人員和性能技術(shù)團(tuán)隊(duì)的杰出工程師,他專注于新渲染技術(shù)的優(yōu)化、產(chǎn)品化和集成。他最近的工作包括地震 II RTX 、帶有 RTX 的地雷探測(cè)器以及各種技術(shù)演示和樣品,如 ReSTIR 和小行星。亞歷克賽擁有博士學(xué)位。莫斯科工程和物理研究所(梅菲州立大學(xué))計(jì)算機(jī)科學(xué)專業(yè)。
審核編輯:郭婷
-
接口
+關(guān)注
關(guān)注
33文章
8459瀏覽量
150748 -
NVIDIA
+關(guān)注
關(guān)注
14文章
4862瀏覽量
102722 -
API
+關(guān)注
關(guān)注
2文章
1475瀏覽量
61760
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論