SPIRV-Cross

SPIRV-Cross 是一个用于解析和转换 SPIR-V 到其他着色器语言的工具。

CI Build Status

特性

SPIRV-Cross 努力从 SPIR-V 中生成可读且简洁的输出。 目标是生成看起来像是人编写的 GLSL 或 MSL,而不是笨拙的 IR/汇编式代码。

注意:各个功能预计已基本完成,但可能某些晦涩的 GLSL 功能尚未得到支持。 但是,在此阶段,大多数缺失的功能预计都是“简单”的改进。

构建

SPIRV-Cross 已经在 Linux、iOS/OSX、Windows 和 Android 上进行了测试。 CMake 是主要的构建系统。

注意:主分支重命名

根据 Khronos 策略,在 2023-01-12,master 已重命名为 main

Linux 和 macOS

推荐使用 CMake 构建,因为它是持续集成中唯一经过测试的构建系统。 它也是唯一具有安装命令和其他有用构建系统功能的构建系统。

但是,如果您只关心 CLI 工具,则可以在命令行上运行 make 作为备选方案。

需要非旧版 GCC (4.8+) 或 Clang (3.x+) 编译器,因为 SPIRV-Cross 广泛使用 C++11。

Windows

推荐使用 CMake 构建,这是定位 MSVC 的唯一方法。 基于 MinGW-w64 的编译可以使用 make 作为备选方案。

Android

SPIRV-Cross 在这里仅用作库。 使用 CMake 构建将 SPIRV-Cross 链接到您的项目。

C++ 异常

make 和 CMake 构建风格提供了将异常视为断言的选项。 要禁用 make 的异常,只需将 SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS=1 附加到命令行。 对于 CMake,附加 -DSPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS=ON。 默认情况下,启用异常。

静态、共享和 CLI

您可以使用 -DSPIRV_CROSS_STATIC=ON/OFF -DSPIRV_CROSS_SHARED=ON/OFF -DSPIRV_CROSS_CLI=ON/OFF 来控制构建(和安装)哪些模块。

安装 SPIRV-Cross (vcpkg)

或者,您可以使用 vcpkg 依赖项管理器构建和安装 SPIRV-Cross

git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh
./vcpkg integrate install
./vcpkg install spirv-cross

vcpkg 中的 SPIRV-Cross 端口由 Microsoft 团队成员和社区贡献者保持最新。 如果版本已过期,请在 vcpkg 存储库上创建一个 issue 或 pull request

用法

使用 C++ API

C++ API 是 SPIRV-Cross 的主要 API。 有关比本 README 中提供的更深入的文档,请查看 Wiki注意:不保证此 API 是 ABI 稳定的,强烈建议静态链接到此 API。 API 通常非常稳定,但它可能会随着时间的推移而变化,有关更高的稳定性,请参阅 C API。

要执行反射并转换为其他着色器语言,您可以使用 SPIRV-Cross API。 例如

#include "spirv_glsl.hpp"
#include <vector>
#include <utility>

extern std::vector<uint32_t> load_spirv_file();

int main()
{
	// Read SPIR-V from disk or similar.
	std::vector<uint32_t> spirv_binary = load_spirv_file();

	spirv_cross::CompilerGLSL glsl(std::move(spirv_binary));

	// The SPIR-V is now parsed, and we can perform reflection on it.
	spirv_cross::ShaderResources resources = glsl.get_shader_resources();

	// Get all sampled images in the shader.
	for (auto &resource : resources.sampled_images)
	{
		unsigned set = glsl.get_decoration(resource.id, spv::DecorationDescriptorSet);
		unsigned binding = glsl.get_decoration(resource.id, spv::DecorationBinding);
		printf("Image %s at set = %u, binding = %u\n", resource.name.c_str(), set, binding);

		// Modify the decoration to prepare it for GLSL.
		glsl.unset_decoration(resource.id, spv::DecorationDescriptorSet);

		// Some arbitrary remapping if we want.
		glsl.set_decoration(resource.id, spv::DecorationBinding, set * 16 + binding);
	}

	// Set some options.
	spirv_cross::CompilerGLSL::Options options;
	options.version = 310;
	options.es = true;
	glsl.set_common_options(options);

	// Compile to GLSL, ready to give to GL driver.
	std::string source = glsl.compile();
}

