I’m working on implementing NonSemantic.Shader.DebugInfo (NSDI) debug info support for HLSL shaders compiled to SPIR-V via clang-dxc. In parallel, I am also extending NSDI to support new SPIR-V extensions and have recently unified its versioning scheme, so we can keep adding new instructions for any SPIR-V extensions that may need new debug instruction support. Additionally, I want to make sure that clang-dxc supports emitting versioned NSDI.
I have been learning how Clang and the SPIR-V backend deal with debug info and I have some ideas and questions that I would like to discuss here. I’m new to the SPIR-V backend, so some of what I say below may be wrong, and I’m looking for corrections. My goal is to understand the design well enough to contribute the HLSL-specific pieces without getting in the way.
Current state of the NSDI pass
The LLVM SPIR-V backend uses SPIRVEmitNonSemanticDI to emit DebugSource, DebugCompilationUnit, DebugTypeBasic, and DebugTypePointer. Nothing is emitted per-function: no function extents, no line numbers, no variable locations.
I found three active PRs extending this:
- #183117: Refactors the pass to add
CompileUnitRegMapand per-function DI emission. - #183121: Fixes
SPIRVModuleAnalysisto use a blacklist for routing NSDI instructions to the correct layout section. - #183122 (WIP): Adds
DebugFunction,DebugTypeFunction, andDebugFunctionDefinition.
There is also #179975, a WIP PR covering the complete NSDI instruction set and a ModulePass conversion. @mgcarrasco, I understand that you are breaking it up into smaller pieces?
Beyond the general NSDI backend work, I found some HLSL-specific issues:
GetSourceLanguage()does not checkLO.HLSL, so HLSL shaders compiled with-gget tagged withDW_LANG_C_plus_plus_14instead ofDW_LANG_HLSL. The NSDI pass correctly mapsDW_LANG_HLSLto language code 5, but the mapping is never triggered. This relates to issues #136929 and #136995.- HLSL vectors (
float4,int3) are lowered tollvm::FixedVectorTypebefore debug info is emitted. The debug metadata contains an anonymous array type rather than the HLSL vector name. NSDI hasDebugTypeVector(opcode 6) for this case. - Same issue for HLSL matrices (
float4x4). NSDI hasDebugTypeMatrix(opcode 108). HLSLAttributedResourceType::CreateType()delegates to the wrapped type.RWBuffer<float>appears asfloatin debug info. The DXIL path was fixed in #119041; the SPIR-V path was not.- No
DebugEntryPoint(opcode 107) emission for HLSL shader entry points. - The NSDI pass is not activated automatically when
-gis passed to a SPIR-V target. For clang-dxc, it should be.
There may be others. I’ve only just begun looking at this.
Design direction
AFAIU, LLVM has two debug writers: DwarfDebug and CodeViewDebug. They target different output formats but share the same architecture. Both implement the AsmPrinterHandler interface and are registered with AsmPrinter as debug handlers. The shared lifecycle is: beginModule collects compile units from llvm.dbg.cu and emits module-scope singletons; beginFunction / endFunction process per-function metadata; endModule flushes deferred output. Type emission is lazy in both: types are lowered on first reference via getOrCreate* helpers, not pre-collected. Neither writer is a pass.
In my view, the NSDI writer should work exactly the same way. The only difference is the output format: it emits OpExtInst NonSemantic.Shader.DebugInfo instructions instead of DWARF DIEs or CodeView records. The IR metadata mapping is direct:
DICompileUnitbecomesDebugCompilationUnitDISubprogrambecomesDebugFunction+DebugFunctionDefinitionDILocalVariablebecomesDebugLocalVariableDILocationbecomesDebugLineDIExpressionbecomesDebugExpression+DebugOperation
Concretely, I would replace SPIRVEmitNonSemanticDI by an AsmPrinterHandler subclass registered in SPIRVAsmPrinter, not extended as a pass. This is identical to how DwarfDebug works: it is a handler owned and driven by the printer, not a pass that runs alongside it. This way, we get:
- No deduplication logic.
beginModuleruns once.DebugCompilationUnitand all module-scope type instructions are singletons by construction. No tracking maps or dedup guards are needed. - Correct placement by construction. Instructions emitted in
beginModuleprecede anyOpFunction. Instructions emitted inbeginFunctionstay in the function body. The placement routing inSPIRVModuleAnalysisbecomes unnecessary for NSDI instructions. - Lazy type deduplication. A
getOrCreateDebugType(DIType *)helper emits each type once and returns its result register. Recursive types are handled via the same deferred-completion patternCodeViewDebuguses. - Topological ordering. Types emitted lazily on first reference produce a valid topological order automatically. No pre-pass to sort types is needed.
- Re-use existing support.
AsmPrinterHandlersubclasses have access toDbgValueHistoryMap(variable location tracking),DebugLocEntry(location lists), andDwarfExpression(DIExpressionlowering). These cover the hardest parts of a debug writer. Reusing them avoids re-implementing variable location logic from scratch. This has been a difficult issue to fix in DXC because SPIRV-Tools optimizer does not handle debug info properly.
I’m not certain whether SPIRVAsmPrinter already supports addDebugHandler or whether that requires additional backend changes. I’m also not familiar enough with the SPIR-V backend’s instruction emission model to know whether an AsmPrinterHandler has direct access to the SPIR-V instruction builder at the points it needs it. These are constraints I may be underestimating, and I’m looking for input.
What I would suggest:
- Land PR #183121 now. It is independent from my concerns.
- Convert
SPIRVEmitNonSemanticDIto anAsmPrinterHandlersubclass registered inSPIRVAsmPrinter. This goes further than theModulePassconversion in #179975 and aligns the NSDI writer structurally withDwarfDebugandCodeViewDebug. - Re-layer the features from #183117 and #183122 on top of the handler.
I may be missing backend constraints that make the AsmPrinterHandler conversion impractical. If so, the ModulePass path from #179975 is a reasonable intermediate step. Either way, I don’t think the MachineFunctionPass model works well long-term.
My focus is on the HLSL-specific pieces that are not covered by the general NSDI backend work:
- Fix
GetSourceLanguage()to checkLO.HLSLbeforeLO.CPlusPlus. This is a small standalone PR; it links to issues #136929 and #136995. - Frontend representation for HLSL vectors and matrices that preserves the HLSL type name in
DICompositeType, enabling correctDebugTypeVectorandDebugTypeMatrixemission. DebugTypeMatrix(opcode 108) emission inSPIRVEmitNonSemanticDI.DebugEntryPoint(opcode 107) emission for HLSL entry points.- Automatic pass activation when
-gis passed to a SPIR-V target.
Questions
- @mgcarrasco: Is there a technical reason the
MachineFunctionPassmodel has to be kept for #183117 and #183122, rather than convertingSPIRVEmitNonSemanticDIto anAsmPrinterHandlersubclass first? TheAsmPrinterHandlerlifecycle (beginModule/beginFunction/endFunction/endModule) maps directly onto NSDI’s module-scope vs. function-scope split, and the features in both PRs can be ported to it without deduplication logic. If there are backend constraints that make this impractical, I’d like to understand them, since they affect how the HLSL-specific contributions should be structured. - Would it be useful to split just the
SPV_KHR_non_semantic_infoOpExtensiondeclaration out of #179975 into a minimal standalone PR? - Is there someone working on
DebugLine/DebugNoLine(opcodes 103, 104)? The prior attempt (#113541) identified two problems that are now addressable. I’m happy to contribute this if it is not already claimed.
@mgcarrasco, @s-perron, @Keenuts, @beanz, @echristo, @dblaikie, and anyone else interested in SPIR-V debug info support: does this sound reasonable? Please correct me on any misunderstandings I may have about how debug info emission should work.
Finally, what is the right forum to discuss design ideas? Here on discourse? As a github issue on llvm-project?
Thanks. Diego.