Skip to content

Conversation

inbelic
Copy link
Contributor

@inbelic inbelic commented Aug 20, 2025

This pr adds the Qstrip-rootsignature as a DXC driver option.

To do so, this pr introduces the BinaryModifyJobClass as an Action to modify a produced object file before its final output.

Further, it registers llvm-objcopy as the tool to modify a produced DXContainer on the HLSL toolchain.

This allows us to specify the Qstrip-rootsignature option to clang-dxc which will invoke llvm-objcopy with a --remove-section=RTS0 argument to implement its functionality.

Resolves: #150275.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' clang:frontend Language frontend issues, e.g. anything involving "Sema" HLSL HLSL Language Support labels Aug 20, 2025
@llvmbot
Copy link
Member

llvmbot commented Aug 20, 2025

@llvm/pr-subscribers-hlsl
@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-driver

Author: Finn Plummer (inbelic)

Changes

It pr adds the Qstrip-rootsignature as a DXC driver option.

To do so, this pr introduces the BinaryModifyJobClass as an Action to modify a produced object file before its final output.

Further, it registers llvm-objcopy as the tool to modify a produced DXContainer on the HLSL toolchain.

This allows us to specify the Qstrip-rootsignature option to clang-dxc which will invoke llvm-objcopy with a --remove-section=RTS0 argument to implement its functionality.

Resolves: #150275.


Full diff: https://github.com/llvm/llvm-project/pull/154454.diff

9 Files Affected:

  • (modified) clang/include/clang/Basic/CodeGenOptions.def (+3)
  • (modified) clang/include/clang/Driver/Action.h (+13-1)
  • (modified) clang/include/clang/Driver/Options.td (+6)
  • (modified) clang/lib/Driver/Action.cpp (+7)
  • (modified) clang/lib/Driver/Driver.cpp (+10)
  • (modified) clang/lib/Driver/ToolChain.cpp (+1)
  • (modified) clang/lib/Driver/ToolChains/HLSL.cpp (+39-2)
  • (modified) clang/lib/Driver/ToolChains/HLSL.h (+15)
  • (added) clang/test/Driver/dxc_strip_rootsignature.hlsl (+22)
diff --git a/clang/include/clang/Basic/CodeGenOptions.def b/clang/include/clang/Basic/CodeGenOptions.def
index 423b696785500..89e9f5b5853bb 100644
--- a/clang/include/clang/Basic/CodeGenOptions.def
+++ b/clang/include/clang/Basic/CodeGenOptions.def
@@ -479,6 +479,9 @@ CODEGENOPT(StaticClosure, 1, 0, Benign)
 /// Assume that UAVs/SRVs may alias
 CODEGENOPT(ResMayAlias, 1, 0, Benign)
 
+/// Omit the root signature from produced DXContainer
+CODEGENOPT(HLSLRootSigStrip, 1, 0, Benign)
+
 /// Controls how unwind v2 (epilog) information should be generated for x64
 /// Windows.
 ENUM_CODEGENOPT(WinX64EHUnwindV2, WinX64EHUnwindV2Mode,
diff --git a/clang/include/clang/Driver/Action.h b/clang/include/clang/Driver/Action.h
index 7aecfd886adb8..6fc515f9e4049 100644
--- a/clang/include/clang/Driver/Action.h
+++ b/clang/include/clang/Driver/Action.h
@@ -76,9 +76,10 @@ class Action {
     StaticLibJobClass,
     BinaryAnalyzeJobClass,
     BinaryTranslatorJobClass,
+    BinaryModifyJobClass,
 
     JobClassFirst = PreprocessJobClass,
-    JobClassLast = BinaryTranslatorJobClass
+    JobClassLast = BinaryModifyJobClass
   };
 
   // The offloading kind determines if this action is binded to a particular
@@ -687,6 +688,17 @@ class BinaryTranslatorJobAction : public JobAction {
   }
 };
 
