使用GLSLC的depfile制作包含文件自动触发CMake中SPIRV的重新编译

Using GLSLC's depfile to make included files automatically trigger recompile of SPIRV in CMake

我正在尝试创建一个 cmake 函数,该函数在着色器文件发生更改时自动将 glsl 重新编译为 spirv。现在直接依赖工作,即我用作编译参数的着色器。但是,我大量使用 glslc 提供的 #include 功能,默认情况下我无法更改这些内容来触发重新编译。我确定我使用的是 Ninja

现在我有以下 CMake 函数和参数:

cmake -DCMAKE_BUILD_TYPE=Debug "-DCMAKE_MAKE_PROGRAM=JETBRAINSPATH/bin/ninja/win/ninja.exe" -G Ninja  "PATH_TO_CURRENT_DIRECTORY"

函数

set(GLSLC "$ENV{VULKAN_SDK}/Bin/glslc")

function(target_shader_function SHADER_TARGET)
    foreach (SHADER_SOURCE_FILEPATH ${ARGN})
        get_filename_component(SHADER_SOURCE_FILENAME ${SHADER_SOURCE_FILEPATH} NAME)
        get_filename_component(SHADER_SOURCE_DIRECTORY ${SHADER_SOURCE_FILEPATH} DIRECTORY)
        set(SHADER_TARGET_NAME "${SHADER_TARGET}_${SHADER_SOURCE_FILENAME}")
        set(SHADER_BINARY_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/spirv")
        set(SHADER_FINAL_BINARY_FILEPATH "${SHADER_BINARY_DIRECTORY}/${SHADER_SOURCE_FILENAME}.spv")
        #we can use depfiles instead
        #
        add_custom_command(
                OUTPUT ${SHADER_FINAL_BINARY_FILEPATH}
                DEPENDS ${SHADER_SOURCE_FILEPATH}
                DEPFILE ${SHADER_SOURCE_FILEPATH}.d
                COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADER_BINARY_DIRECTORY}
                COMMAND ${GLSLC} -MD -MF ${SHADER_SOURCE_FILEPATH}.d -O ${SHADER_SOURCE_FILEPATH} -o ${SHADER_FINAL_BINARY_FILEPATH} --target-env=vulkan1.2 -I ${CMAKE_SOURCE_DIR}/shaderutils
                DEPENDS ${SHADER_SOURCE_FILEPATH}
                #                BYPRODUCTS ${SHADER_FINAL_BINARY_FILEPATH} ${SHADER_SOURCE_FILEPATH}.d causes ninja to no longer work
                COMMENT "Compiling SPIRV for \nsource: \n\t${SHADER_SOURCE_FILEPATH} \nbinary: \n\t${SHADER_FINAL_BINARY_FILEPATH} \n"
        )

        add_custom_target(${SHADER_TARGET_NAME} DEPENDS ${SHADER_FINAL_BINARY_FILEPATH} ${SHADER_SOURCE_FILEPATH}.d)
        add_dependencies(${SHADER_TARGET} ${SHADER_TARGET_NAME})
    endforeach (SHADER_SOURCE_FILEPATH)
endfunction()

我是这样使用它的:

cmake_minimum_required(VERSION 3.21)
cmake_policy(SET CMP0116 NEW)
project(my_workspace)
add_executable(my_target main.cpp)
...
target_shader_function(my_target
        ${CMAKE_CURRENT_SOURCE_DIR}/shaders/example.comp
        )

main.cpp

#include <iostream>

int main(){
std::cout << "hello world!" << std::endl;
return 0; 
}

同样,如果我进行更改,一切正常,例如,example.comp

但是,假设我有以下着色器(假设这是 example.comp):

#version 460
#include "fooutils.glsl"
#define WORKGROUP_SIZE 1024
layout (local_size_x = WORKGROUP_SIZE, local_size_y = 1, local_size_z = 1) in;
layout(set = 0, binding = 0) buffer MyBufferBlock{
    float data[];
}
void main(){
   uint tidx = gl_GlobalInvocationID.x;
   data[tidx] += foo(tidx); 
}

我包括以下内容:

#ifndef FOOUTILS_GLSL
#define FOOUTILS_GLSL

float foo(uint tidx){
    return mod(tidx, 4.51); 
}

#endif //FOOUTILS_GLSL

我在所有内容编译一次后更改 fooutils.glsl(例如以阻止编译的方式),

#ifndef FOOUTILS_GLSL
#define FOOUTILS_GLSL

float foo(uint tidx){
    return x; 
    return mod(tidx, 4.51); 
}

#endif //FOOUTILS_GLSL

我没有触发重新编译。我曾假设忍者会使用此信息来完成此操作,但我还没有看到它发生。

如何使用这个 depfile 在包含依赖项更改时强制重新编译?

这是我的工作实现。但首先,这是我的终端输出,所以你可以看到它正在工作:

