NVIDIA GPU 指令集中存在一些標準圖形 API 中不包含的有用內部函數。
更新自 2016 年原始博文,添加了有關 DirectX 和 Vulkan 中新的內部結構和跨供應商 API 的信息。
例如,著色器可以使用線程束 shuffle 指令在線程束中的線程之間交換數據,而無需通過共享內存,這在沒有共享內存的像素著色器中尤其重要。或者,著色器可以在全局內存中對半精度浮點數執行原子添加。
我們的文章 線程之間的讀取:著色器內部函數 向您展示了內部指令的工作原理。現在,我將帶您深入了解如何讓它們在 DirectX 中運行。
在標準 DirectX 或 OpenGL 中,所有這些內部結構都不可能實現。[2023 年:這不再是事實。更多信息將在本文稍后分享。]但它們在 CUDA 中得到了多年的支持和詳細記錄。在 DirectX 中支持它們的機制已經推出一段時間,但沒有得到廣泛的記錄。我的系統恰好從 2014 年 10 月開始就有舊的 NVAPI 版本 343,該版本(可能是更早的版本)在 DirectX 中支持內部函數。本文介紹了在 DirectX 中使用它們的機制。
遺憾的是,與 OpenGL 或 Vulkan 不同,DirectX 沒有針對特定供應商的擴展程序的原生機制。但是,仍然可以通過自定義內部函數在 DirectX 11 或 12 中使用所有這些功能。這種機制在圖形驅動程序中實現,并可通過 NVAPI 庫 來訪問。
擴展 HLSL 著色器
要使用內部函數,必須將其編碼為常規 HLSL 指令的特殊序列,以便驅動識別并轉換為預期操作。這些特殊序列在 NVAPI SDK 隨附的其中一個頭文件中提供:nvHLSLExtns.h
.
這些指令序列的一個重要方面是,它們必須在不進行優化的情況下通過 HLSL 編譯器,因為編譯器不理解它們的真正含義,因此可以修改它們,改變它們的順序,甚至完全刪除它們。
為了防止編譯器這樣做,序列在 UAV 緩沖區上使用原子操作。HLSL 編譯器無法優化這些指令,因為它不知道可能的依賴項,即使沒有依賴項。UAV 緩沖區基本上是假的,在通過 NVIDIA GPU 驅動程序后,實際著色器不會使用它。但應用程序仍然必須為其分配 UAV 插槽,并告訴驅動程序哪個插槽。
例如,NvShfl
實現 Warp shuffle 的函數類似于以下代碼示例,nvHLSLExtns.h
:
int NvShfl(int val, uint srcLane, int width = NV_WARP_SIZE) { uint index = g_NvidiaExt.IncrementCounter(); g_NvidiaExt[index].src0u.x = val; // variable to be shuffled g_NvidiaExt[index].src0u.y = srcLane; // source lane g_NvidiaExt[index].src0u.z = __NvGetShflMaskFromWidth(width); g_NvidiaExt[index].opcode = NV_EXTN_OP_SHFL; // result is returned as the return value of IncrementCounter on fake UAV slot return g_NvidiaExt.IncrementCounter(); }
使用此函數的著色器類似于以下代碼示例:
// Declare that the driver should use UAV 0 to encode the instruction sequences. // It's a pixel shader with one output, so u0 is taken by the render target - use u1. #define NV_SHADER_EXTN_SLOT u1 // On DirectX12 and Shader Model 5.1, you can also define the register space for that UAV. #define NV_SHADER_EXTN_REGISTER_SPACE space0 // Include the header - note that the UAV slot has to be declared before including it. #include "nvHLSLExtns.h" Texture2D tex : register(t0); SamplerState samp : register(s0); float4 main(in float2 texCoord : UV) : SV_Target { float4 color = tex.Sample(samp, texCoord); // Use NvShfl to distribute the color from lane 0 to all other lanes in the warp. // The NvShfl function accepts and returns uint data, so use asuint/asfloat to pass float values. color.r = asfloat(NvShfl(asuint(color.r), 0)); color.g = asfloat(NvShfl(asuint(color.g), 0)); color.b = asfloat(NvShfl(asuint(color.b), 0)); color.a = asfloat(NvShfl(asuint(color.a), 0)); return color; }
這個示例看起來可能是在做一些毫無意義的事情,而且確實如此。圖形應用程序中內部函數的真實用例通常很復雜。例如,Warp shuffle 可用于優化算法(如光線消除)中的內存訪問。VXGI 中使用浮點原子來在體素化期間累加發射。但是,這些應用程序需要大量著色器和主機代碼才能正常工作。另一方面,這個示例幾乎可以插入任何像素著色器,效果很明顯。
編譯此著色器時,每次調用NvShfl
擴展到此序列中,指定或獲取寄存器名稱:
imm_atomic_alloc r1.x, u1 mov r3.yz, l(0,0,31,0) mov r3.x, r2.z store_structured u1.xyz, r1.x, l(76), r3.xyzx store_structured u1.x, r1.x, l(0), l(1) imm_atomic_alloc r0.y, u1
當此著色器通過驅動程序的 JIT 編譯器時,NvShfl
函數映射到一個 GPU 指令:
SHFL.IDX PT, R3, R3, RZ, 0x1f;
在 DirectX 11 中創建擴展著色器
要實際使用此著色器,必須以特殊方式創建其運行時對象。定期調用ID3D11Device::CreatePixelShader
這還不夠,因為驅動程序必須知道著色器打算使用內部函數。它還必須知道使用哪個 UAV 插槽。
如果您使用的是 DirectX 11,請使用NvAPI_D3D11_SetNvShaderExtnSlot
函數調用之前和之后CreatePixelShader
:
// Do this one time during app initialization. NvAPI_Initialize(); ID3D11PixelShader* pShader = nullptr; HRESULT D3DResult = E_FAIL; // First, enable compilation of intrinsics. // The second parameter is the UAV slot index that is used in the shader: u1. NvAPI_Status NvapiStatus = NvAPI_D3D11_SetNvShaderExtnSlot(pDevice, 1); if(NvapiStatus == NVAPI_OK) { // Then create the shader as usual... D3DResult = pDevice->CreatePixelShader(pBytecode, BytecodeLength, nullptr, &pShader); // And disable again by telling the driver to use an invalid UAV slot. NvAPI_D3D11_SetNvShaderExtnSlot(pDevice, ~0u); } if(FAILED(D3DResult)) { // ...Handle the error... }
此方法適用于任何可以引用 UAV 的著色器。因此,在 DirectX 11.0 中,它適用于像素和計算著色器。在 DirectX 11.1 及更高版本中,它應該適用于各種著色器。
在 DirectX 12 中創建擴展的工作流狀態對象
如果您使用的是 DirectX 12,則不存在單獨的著色器對象,而是創建完整的工作流狀態 (PSO).
還有其他各種特定于 NVIDIA 的工作流狀態擴展程序可通過 NVAPI 訪問,因此為了避免使用各種擴展程序創建 PSO 的功能組合爆炸, NVIDIA 僅制作了兩個功能,一個用于圖形,另一個用于計算,可接受使用的擴展程序列表:
NvAPI_D3D12_CreateGraphicsPipelineState
NvAPI_D3D12_CreateComputePipelineState
HLSL 擴展由NVAPI_D3D12_PSO_SET_SHADER_EXTENSION_SLOT_DESC
結構。不過,整個工作流狀態只有一個,因此,如果工作流中的兩個或多個著色器使用內部函數,它們必須為其使用相同的 UAV 插槽。
// Do this one time during app initialization. NvAPI_Initialize(); // Fill the PSO description structure D3D12_GRAPHICS_PIPELINE_STATE_DESC PsoDesc; PsoDesc.VS = { pVSBytecode, VSBytecodeLength }; // ...And so on, as usual... // Also fill the extension structure. // Use the same UAV slot index and register space that are declared in the shader. NVAPI_D3D12_PSO_SET_SHADER_EXTENSION_SLOT_DESC ExtensionDesc; ExtensionDesc.baseVersion = NV_PSO_EXTENSION_DESC_VER; ExtensionDesc.psoExtension = NV_PSO_SET_SHADER_EXTNENSION_SLOT_AND_SPACE; ExtensionDesc.version = NV_SET_SHADER_EXTENSION_SLOT_DESC_VER; ExtensionDesc.uavSlot = 1; ExtensionDesc.registerSpace = 0; // Put the pointer to the extension into an array. There can be multiple extensions enabled at one time. // Other supported extensions are: // - Extended rasterizer state // - Pass-through geometry shader, implicit or explicit // - Depth bound test const NVAPI_D3D12_PSO_EXTENSION_DESC* pExtensions[] = { &ExtensionDesc }; // Now create the PSO. ID3D12PipelineState* pPSO = nullptr; NvAPI_Status NvapiStatus = NvAPI_D3D12_CreateGraphicsPipelineState(pDevice, &PsoDesc, ARRAYSIZE(pExtensions), pExtensions, &pPSO); if(NvapiStatus != NVAPI_OK) { // ...Handle the error... } }
查詢 GPU 功能支持
最后,在嘗試使用內部函數之前,您可能想知道應用所用的設備是否實際上支持這些內部函數。有兩個 NVAPI 函數可以告訴您:
NvAPI_D3D11_IsNvShaderExtnOpCodeSupported
NvAPI_D3D12_IsNvShaderExtnOpCodeSupported
我們opCode
parameter 標識您感興趣的特定操作。操作代碼在nvShaderExtnEnums.h
NVAPI SDK 隨附的文件。例如,要測試 DirectX 11 設備是否支持 Warp shuffle,請使用以下代碼示例:
#include "nvShaderExtnEnums.h" bool bSupported = false; NvAPI_Status NvapiStatus = NvAPI_D3D11_IsNvShaderExtnOpCodeSupported(pDevice, NV_EXTN_OP_SHFL, &bSupported); if(NvapiStatus == NVAPI_OK && bSupported) { // Yay, the device is no older than 2012! }
2023 年更新:新的內部函數和跨供應商 API
NVIDIA GPU 支持的內部函數并不僅限于線程束 shuffle。事實上,線程束 shuffle 和相關函數現在可以通過 DirectX 12 和 Vulkan 中的跨供應商內部函數獲得,因此無需使用 NVAPI。有關 DirectX 12 波內部函數的更多信息,請參閱Wave 內部函數。有關 Vulkan 子組操作的更多信息,請參閱Vulkan 子組教程。
NVIDIA GPU 支持的內部函數的完整列表可在名為 nvHLSLExtns.h 的文件中找到,現已在 GitHub 上提供。此文件中聲明的函數可細分為幾個通用類別:
- 較舊的線程束運算:shuffle、vote、ballot、通道索引 (
NvShfl*
,NvAny
,NvAll
,NvBallot
,NvGetLaneId
) - 更新的線程束運算:波形匹配 (
NvWaveMatch
).NvWaveMatch
返回線程束中活動通道的遮罩,這些通道的參數值與當前通道相同。 - 特殊寄存器訪問權限(
NvGetSpecial
) - FP16、FP32 和 Uint64 變量上的擴展原子運算 (
NvInterlocked*
) - 可變速率著色(
NvGetShadingRate
,NvEvaluateAttribute*
) - 紋理足跡評估(
NvFootprint*
) - WaveMultiPrefix 函數(
NvWaveMultiPrefix*
這些函數只是基于其他內部函數構建的算法。 - 光線追蹤微圖擴展程序(
NvRtMicroTriangle*
,NvRtMicroVertex*
) - 光線追蹤著色器執行重排序(
NvHitObject
,NvReorderThread
)
更新:編譯具有正確選項的著色器
目前, NVIDIA GPU 驅動存在一個影響 HLSL 內部函數的已知問題。具體來說,如果著色器使用D3DCOMPILE_SKIP_OPTIMIZATION
標志或/Od
傳遞給 FXC 的命令行選項。如果您看到內部函數不起作用,請確保未指定此標志。
結束語
有關 NVAPI 函數和結構的更多信息,請參閱 NVAPI 頭文件中的注釋。有關更多用例和內部函數示例,請參閱以下資源:
- 線程間讀取:著色器內置函數
- NVIDIA NVAPI SDK:現在可在 GitHub /NVIDIA/nvapi 庫中找到!
- 在 Kepler 架構上實現更快的并行歸約
- CUDA 專業技巧:使用 Kepler Shuffle
- CUDA Pro 提示:利用 Warp 聚合原子操作進行優化過濾
- Shuffle:技巧與竅門(GTC 會議)
?