+class BinaryModifyJobAction : public JobAction {
+  void anchor() override;
+
+public:
+  BinaryModifyJobAction(Action *Input, types::ID Type);
+
+  static bool classof(const Action *A) {
+    return A->getKind() == BinaryModifyJobClass;
+  }
+};
+
 } // namespace driver
 } // namespace clang
 
diff --git a/clang/include/clang/Driver/Options.td b/clang/include/clang/Driver/Options.td
index 6aab43c9ed57f..1bd3f09ae1df1 100644
--- a/clang/include/clang/Driver/Options.td
+++ b/clang/include/clang/Driver/Options.td
@@ -9381,6 +9381,12 @@ def res_may_alias : Option<["/", "-"], "res-may-alias", KIND_FLAG>,
   Visibility<[DXCOption, ClangOption, CC1Option]>,
   HelpText<"Assume that UAVs/SRVs may alias">,
   MarshallingInfoFlag<CodeGenOpts<"ResMayAlias">>;
+def dxc_strip_rootsignature :
+  Option<["/", "-"], "Qstrip-rootsignature", KIND_FLAG>,
+  Group<dxc_Group>,
+  Visibility<[DXCOption]>,
+  HelpText<"Omit the root signature from produced DXContainer">,
+  MarshallingInfoFlag<CodeGenOpts<"HLSLRootSigStrip">>;
 def target_profile : DXCJoinedOrSeparate<"T">, MetaVarName<"<profile>">,
   HelpText<"Set target profile">,
   Values<"ps_6_0, ps_6_1, ps_6_2, ps_6_3, ps_6_4, ps_6_5, ps_6_6, ps_6_7,"
diff --git a/clang/lib/Driver/Action.cpp b/clang/lib/Driver/Action.cpp
index ec09726044812..fbf80b3c6f124 100644
--- a/clang/lib/Driver/Action.cpp
+++ b/clang/lib/Driver/Action.cpp
@@ -52,6 +52,8 @@ const char *Action::getClassName(ActionClass AC) {
     return "binary-analyzer";
   case BinaryTranslatorJobClass:
     return "binary-translator";
+  case BinaryModifyJobClass:
+    return "binary-modifier";
   }
 
   llvm_unreachable("invalid class");
@@ -467,3 +469,8 @@ void BinaryTranslatorJobAction::anchor() {}
 BinaryTranslatorJobAction::BinaryTranslatorJobAction(Action *Input,
                                                      types::ID Type)
     : JobAction(BinaryTranslatorJobClass, Input, Type) {}
+
+void BinaryModifyJobAction::anchor() {}
+
+BinaryModifyJobAction::BinaryModifyJobAction(Action *Input, types::ID Type)
+    : JobAction(BinaryModifyJobClass, Input, Type) {}
diff --git a/clang/lib/Driver/Driver.cpp b/clang/lib/Driver/Driver.cpp
index 586f287843f3e..59a4eadcd5d85 100644
--- a/clang/lib/Driver/Driver.cpp
+++ b/clang/lib/Driver/Driver.cpp
@@ -4601,6 +4601,16 @@ void Driver::BuildActions(Compilation &C, DerivedArgList &Args,
         Actions.push_back(C.MakeAction<BinaryTranslatorJobAction>(
             LastAction, types::TY_DX_CONTAINER));
     }
+    if (TC.requiresObjcopy(Args)) {
+      Action *LastAction = Actions.back();
+      // llvm-objcopy expects a DXIL container, which can either be
+      // validated (in which case they are TY_DX_CONTAINER), or unvalidated
+      // (TY_OBJECT).
+      if (LastAction->getType() == types::TY_DX_CONTAINER ||
+          LastAction->getType() == types::TY_Object)
+        Actions.push_back(C.MakeAction<BinaryModifyJobAction>(
+            LastAction, types::TY_DX_CONTAINER));
+    }
   }
 
   // Claim ignored clang-cl options.
