Vulkan 中的 HLSL

Vulkan 不直接使用人类可读的文本格式的着色器,而是使用 SPIR-V 作为中间表示。这为使用除 GLSL 之外的着色器语言打开了选项,只要它们可以针对 Vulkan SPIR-V 环境即可。

其中一种语言是 Microsoft 的高级着色语言 (HLSL),DirectX 使用了它。得益于 最近添加到 Vulkan 1.2 的内容,它现在被认为是 Vulkan 的一流着色语言,可以像使用 GLSL 一样轻松使用。

除了 一些例外,所有 GLSL 可用的 Vulkan 功能和着色器阶段也可以与 HLSL 一起使用,包括最近添加的 Vulkan 功能,如硬件加速光线追踪。另一方面,HLSL 到 SPIR-V 支持 Vulkan 独有的功能,这些功能在 DirectX 中(尚未)可用。

what_is_spriv_dxc.png

教育资源

如果您是 HLSL 的新手,一个好的起点是 Microsoft Learn 上的 HLSL 资源。另一个很好的来源是 DirectX-Specs 文档。它包含有关最新着色器功能和 HLSL 着色器模型的宝贵信息。

从应用程序的角度来看

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

HLSL 到 SPIR-V 功能映射手册

通过 SPIR-V 在 Vulkan 中使用 HLSL 的一个很好的起点是 HLSL 到 SPIR-V 功能映射手册。它包含有关语义、语法、支持的功能和扩展等详细信息,并且是必读的。解码环还提供了 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.Pos = mul(ubo.projectionMatrix, mul(ubo.viewMatrix, mul(ubo.modelMatrix, float4(input.Position.xyz, 1.0))));
	return output;
}

除了语法差异之外,内置函数使用 HLSL 名称。例如,gl_vertex 在 HLSL 中变为 VertexIndex。GLSL 到 HLSL 内置映射的列表可以在 此处找到。

DirectXShaderCompiler (DXC)

与 GLSL 到 SPIR-V 的情况一样,要在 Vulkan 中使用 HLSL,需要一个着色器编译器。虽然 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 一样。

使用库进行运行时编译

DXC 也可以使用 DirectX 编译器 API 集成到 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_BIT

vs

VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT

hs

在 HLSL 术语中称为 Hull 着色器

VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT

ds

在 HLSL 术语中称为 Domain 着色器

VK_SHADER_STAGE_GEOMETRY_BIT

gs

VK_SHADER_STAGE_FRAGMENT_BIT

ps

在 HLSL 术语中称为 Pixel 着色器

VK_SHADER_STAGE_COMPUTE_BIT

cs

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_BIT

as

在 HLSL 术语中称为放大着色器。必须至少使用着色器模型 6.5(例如 as_6_5)。

VK_SHADER_STAGE_MESH_BIT

ms

必须至少使用着色器模型 6.5(例如 ms_6_5)。

因此,例如,如果要编译针对着色器模型 6.6 功能的计算着色器,则目标着色器配置文件将为 cs_6_6。对于光线追踪的任何命中着色器,它将为 lib_6_3

着色器模型覆盖范围

DirectX 和 HLSL 使用固定的着色器模型概念来描述支持的功能集。这与 Vulkan 和 SPIR-V 的灵活扩展方式不同,后者允许向着色器添加功能。下表尝试列出 HLSL 着色器模型对 Vulkan 的覆盖范围,但不保证完整性

表 1. 着色器模型
着色器模型 支持 备注

着色器模型 5.1 及以下

不包括没有 Vulkan 等效项的功能

着色器模型 6.0

波形内在函数,64 位整数

着色器模型 6.1

SV_ViewID, SV_Barycentrics

着色器模型 6.2

16 位类型,非规范模式

着色器模型 6.3

硬件加速光线追踪

着色器模型 6.4

着色器整数点积,SV_ShadingRate

着色器模型 6.5

⚠️ (部分支持)

DXR1.1 (KHR 光线追踪), 网格和放大着色器, 附加的波形内在函数

着色器模型 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