使用 C API 封装器

为了方便 C 兼容性和与外语编程语言的兼容性,提供了 C89 兼容的 API 封装器。 与 C++ API 不同,此封装器的目标是完全稳定,包括 API 和 ABI 两方面。 这是将 SPIRV-Cross 构建为共享库时唯一支持的接口。

封装器的一个重要点是,所有内存分配都包含在 spvc_context 中。 这大大简化了 API 的使用。 但是,您应该尽快销毁上下文,或者如果您打算很快再次重用 spvc_context 对象,请使用 spvc_context_release_allocations()

大多数函数返回一个 spvc_result,其中 SPVC_SUCCESS 是唯一的成功代码。 为了简洁起见,下面的代码没有进行任何错误检查。

#include <spirv_cross_c.h>

const SpvId *spirv = get_spirv_data();
size_t word_count = get_spirv_word_count();

spvc_context context = NULL;
spvc_parsed_ir ir = NULL;
spvc_compiler compiler_glsl = NULL;
spvc_compiler_options options = NULL;
spvc_resources resources = NULL;
const spvc_reflected_resource *list = NULL;
const char *result = NULL;
size_t count;
size_t i;

// Create context.
spvc_context_create(&context);

// Set debug callback.
spvc_context_set_error_callback(context, error_callback, userdata);

// Parse the SPIR-V.
spvc_context_parse_spirv(context, spirv, word_count, &ir);

// Hand it off to a compiler instance and give it ownership of the IR.
spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir, SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, &compiler_glsl);

// Do some basic reflection.
spvc_compiler_create_shader_resources(compiler_glsl, &resources);
spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_UNIFORM_BUFFER, &list, &count);

for (i = 0; i < count; i++)
{
    printf("ID: %u, BaseTypeID: %u, TypeID: %u, Name: %s\n", list[i].id, list[i].base_type_id, list[i].type_id,
           list[i].name);
    printf("  Set: %u, Binding: %u\n",
           spvc_compiler_get_decoration(compiler_glsl, list[i].id, SpvDecorationDescriptorSet),
           spvc_compiler_get_decoration(compiler_glsl, list[i].id, SpvDecorationBinding));
}

// Modify options.
spvc_compiler_create_compiler_options(compiler_glsl, &options);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 330);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_FALSE);
spvc_compiler_install_compiler_options(compiler_glsl, options);

spvc_compiler_compile(compiler_glsl, &result);
printf("Cross-compiled source: %s\n", result);

// Frees all memory we allocated so far.
spvc_context_destroy(context);

链接

CMake add_subdirectory()

如果您使用 CMake 并希望静态链接到 SPIRV-Cross,这是推荐的方法。

将 SPIRV-Cross 集成到自定义构建系统中

要将 SPIRV-Cross 添加到您自己的代码库中,只需从根目录复制源文件和头文件,并构建您需要的相关 .cpp 文件。 确保使用 C++11 支持进行构建,例如 GCC 和 Clang 中的 -std=c++11。 或者,Makefile 在构建期间生成一个 libspirv-cross.a 静态库,可以链接到其中。

作为系统库链接到 SPIRV-Cross

可以将 SPIRV-Cross 作为系统库安装时链接到它,这主要与类 Unix 平台相关。

pkg-config

对于基于 Unix 的系统,为 C API 安装了 pkg-config,例如

$ pkg-config spirv-cross-c-shared --libs --cflags
-I/usr/local/include/spirv_cross -L/usr/local/lib -lspirv-cross-c-shared
CMake

如果安装了项目,可以使用 find_package() 找到它,例如

cmake_minimum_required(VERSION 3.5)
set(CMAKE_C_STANDARD 99)
project(Test LANGUAGES C)

find_package(spirv_cross_c_shared)
if (spirv_cross_c_shared_FOUND)
        message(STATUS "Found SPIRV-Cross C API! :)")
else()
        message(STATUS "Could not find SPIRV-Cross C API! :(")
endif()

add_executable(test test.c)
target_link_libraries(test spirv-cross-c-shared)

test.c

#include <spirv_cross_c.h>

int main(void)
{
        spvc_context context;
        spvc_context_create(&context);
        spvc_context_destroy(context);
}