diff --git a/clang/lib/Driver/ToolChain.cpp b/clang/lib/Driver/ToolChain.cpp
index 25c6b5a486fd5..713bda1d7244d 100644
--- a/clang/lib/Driver/ToolChain.cpp
+++ b/clang/lib/Driver/ToolChain.cpp
@@ -652,6 +652,7 @@ Tool *ToolChain::getTool(Action::ActionClass AC) const {
   case Action::VerifyDebugInfoJobClass:
   case Action::BinaryAnalyzeJobClass:
   case Action::BinaryTranslatorJobClass:
+  case Action::BinaryModifyJobClass:
     llvm_unreachable("Invalid tool kind.");
 
   case Action::CompileJobClass:
diff --git a/clang/lib/Driver/ToolChains/HLSL.cpp b/clang/lib/Driver/ToolChains/HLSL.cpp
index 38f4643abad98..a554c15f1e7c6 100644
--- a/clang/lib/Driver/ToolChains/HLSL.cpp
+++ b/clang/lib/Driver/ToolChains/HLSL.cpp
@@ -247,6 +247,30 @@ void tools::hlsl::MetalConverter::ConstructJob(
                                          Exec, CmdArgs, Inputs, Input));
 }
 
+void tools::hlsl::LLVMObjcopy::ConstructJob(Compilation &C, const JobAction &JA,
+                                            const InputInfo &Output,
+                                            const InputInfoList &Inputs,
+                                            const ArgList &Args,
+                                            const char *LinkingOutput) const {
+
+  std::string ObjcopyPath = getToolChain().GetProgramPath("llvm-objcopy");
+  const char *Exec = Args.MakeArgString(ObjcopyPath);
+
+  ArgStringList CmdArgs;
+  assert(Inputs.size() == 1 && "Unable to handle multiple inputs.");
+  const InputInfo &Input = Inputs[0];
+  CmdArgs.push_back(Input.getFilename());
+  CmdArgs.push_back(Output.getFilename());
+
+  if (Args.hasArg(options::OPT_dxc_strip_rootsignature)) {
+    const char *Frs = Args.MakeArgString("--remove-section=RTS0");
+    CmdArgs.push_back(Frs);
+  }
+
+  C.addCommand(std::make_unique<Command>(JA, *this, ResponseFileSupport::None(),
+                                         Exec, CmdArgs, Inputs, Input));
+}
+
 /// DirectX Toolchain
 HLSLToolChain::HLSLToolChain(const Driver &D, const llvm::Triple &Triple,
                              const ArgList &Args)
@@ -267,6 +291,10 @@ Tool *clang::driver::toolchains::HLSLToolChain::getTool(
     if (!MetalConverter)
       MetalConverter.reset(new tools::hlsl::MetalConverter(*this));
     return MetalConverter.get();
+  case Action::BinaryModifyJobClass:
+    if (!LLVMObjcopy)
+      LLVMObjcopy.reset(new tools::hlsl::LLVMObjcopy(*this));
+    return LLVMObjcopy.get();
   default:
     return ToolChain::getTool(AC);
   }
@@ -404,16 +432,25 @@ bool HLSLToolChain::requiresBinaryTranslation(DerivedArgList &Args) const {
   return Args.hasArg(options::OPT_metal) && Args.hasArg(options::OPT_dxc_Fo);
 }
 
