[llvm] [HLSL][DirectX] Lower llvm.dx.load.input and llvm.dx.store.output to DXIL (PR #208871)

Dan Brown via llvm-commits llvm-commits at lists.llvm.org
Fri Jul 10 20:52:00 PDT 2026


https://github.com/danbrown-amd updated https://github.com/llvm/llvm-project/pull/208871

>From 2fef0980adc4256cf49df857bccc584bfbb350c9 Mon Sep 17 00:00:00 2001
From: danbrown-amd <danbrown at amd.com>
Date: Wed, 8 Jul 2026 18:56:41 -0600
Subject: [PATCH] [HLSL][DirectX] Lower llvm.dx.load.input and
 llvm.dx.store.output to DXIL

Addresses #189766.

Assisted-by: Claude Sonnet 4
---
 llvm/lib/Target/DirectX/DXIL.td               | 19 ++++
 .../Target/DirectX/DXILIntrinsicExpansion.cpp | 91 +++++++++++++++++++
 llvm/lib/Target/DirectX/DXILOpLowering.cpp    | 62 +++++++++++++
 llvm/test/CodeGen/DirectX/LoadInput.ll        | 43 +++++++++
 llvm/test/CodeGen/DirectX/StoreOutput.ll      | 41 +++++++++
 5 files changed, 256 insertions(+)
 create mode 100644 llvm/test/CodeGen/DirectX/LoadInput.ll
 create mode 100644 llvm/test/CodeGen/DirectX/StoreOutput.ll

diff --git a/llvm/lib/Target/DirectX/DXIL.td b/llvm/lib/Target/DirectX/DXIL.td
index a268276b07655..ba5c2973d3b3d 100644
--- a/llvm/lib/Target/DirectX/DXIL.td
+++ b/llvm/lib/Target/DirectX/DXIL.td
@@ -412,6 +412,25 @@ class DXILOp<int opcode, DXILOpClass opclass> {
 //
 // This are sorted by ascending value of the DXIL Opcodes
 
+def LoadInput : DXILOp<4, loadInput> {
+  let Doc = "Loads a scalar value from a shader input register component.";
+  // inputSigId, rowIndex, colIndex
+  let arguments = [Int32Ty, Int32Ty, Int8Ty];
+  let result = OverloadTy;
+  let overloads = [Overloads<DXIL1_0, [HalfTy, FloatTy, Int16Ty, Int32Ty]>];
+  let stages = [Stages<DXIL1_0, [all_stages]>];
+  let attributes = [Attributes<DXIL1_0, [ReadOnly]>];
+}
+
+def StoreOutput : DXILOp<5, storeOutput> {
+  let Doc = "Stores a scalar value to a shader output register component.";
+  // outputSigId, rowIndex, colIndex, value
+  let arguments = [Int32Ty, Int32Ty, Int8Ty, OverloadTy];
+  let result = VoidTy;
+  let overloads = [Overloads<DXIL1_0, [HalfTy, FloatTy, Int16Ty, Int32Ty]>];
+  let stages = [Stages<DXIL1_0, [all_stages]>];
+}
+
 def Abs : DXILOp<6, unary> {
   let Doc = "Returns the absolute value of the input.";
   let intrinsics = [IntrinSelect<int_fabs>];
diff --git a/llvm/lib/Target/DirectX/DXILIntrinsicExpansion.cpp b/llvm/lib/Target/DirectX/DXILIntrinsicExpansion.cpp
index 1025015c09a1f..0b44cffe372d4 100644
--- a/llvm/lib/Target/DirectX/DXILIntrinsicExpansion.cpp
+++ b/llvm/lib/Target/DirectX/DXILIntrinsicExpansion.cpp
@@ -252,6 +252,10 @@ static bool isIntrinsicExpansion(Function &F) {
   case Intrinsic::dx_resource_store_typedbuffer:
     return resourceAccessNeeds64BitExpansion(
         F.getParent(), F.getFunctionType()->getParamType(2), /*IsRaw*/ false);
+  case Intrinsic::dx_load_input:
+    return isa<VectorType>(F.getReturnType());
+  case Intrinsic::dx_store_output:
+    return isa<VectorType>(F.getFunctionType()->getParamType(5));
   }
   return false;
 }
@@ -1263,6 +1267,87 @@ static Value *expandMatrixTranspose(CallInst *Orig) {
   return Builder.CreateShuffleVector(Mat, Mask);
 }
 
+// Scalarize a vector int_dx_store_output call into per-component scalar calls.
+// The DXIL StoreOutput op is per-component; vector intrinsics are split here
+// so that DXILOpLowering sees only scalar variants.
+static bool expandStoreOutput(CallInst *Orig) {
+  auto *VT = dyn_cast<FixedVectorType>(Orig->getArgOperand(5)->getType());
+  if (!VT)
+    return false; // already scalar, nothing to expand
+
+  IRBuilder<> Builder(Orig);
+  Module *M = Orig->getModule();
+  Type *Int8Ty = Builder.getInt8Ty();
+  Type *Int32Ty = Builder.getInt32Ty();
+  Type *ScalarTy = VT->getElementType();
+  unsigned NumElems = VT->getNumElements();
+
+  Value *OutputSigId = Orig->getArgOperand(0);
+  Value *RowIndex = Orig->getArgOperand(1);
+  Value *StartCol = Orig->getArgOperand(3); // i8
+  Value *Data = Orig->getArgOperand(5);
+  Value *Unused = PoisonValue::get(Int32Ty);
+  Value *StartColI32 = Builder.CreateZExt(StartCol, Int32Ty);
+
+  Function *ScalarFn = Intrinsic::getOrInsertDeclaration(
+      M, Intrinsic::dx_store_output, {ScalarTy});
+
+  for (unsigned I = 0; I < NumElems; ++I) {
+    Value *Scalar =
+        Builder.CreateExtractElement(Data, ConstantInt::get(Int32Ty, I));
+    Value *ColIdx =
+        Builder.CreateAdd(StartColI32, ConstantInt::get(Int32Ty, I));
+    Value *ColI8 = Builder.CreateTrunc(ColIdx, Int8Ty);
+    // Args: (outputSigId, rowIndex, colIndex:i32, startCol:i8, unused:i32,
+    // value) startCol (arg 3) carries the per-component column used by
+    // lowerStoreOutput.
+    Builder.CreateCall(ScalarFn,
+                       {OutputSigId, RowIndex, ColIdx, ColI8, Unused, Scalar});
+  }
+
+  Orig->eraseFromParent();
+  return true;
+}
+
+// Scalarize a vector int_dx_load_input call into per-component scalar calls
+// and reassemble the vector. The DXIL LoadInput op is per-component.
+static Value *expandLoadInput(CallInst *Orig) {
+  auto *VT = dyn_cast<FixedVectorType>(Orig->getType());
+  if (!VT)
+    return nullptr; // already scalar, nothing to expand
+
+  IRBuilder<> Builder(Orig);
+  Module *M = Orig->getModule();
+  Type *Int8Ty = Builder.getInt8Ty();
+  Type *Int32Ty = Builder.getInt32Ty();
+  Type *ScalarTy = VT->getElementType();
+  unsigned NumElems = VT->getNumElements();
+
+  Value *InputSigId = Orig->getArgOperand(0);
+  Value *RowIndex = Orig->getArgOperand(1);
+  Value *StartCol = Orig->getArgOperand(3); // i8
+  Value *Unused = PoisonValue::get(Int32Ty);
+  Value *StartColI32 = Builder.CreateZExt(StartCol, Int32Ty);
+
+  Function *ScalarFn = Intrinsic::getOrInsertDeclaration(
+      M, Intrinsic::dx_load_input, {ScalarTy});
+
+  Value *Vec = PoisonValue::get(VT);
+  for (unsigned I = 0; I < NumElems; ++I) {
+    Value *ColIdx =
+        Builder.CreateAdd(StartColI32, ConstantInt::get(Int32Ty, I));
+    Value *ColI8 = Builder.CreateTrunc(ColIdx, Int8Ty);
+    // Args: (inputSigId, rowIndex, colIndex:i32, startCol:i8, unused:i32)
+    // colIndex (arg 2) carries the per-component column used by lowerLoadInput.
+    Value *Scalar = Builder.CreateCall(
+        ScalarFn, {InputSigId, RowIndex, ColIdx, ColI8, Unused});
+    Vec =
+        Builder.CreateInsertElement(Vec, Scalar, ConstantInt::get(Int32Ty, I));
+  }
+
+  return Vec;
+}
+
 static bool expandIntrinsic(Function &F, CallInst *Orig) {
   Value *Result = nullptr;
   Intrinsic::ID IntrinsicId = F.getIntrinsicID();
@@ -1341,6 +1426,12 @@ static bool expandIntrinsic(Function &F, CallInst *Orig) {
   case Intrinsic::dx_radians:
     Result = expandRadiansIntrinsic(Orig);
     break;
+  case Intrinsic::dx_load_input:
+    Result = expandLoadInput(Orig);
+    break;
+  case Intrinsic::dx_store_output:
+    expandStoreOutput(Orig);
+    return true;
   case Intrinsic::dx_interlocked_add:
     Result = expandInterlockedIntrinsic(Orig, AtomicRMWInst::Add);
     break;
diff --git a/llvm/lib/Target/DirectX/DXILOpLowering.cpp b/llvm/lib/Target/DirectX/DXILOpLowering.cpp
index 93d5a08a6e0a2..2b835831bd18b 100644
--- a/llvm/lib/Target/DirectX/DXILOpLowering.cpp
+++ b/llvm/lib/Target/DirectX/DXILOpLowering.cpp
@@ -977,6 +977,62 @@ class OpLowerer {
     });
   }
 
+  [[nodiscard]] bool lowerStoreOutput(Function &F) {
+    IRBuilder<> &IRB = OpBuilder.getIRB();
+    Type *Int8Ty = IRB.getInt8Ty();
+
+    return replaceFunction(F, [&](CallInst *CI) -> Error {
+      IRB.SetInsertPoint(CI);
+
+      // int_dx_store_output (scalar): (outputSigId, rowIndex, colIndex,
+      //                                startCol:i8, unused, value)
+      // Vectors are scalarized by DXILIntrinsicExpansion; startCol (arg 3)
+      // holds the per-component column index for the DXIL op.
+      Value *OutputSigId = CI->getArgOperand(0);
+      Value *RowIndex = CI->getArgOperand(1);
+      Value *StartCol = CI->getArgOperand(3); // i8
+      Value *Data = CI->getArgOperand(5);
+      Value *ColI8 = IRB.CreateZExtOrBitCast(StartCol, Int8Ty);
+
+      SmallVector<Value *, 4> Args{OutputSigId, RowIndex, ColI8, Data};
+      Expected<CallInst *> OpCall =
+          OpBuilder.tryCreateOp(dxil::OpCode::StoreOutput, Args);
+      if (Error E = OpCall.takeError())
+        return E;
+
+      CI->eraseFromParent();
+      return Error::success();
+    });
+  }
+
+  [[nodiscard]] bool lowerLoadInput(Function &F) {
+    IRBuilder<> &IRB = OpBuilder.getIRB();
+    Type *Int8Ty = IRB.getInt8Ty();
+
+    return replaceFunction(F, [&](CallInst *CI) -> Error {
+      IRB.SetInsertPoint(CI);
+
+      // int_dx_load_input: (inputSigId, rowIndex, colIndex, startCol, unused)
+      // After expansion, colIndex (arg 2) already encodes the component column;
+      // use it directly as the DXIL colIndex (i8).
+      Value *InputSigId = CI->getArgOperand(0);
+      Value *RowIndex = CI->getArgOperand(1);
+      Value *ColI32 = CI->getArgOperand(2);
+      Value *ColI8 = IRB.CreateTrunc(ColI32, Int8Ty);
+
+      Type *ScalarTy = CI->getType();
+      SmallVector<Value *, 3> Args{InputSigId, RowIndex, ColI8};
+      Expected<CallInst *> OpCall = OpBuilder.tryCreateOp(
+          dxil::OpCode::LoadInput, Args, CI->getName(), ScalarTy);
+      if (Error E = OpCall.takeError())
+        return E;
+
+      CI->replaceAllUsesWith(*OpCall);
+      CI->eraseFromParent();
+      return Error::success();
+    });
+  }
+
   [[nodiscard]] bool lowerCtpopToCountBits(Function &F) {
     IRBuilder<> &IRB = OpBuilder.getIRB();
     Type *Int32Ty = IRB.getInt32Ty();
@@ -1213,6 +1269,12 @@ class OpLowerer {
       case Intrinsic::dx_resource_getdimensions_x:
         HasErrors |= lowerGetDimensionsX(F);
         break;
+      case Intrinsic::dx_load_input:
+        HasErrors |= lowerLoadInput(F);
+        break;
+      case Intrinsic::dx_store_output:
+        HasErrors |= lowerStoreOutput(F);
+        break;
       case Intrinsic::ctpop:
         HasErrors |= lowerCtpopToCountBits(F);
         break;
diff --git a/llvm/test/CodeGen/DirectX/LoadInput.ll b/llvm/test/CodeGen/DirectX/LoadInput.ll
new file mode 100644
index 0000000000000..db648b4d62179
--- /dev/null
+++ b/llvm/test/CodeGen/DirectX/LoadInput.ll
@@ -0,0 +1,43 @@
+; RUN: opt -S -dxil-intrinsic-expansion -dxil-op-lower %s | FileCheck %s
+
+target triple = "dxil-pc-shadermodel6.0-pixel"
+
+; Scalar float load: one LoadInput call, result forwarded directly.
+; CHECK-LABEL: define float @load_scalar_f32
+define float @load_scalar_f32() {
+  ; CHECK: [[V:%.*]] = call float @dx.op.loadInput.f32(i32 4, i32 0, i32 0, i8 0)
+  ; CHECK-NEXT: ret float [[V]]
+  ; CHECK-NOT: llvm.dx.load.input
+  %v = call float @llvm.dx.load.input.f32(i32 0, i32 0, i32 0, i8 0, i32 poison)
+  ret float %v
+}
+
+; Vector float4 load: four per-component LoadInput calls reassembled into a vector.
+; CHECK-LABEL: define <4 x float> @load_v4f32
+define <4 x float> @load_v4f32() {
+  ; CHECK: [[S0:%.*]] = call float @dx.op.loadInput.f32(i32 4, i32 1, i32 0, i8 0)
+  ; CHECK-NEXT: insertelement <4 x float> {{undef|poison}}, float [[S0]], i32 0
+  ; CHECK: [[S1:%.*]] = call float @dx.op.loadInput.f32(i32 4, i32 1, i32 0, i8 1)
+  ; CHECK-NEXT: insertelement {{.*}}, float [[S1]], i32 1
+  ; CHECK: [[S2:%.*]] = call float @dx.op.loadInput.f32(i32 4, i32 1, i32 0, i8 2)
+  ; CHECK-NEXT: insertelement {{.*}}, float [[S2]], i32 2
+  ; CHECK: [[S3:%.*]] = call float @dx.op.loadInput.f32(i32 4, i32 1, i32 0, i8 3)
+  ; CHECK-NEXT: insertelement {{.*}}, float [[S3]], i32 3
+  ; CHECK-NOT: llvm.dx.load.input
+  %v = call <4 x float> @llvm.dx.load.input.v4f32(i32 1, i32 0, i32 0, i8 0, i32 poison)
+  ret <4 x float> %v
+}
+
+; Scalar int load: one LoadInput call, result forwarded directly.
+; CHECK-LABEL: define i32 @load_scalar_i32
+define i32 @load_scalar_i32() {
+  ; CHECK: [[V:%.*]] = call i32 @dx.op.loadInput.i32(i32 4, i32 2, i32 0, i8 0)
+  ; CHECK-NEXT: ret i32 [[V]]
+  ; CHECK-NOT: llvm.dx.load.input
+  %v = call i32 @llvm.dx.load.input.i32(i32 2, i32 0, i32 0, i8 0, i32 poison)
+  ret i32 %v
+}
+
+declare float @llvm.dx.load.input.f32(i32, i32, i32, i8, i32)
+declare <4 x float> @llvm.dx.load.input.v4f32(i32, i32, i32, i8, i32)
+declare i32 @llvm.dx.load.input.i32(i32, i32, i32, i8, i32)
diff --git a/llvm/test/CodeGen/DirectX/StoreOutput.ll b/llvm/test/CodeGen/DirectX/StoreOutput.ll
new file mode 100644
index 0000000000000..a8f30c8b36f3d
--- /dev/null
+++ b/llvm/test/CodeGen/DirectX/StoreOutput.ll
@@ -0,0 +1,41 @@
+; RUN: opt -S -dxil-intrinsic-expansion -dxil-op-lower %s | FileCheck %s
+
+target triple = "dxil-pc-shadermodel6.0-pixel"
+
+; Scalar float store: one StoreOutput call, no residual intrinsic.
+; CHECK-LABEL: define void @store_scalar_f32
+define void @store_scalar_f32(float %val) {
+  ; CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 0, float %val)
+  ; CHECK-NOT: llvm.dx.store.output
+  call void @llvm.dx.store.output.f32(i32 0, i32 0, i32 0, i8 0, i32 poison, float %val)
+  ret void
+}
+
+; Vector float4 store: four per-component StoreOutput calls, col indices 0..3.
+; CHECK-LABEL: define void @store_v4f32
+define void @store_v4f32(<4 x float> %val) {
+  ; CHECK: [[E0:%.*]] = extractelement <4 x float> %val, i32 0
+  ; CHECK-NEXT: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 0, float [[E0]])
+  ; CHECK-NEXT: [[E1:%.*]] = extractelement <4 x float> %val, i32 1
+  ; CHECK-NEXT: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 1, float [[E1]])
+  ; CHECK-NEXT: [[E2:%.*]] = extractelement <4 x float> %val, i32 2
+  ; CHECK-NEXT: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 2, float [[E2]])
+  ; CHECK-NEXT: [[E3:%.*]] = extractelement <4 x float> %val, i32 3
+  ; CHECK-NEXT: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 3, float [[E3]])
+  ; CHECK-NOT: llvm.dx.store.output
+  call void @llvm.dx.store.output.v4f32(i32 1, i32 0, i32 0, i8 0, i32 poison, <4 x float> %val)
+  ret void
+}
+
+; Scalar int store: one StoreOutput call, no residual intrinsic.
+; CHECK-LABEL: define void @store_scalar_i32
+define void @store_scalar_i32(i32 %val) {
+  ; CHECK: call void @dx.op.storeOutput.i32(i32 5, i32 2, i32 0, i8 0, i32 %val)
+  ; CHECK-NOT: llvm.dx.store.output
+  call void @llvm.dx.store.output.i32(i32 2, i32 0, i32 0, i8 0, i32 poison, i32 %val)
+  ret void
+}
+
+declare void @llvm.dx.store.output.f32(i32, i32, i32, i8, i32, float)
+declare void @llvm.dx.store.output.v4f32(i32, i32, i32, i8, i32, <4 x float>)
+declare void @llvm.dx.store.output.i32(i32, i32, i32, i8, i32, i32)



More information about the llvm-commits mailing list