CLI

CLI 适用于基本的交叉编译任务,但它无法支持 API 可以实现的全部灵活性。 下面是一些例子。

使用 glslang 从 GLSL 创建 SPIR-V 文件

glslangValidator -H -V -o test.spv test.frag

将 SPIR-V 文件转换为 GLSL ES

glslangValidator -H -V -o test.spv shaders/comp/basic.comp
./spirv-cross --version 310 --es test.spv

转换为桌面 GLSL

glslangValidator -H -V -o test.spv shaders/comp/basic.comp
./spirv-cross --version 330 --no-es test.spv --output test.comp

禁用美化优化

glslangValidator -H -V -o test.spv shaders/comp/basic.comp
./spirv-cross --version 310 --es test.spv --output test.comp --force-temporary

使用从 C++ 后端生成的着色器

请参阅 samples/cpp,其中一些 GLSL 着色器被编译为 SPIR-V,反编译为 C++ 并使用测试数据运行。 阅读这些示例应该可以解释如何使用 C++ 接口。 包含一个简单的 Makefile 用于构建目录中的所有着色器。

实现说明

当使用 SPIR-V 和 SPIRV-Cross 作为在高级语言之间进行交叉编译的中间步骤时,需要考虑一些事项,因为一种高级语言使用的并非所有特性都必然受到目标着色器语言的本地支持。 SPIRV-Cross 旨在提供以干净和稳健的方式处理这些场景所需的工具,但需要一些手动操作才能保持兼容性。

HLSL 源码到 GLSL

HLSL 入口点

当使用从 HLSL 编译的 SPIR-V 着色器时,您需要注意一些额外的事情。 首先,确保正确使用了入口点。 如果您忘记在 glslangValidator (-e MyFancyEntryPoint) 中正确设置入口点,您可能会遇到此错误消息

Cannot end a function before ending the current block.
Likely cause: If this SPIR-V was created from glslang HLSL, make sure the entry point is valid.
顶点/片段接口链接

HLSL 依赖于语义来有效地将着色器阶段链接在一起。 在 glslang 生成的 SPIR-V 中,从 HLSL 到 GLSL 的转换最终看起来像

struct VSOutput {
   // SV_Position is rerouted to gl_Position
   float4 position : SV_Position;
   float4 coord : TEXCOORD0;
};

VSOutput main(...) {}
struct VSOutput {
   float4 coord;
}
layout(location = 0) out VSOutput _magicNameGeneratedByGlslang;

虽然这可行,但请注意顶点阶段和片段阶段中使用的结构体的类型。 如果顶点阶段和片段阶段的结构类型名称不同,则可能会出现问题。

您可以使用反射接口来强制结构体类型的名称。

// Something like this for both vertex outputs and fragment inputs.
compiler.set_name(varying_resource.base_type_id, "VertexFragmentLinkage");

某些平台可能需要顶点输出和片段输入的变量名称相同。(例如 MacOSX)要基于位置重命名变量,请添加

--rename-interface-variable <in|out> <location> <new_variable_name>

HLSL 源码到旧版 GLSL/ESSL

HLSL 倾向于发出 varying 结构体类型以在顶点和片段之间传递数据。 这在旧版 GL/GLES 目标中不受支持,因此为了支持这一点,varying 结构体被展平。 这是自动完成的,但 API 用户可能需要知道正在发生这种情况才能支持所有情况。

现代 GLES 代码像这样

struct Output {
   vec4 a;
   vec2 b;
};
out Output vout;

被转换成

struct Output {
   vec4 a;
   vec2 b;
};
varying vec4 Output_a;
varying vec2 Output_b;

请注意,现在,结构体名称和成员名称都将参与顶点和片段之间的链接接口,因此 API 用户可能需要确保结构体名称和成员名称都匹配,以便顶点输出和片段输入可以正确链接。

为不支持它的后端(GLSL)分离图像采样器(HLSL/Vulkan)

您需要记住的另一件事是,在 HLSL 中使用采样器和纹理时,这些是可分离的,并且与 GLSL 不直接兼容。 如果您需要在桌面 GL/GLES 中使用此功能,则需要在调用 Compiler::compile 之前先调用 Compiler::build_combined_image_samplers,否则您将收到异常。

