Search code examples
cmakehlsl

CMake: How to make add_custom_command trigger a custom command only when its dependencies are changed?


I have certain files (.hlsl) in one of my targets that are compiled using a custom compiler (glslc). After compilation, I use a different tool to embed the compiled binary in a c-file containing c-array like described here.

This workflow is summarized in the following code which invokes 2 custom commands:

function(build_hlsl_shader shader_file)
    get_source_file_property(shader_type ${shader_file} ShaderType)    
    get_filename_component(shader_name ${shader_file} NAME_WE)

    # special command: .hlsl file to a .c / .h file
    add_custom_command(  
        TARGET shaders_custom_target # a custom target
        #OUTPUT ${shader_name}.cpp # maybe this? 
        #DEPENDS ${shader_file} # maybe that?
        MAIN_DEPENDENCY ${shader_file} # or maybe thee?
        COMMAND # special compiler
        ${glslc_executable} 
        -fshader-stage=${shader_type} 
        ${CMAKE_CURRENT_SOURCE_DIR}/${shader_file}
        -o
        ${CMAKE_CURRENT_SOURCE_DIR}/${shader_name}.spv
        COMMAND # special tool that generates the c header.
        bin2h 
        ${CMAKE_CURRENT_SOURCE_DIR}/${shader_name}.spv
    )
endfunction()

I then define a custom target that that generates the said files whenever it is built.

add_custom_target(
    shaders_custom_target 
    DEPENDS
    "shader.hlsl"
)

build_hlsl_shader("shader.hlsl")

# this library consumes the headers generated by the first target
add_library(library2 STATIC "file1.cpp""file1.hpp")
add_dependencies(library2  shaders_custom_target)

The main issue is that the custom target build process is triggered every time I build my project regardless of the fact the none of the .hlsl files are changed. This is undesirable, I want my custom command to work just like when compiling regular .c or cpp files. They are compiled only when changed.

My requirements are as follows:

  • Each time one of the .hlsl files are changed, the custom command should trigger.
  • The custom command should not get triggered whenever I build the target / project.

So how can this achieved?

I tried several methods like using DEPENDS, MAIN_DEPENDENCY or using OUTPUT instead of TARGET. None worked. I also tried the solution offered here and here. They all suffer from the same issue.


As requested in the comments, Here is my attempt to use OUTPUT instead of TARGET:

function(build_hlsl_shader shader_file)
    get_source_file_property(shader_type ${shader_file} ShaderType)    
    get_filename_component(shader_name ${shader_file} NAME_WE)

    add_custom_command(
        #TARGET shaders_custom_target
        OUTPUT 
        #${shader_name}.hpp # the generated header, tried with or without
        ${shader_file} # tried with and without
        DEPENDS ${shader_file} # tried with and without
        #MAIN_DEPENDENCY ${shader_file} # tried with and without

        COMMAND
        ${glslc_executable}
        -fshader-stage=${shader_type} 
        ${CMAKE_CURRENT_SOURCE_DIR}/${shader_file}
        -o
        ${CMAKE_CURRENT_SOURCE_DIR}/${shader_name}.spv
        COMMAND
        bin2h
        ${CMAKE_CURRENT_SOURCE_DIR}/${shader_name}.spv
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
     )
endfunction()

Solution

  • It depends on whether you consider the shaders to be "part of the library" or just some data you need at runtime.

    For my projects that produce HLSL shaders, I use a custom target similar to what you are doing:

    add_custom_target(shaders)
    
    set_source_files_properties(VertexShader.hlsl PROPERTIES ShaderType "vs")
    set_source_files_properties(PixelShader.hlsl PROPERTIES ShaderType "ps")
        
    foreach(FILE VertexShader.hlsl PixelShader.hlsl)
        get_filename_component(FILE_WE ${FILE} NAME_WE)
        get_source_file_property(shadertype ${FILE} ShaderType)
        add_custom_command(TARGET shaders
            COMMAND dxc.exe /nologo /Emain /T${shadertype}_6_0 $<IF:$<CONFIG:DEBUG>,/Od,/O3> /Zi /Fo ${CMAKE_BINARY_DIR}/${FILE_WE}.cso /Fd ${CMAKE_BINARY_DIR}/${FILE_WE}.pdb ${FILE}
        MAIN_DEPENDENCY ${FILE}
        COMMENT "HLSL ${FILE}"
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        VERBATIM)
    endforeach(FILE)
    
    add_dependencies(${PROJECT_NAME} shaders)
    

    This essentially sets up a 'post build' event. If any of the shader source files change, then they all get rebuilt.

    The other way to set it up is to use an output file, but here you need some extra cmake logic so you can add the output files to the add_library or add_executable:

    # SOURCES variable is all the C/C++ source files in the library
    
    # Build HLSL shaders
    set_source_files_properties(VertexShader.hlsl PROPERTIES ShaderType "vs")
    set_source_files_properties(PixelShader.hlsl PROPERTIES ShaderType "ps")
        
    foreach(FILE VertexShader.hlsl PixelShader.hlsl)
        get_filename_component(FILE_WE ${FILE} NAME_WE)
        get_source_file_property(shadertype ${FILE} ShaderType)
        list(APPEND CSO_FILES ${CMAKE_BINARY_DIR}/${FILE_WE}.cso)
        add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/${FILE_WE}.cso
            COMMAND dxc.exe /nologo /Emain /T${shadertype}_6_0 $<IF:$<CONFIG:DEBUG>,/Od,/O3> /Zi /Fo ${CMAKE_BINARY_DIR}/${FILE_WE}.cso /Fd ${CMAKE_BINARY_DIR}/${FILE_WE}.pdb ${FILE}
        MAIN_DEPENDENCY ${FILE}
        COMMENT "HLSL ${FILE}"
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        VERBATIM)
    endforeach(FILE)
    
    add_library(library2 ${SOURCES} ${CSO_FILES})
    

    This second version only builds a specific output file if it's source changed, but generally speaking you don't need this level of granularity with shaders.