third_party_glslang:基于 Khronos 标准的着色器编译与验证工具项目

用户可使用该项目进行 GLSL、ESSL、HLSL 着色器的编译、验证及转换为 SPIR-V 中间语言。核心功能包括参考级验证前端、AST 转换后端、反射信息提取及命令行工具,支持多平台构建与测试。【此简介由AI生成】

分支264Tags37
当前项目代码仓暂无内容

持续集成 持续部署 OpenSSF 评分卡

最新动态

  1. --shift-texture-binding[s] 选项不再影响组合采样器。应使用新的 --shift-combined-sampler-binding[s] 选项来独立于单独纹理控制组合采样器的绑定。将这两个选项设置为相同值可实现旧有行为。

  2. glslang 中的 spirv-remap 工具已移植到 SPIRV-Tools 代码库,作为名为 canonicalize-ids 的新优化通道,可在 spirv-opt 中使用。有关使用详情,请参见 spirv-opt --help。

  3. 现在可以将 glslang 构建为 DLL 或共享库,并且对此提供支持。

Glslang 组件及状态

包含以下几个组件:

参考验证器与 GLSL/ESSL -> AST 前端

一个 OpenGL GLSL 和 OpenGL|ES GLSL(ESSL)前端,用于参考验证以及将 GLSL/ESSL 转换为内部抽象语法树(AST)。

状态:基本完成,其结果与规范具有同等重要性。

HLSL -> AST 前端

一个 HLSL 前端,用于将 HLSL 的近似版本转换为 glslang 的 AST 形式。

状态:部分完成。语义并非参考级质量,输入未经过验证。 这与 DXC 项目 形成对比,后者获得了更大投入,并致力于实现明确的/参考级语义。

有关当前状态,请参见 issue 362issue 701

AST -> SPIR-V 后端

将 glslang 的 AST 转换为 Khronos 规定的 SPIR-V 中间语言。

状态:基本完成。

反射器

一个用于从AST获取反射信息的API,可从高级语言(HLL)源代码(非SPIR-V)中反射类型/变量等信息。

状态:已实现大量功能,但尚无衡量完整性的规范/目标。对于输入的高级语言和AST而言是准确的,但对于后续将生成的SPIR-V内容仅为近似值。

独立包装器

glslang是一个用于访问上述功能的命令行工具。

状态:已完成。

待完成任务记录在GitHub issues中。

其他参考资料

另请参阅Khronos上关于glslang作为参考前端的登录页面:

https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/

上述页面虽未保持最新,但包含了有关glslang作为参考验证器的其他信息。

如何使用Glslang

独立包装器的执行

要使用独立二进制形式,请执行glslang,它将打印使用说明。基本操作是向其提供包含着色器的文件,它将输出警告/错误,并可选择输出AST。

应用的特定阶段规则基于文件扩展名:

  • .vert 用于顶点着色器
  • .tesc 用于细分控制着色器
  • .tese 用于细分评估着色器
  • .geom 用于几何着色器
  • .frag 用于片段着色器
  • .comp 用于计算着色器

对于光线追踪管线着色器:

  • .rgen 用于光线生成着色器
  • .rint 用于光线相交着色器
  • .rahit 用于光线任意命中着色器
  • .rchit 用于光线最近命中着色器
  • .rmiss 用于光线未命中着色器
  • .rcall 用于可调用着色器

还有一个非着色器扩展名:

  • .conf 用于限制的配置文件,示例见使用说明

构建(CMake)

除了手动构建外,您还可以直接从GitHub上的main-tot release下载适用于您平台的二进制文件。这些二进制文件由构建机器人在测试成功后自动上传,始终反映main分支当前的最新代码。