+bool HLSLToolChain::requiresObjcopy(DerivedArgList &Args) const {
+  return Args.hasArg(options::OPT_dxc_Fo) &&
+         Args.hasArg(options::OPT_dxc_strip_rootsignature);
+}
+
 bool HLSLToolChain::isLastJob(DerivedArgList &Args,
                               Action::ActionClass AC) const {
   bool HasTranslation = requiresBinaryTranslation(Args);
   bool HasValidation = requiresValidation(Args);
+  bool HasObjcopy = requiresObjcopy(Args);
   // If translation and validation are not required, we should only have one
   // action.
-  if (!HasTranslation && !HasValidation)
+  if (!HasTranslation && !HasValidation && !HasObjcopy)
     return true;
   if ((HasTranslation && AC == Action::BinaryTranslatorJobClass) ||
-      (!HasTranslation && HasValidation && AC == Action::BinaryAnalyzeJobClass))
+      (!HasTranslation && HasValidation &&
+       AC == Action::BinaryAnalyzeJobClass) ||
+      (!HasTranslation && !HasValidation && HasObjcopy &&
+       AC == Action::BinaryModifyJobClass))
     return true;
   return false;
 }
diff --git a/clang/lib/Driver/ToolChains/HLSL.h b/clang/lib/Driver/ToolChains/HLSL.h
index 3824b4252324b..b81141b26b4e6 100644
--- a/clang/lib/Driver/ToolChains/HLSL.h
+++ b/clang/lib/Driver/ToolChains/HLSL.h
@@ -42,6 +42,19 @@ class LLVM_LIBRARY_VISIBILITY MetalConverter : public Tool {
                     const llvm::opt::ArgList &TCArgs,
                     const char *LinkingOutput) const override;
 };
+
+class LLVM_LIBRARY_VISIBILITY LLVMObjcopy : public Tool {
+public:
+  LLVMObjcopy(const ToolChain &TC)
+      : Tool("hlsl::LLVMObjcopy", "llvm-objcopy", TC) {}
+
+  bool hasIntegratedCPP() const override { return false; }
+
+  void ConstructJob(Compilation &C, const JobAction &JA,
+                    const InputInfo &Output, const InputInfoList &Inputs,
+                    const llvm::opt::ArgList &TCArgs,
+                    const char *LinkingOutput) const override;
+};
 } // namespace hlsl
 } // namespace tools
 
@@ -65,6 +78,7 @@ class LLVM_LIBRARY_VISIBILITY HLSLToolChain : public ToolChain {
   static std::optional<std::string> parseTargetProfile(StringRef TargetProfile);
   bool requiresValidation(llvm::opt::DerivedArgList &Args) const;
   bool requiresBinaryTranslation(llvm::opt::DerivedArgList &Args) const;
+  bool requiresObjcopy(llvm::opt::DerivedArgList &Args) const;
   bool isLastJob(llvm::opt::DerivedArgList &Args, Action::ActionClass AC) const;
 
   // Set default DWARF version to 4 for DXIL uses version 4.
@@ -73,6 +87,7 @@ class LLVM_LIBRARY_VISIBILITY HLSLToolChain : public ToolChain {
 private:
   mutable std::unique_ptr<tools::hlsl::Validator> Validator;
   mutable std::unique_ptr<tools::hlsl::MetalConverter> MetalConverter;
+  mutable std::unique_ptr<tools::hlsl::LLVMObjcopy> LLVMObjcopy;
 };
 
 } // end namespace toolchains
diff --git a/clang/test/Driver/dxc_strip_rootsignature.hlsl b/clang/test/Driver/dxc_strip_rootsignature.hlsl
new file mode 100644
index 0000000000000..b4af1f4b1541d
--- /dev/null
+++ b/clang/test/Driver/dxc_strip_rootsignature.hlsl
@@ -0,0 +1,22 @@
+// RUN: %clang_dxc -Qstrip-rootsignature -T cs_6_0 -HV 202x -Vd /Fo %t %s
+// RUN: obj2yaml %t | FileCheck %s
+// RUN: obj2yaml %t | FileCheck %s --check-prefix=OTHERS
+
+// Test to demonstrate that we specify to the root signature with the
+// -Qstrip-rootsignature option
+
+// CHECK-NOT: RTS0
+
+// OTHERS: FileSize:       1964
+// OTHERS-NEXT: PartCount:      6
+// OTHERS: Parts:
+// OTHERS: Name:            DXIL
+// OTHERS: Name:            SFI0
+// OTHERS: Name:            HASH
+// OTHERS: Name:            ISG1
+// OTHERS: Name:            OSG1
+// OTHERS: Name:            PSV0
+
+[shader("compute"), RootSignature("")]
+[numthreads(1,1,1)]
+void EmptyEntry() {}

