跳到主要内容

Vulkan 中的 HLSL

Vulkan 使用的着色器不是可读的文本格式,而是 SPIR-V 格式,不仅仅GLSL,其他着色器语言只要可以转化成Vulkan SPIR-V,那都可以使用。

其中一种语言是 DirectX 使用的高级着色语言 (HLSL)。由于 Vulkan 1.2 最近添加了一些功能,它现在是 Vulkan 第一梯队的着色器语言,可以像 GLSL 一样轻松使用。

除了少数例外情况,GLSL 提供的所有 Vulkan 功能和着色器阶段同样适用于 HLSL,包括最近添加的 Vulkan 功能,如通过硬件加速的光线追踪。另一方面,HLSL转化的SPIR-V 支持 DirectX 中尚未提供的 Vulkan 的独有功能。

what_is_spriv_dxc.png

学习资源

如果你不熟悉 HLSL,最好从 Microsoft Learn 上的 HLSL 资源开始。另一个很好的资源是 DirectX-Specs 文档。它包含了HLSL的最新着色器功能和着色器模型的内容。

从应用程序的角度来看

从应用程序的角度来看,使用 HLSL 与使用 GLSL 完全相同。由于应用程序始终使用 SPIR-V 格式的着色器,因此唯一的区别在于着色语言生成 SPIR-V 着色器使用的工具。

HLSL 到 SPIR-V 功能对照手册

在 Vulkan 中使用 HLSL 的一个很好的参考是 HLSL 到 SPIR-V 功能对照手册。它包含语义、语法、支持的功能和扩展等详细的信息,是必读的。decoder ring还有一个翻译页面,包含了Vulkan 和 DirectX 中使用的概念和术语。

Vulkan HLSL 命名空间

为了使 HLSL 与 Vulkan 兼容,引入了一个隐式命名空间,为 Vulkan 特定的功能提供接口。

语法比较

与常规编程语言类似,HLSL 和 GLSL 的语法不同。GLSL 更注重过程(像 C),而 HLSL 更注重对象(像 C++)。

以下是用两种语言编写的相同功能的着色器,快速比较它们的基本差异,包括命名空间,添加显式位置:

GLSL

#version 450

layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec3 inColor;

layout (binding = 0) uniform UBO
{
mat4 projectionMatrix;
mat4 modelMatrix;
mat4 viewMatrix;
} ubo;

layout (location = 0) out vec3 outColor;

void main()
{
outColor = inColor * float(gl_VertexIndex);
gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPosition.xyz, 1.0);
}

HLSL

struct VSInput
{
[[vk::location(0)]] float3 Position : POSITION0;
[[vk::location(1)]] float3 Color : COLOR0;
};

struct UBO
{
float4x4 projectionMatrix;
float4x4 modelMatrix;
float4x4 viewMatrix;
};

cbuffer ubo : register(b0, space0) { UBO ubo; }

struct VSOutput
{
float4 Pos : SV_POSITION;
[[vk::location(0)]] float3 Color : COLOR0;
};

VSOutput main(VSInput input, uint VertexIndex : SV_VertexID)
{
VSOutput output = (VSOutput)0;
output.Color = input.Color * float(VertexIndex);
output.Position = mul(ubo.projectionMatrix, mul(ubo.viewMatrix, mul(ubo.modelMatrix, float4(input.Position.xyz, 1.0))));
return output;
}