依赖项

  • 一个 C++17 编译器。 (对于 MSVS:使用 2019 或更高版本。)
  • CMake:用于生成编译目标。
  • make:Linux,如果已配置,ninja 可作为替代方案。
  • Python 3.x:用于执行 SPIRV-Tools 脚本。(如果不使用 SPIRV-Tools 且“External”子目录不存在,则为可选。)
  • bison可选,但在更改语法(glslang.y)时需要。
  • googletest可选,但如果对 glslang 进行任何更改,建议使用。

构建步骤

以下步骤假设使用 Bash shell。在 Windows 上,可以使用 Git Bash shell 或其他您选择的 shell。

1) 检出此项目

cd <parent of where you want glslang to be>
git clone https://github.com/KhronosGroup/glslang.git

2) 检出外部项目

./update_glslang_sources.py

3) 配置

假设源目录为 $SOURCE_DIR,构建目录为 $BUILD_DIR。 如果 $BUILD_DIR 不存在,CMake 会为用户创建该目录。

首先更改工作目录:

cd $SOURCE_DIR

在 Linux 上构建:

cmake -B $BUILD_DIR -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$(pwd)/install"
# "Release" (for CMAKE_BUILD_TYPE) could also be "Debug" or "RelWithDebInfo"

在 Android 上构建:

cmake -B $BUILD_DIR -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX="$(pwd)/install" -DANDROID_ABI=arm64-v8a -DCMAKE_BUILD_TYPE=Release -DANDROID_STL=c++_static -DANDROID_PLATFORM=android-24 -DCMAKE_SYSTEM_NAME=Android -DANDROID_TOOLCHAIN=clang -DANDROID_ARM_MODE=arm -DCMAKE_MAKE_PROGRAM=$ANDROID_NDK_HOME/prebuilt/linux-x86_64/bin/make -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake
# If on Windows will be -DCMAKE_MAKE_PROGRAM=%ANDROID_NDK_HOME%\prebuilt\windows-x86_64\bin\make.exe
# -G is needed for building on Windows
# -DANDROID_ABI can also be armeabi-v7a for 32 bit

在 Windows 上构建:

cmake -B $BUILD_DIR -DCMAKE_INSTALL_PREFIX="$(pwd)/install"
# The CMAKE_INSTALL_PREFIX part is for testing (explained later).

此外,在 Windows 上可考虑使用 git config --global core.fileMode false(或使用 --local)来防止文件被添加执行权限。

4) 构建和安装

# for Linux:
make -j4 install

# for Windows:
cmake --build . --config Release --target install
# "Release" (for --config) could also be "Debug", "MinSizeRel", or "RelWithDebInfo"

如果使用 MSVC,运行 CMake 完成配置后,请使用配置管理器检查 INSTALL 项目。

构建(GN)

glslang 也可以通过 GN 构建系统 进行构建。

1) 安装 depot_tools

下载 depot_tools.zip,将其解压到一个目录,并将该目录添加到您的 PATH 中。

2) 同步依赖项并生成构建文件

此步骤仅需在更新 glslang 后执行一次。

将当前目录设置为您的 glslang 检出目录,然后输入:

./update_glslang_sources.py
gclient sync --gclientfile=standalone.gclient
gn gen out/Default

3) 构建

将当前目录设置为您的 glslang 检出目录,然后输入:

cd out/Default
ninja

如果你需要修改 GLSL 语法

glslang/MachineIndependent/glslang.y 中的语法发生变更,则必须使用 bison 重新编译该语法,生成的输出文件会提交到代码库中,这样就避免了每位开发者在语法变更(这种情况并不常见)时都需要配置 bison 才能编译项目。在 Windows 系统上,你可以从 GnuWin32 获取二进制文件。

重新构建的命令如下:

bison --defines=MachineIndependent/glslang_tab.cpp.h
      -t MachineIndependent/glslang.y
      -o MachineIndependent/glslang_tab.cpp

上述命令也可在 updateGrammar 中的 bash 脚本中找到,需从 glslang 仓库的 glslang 子目录执行。

为 Web 和 Node 构建 WASM

为 Web 和 Node 构建独立的 JS/WASM 库

