SPIR-V compiler custom_command has unexpected execution order

Hi all! First post. I’ve been using CMake with C++ for a while, but have avoided custom commands and targets until now.

Here’s my CMakeLists.txt: https://paste.ubuntu.com/p/KTMTk473qm/

Background

I have a C++ library target that depends on some generated C++ source files. A CMake custom_command calls a script to generate the C++ sources from compiled shaders, which themselves depend on shader source files via a custom_command. Each shader input file has a separate custom_command since I didn’t want to rebuild all shaders if any one of them changed. (Is this wrong?)

Custom commands

So, when I change a shader source file, a custom_command must recompile that file, and then a custom_command must run the generator to create the C++ source file that the shader library depends on, and then add_library uses this generated file.

I know separate directory makes it not work

The shaders and their CMakeLists are in a separate directory alongside the shader library.

This seems straightforward, but it doesn’t work – I suppose this is because the custom command must be declared in the same CMakeLists file as the target depending on its output. Is there a better way to do this?


So, I’ve created a custom_target which I manually build. As described in Background, the custom_target generates source files, and depends on the output of several custom_commands.

The unexpected behavior is that the COMMAND for the custom_target gets run before the COMMANDs for the custom_commands it depends on. For this reason, I always have to build it twice in order to get the correctly generated files.

In other words, line 60 (${PHX_CLI} gen ...) is getting run before the shaders are compiled (e.g., line 17, ${SPV_COMPILER} -V ...), so the generated files are always from the previous version of the shaders.

For this reason, manually setting up the dependencies using add_dependencies causes the resulting binary to have the wrong versions of the shaders, so I’ve had to do it manually so far. I suppose I could just put it all in one giant custom target, but I was hoping not to recompile and rebuild every shader every time.

Can anyone help clarify what’s going on here?

Not sure why your example does not work, may be a problem with the implicit CMAKE_CURRENT_SOURCE_DIR location in your custom command/target.

I suggest this example which looks ok to me:
custom_tgt_cmd_ex.zip (2.5 KB)

I used a python script as “pseudo compiler”.

$ mkdir b
$ cd b
$ cmake ../custom_tgt_cmd_ex
-- Found Python3: /home/enoulard/VEnvs/py36torch1/bin/python3.6 (found version "3.6.6") found components:  Interpreter 
-- Configuring done
-- Generating done
-- Build files have been written to: /home/enoulard/Research/CMake/b
$ make GenerateShaderResources
Scanning dependencies of target GenerateShaderResources
[ 20%] Compiling pushconstants.frag shader...
[ 40%] Compiling triangle.vert shader...
[ 60%] Compiling triangle.frag shader...
[ 80%] Compiling pushconstants.vert shader...
[100%] Generating assets ...
Reading from . writing to /home/enoulard/Research/CMake/b/dep/res
[100%] Built target GenerateShaderResources
$ touch ../custom_tgt_cmd_ex/vk_renderer_pushconstants.vert.glsl 
$ make GenerateShaderResources
[ 20%] Compiling pushconstants.vert shader...
[ 40%] Generating assets ...
Reading from . writing to /home/enoulard/Research/CMake/b/dep/res
[100%] Built target GenerateShaderResources
$ touch ../custom_tgt_cmd_ex/*triangl*glsl
$ make GenerateShaderResources
[ 20%] Compiling triangle.vert shader...
[ 40%] Compiling triangle.frag shader...
[ 60%] Generating assets ...
Reading from . writing to /home/enoulard/Research/CMake/b/dep/res
[100%] Built target GenerateShaderResources
$

Outputs of a custom command are “attached” to a single target. (Well, they can be attached to multiple, but that doesn’t work in general for reasons related to various generators.) So in your example, I would do a hybrid of your last two bits:

add_custom_command(
    OUTPUT
	../dep/res/res/triangle_frag_spv_decl.cxx ../dep/res/res/triangle_frag_spv_real.cxx
	../dep/res/res/triangle_vert_spv_decl.cxx ../dep/res/res/triangle_vert_spv_real.cxx
	../dep/res/res/pushconstants_frag_spv_decl.cxx  ../dep/res/res/pushconstants_frag_spv_real.cxx
	../dep/res/res/pushconstants_vert_spv_decl.cxx  ../dep/res/res/pushconstants_vert_spv_real.cxx
    DEPENDS
	triangle.vert.spv triangle.frag.spv
	pushconstants.vert.spv pushconstants.frag.spv
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    COMMAND ${PHX_CLI} gen --skip-finalize -l 9 --from . --to ../dep/res --match ".*\.spv"
    COMMENT "Generating assets..."
)

add_custom_target(GenerateShaderResources
  DEPENDS
	../dep/res/res/triangle_frag_spv_decl.cxx ../dep/res/res/triangle_frag_spv_real.cxx
	../dep/res/res/triangle_vert_spv_decl.cxx ../dep/res/res/triangle_vert_spv_real.cxx
	../dep/res/res/pushconstants_frag_spv_decl.cxx  ../dep/res/res/pushconstants_frag_spv_real.cxx
	../dep/res/res/pushconstants_vert_spv_decl.cxx  ../dep/res/res/pushconstants_vert_spv_real.cxx
)

I would also suggest using absolute paths (based on CMAKE_CURRENT_BINARY_DIR) rather than relative paths. If ninja builds it properly then, I’d put a lot more trust into it. Visual Studio is the next “oddball” if you want to support it.

Thanks Ben, that mostly sense except for this bit:

Since these sources are generated in the source tree, won’t they then be inaccessible to the target that depends on them?


Edit: So, this seems to work (I used CMAKE_CURRENT_SOURCE_DIR) with the manual target – and it also seems to pick up the changes in the dependent package, when I go to build it. It seems to defer the final .cxx source generation to just before the dependent package is built, rather than generating sources when I build the custom target. This is a big improvement over a seemingly unnecessary double-build.

Now, if I add_dependencies(Resource GenerateShaderResources) later, after the subdirectory containing Resource is added, my expectation is that the sources will be generated before Resource is built, but in reality a) it always runs this build (I know this is because custom targets are always stale, but I’m not sure if there’s a better approach here), and b) it does not generate the .cxx sources the first time. Then I need to build again to get the fresh generated sources. Odd.

For example, I would expect these steps to run in opposite order (step 1 after steps 2-5): https://paste.ubuntu.com/p/n2jkT9TjK5/

Hmm. I would debug using the Ninja generator to see what -t explain is saying why it builds it the second time. I suspect there are missing dependencies somewhere in there somewhere.