• <xmp id="om0om">
  • <table id="om0om"><noscript id="om0om"></noscript></table>
  • 內容創建/渲染

    在 HLSL 中解鎖 GPU 內部架構

    ?

    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

    我們opCodeparameter 標識您感興趣的特定操作。操作代碼在nvShaderExtnEnums.hNVAPI 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 頭文件中的注釋。有關更多用例和內部函數示例,請參閱以下資源:

    ?

    0

    標簽

    人人超碰97caoporen国产