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 的独有功能。
学习资源
如果你不熟悉 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);