1,214
社区成员
这篇文章最初发表在 NVIDIA 技术博客上。有关此类的更多内容,请参阅最新的 内容创建/渲染 新闻和教程。
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 指令的特殊序列,以便驱动识别并转换为预期操作。这些特殊序列在 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; color.r = asfloat(NvShfl(asuint(color.r), 0)); color.b = asfloat(NvShfl(asuint(color.b), 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;
要实际使用此着色器,必须以特殊方式创建其运行时对象。定期调用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,则不存在单独的着色器对象,而是创建完整的工作流状态 (PSO).
还有其他各种特定于 NVIDIA 的工作流状态扩展程序可通过 NVAPI 访问,因此为了避免使用各种扩展程序创建 PSO 的功能组合爆炸, NVIDIA 仅制作了两个功能,一个用于图形,另一个用于计算,可接受使用的扩展程序列表:
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... } }
最后,在尝试使用内部函数之前,您可能想知道应用所用的设备是否实际上支持这些内部函数。有两个 NVAPI 函数可以告诉您:
我们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! }
NVIDIA GPU 支持的内部函数并不仅限于线程束 shuffle。事实上,线程束 shuffle 和相关函数现在可以通过 DirectX 12 和 Vulkan 中的跨供应商内部函数获得,因此无需使用 NVAPI。有关 DirectX 12 波内部函数的更多信息,请参阅Wave 内部函数。有关 Vulkan 子组操作的更多信息,请参阅Vulkan 子组教程。
NVIDIA GPU 支持的内部函数的完整列表可在名为 nvHLSLExtns.h 的文件中找到,现已在 GitHub 上提供。此文件中声明的函数可细分为几个通用类别:
目前, NVIDIA GPU 驱动存在一个影响 HLSL 内部函数的已知问题。具体来说,如果着色器使用D3DCOMPILE_SKIP_OPTIMIZATION标志或/Od传递给 FXC 的命令行选项。如果您看到内部函数不起作用,请确保未指定此标志。
有关 NVAPI 函数和结构的更多信息,请参阅 NVAPI 头文件中的注释。有关更多用例和内部函数示例,请参阅以下资源: