-
Notifications
You must be signed in to change notification settings - Fork 2
[DirectX] Write DXIL with debug info to ILDB part #158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dx-debug-main
Are you sure you want to change the base?
Changes from all commits
b495ef3
28108be
b13ae6d
5560932
0281db9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| #include "llvm/ADT/StringRef.h" | ||
| #include "llvm/Analysis/ModuleSummaryAnalysis.h" | ||
| #include "llvm/IR/Constants.h" | ||
| #include "llvm/IR/DebugInfo.h" | ||
| #include "llvm/IR/DerivedTypes.h" | ||
| #include "llvm/IR/GlobalVariable.h" | ||
| #include "llvm/IR/IntrinsicInst.h" | ||
|
|
@@ -28,6 +29,7 @@ | |
| #include "llvm/InitializePasses.h" | ||
| #include "llvm/Pass.h" | ||
| #include "llvm/Support/Alignment.h" | ||
| #include "llvm/Transforms/Utils/Cloning.h" | ||
| #include "llvm/Transforms/Utils/ModuleUtils.h" | ||
|
|
||
| using namespace llvm; | ||
|
|
@@ -138,7 +140,72 @@ static void removeLifetimeIntrinsics(Module &M) { | |
| } | ||
| } | ||
|
|
||
| static void replaceNamedMetadataArray(Module &M, StringRef Name, | ||
| ArrayRef<Metadata *> NewOps) { | ||
| NamedMDNode *NMD = M.getNamedMetadata(Name); | ||
| if (!NMD) | ||
| return; | ||
| NMD->eraseFromParent(); | ||
| M.getOrInsertNamedMetadata(Name)->addOperand( | ||
| MDTuple::get(M.getContext(), NewOps)); | ||
| } | ||
|
|
||
| class EmbedDXILPass : public llvm::ModulePass { | ||
| std::string writeModule(Module &M, bool HasDebugInfo, bool WriteDebug) { | ||
| std::string Data; | ||
| llvm::raw_string_ostream OS(Data); | ||
|
|
||
| if (HasDebugInfo) { | ||
| if (WriteDebug) { | ||
| // Replace dx.source metadata nodes with stubs. | ||
| // TODO: Add /Qsource_in_debug_module flag to enable/disable this. | ||
| LLVMContext &Ctx = M.getContext(); | ||
| MDString *EmptyString = MDString::get(Ctx, ""); | ||
| replaceNamedMetadataArray(M, "dx.source.contents", | ||
| {EmptyString, EmptyString}); | ||
| replaceNamedMetadataArray(M, "dx.source.defines", {}); | ||
| replaceNamedMetadataArray(M, "dx.source.mainFileName", {EmptyString}); | ||
| replaceNamedMetadataArray(M, "dx.source.args", {}); | ||
| } else { | ||
| // If we have an ILDB part, strip DXIL from all debug info. | ||
| StripDebugInfo(M); | ||
|
|
||
| // Also, manually remove debug version flags and dx.source nodes. | ||
| if (NamedMDNode *Flags = M.getModuleFlagsMetadata()) { | ||
| SmallVector<llvm::Module::ModuleFlagEntry, 4> FlagEntries; | ||
| M.getModuleFlagsMetadata(FlagEntries); | ||
| Flags->eraseFromParent(); | ||
| for (llvm::Module::ModuleFlagEntry &Entry : FlagEntries) { | ||
| if (Entry.Key->getString() == "Dwarf Version" || | ||
| Entry.Key->getString() == "Debug Info Version") { | ||
| continue; | ||
| } | ||
| M.addModuleFlag(Entry.Behavior, Entry.Key->getString(), Entry.Val); | ||
| } | ||
| } | ||
| for (NamedMDNode &NMD : llvm::make_early_inc_range(M.named_metadata())) | ||
| if (NMD.getName().starts_with("dx.source")) | ||
| NMD.eraseFromParent(); | ||
| } | ||
| } | ||
| const auto DIMap = DXILDebugInfoPass::run(M); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move this under under
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The call to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hvdijk mentioned that we can pass an empty map.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Like this? I tried, it crashes everything.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be like const DXILDebugInfoMap DIMap;
...
if (IsDebug) {
...
DIMap = DebugInfoPass::run(M);
} else {
... // No DebugInfoPass::run(M);
}
...
WriteDXILToFile(M, OS, DIMap);
...
}Does it crash in such case as well?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No, it means we may have an intermediate stage where both parts are emitted with debug info. I think it will take longer to get
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We can stack this on top of dx.ildb emission patch as well (it will already depend on some other patches anyway).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, I see, that is fine for what gets merged into LLVM
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Well, we could try sending Yes, if we choose the approach to check debug info presence mainly with StripDebugInfo, then changes for both sections depend on StripDebugInfo call correctness.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
AFAIK if we choose this option from https://llvm.org/docs/GitHub.html#stacked-pull-requests:
We can make a PR which depends on two other PRs. |
||
| WriteDXILToFile(M, OS, DIMap); | ||
| return Data; | ||
| } | ||
|
|
||
| GlobalVariable *createSectionGlobal(Module &M, StringRef Data, | ||
| StringRef GlobalName, | ||
| StringRef SectionName) { | ||
| Constant *ModuleConstant = | ||
| ConstantDataArray::get(M.getContext(), arrayRefFromStringRef(Data)); | ||
| auto *GV = new llvm::GlobalVariable(M, ModuleConstant->getType(), true, | ||
| GlobalValue::PrivateLinkage, | ||
| ModuleConstant, GlobalName); | ||
| GV->setSection(SectionName); | ||
| GV->setAlignment(Align(4)); | ||
| return GV; | ||
| } | ||
|
|
||
| public: | ||
| static char ID; // Pass identification, replacement for typeid | ||
| EmbedDXILPass() : ModulePass(ID) { | ||
|
|
@@ -148,29 +215,38 @@ class EmbedDXILPass : public llvm::ModulePass { | |
| StringRef getPassName() const override { return "DXIL Embedder"; } | ||
|
|
||
| bool runOnModule(Module &M) override { | ||
| std::string Data; | ||
| llvm::raw_string_ostream OS(Data); | ||
|
|
||
| // Perform late legalization of lifetime intrinsics that would otherwise | ||
| // fail the Module Verifier if performed in an earlier pass | ||
| legalizeLifetimeIntrinsics(M); | ||
|
|
||
| const auto DIMap = DXILDebugInfoPass::run(M); | ||
| WriteDXILToFile(M, OS, DIMap); | ||
| bool HasDebugInfo = !M.debug_compile_units().empty(); | ||
| std::string ILDBData; | ||
| if (HasDebugInfo) { | ||
| // Write DXIL with debug info to ILDB part. | ||
| // Clone the module to avoid alternating it with DebugInfoPass | ||
| // before stripping the debug info later. | ||
| ILDBData = | ||
| writeModule(*llvm::CloneModule(M), HasDebugInfo, /*WriteDebug=*/true); | ||
| } | ||
|
|
||
| // Clone the module to save dx.source metadata nodes from stripping, as they | ||
| // are needed for DXILMetadataAnalysisWrapperPass. | ||
| std::string DXILData = | ||
| writeModule(*llvm::CloneModule(M), HasDebugInfo, /*WriteDebug=*/false); | ||
|
|
||
| // We no longer need lifetime intrinsics after bitcode serialization, so we | ||
| // simply remove them to keep the Module Verifier happy after our | ||
| // not-so-legal legalizations | ||
| removeLifetimeIntrinsics(M); | ||
|
|
||
| Constant *ModuleConstant = | ||
| ConstantDataArray::get(M.getContext(), arrayRefFromStringRef(Data)); | ||
| auto *GV = new llvm::GlobalVariable(M, ModuleConstant->getType(), true, | ||
| GlobalValue::PrivateLinkage, | ||
| ModuleConstant, "dx.dxil"); | ||
| GV->setSection("DXIL"); | ||
| GV->setAlignment(Align(4)); | ||
| appendToCompilerUsed(M, {GV}); | ||
| SmallVector<GlobalValue *, 2> Globals; | ||
| if (HasDebugInfo) { | ||
| // Create a GV after both parts are written, otherwise it gets | ||
| // added to DXIL when `writeModule` is called the second time. | ||
| Globals.emplace_back(createSectionGlobal(M, ILDBData, "dx.ildb", "ILDB")); | ||
| } | ||
| Globals.emplace_back(createSectionGlobal(M, DXILData, "dx.dxil", "DXIL")); | ||
| appendToCompilerUsed(M, Globals); | ||
| return true; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| ; RUN: opt %s -dxil-embed -dxil-globals -S -o - | FileCheck %s | ||
| ; RUN: llc %s --filetype=obj -o %t.bc | ||
| ; RUN: obj2yaml %t.bc | FileCheck %s --check-prefix=YAML | ||
| ; RUN: llvm-objcopy --dump-section=ILDB=%t.ildb %t.bc | ||
| ; RUN: llvm-objcopy --dump-section=DXIL=%t.dxil %t.bc | ||
| ; RUN: llvm-dis %t.ildb -o - | FileCheck %s --check-prefix=ILDB-DIS | ||
| ; RUN: llvm-dis %t.dxil -o - | FileCheck %s --check-prefix=DXIL-DIS | ||
|
|
||
| target triple = "dxil-unknown-shadermodel6.5-library" | ||
| ; CHECK: target triple = "dxil-unknown-shadermodel6.5-library" | ||
|
|
||
| define i32 @add(i32 %a, i32 %b) { | ||
| %sum = add i32 %a, %b | ||
| ret i32 %sum | ||
| } | ||
|
|
||
| !llvm.dbg.cu = !{!0} | ||
| !llvm.module.flags = !{!3, !4} | ||
| !dx.source.contents = !{!5} | ||
| !dx.source.defines = !{!6} | ||
| !dx.source.mainFileName = !{!7} | ||
| !dx.source.args = !{!8} | ||
|
|
||
| !0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "Some Compiler", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, enums: !2, splitDebugInlining: false, nameTableKind: None) | ||
| !1 = !DIFile(filename: "hlsl.hlsl", directory: "/some-path") | ||
| !2 = !{} | ||
| !3 = !{i32 7, !"Dwarf Version", i32 2} | ||
| !4 = !{i32 2, !"Debug Info Version", i32 3} | ||
| !5 = !{!"hlsl.hlsl", !"int add(int a, int b) { return a + b; }"} | ||
| !6 = !{} | ||
| !7 = !{!"hlsl.hlsl"} | ||
| !8 = !{!"-T", !"lib_6_5", !"-g", !"hlsl.hlsl"} | ||
|
|
||
| ; Check that both parts are emitted as a GV and used by the compiler. | ||
|
|
||
| ; CHECK: @dx.ildb = private constant [[BC_TYPE:\[[0-9]+ x i8\]]] c"BC\C0\DE{{[^"]+}}", section "ILDB", align 4 | ||
| ; CHECK: @dx.dxil = private constant [[BC_TYPE:\[[0-9]+ x i8\]]] c"BC\C0\DE{{[^"]+}}", section "DXIL", align 4 | ||
| ; CHECK: @llvm.compiler.used = appending global {{\[[0-9]+ x ptr\]}} [ptr @dx.ildb, ptr @dx.dxil | ||
|
|
||
| ; This is using regex matches on some sizes, offsets and fields. These are all | ||
| ; going to change as the DirectX backend continues to evolve and implement more | ||
| ; features. Rather than extending this test to cover those future features, this | ||
| ; test's matches are extremely fuzzy so that it won't break. | ||
|
|
||
| ; YAML: --- !dxcontainer | ||
| ; YAML-NEXT: Header: | ||
| ; YAML-NEXT: Hash: [ 0x0, 0x0, 0x0, | ||
| ; YAML: Version: | ||
| ; YAML-NEXT: Major: 1 | ||
| ; YAML-NEXT: Minor: 0 | ||
| ; YAML-NEXT: FileSize: [[#]] | ||
| ; YAML-NEXT: PartCount: [[#]] | ||
| ; YAML-NEXT: PartOffsets: [ {{[0-9, ]+}} | ||
| ; YAML: Parts: | ||
|
|
||
| ; In verifying the DXIL and ILDB parts, this test captures the size of the part, | ||
| ; and derives the program header and dxil size fields from the part's size. | ||
|
|
||
| ; YAML: - Name: ILDB | ||
| ; YAML-NEXT: Size: [[#ILDBSIZE:]] | ||
| ; YAML-NEXT: Program: | ||
| ; YAML-NEXT: MajorVersion: 6 | ||
| ; YAML-NEXT: MinorVersion: 5 | ||
| ; YAML-NEXT: ShaderKind: 6 | ||
| ; YAML-NEXT: Size: [[#div(ILDBSIZE,4)]] | ||
| ; YAML-NEXT: DXILMajorVersion: 1 | ||
| ; YAML-NEXT: DXILMinorVersion: 5 | ||
| ; YAML-NEXT: DXILSize: [[#ILDBSIZE - 24]] | ||
| ; YAML-NEXT: DXIL: [ 0x42, 0x43, 0xC0, 0xDE, | ||
| ; YAML: - Name: DXIL | ||
| ; YAML-NEXT: Size: [[#DXILSIZE:]] | ||
| ; YAML-NEXT: Program: | ||
| ; YAML-NEXT: MajorVersion: 6 | ||
| ; YAML-NEXT: MinorVersion: 5 | ||
| ; YAML-NEXT: ShaderKind: 6 | ||
| ; YAML-NEXT: Size: [[#div(DXILSIZE,4)]] | ||
| ; YAML-NEXT: DXILMajorVersion: 1 | ||
| ; YAML-NEXT: DXILMinorVersion: 5 | ||
| ; YAML-NEXT: DXILSize: [[#DXILSIZE - 24]] | ||
| ; YAML-NEXT: DXIL: [ 0x42, 0x43, 0xC0, 0xDE, | ||
|
|
||
| ; Check that ILDB has the debug info, and DXIL does not: | ||
|
|
||
| ; ILDB-DIS: define i32 @add(i32 %a, i32 %b) | ||
| ; ILDB-DIS: !llvm.dbg.cu | ||
| ; ILDB-DIS: !DICompileUnit | ||
| ; ILDB-DIS: !DIFile | ||
| ; ILDB-DIS: !"Dwarf Version" | ||
| ; ILDB-DIS: !"Debug Info Version" | ||
|
|
||
| ; DXIL-DIS: define i32 @add(i32 %a, i32 %b) | ||
| ; DXIL-DIS-NOT: !llvm.dbg.cu | ||
| ; DXIL-DIS-NOT: !dx.source | ||
| ; DXIL-DIS-NOT: !DICompileUnit | ||
| ; DXIL-DIS-NOT: !DIFile | ||
| ; DXIL-DIS-NOT: !"Dwarf Version" | ||
| ; DXIL-DIS-NOT: !"Debug Info Version" | ||
| ; DXIL-DIS-NOT: "hlsl.hlsl" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function returns a
boolindicating whether any debug info was stripped, we can use this to avoid checking throughdebug_compile_units().empty()and get a more reliable answer.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we specifically need to strip debug info only if we already know if there is debug info. Is there any other way to know this?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK if
!llvm.dbg.cuis empty, LLVM assumes no debug info is present in the file (see #150 (comment)). Are you aware of cases when we have correct and full LLVM IR with debug info but without compile units?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no harm in stripping debug info if there is no debug info, the function will just tell you there was nothing to strip, and it will have done a better search than the current code. So something like
std::unique_ptr<Module> Clone = llvm::CloneModule(M); bool HaveDebugInfo = llvm::StripDebugInfo(*Clone);, then write theDXILsection from*Clone(which assuredly has no debug info, regardless of whether there was originally debug info), then ifHaveDebugInfois true, create your second clone and write theILDBsection from that.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it performs an extra pass over the whole module which can be avoided when shader is compiled without debug info.
(Anyway, my opinion on that matter should not block this PR.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be, sure, but...
...the issue won't be hidden: it will accurately write a DXIL section without debug info, and an ILDB section with the weird debug info.
I'd be fine with turning this into an assert instead somewhere, but silently leaving debug info in the section that specifically is meant to not have debug info seems wrong, especially if we don't know if there may be legitimate reasons for it that we're just not thinking of now.
Note that I didn't say no
DICompileUnit, just no!llvm.dbg.cunamed metadata. There are loads of LLVM tests that have aDICompileUnitthat is not linked through named metadata, a random example isllvm/test/Transforms/Coroutines/declare-value.ll.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Such IRs are definitely incorrect from the Verifier's point of view - see
Verifier::verifyCompileUnits()- it discards debug info containing compile units that are not listed in!llvm.dbg.cu,For the test you referenced:
Tests from
llvm\test\Transformssometimes do not care about that - they run a single pass, not full llc pipeline (andoptsilences invalid debug info warning). So it may have happened that there they've been unverified.It may be easier to spot a problem when debug info section requested by the user is not created than when it is created but later debug info is discarded by IR consumers. Usually, IR loader does not raise an error if debug info is incorrect - it silently strips it (or it raises a warning if user explicitly invokes llvm-dis, but not an error).
That's definitely worth doing if we leave unconditional Strip call 👍
We could also do it the other way around: detect debug info presence by checking compile units, and add an
assert(StripDebugInfo(M) == HasDebugInfo && "This module must not contain anything to be stripped")There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, what's the consensus? Do we change the check
!M.debug_compile_units().empty()to a call toStripDebugInfowith an assert?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I won't be blocking it if we use Strip as the source of truth here, but an extra assert for
!M.debug_compile_units().empty()value would be nice :)Motivation: other passes (either DX-specific or generic IR passes) may check compile units list still, to check if debug info should be processed, and they won't be calling StripDebugInfo wherever they want to check for debug info presence. So we want to be sure that both debug info presence indicators converge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The details of how you do it are up to you, but what matters to me is that if we have debug info, even weird debug info, we don't enter the "no debug info" case, at least in an assertions-enabled build. Checking the result of
StripDebugInfoseemed to me like the simplest way of achieving that, but any other way that also achieves that same goal works for me.