[SPIR-V] /* multiple #lines overlapping */ in RenderDoc
Description Since we've updated DXC to commit 50f53c6c200fd6b53f65268912e6f9e444ce9242 (6 jun 2025), we are not able to debug our shaders in RenderDoc due to all source HLSL lines being duplicated (each HLSL line is present 13 times in the source file). This doesn't occur when compiling the shaders using an older version of DXC (b26fd8099aa435c6a46b10b179549280ba3930b0, 8 oct 2025)
Steps to Reproduce
dxc.exe blit_hdr.hlsl.txt -E mainfragIGSpecific -T ps_6_0 -spirv -fspv-target-env=vulkan1.1 -Wall -Wno-unused-const-variable -D imBINDLESS=1 -fspv-extension=SPV_EXT_descriptor_indexing -fspv-extension=SPV_KHR_non_semantic_info -fspv-debug=vulkan-with-source
Actual Behavior
Environment
- DXC version dxcompiler.dll: 1.9 - 1.8.0.4946 (9efbb6c32); dxil.dll: 1.4(10.0.18362.1)
- Host Operating System windows 11
Sorry, I'm not sure what to do for this issue. I tried looking at the debug info that is generated, the differences are so large it is hard to see what is relevant. I cannot load the shader in renderdoc without an application built around it. Are you able to use git bisect to find the first bad commit? That could help narrow down the difference.
Hmmm, that would require a huge amount of time to bisect between 8 oct 2024 and 6 jun 2025, there have been quite a few in between... :/
I started to bisect anyway, as I would really be glad to be able to use a newer version of DXC...
However, when using an older version (I tried one from 3 apr), my shaders don't compile because of other issues reported (#7181 & #7182) - which have been fixed recently.
So I'm afraid I have no way to proceed the bisection with our shaders code :/
Okay. I'll keep playing with renderdoc to see if I can reproduce it.
It seems like renderdoc does not handle the #line directives very well. I'm not sure if the SPIR-V should be different or not. Here is a smaller example: https://godbolt.org/z/q5oao4n1q.
Note that there are two DebugSource instructions with the same string %8, but with different filenames. That is what confuses renderdoc. I don't know its internal implementation, but playing around with the line directive seems to change the behaviour.
As a workaround, you could filter out all of the #line directives from your source file, or you can try to compile with DXC using the original sources before they are preprocessed. I don't know your workflow, so I don't know what will be best for you.
As for the best SPIR-V to produce, I'm thinking we should not output the DebugSource for files for which we do not have the original source file. Leave it to the debuggers to find the right line based on the #line directives, if they want.
FWIW, I just try a similar C example in lldb. If there is a #line directive in the source file, but the source file referenced is not available to the debugger, the debugging experience is not as good. I don't think we will be able to provide a good experience either.
Thanks for your answer, I'll check tomorrow morning once in the office...
@baldurk what would you recommend, any idea ?
The SPIR-V output looks wrong to me. If nothing else it is wrong to give the same source string for both otherfile.hlsl and example.hlsl because they don't have the same effective source - otherfile.hlsl's line 1 comes from line 3 in otherfile.hlsl - the struct VertexOutput line.
It's important that the source provided in the debug info is the original source used as input to compilation, not anything derived, to ensure the debug info can be used to recompile cleanly. RenderDoc does also reconstructs the original files for display in the UI by processing #line directives exactly because it is very common for the final shader compile step to come from a munged/combined uberfile, and seeing the original files is much more useful. I wish it weren't common, but it's standard practice in many large engines.
IMO DebugSource should only contain source for actual input files, for any virtual files if it's present the text parameter should be omitted. Otherwise there's no real way for a tool to know if the source was really there in the input files or not.
@baldurk comment seems similar to my thoughts. We'll need to figure out the best way to handle the DebugSource instructions generated because of the #line directive.
This is my proposal:
When an #line is encountered, we continue to do what we do except, when generating the DebugSource, the text operand for the not included. This will indicate that the original source file was not available. For the example above, we currently have:
%7 = OpString "otherfile.hlsl"
%8 = OpString "<entire original file>"
%25 = OpExtInst %void %1 DebugSource %7 %8
%26 = OpExtInst %void %1 DebugCompilationUnit %uint_1 %uint_4 %25 %uint_5
%34 = OpExtInst %void %1 DebugTypeComposite %10 %uint_1 %25 %uint_1 %uint_8 %26 %10 %uint_96 %uint_3 %30
This will become
%7 = OpString "otherfile.hlsl"
%25 = OpExtInst %void %1 DebugSource %7 ; No text operand.
%26 = OpExtInst %void %1 DebugCompilationUnit %uint_1 %uint_4 %25 %uint_5
%34 = OpExtInst %void %1 DebugTypeComposite %10 %uint_1 %25 %uint_1 %uint_8 %26 %10 %uint_96 %uint_3 %30
I see that we are generating a DebugCompitationUnit for otherfile.hlsl (%26). I believe that this is wrong. However, I don't think fixing that is necessary for this issue.
Note that this will in general break source debugging when the preprocess file is provided because no real source file belong to the given preprocessed source. If we wanted to try to provide the best experience, DXC could try to look up the file otherfile.hlsl to see if it can find it. If it can, use that file as the text for the DebugSource. If it cannot, omit the text operand. This is a reasonable solution, but will probably have hard to understand behaviour users. The debug information will be different depending on the layout of the filesystem. For a preprocess source file, this might be unexpected.
@SteveUrquhart Do you have any thoughts? This is probably related to (not caused by) https://github.com/microsoft/DirectXShaderCompiler/pull/6085.
I think example.hlsl should report a 1 line DebugSource text operand consisting of the initial input string’s blank line 1. The #line 1 "otherfile.hlsl" should not be represented in a DebugSource text field, but the remaining lines in example.hlsl should all appear in the "otherfile.hlsl" DebugSource instruction. There's no need to try and find an otherfile.hlsl on disk, we're told the rest of the input string is really from otherfile.hlsl, so we should just honor that. The preprocessing may have occurred on another machine, and there's no guarantee we can find otherfile.hlsl on disk.
If we consider OP's blit_hdr.hlsl, the DebugSource for blit_hdr.hlsl needs no output to account for #line 1 "imagine_core.hlsl" at input line 21, or #line 1 "tonemapping.hlsl" at input line 1068. Blit_hdr.hlsl’s DebugSource requires only input string lines 1189 through 1337. I think any input containing #line instructions must have been preprocessed, and this model preserves the original source for debuggers. I think this is due to https://github.com/microsoft/DirectXShaderCompiler/pull/6085 as the OP notes strings are repeated 13 times, and there are 13 files mentioned in the input string. So my plan is to make a PR that splits the input into 13 distinct and non-overlapping DebugSource instructions as outlined here.
I already have a change-set for restricting DebugCompilationUnit to one occurrence per SPIR-V module, and I will request permission to PR that.
FWIW I strongly disagree and this differs from all other shader debug info that I've seen including what DXC outputs in DXIL mode. The debug info normally contains exactly the input files used to compile, no more and no less. This is simpler than trying to parse preprocessor definitions into different files in the debug info or looking up files on disk which as you point out may not exist or may be impossible to locate without further path information. If one file is passed to the compiler, one DebugSource with text is emitted with the contents of that file - whether or not it includes any particular preprocessor macros.
With your proposal - how would a tool know how to stitch together the disparate files if all the #line information is processed out? without more information I don't see how shader editing would work since you'd know that the compilation unit is example.hlsl which is an empty file, and then have no information on which other files to build and which order.
Think about it more. I'm generally in agreement with @baldurk that trying too split up the preprocessed file will be a too difficult and the results will generally not be useful. If many macros have been filled in.
Also, if the original source files are not available, the we output a DebugSource with no text for the source file. I would be okay with us looking on the disk for the source file the same way a debugger like gdb and lldb do. It is not necessary.
This will lead to a less satisfying user experience for those debugging spir-v compile from a preprocessed file, but that is consistent with debugging a C/C++ application when the original source files are not available to the debugger.
If one file is passed to the compiler, one DebugSource with text is emitted with the contents of that file - whether or not it includes any particular preprocessor macros.
I don't know what you mean by "one DebugSource". Should there be only one DebugSource instruction and all debug instruction should point to a line number in that file? Or do you mean there should be only one DebugSource instruction that has the optional text operand?
If you mean the latter, we are in agreement. This would make us consistent with what GLSL does: https://godbolt.org/z/xhK69fo5P.
If you mean the former, It may not be feasible to implement. I don't know the debug code generation too well, but this is what I assume is happening. The AST will be generated form the preprocessed file, whether it was presproccess before or after the source was give on the command line. This would force us to add a special case to the AST creation that says if the line directive came from the original source, the AST should ignore it. Note that someone may have modified the preprocessed file to add a new include file. It does not seem reasonable to try to distinguish these cases.
I mean the latter yes - I'm suggesting less work and processing not more. If dxc does fopen("foo.hlsl") then the output should include DebugSource %foo_hlsl %foo_hlsl_contents with exactly what dxc read out of foo.hlsl. I say one DebugSource with text per file because if there are further files read for #include directives they would also be output 1:1 as a DebugSource %bar_hlsl %bar_hlsl_contents - again regardless of any preprocessor directives within them.
The line number information on instructions can still respect #line directives, and e.g. this is why dxc in DXIL mode and other tools like in your example will output a synthetic DebugSource %otherfile_hlsl with no text field in order to reference them in later DebugLine directives. It is the responsibility of a debugger or any other tool processing this debug information to match this up and understand how preprocessing affects this, but importantly no information is lost as a result of compilation and doing a recompile has the exact same inputs.
As I say this is how dxc works for DXIL and how other compilers work so that seems sensible to do here too.
We are in agreement.
Now we stumbled upon this in our engine too. It looks like the issue appeared somewhere between v1.8.2407 and v1.8.2502. I don't quite understand how it's RenderDoc's fault, because the issue depends on the dxcompiler.dll version. I tried to investigate it further, but I stumbled upon so many strange interactions that I don't even understand how it should work in the first place.
For example, it looks like DXC tries to find sources from disk when it encounters #line during compilation. The issue, however, is that it completely ignores the include handler and goes directly to disk. This seems wrong, because what's the point of the include handler then? We, for example, have our own virtual filesystem in the engine, and the paths in includes don't exist on disk. Moreover, it's bad for compilation performance when we can't control disk reads.
And it looks like this behavior existed even before v1.8.2502. We just didn't notice it, because in our case DXC always failed to find paths on disk, but debugging worked anyway. But starting from v1.8.2502, it looks like the #line directive now fails to produce a result that can be used by RenderDoc to debug a shader.