@inbelic inbelic linked an issue Aug 20, 2025 that may be closed by this pull request
3 tasks
// llvm-objcopy expects a DXIL container, which can either be
// validated (in which case they are TY_DX_CONTAINER), or unvalidated
// (TY_OBJECT).
if (LastAction->getType() == types::TY_DX_CONTAINER ||
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be running this before validation rather than after? What does DXC do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, DXC will construct the final output blob before running the validator. So it would validate with the parts removed.

@inbelic inbelic changed the base branch from users/inbelic/pr-153246 to main August 21, 2025 18:53
- DXC will invoke the validator after the final blob has been generated
(in this case after the part is removed)
@@ -4647,6 +4647,16 @@ void Driver::BuildDefaultActions(Compilation &C, DerivedArgList &Args,
// Only add action when needValidation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments around the isDXIL block look like they're out of date or at least misplaced (both this one and the "Call validator" one above.

Comment on lines 48 to 58
class LLVM_LIBRARY_VISIBILITY LLVMObjcopy : public Tool {
public:
LLVMObjcopy(const ToolChain &TC) : Tool("LLVMObjcopy", "llvm-objcopy", TC) {}

bool hasIntegratedCPP() const override { return false; }

void ConstructJob(Compilation &C, const JobAction &JA,
const InputInfo &Output, const InputInfoList &Inputs,
const llvm::opt::ArgList &TCArgs,
const char *LinkingOutput) const override;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLVMObjcopy fairly generic, even if we are only using it in the HLSL toolchain right now. Should this class be defined somewhere less toolchain specific?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it is generic, the logic of ConstructJob is specific to just the hlsl toolchain.

It is convention for the other toolchains to each redefine their own version of the struct with their namespace prefixed. See Linker etc.

So I have added back the hlsl namespace to this struct.

@farzonl
Copy link
Member

farzonl commented Aug 22, 2025

Wait for Justin or Chris, this isn't my area of expertise.

- following convention used from other toolchains (eg. `Linker`), each
toolchain will need to define a custom override of `ConstructJob` so
that it converts the applicable arguments accordingly

- since this will be toolchain specific, it is convention to prefix it
with the toolchain namespace (hlsl::)
Comment on lines 461 to 505
if (!HasTranslation && !HasValidation)
if (!HasTranslation && !HasValidation && !HasObjcopy)
return true;
if ((HasTranslation && AC == Action::BinaryTranslatorJobClass) ||
(!HasTranslation && HasValidation && AC == Action::BinaryAnalyzeJobClass))
(!HasTranslation && HasValidation &&
AC == Action::BinaryAnalyzeJobClass) ||
(!HasTranslation && !HasValidation && HasObjcopy &&
AC == Action::ObjcopyJobClass))
return true;
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is getting a bit unweildy. Would it simplify things to work from the last job backwards? That is,

if (HasObjcopy)
  return AC == Action::ObjcopyJobClass;
if (HasValidation)
  return AC == Action::BinaryAnalyzeJobClass;
if (HasTranslation)
  return AC == Action::BinaryTranslatorJobClass;
// If none of translation, validation, or objcopy are required, we should only
// have one action
return true;

Comment on lines 482 to 483
/// Omit the root signature from produced DXContainer
CODEGENOPT(HLSLRootSigStrip, 1, 0, Benign)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this anymore? I don't see it being used, and I would think that making this a separate tool's job would make this unnecessary.

@inbelic inbelic merged commit 74a4d81 into llvm:main Aug 26, 2025
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category HLSL HLSL Language Support
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[HLSL][RootSignature] Add support for Qstrip_rootsignature compiler flag
5 participants