除了语法差异之外,内置变量的名称也有差异,例如gl_vertex在 HLSL中是`VertexIndex,参阅GLSL 到 HLSL 内置变量映射表

DirectXShaderCompiler (DXC)

与 GLSL 到 SPIR-V 的情况一样,要将 HLSL 与 Vulkan 一起使用,需要一个着色器编译器。glslang 是 GLSL 到 SPIR-V 使用的编译器,而 DirectXShaderCompiler (DXC) 是 HLSL 到 SPIR-V 的编译器。由于开源的贡献,DXC 的 SPIR-V 后端现在已经包含在官方发行版中,并且可以开箱即用。虽然 glslang 等其他着色器编译工具也支持HLSL,但 DXC 具有最完整和最新的支持,推荐使用它将 HLSL 生成 SPIR-V。

获取途径

LunarG Vulkan SDK 包括预编译的 DXC 二进制文件、库和标头。如果您正在寻找最新版本,请查看官方 DXC 存储库

使用独立编译器离线编译

通过dxc 离线编译着色器与使用 glslang 进行编译类似:

dxc.exe -spirv -T vs_6_0 -E main .\triangle.vert -Fo .\triangle.vert.spv

-T编译着色器的配置文件(vs_6_0= 顶点着色器模型 6、ps_6_0= 像素/片段着色器模型 6 等)。

-E着色器的主入口函数。

扩展是根据使用的功能隐式启用的,也可以显式指定:

dxc.exe -spirv -T vs_6_1 -E main .\input.vert -Fo .\output.vert.spv -fspv-extension=SPV_EXT_descriptor_indexing

后面就可以直接使用得到的 SPIR-V,与从 GLSL 生成的 SPIR-V 一样。

使用库运行时编译

还可以使用 DirectX 编译器 API 集成DXC到 Vulkan 应用程序中,这可以让着色器进行运行时编译。应用需要包含dxcapi.h头文件和链接dxcompiler库来集成。最简单的方法是使用动态库并将其与您的应用程序一起分发(例如 Windows 上的dxcompiler.dll)。

在运行时将 HLSL 编译为 SPIR-V 非常简单:

#include "include/dxc/dxcapi.h"

...

HRESULT hres;

// Initialize DXC library
CComPtr<IDxcLibrary> library;
hres = DxcCreateInstance(CLSID_DxcLibrary, IID_PPV_ARGS(&library));
if (FAILED(hres)) {
throw std::runtime_error("Could not init DXC Library");
}

// Initialize DXC compiler
CComPtr<IDxcCompiler3> compiler;
hres = DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&compiler));
if (FAILED(hres)) {
throw std::runtime_error("Could not init DXC Compiler");
}

// Initialize DXC utility
CComPtr<IDxcUtils> utils;
hres = DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils));
if (FAILED(hres)) {
throw std::runtime_error("Could not init DXC Utiliy");
}

// Load the HLSL text shader from disk
uint32_t codePage = DXC_CP_ACP;
CComPtr<IDxcBlobEncoding> sourceBlob;
hres = utils->LoadFile(filename.c_str(), &codePage, &sourceBlob);
if (FAILED(hres)) {
throw std::runtime_error("Could not load shader file");
}

// Select target profile based on shader file extension
LPCWSTR targetProfile{};
size_t idx = filename.rfind('.');
if (idx != std::string::npos) {
std::wstring extension = filename.substr(idx + 1);
if (extension == L"vert") {
targetProfile = L"vs_6_1";
}
if (extension == L"frag") {
targetProfile = L"ps_6_1";
}
// Mapping for other file types go here (cs_x_y, lib_x_y, etc.)
}

// Configure the compiler arguments for compiling the HLSL shader to SPIR-V
std::vector<LPCWSTR> arguments = {
// (Optional) name of the shader file to be displayed e.g. in an error message
filename.c_str(),
// Shader main entry point
L"-E", L"main",
// Shader target profile
L"-T", targetProfile,
// Compile to SPIRV
L"-spirv"
};

// Compile shader
DxcBuffer buffer{};
buffer.Encoding = DXC_CP_ACP;
buffer.Ptr = sourceBlob->GetBufferPointer();
buffer.Size = sourceBlob->GetBufferSize();

CComPtr<IDxcResult> result{ nullptr };
hres = compiler->Compile(
&buffer,
arguments.data(),
(uint32_t)arguments.size(),
nullptr,
IID_PPV_ARGS(&result));

if (SUCCEEDED(hres)) {
result->GetStatus(&hres);
}

// Output error if compilation failed
if (FAILED(hres) && (result)) {
CComPtr<IDxcBlobEncoding> errorBlob;
hres = result->GetErrorBuffer(&errorBlob);
if (SUCCEEDED(hres) && errorBlob) {
std::cerr << "Shader compilation failed :\n\n" << (const char*)errorBlob->GetBufferPointer();
throw std::runtime_error("Compilation failed");
}
}

// Get compilation result
CComPtr<IDxcBlob> code;
result->GetResult(&code);

// Create a Vulkan shader module from the compilation result
VkShaderModuleCreateInfo shaderModuleCI{};
shaderModuleCI.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
shaderModuleCI.codeSize = code->GetBufferSize();
shaderModuleCI.pCode = (uint32_t*)code->GetBufferPointer();
VkShaderModule shaderModule;
vkCreateShaderModule(device, &shaderModuleCI, nullptr, &shaderModule);

Vulkan 着色器阶段到 HLSL 的配置映射文件

使用 DXC 编译 HLSL 时,需要选择目标着色器的配置文件,配置文件的名称由着色器类型和着色器模型组成。

Vulkan 着色器阶段HLSL 目标着色器配置文件言论
VK_SHADER_STAGE_VERTEX_BITvs
VK_SHADER_STAGE_TESSELLATION_CONTROL_BIThsHLSL 中的Hull shader
VK_SHADER_STAGE_TESSELLATION_EVALUATION_BITdsHLSL 中的Domain shader
VK_SHADER_STAGE_GEOMETRY_BITgs
VK_SHADER_STAGE_FRAGMENT_BITpsHLSL 中的Pixel shader
VK_SHADER_STAGE_COMPUTE_BITcs
VK_SHADER_STAGE_RAYGEN_BIT_KHR,
VK_SHADER_STAGE_ANY_HIT_BIT_KHR,
VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR,
VK_SHADER_STAGE_MISS_BIT_KHR,
VK_SHADER_STAGE_INTERSECTION_BIT_KHR,
VK_SHADER_STAGE_CALLABLE_BIT_KHR
lib所有与光线跟踪相关的着色器都是使用lib着色器目标配置文件构建的,并且必须至少使用着色器模型 6.3(例如lib_6_3)。
VK_SHADER_STAGE_TASK_BITasHLSL 中的Amplification shader。必须至少使用 shader model 6.5(例如as_6_5)。
VK_SHADER_STAGE_MESH_BITms必须至少使用 shader model 6.5(例如ms_6_5 )。

因此,如果要编译着色器模型 6.6 功能的计算着色器,则目标着色器配置文件将是cs_6_6,光线追踪的着色器都是lib_6_3

着色器模型覆盖范围

DirectX 和 HLSL 使用固定的着色器模型来描述支持的功能集,这与 Vulkan 和 SPIR-V 在着色器中灵活添加扩展功能的方式有所不同。下表列出了 Vulkan 对 HLSL 着色器模型的覆盖范围,但不保证完整:

着色器模型支持情况备注
着色器模型 5.1 及更低版本排除没有 Vulkan 等效项的功能
着色器模型 6.0Wave 内部函数,64 位整数
着色器模型 6.1SV_ViewID、SV_Barycentrics
着色器模型 6.216 位类型,Denorm 模式
着色器模型 6.3硬件加速光线追踪
着色器模型 6.4着色器整数点积,SV_ShadingRate
着色器模型 6.5⚠️ (部分)DXR1.1(KHR 光线追踪)、
Mesh和Amplification着色器、
其他 Wave 内部函数
着色器模型 6.6⚠️ (部分)VK_NV_compute_shader_derivatives、
VK_KHR_shader_atomic_int64、
VK_EXT_descriptor_buffer、
VK_EXT_mutable_descriptor_type
着色器模型 6.7⚠️ (部分)VK_KHR_shader_quad_control,
VkPhysicalDeviceFeatures::shaderStorageImageMultisample