// From main.cpp
// Builds a mapping for all combinations of images and samplers.
compiler->build_combined_image_samplers();

// Give the remapped combined samplers new names.
// Here you can also set up decorations if you want (binding = #N).
for (auto &remap : compiler->get_combined_image_samplers())
{
   compiler->set_name(remap.combined_id, join("SPIRV_Cross_Combined", compiler->get_name(remap.image_id),
            compiler->get_name(remap.sampler_id)));
}

如果您的目标是 Vulkan GLSL,--vulkan-semantics 将按您的预期发出单独的图像采样器。 命令行客户端会自动调用 Compiler::build_combined_image_samplers,但是如果您调用的是库,则需要自己执行此操作。

对于不支持它们的后端(pre HLSL 5.1 / GLSL)的描述符集(Vulkan GLSL)

描述符集是 Vulkan 独有的,因此请确保将描述符集 + 绑定重新映射到平面绑定方案(始终将 set 设置为 0),以便其他 API 可以理解绑定。 这可以使用 Compiler::set_decoration(id, spv::DecorationDescriptorSet) 完成。 对于 MSL 和 HLSL 等其他后端,可以使用描述符集,但有一些小的注意事项,请参见下文。

MSL 2.0+

Metal 支持间接参数缓冲区(--msl-argument-buffers)。 在这种情况下,描述符集成为参数缓冲区,并且绑定被映射到参数缓冲区内的 [[id(N)]]。 一个奇怪的地方是,资源数组会消耗多个 ID,而 Vulkan 则不会。 这可以通过着色器创作阶段解决,也可以根据需要重新映射绑定以避免重叠。 还有一个丰富的 API 来声明重新映射方案,该方案旨在像 Vulkan 中的管线布局一样工作。 请参阅 CompilerMSL::add_msl_resource_binding。 例如,重新映射组合的图像采样器必须在 MSL 中拆分为两个绑定,因此可以分别为纹理和采样器绑定声明一个 ID。

HLSL - SM 5.1+

在 SM 5.1+ 中,描述符集绑定直接被解释为寄存器空间。 然而,在 HLSL 中,资源数组会消耗多个绑定槽,而 Vulkan 不会,因此如果 SPIR-V 的创作没有考虑到这一点,则可能会发生重叠。 这可以通过着色器创作阶段解决(不要分配重叠的绑定),也可以在 SPIRV-Cross 中根据需要重新映射绑定以避免重叠。

对于不支持显式位置的目标,按名称链接(旧版 GLSL/ESSL)

现代的 GLSL 和 HLSL 源码(以及 SPIR-V)依赖于显式的 `layout(location)` 限定符来引导着色器阶段之间的链接过程,但较旧的 GLSL 则依赖于符号名称来执行链接。当发出旧版本的着色器时,这些 layout 语句将被移除,因此 API 用户必须确保 I/O 变量的名称经过清理,以便链接能够正常工作。反射 API 可以使用 Compiler::set_name 及其友元函数来重命名变量、结构体类型和结构体成员,以处理这些情况。

裁剪空间约定

SPIRV-Cross 可以通过启用 CompilerGLSL::Options.vertex.fixup_clipspace 对 `gl_Position`/`SV_Position` 执行一些常见的裁剪空间转换。虽然这很方便,但建议修改投影矩阵,因为这样可以达到同样的效果。

对于 GLSL 目标,启用此选项会将假定 `[0, w]` 深度范围(Vulkan / D3D / Metal)的着色器转换为 `[-w, w]` 范围。对于 MSL 和 HLSL 目标,启用此选项会将 `[-w, w]` 深度范围(OpenGL)的着色器转换为 `[0, w]` 深度范围。

默认情况下,CLI 不会启用 `fixup_clipspace`,但在 API 中,你可能希望使用 CompilerGLSL::set_options() 设置一个显式值。

也支持 `gl_Position` 和类似变量的 Y 轴翻转。 不建议使用此功能,因为依赖顶点着色器 Y 轴翻转往往会变得非常混乱。要启用此功能,请在 CLI 中设置 CompilerGLSL::Options.vertex.flip_vert_y--flip-vert-y

保留标识符