请使用构建步骤中的步骤,但需注意以下说明/例外情况:

  • 你的可执行文件搜索路径(Bash 类环境中的 PATH)中需要包含 emsdk
  • 使用 emcmake cmake 包装 cmake 调用。
  • 设置 -DENABLE_OPT=OFF
  • 如果不需要 HLSL,请设置 -DENABLE_HLSL=OFF
  • 若要构建独立的 JS/WASM 库,请开启 -DENABLE_GLSLANG_JS=ON
  • 若要获得完全最小化的构建,请确保使用 brotli 压缩 .js 和 .wasm 文件。
  • 注意,Emscripten 默认分配的栈大小非常小,这在编译大型着色器时可能导致栈溢出。请使用STACK_SIZE编译器设置来增加栈大小。

示例:

emcmake cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_GLSLANG_JS=ON \
    -DENABLE_HLSL=OFF -DENABLE_OPT=OFF ..

构建 glslang - 使用 vcpkg

您可以通过 vcpkg 依赖管理器下载并安装 glslang:

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

vcpkg 中的 glslang 端口由 Microsoft 团队成员和社区贡献者保持更新。如果版本已过时,请在 vcpkg 代码库上创建 issue 或 pull request

测试

目前,glslang 中有两个测试工具:一个是 Google Test,另一个是 runtests 脚本。前者运行单元测试和单着色器单线程集成测试,而后者运行多着色器链接测试和多线程测试。

如果使用 ALLOW_EXTERNAL_SPIRV_TOOLS 时,所使用的提交不是 known_good.json 中指定的提交,则测试可能会错误地失败或通过。

运行测试

runtests 脚本 要求已编译的二进制文件安装到 $BUILD_DIR/install。请确保在构建时已向 CMake 提供了正确的配置(使用 -DCMAKE_INSTALL_PREFIX);否则,您可能需要修改 runtests 脚本中的路径。

运行基于 Google Test 的测试:

cd $BUILD_DIR

# for Linux:
ctest

# for Windows:
ctest -C {Debug|Release|RelWithDebInfo|MinSizeRel}

# or, run the test binary directly
# (which gives more fine-grained control like filtering):
<dir-to-glslangtests-in-build-dir>/glslangtests

运行 runtests 脚本支持的测试:

cd $SOURCE_DIR/Test && ./runtests

如果某些测试因验证错误而失败,可能是系统上的 spirv-val 版本与 glslang 版本不匹配。在这种情况下,需要运行 update_glslang_sources.py。有关更多详细信息,请参见上文的“签出外部项目”。

贡献测试

修改功能的拉取请求应始终包含测试结果。

如果编写单元测试,请使用 Google Test 框架,并将测试放在 gtests/ 目录下。

集成测试位于 Test/ 目录中。该目录包含测试输入以及一个 baseResults/ 子目录,其中包含测试的预期结果。测试和 baseResults/ 均受源代码控制。