$ tree
.
├── CMakeLists.txt
├── main.cpp
├── shaders
│   └── example.comp
└── shaderutils
    └── fooutils.glsl
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
...
$ cmake --build build/
[1/3] Compiling SPIRV: shaders/example.comp -> spirv/example.spv
[2/3] Building CXX object CMakeFiles/my_target.dir/main.cpp.o
[3/3] Linking CXX executable my_target
$ cmake --build build/
ninja: no work to do.
$ touch shaderutils/fooutils.glsl 
$ cmake --build build/
[1/1] Compiling SPIRV: shaders/example.comp -> spirv/example.spv
$ cat build/spirv/example.d 
spirv/example.spv: /path/to/shaders/example.comp /path/to/shaderutils/fooutils.glsl
$ cat build/CMakeFiles/d/*.d 
spirv/example.spv: \
  ../shaders/example.comp \
  ../shaderutils/fooutils.glsl

现在开始实施

cmake_minimum_required(VERSION 3.22)
project(test)

function(target_shader_function TARGET)
    find_package(Vulkan REQUIRED)

    if (NOT TARGET Vulkan::glslc)
        message(FATAL_ERROR "Could not find glslc")
    endif ()

    foreach (source IN LISTS ARGN)
        cmake_path(ABSOLUTE_PATH source OUTPUT_VARIABLE source_abs)
        cmake_path(GET source STEM basename)

        set(depfile "spirv/${basename}.d")
        set(output "spirv/${basename}.spv")
        set(dirs "$<TARGET_PROPERTY:${TARGET},INCLUDE_DIRECTORIES>")
        set(include_flags "$<$<BOOL:${dirs}>:-I$<JOIN:${dirs},;-I>>")

        add_custom_command(
            OUTPUT "${output}"
            COMMAND "${CMAKE_COMMAND}" -E make_directory spirv
            COMMAND Vulkan::glslc -MD -MF "${depfile}" -O "${source_abs}"
                    -o "${output}" --target-env=vulkan1.2 "${include_flags}"
            DEPENDS "${source_abs}"
            BYPRODUCTS "${depfile}"
            COMMENT "Compiling SPIRV: ${source} -> ${output}"
            DEPFILE "${depfile}"
            VERBATIM
            COMMAND_EXPAND_LISTS
        )

        set(shader_target "${TARGET}_${basename}")
        add_custom_target("${shader_target}"
                          DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${output}")
        add_dependencies("${TARGET}" "${shader_target}")
    endforeach ()
endfunction()

add_executable(my_target main.cpp)
target_shader_function(my_target shaders/example.comp)
target_include_directories(
    my_target PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/shaderutils")

如果 CMake 最低版本为 3.20 或更高,将设置 CMP0116,这会将使用相对路径生成的 depfile 调整为相对于 top-level 二进制目录。您可以在最后两个命令输出中看到这一点。

为了与此策略兼容,调用 glslc 的命令小心地仅使用绝对路径或相对于 ${CMAKE_CURRENT_BINARY_DIR}.

的路径

为了增加此函数的可重用性,我让它重新使用 TARGET 中的包含路径而不是 hard-coding shaderutils.

还请记住始终将绝对路径传递给 add_custom_{command,target}DEPENDS 参数,以避免意外的路径解析行为。

最后,由于CMake实际上自带了一个可以定位glslc的FindVulkan模块,我们用它来获取Vulkan::glslc目标。根据 the documentation,可以通过设置 Vulkan_GLSLC_EXECUTABLE.

来覆盖它

Windows 上带有 MSVC 的 VS2022 终端日志:

> cmake -S . -B build
...
> cmake --build build --config Release
  Checking Build System
  Compiling SPIRV: shaders/example.comp -> spirv/example.spv
  Building Custom Rule D:/test/CMakeLists.txt
  Building Custom Rule D:/test/CMakeLists.txt
  main.cpp
  my_target.vcxproj -> D:\test\build\Release\my_target.exe
  Building Custom Rule D:/test/CMakeLists.txt

> cmake --build build --config Release -- -noLogo
  my_target.vcxproj -> D:\test\build\Release\my_target.exe

> notepad shaderutils\fooutils.glsl
> cmake --build build --config Release -- -noLogo
  Compiling SPIRV: shaders/example.comp -> spirv/example.spv
  my_target.vcxproj -> D:\test\build\Release\my_target.exe

> cmake --build build --config Release -- -noLogo
  my_target.vcxproj -> D:\test\build\Release\my_target.exe

再次使用 Ninja 而不是 msbuild:

> cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Release ^
        -DVulkan_ROOT=C:/VulkanSDK/1.2.198.1
...
> powershell "cmake --build build | tee output.txt"
[1/3] Compiling SPIRV: shaders/example.comp -> spirv/example.spv
[2/3] Building CXX object CMakeFiles\my_target.dir\main.cpp.obj
[3/3] Linking CXX executable my_target.exe

> powershell "cmake --build build | tee output.txt"
ninja: no work to do.

> notepad shaderutils\fooutils.glsl
> powershell "cmake --build build | tee output.txt"
[1/1] Compiling SPIRV: shaders/example.comp -> spirv/example.spv

powershell + tee 小技巧只是为了防止 Ninja 命令日志覆盖自身。我可以使用 --verbose,但是会打印 完整 命令行,而不是整洁的摘要。