在交叉编译时,某些标识符被认为是被实现保留的。SPIRV-Cross 生成的代码不能发出这些标识符,因为它们被保留并用于各种内部目的,此类变量通常会显示为 `_RESERVED_IDENTIFIER_FIXUP_` 或类似的名称,以便更明显地表明标识符已被重命名。

反射输出将遵循 SPIR-V 模块中指定的精确名称。它可能不是 C 意义上的有效标识符,因为它可能包含非字母数字/非下划线字符。

当前实现假设的保留标识符是(伪正则表达式):

结构体成员也有一个保留的标识符

贡献

欢迎对 SPIRV-Cross 做出贡献。有关详细信息,请参见测试和许可部分。

测试

SPIRV-Cross 维护着一套着色器测试套件,其中包含经过 glslangValidator/spirv-as 往返处理后,再通过 SPIRV-Cross 处理后的参考输出。参考文件存储在存储库中,以便能够跟踪回归。

所有 pull request 都应确保测试输出不会意外更改。可以使用以下命令进行测试:

./checkout_glslang_spirv_tools.sh # Checks out glslang and SPIRV-Tools at a fixed revision which matches the reference output.
./build_glslang_spirv_tools.sh    # Builds glslang and SPIRV-Tools.
./test_shaders.sh                 # Runs over all changes and makes sure that there are no deltas compared to reference files.

./test_shaders.sh 当前需要使用 GCC/Clang 设置的 Makefile。但是,在 Windows 上,如果未设置 MinGW 环境,这可能会非常不方便。要使用你使用 CMake(或其他方式)构建的 spirv-cross 二进制文件,你可以传入一个环境变量,如下所示:

SPIRV_CROSS_PATH=path/to/custom/spirv-cross ./test_shaders.sh

但是,在改进 SPIRV-Cross 时,当然存在参考输出应该更改的合理情况。在这种情况下,运行

./update_test_shaders.sh          # SPIRV_CROSS_PATH also works here.

来更新参考文件,并将这些更改作为 pull request 的一部分包含在内。在更新参考文件时,请始终确保运行正确版本的 glslangValidator 以及 SPIRV-Tools。请参阅 checkout_glslang_spirv_tools.sh,了解当前预期的修订版本。修订版本会定期更改。

简而言之,主分支应该始终能够运行 ./test_shaders.py shaders 及其友元函数而不会失败。 SPIRV-Cross 使用 Travis CI 来测试所有 pull request,因此如果你在本地运行遇到问题,则不需要严格地自己执行测试。但是,不通过 Travis 测试的 pull request 将不会被接受。

在向 SPIRV-Cross 添加对新功能的支持时,应添加一个新的着色器和参考文件,其中涵盖了相关的新着色器功能的用法。Travis CI 通过运行 ctest 使用 CMake 运行测试套件。这是 ./test_shaders.sh 的更直接的替代方案。

许可

新文件的贡献者应在每个新源代码文件的顶部添加一个版权头,其中包含其版权以及 Apache 2.0 许可存根。

格式化

SPIRV-Cross 使用 clang-format 自动格式化代码。请在使用 .clang-format 中的样式表使用 clang-format 自动格式化代码,然后再提交 pull request。

为了简化操作,可以使用 format_all.sh 脚本来格式化库中的所有源文件。在此目录中,从命令行运行以下命令

./format_all.sh

回归测试

shaders/ 中维护着一系列着色器,用于回归测试。当前的参考输出包含在 reference/ 中。可以运行 ./test_shaders.py shaders 来执行回归测试。

有关更多信息,请参见 ./test_shaders.py --help

Metal 后端

要测试往返路径 GLSL -> SPIR-V -> MSL,可以添加 --msl,例如 ./test_shaders.py --msl shaders-msl

HLSL 后端

要测试往返路径 GLSL -> SPIR-V -> HLSL,可以添加 --hlsl,例如 ./test_shaders.py --hlsl shaders-hlsl

更新回归测试

当发现合法的更改时,使用 --update 标志更新回归文件。 否则,./test_shaders.py 将失败并显示错误代码。

Mali 离线编译器周期计数

要在经过 spirv-cross 之前和之后获得静态着色器周期计数的 CSV 文件,请将 --malisc 标志添加到 ./test_shaders。 这需要在 PATH 中安装 Mali 离线编译器。