[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