2

I'm trying to create a cmake function that automatically recompiles glsl to spirv upon changes to the shader files. Right now direct dependencies work, ie the shaders I use as compile arguments. However I make heavy use of #include feature that glslc provides, and by default I can't get changes in that stuff to trigger recompile. I made sure that I'm using the Ninja

Right now I have the following CMake function and arguments:

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

function

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
        #https://stackoverflow.com/questions/60420700/cmake-invocation-of-glslc-with-respect-to-includes-dependencies
        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()

and I use it like this:

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; 
}

Again, everything works fine if I change, for example, example.comp.

However, lets say I have the following shader (lets say that this is 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); 
}

and I include the following:

#ifndef FOOUTILS_GLSL
#define FOOUTILS_GLSL

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

#endif //FOOUTILS_GLSL

and I change fooutils.glsl after everything is compiled once (for example in a way that stops it from compiling),

#ifndef FOOUTILS_GLSL
#define FOOUTILS_GLSL

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

#endif //FOOUTILS_GLSL

I don't get a recompile triggered. I had assumed that ninja would use this info to accomplish this, but I haven't seen it happen.

How do I use this depfile to force a recompile when an include dependency changes?

Krupip
  • 4,404
  • 2
  • 32
  • 54

1 Answers1

2

Here's my working implementation. But first, here's my terminal output so you can see it's working:

$ 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

Now on to the implementation

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")

With a CMake minimum version of 3.20 or greater, CMP0116 will be set, which adjusts depfiles that were generated with relative paths to be relative to the top-level binary directory. You can see this in action in the last two command outputs.

For compatibility with this policy, the command to invoke glslc is careful to use only absolute paths or paths relative to ${CMAKE_CURRENT_BINARY_DIR}.

To increase the reusability of this function, I had it reuse the include paths from the TARGET rather than hard-coding shaderutils.

Also remember to always pass absolute paths to the DEPENDS arguments of add_custom_{command,target} to avoid surprising path resolution behaviors.

Finally, since CMake actually comes with a FindVulkan module that can locate glslc, we use that to get the Vulkan::glslc target. Per the documentation, it can be overridden by setting Vulkan_GLSLC_EXECUTABLE.


Terminal logs for VS2022 on Windows with MSVC:

> 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

and again with Ninja instead of 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

The little powershell + tee trick is just to keep the Ninja command log from overwriting itself. I could use --verbose, but then the full command lines would be printed, rather than the tidy summaries.

Alex Reinking
  • 16,724
  • 5
  • 52
  • 86
  • There are a few elaborations that could be made on this, for instance making the `--target-env` flag configurable by a cache variable or target property, enabling `-O` on non-debug builds via `$<$>:-O>`, and taking better care to avoid basename collisions by, e.g. hashing paths. – Alex Reinking Feb 06 '22 at 06:45
  • Also, you can try lowering the minimum required version, but I tested on 3.22.2. It will _certainly_ not work on versions below 3.20 because of CMP0116 and my use of `cmake_path`. – Alex Reinking Feb 06 '22 at 06:47
  • So I tried this, and the solution worked... but I'm still at a loss as for what exactly you did that made the difference here? – Krupip Feb 06 '22 at 07:05
  • I'm not sure either. I couldn't make your MRE run (it failed with missing references to the depfile, probably needed to uncomment `BYPRODUCTS`). It most likely had something to do with the contents of your depfile and the relative/absolute status of the arguments to `glslc`. – Alex Reinking Feb 06 '22 at 07:18
  • Okay, actually there's a problem with yours, though I don't know if it's solvable. Yours will recompile no matter what. Mine only compiles when changes are detected in source shader file. I can't figure out why yours recompiles every time though, the separation between add_custom_command and add_custom_target I thought was supposed to fix this, not sure why it doesn't in yours. – Krupip Feb 06 '22 at 07:59
  • @Krupip - I do not observe that. It does _not_ recompile every time for me. Look at the command log I posted for `ninja: no work to do.`. Do you have one of the files open in an editor that touches the timestamps frequently? – Alex Reinking Feb 06 '22 at 08:01
  • I was using Clion, but I was able to reproduce this entirely with out an IDE. Is this a windows vs linux thing? I get `[1/1] Compiling SPIRV: shaders/example.comp -> spirv/example.spv` even if I don't change the file (running the same commands you do). Apparently this problem is across mingw and msvc as well, since I accidentally just tested this with both compilers, it defaulted to mingw64 when I did it with out any IDE. I also get my ninja version and my CMake version from clion, not sure if that matters. – Krupip Feb 06 '22 at 08:20
  • @Krupip - I guess since you accepted my answer, things are working. I'm glad for that! For what it's worth, I was _unable_ to produce a bad behavior on MSVC / Windows with either Ninja or Visual Studio 2022 backends and have updated my answer with terminal logs. I haven't tried with MinGW yet, though. – Alex Reinking Feb 07 '22 at 08:46