Google Test 通过读取测试输入、编译它们,然后与 baseResults/ 中的预期结果进行比较来运行这些集成测试。通过 Google Test 运行的集成测试在各种 gtests/*.FromFile.cpp 源文件中注册。glslangtests 提供了一个命令行选项 --update-mode,如果提供该选项,将使用该次调用的实际输出覆盖 baseResults/ 目录下的黄金文件。有关更多信息,请查看 gtests/ 目录的 README

对于 runtests 脚本,它将在 localResults/ 目录中生成当前结果,并将其与 baseResults/ 进行 diff 比较。当你想要更新跟踪的测试结果时,需要将它们从 localResults/ 复制到 baseResults/。这可以通过 bump shell 脚本来完成。

你可以使用 localtestlist 列出未跟踪的测试,从而添加自己的私有测试列表(不公开跟踪)。runtests 会自动读取此文件,并将其包含在 diffbump 过程中。

编程接口

其他软件可以通过以下两种不同的接口以编程方式将着色器转换为 AST:

  • 新的面向 C++ 类的接口,或
  • 原始的 C 函数式接口

StandAlone/StandAlone.cpp 中的 main() 展示了使用这两种风格的示例。

C++ 类接口(新的,推荐使用)

此接口大致位于 ShaderLang.h 的后 1/3 部分。它位于 glslang 命名空间中,包含以下内容,此处提供了生成 SPIR-V 的建议调用:

const char* GetEsslVersionString();
const char* GetGlslVersionString();
bool InitializeProcess();
void FinalizeProcess();

class TShader
    setStrings(...);
    setEnvInput(EShSourceHlsl or EShSourceGlsl, stage,  EShClientVulkan or EShClientOpenGL, 100);
    setEnvClient(EShClientVulkan or EShClientOpenGL, EShTargetVulkan_1_0 or EShTargetVulkan_1_1 or EShTargetOpenGL_450);
    setEnvTarget(EShTargetSpv, EShTargetSpv_1_0 or EShTargetSpv_1_3);
    bool parse(...);
    const char* getInfoLog();

class TProgram
    void addShader(...);
    bool link(...);
    const char* getInfoLog();
    Reflection queries

仅用于验证(不生成代码)时,请替换以下调用:

    setEnvInput(EShSourceHlsl or EShSourceGlsl, stage,  EShClientNone, 0);
    setEnvClient(EShClientNone, 0);
    setEnvTarget(EShTargetNone, 0);

有关更多详细信息,请参见 ShaderLang.h 以及 StandAlone/StandAlone.cpp 中对它的使用。在 setEnvInputsetEnvClientsetEnvTarget 的调用上方,有一个块注释提供了更多详细信息。

C 函数接口(原始)

此接口大致位于 ShaderLang.h 的前 2/3 部分,被称为 Sh*() 接口,因为所有入口点均以 Sh 开头。

Sh*() 接口接受一个“编译器”回调对象,该对象在构建完回调后被调用,此回调会接收 AST(抽象语法树),然后可以在其上执行后端操作。

以下是简化后的运行时调用栈:

ShCompile(shader, compiler) -> compiler(AST) -> <back end>

在实际应用中,ShCompile() 接收着色器字符串、默认版本以及用于控制编译的警告/错误等选项。

C 函数接口(新增)

此接口位于 glslang_c_interface.h 中,提供与 C++ 接口类似的功能。以下代码片段是一个完整示例,展示了如何将 GLSL 编译为适用于 Vulkan 1.2 的 SPIR-V 1.5。

#include <glslang/Include/glslang_c_interface.h>

// Required for use of glslang_default_resource
#include <glslang/Public/resource_limits_c.h>

typedef struct SpirVBinary {
    uint32_t *words; // SPIR-V words
    int size; // number of words in SPIR-V binary
} SpirVBinary;

SpirVBinary compileShaderToSPIRV_Vulkan(glslang_stage_t stage, const char* shaderSource, const char* fileName) {
    const glslang_input_t input = {
        .language = GLSLANG_SOURCE_GLSL,
        .stage = stage,
        .client = GLSLANG_CLIENT_VULKAN,
        .client_version = GLSLANG_TARGET_VULKAN_1_2,
        .target_language = GLSLANG_TARGET_SPV,
        .target_language_version = GLSLANG_TARGET_SPV_1_5,
        .code = shaderSource,
        .default_version = 100,
        .default_profile = GLSLANG_NO_PROFILE,
        .force_default_version_and_profile = false,
        .forward_compatible = false,
        .messages = GLSLANG_MSG_DEFAULT_BIT,
        .resource = glslang_default_resource(),
    };

    glslang_shader_t* shader = glslang_shader_create(&input);

    SpirVBinary bin = {
        .words = NULL,
        .size = 0,
    };
    if (!glslang_shader_preprocess(shader, &input))	{
        printf("GLSL preprocessing failed %s\n", fileName);
        printf("%s\n", glslang_shader_get_info_log(shader));
        printf("%s\n", glslang_shader_get_info_debug_log(shader));
        printf("%s\n", input.code);
        glslang_shader_delete(shader);
        return bin;
    }

    if (!glslang_shader_parse(shader, &input)) {
        printf("GLSL parsing failed %s\n", fileName);
        printf("%s\n", glslang_shader_get_info_log(shader));
        printf("%s\n", glslang_shader_get_info_debug_log(shader));
        printf("%s\n", glslang_shader_get_preprocessed_code(shader));
        glslang_shader_delete(shader);
        return bin;
    }

    glslang_program_t* program = glslang_program_create();
    glslang_program_add_shader(program, shader);

    if (!glslang_program_link(program, GLSLANG_MSG_SPV_RULES_BIT | GLSLANG_MSG_VULKAN_RULES_BIT)) {
        printf("GLSL linking failed %s\n", fileName);
        printf("%s\n", glslang_program_get_info_log(program));
        printf("%s\n", glslang_program_get_info_debug_log(program));
        glslang_program_delete(program);
        glslang_shader_delete(shader);
        return bin;
    }

    glslang_program_SPIRV_generate(program, stage);

    bin.size = glslang_program_SPIRV_get_size(program);
    bin.words = malloc(bin.size * sizeof(uint32_t));
    glslang_program_SPIRV_get(program, bin.words);

    const char* spirv_messages = glslang_program_SPIRV_get_messages(program);
    if (spirv_messages)
        printf("(%s) %s\b", fileName, spirv_messages);

    glslang_program_delete(program);
    glslang_shader_delete(shader);

    return bin;
}

基本内部操作

  • 初始词法分析由 MachineIndependent/Preprocessor 中的预处理器完成,然后由 MachineIndependent/Scan.cpp 中的 GLSL 扫描器进行优化。目前未使用 flex。

  • 代码解析通过 bison 处理 MachineIndependent/glslang.y 文件,并借助符号表和 AST(抽象语法树)实现。符号表不会传递到后端;中间表示独立存在。语法产生式(其中许多已转移至 ParseHelper.cpp)和 Intermediate.cpp 共同构建该语法树。

  • 中间表示是非常高级的,以内存树的形式呈现。这有助于完整保留原始程序的所有信息,并实现从解析结果到后端的高效传输。在 AST 中,常量会进行传播和折叠,同时会消除极少量的死代码。

    为辅助链接和反射,AST 中的最后一个顶级分支会列出所有全局符号。

  • 后端编译器的主要算法是遍历该语法树(高级中间表示),并创建内部目标代码表示。MachineIndependent/intermOut.cpp 中提供了此类操作的示例。

  • 将语法树简化为线性字节码风格的低级中间表示,可能是生成完全优化代码的有效方法。

  • 目前仍存在一些过时的链接器类型死代码。

  • 内存池:解析使用从 C++ std 类型派生的类型,并通过自定义分配器将它们放入内存池。这使得单个容器/内容的分配仅需几个周期,且释放无需额外成本。在 AST 创建和处理完成后,该内存池会被释放。

    使用方法简单:当你要调用 new 时,存在以下三种情况:

    • 对象来自内存池(其基类包含宏 POOL_ALLOCATOR_NEW_DELETE),此时无需调用 delete

    • 对象是 TString,此时需调用 NewPoolTString(),该函数从内存池获取对象,且无需对应的 delete

    • 对象不来自内存池,此时你需要对所 new 的对象进行常规的 C++ 内存管理

  • 功能可通过版本/扩展/阶段/配置文件进行保护: 详见 glslang/MachineIndependent/Versions.cpp 中的注释。

项目介绍

用户可使用该项目进行 GLSL、ESSL、HLSL 着色器的编译、验证及转换为 SPIR-V 中间语言。核心功能包括参考级验证前端、AST 转换后端、反射信息提取及命令行工具,支持多平台构建与测试。【此简介由AI生成】

定制我的领域