Skip to content

Commit b4ecebe

Browse files
Icohedroninbelicfarzonlllvm-beanzbogner
authored
[HLSL] [DXIL] Implement the AddUint64 HLSL function and the UAddc DXIL op (#127137)
Fixes #99205. - Implements the HLSL intrinsic `AddUint64` used to perform unsigned 64-bit integer addition by using pairs of unsigned 32-bit integers instead of native 64-bit types - The LLVM intrinsic `uadd_with_overflow` is used in the implementation of `AddUint64` in `CGBuiltin.cpp` - The DXIL op `UAddc` was defined in `DXIL.td`, and a lowering of the LLVM intrinsic `uadd_with_overflow` to the `UAddc` DXIL op was implemented in `DXILOpLowering.cpp` Notes: - `__builtin_addc` was not able to be used to implement `AddUint64` in `hlsl_intrinsics.h` because its `CarryOut` argument is a pointer, and pointers are not supported in HLSL - A lowering of the LLVM intrinsic `uadd_with_overflow` to SPIR-V [already exists](https://github.com/llvm/llvm-project/blob/main/llvm/test/CodeGen/SPIRV/llvm-intrinsics/uadd.with.overflow.ll) - When lowering the LLVM intrinsic `uadd_with_overflow` to the `UAddc` DXIL op, the anonymous struct type `{ i32, i1 }` is replaced with a named struct type `%dx.types.i32c`. This aspect of the implementation may be changed when issue #113192 gets addressed - Fixes issues mentioned in the comments on the original PR #125319 --------- Co-authored-by: Finn Plummer <[email protected]> Co-authored-by: Farzon Lotfi <[email protected]> Co-authored-by: Chris B <[email protected]> Co-authored-by: Justin Bogner <[email protected]>
1 parent 5b3ba26 commit b4ecebe

File tree

12 files changed

+464
-0
lines changed

12 files changed

+464
-0
lines changed

clang/include/clang/Basic/Builtins.td

+6
Original file line numberDiff line numberDiff line change
@@ -4765,6 +4765,12 @@ def GetDeviceSideMangledName : LangBuiltin<"CUDA_LANG"> {
47654765
}
47664766

47674767
// HLSL
4768+
def HLSLAddUint64: LangBuiltin<"HLSL_LANG"> {
4769+
let Spellings = ["__builtin_hlsl_adduint64"];
4770+
let Attributes = [NoThrow, Const];
4771+
let Prototype = "void(...)";
4772+
}
4773+
47684774
def HLSLResourceGetPointer : LangBuiltin<"HLSL_LANG"> {
47694775
let Spellings = ["__builtin_hlsl_resource_getpointer"];
47704776
let Attributes = [NoThrow];

clang/include/clang/Basic/DiagnosticSemaKinds.td

+5
Original file line numberDiff line numberDiff line change
@@ -10709,6 +10709,11 @@ def err_vector_incorrect_num_elements : Error<
1070910709
"%select{too many|too few}0 elements in vector %select{initialization|operand}3 (expected %1 elements, have %2)">;
1071010710
def err_altivec_empty_initializer : Error<"expected initializer">;
1071110711

10712+
def err_vector_incorrect_bit_count : Error<
10713+
"incorrect number of bits in vector operand (expected %select{|a multiple of}0 %1 bits, have %2)">;
10714+
def err_integer_incorrect_bit_count : Error<
10715+
"incorrect number of bits in integer (expected %0 bits, have %1)">;
10716+
1071210717
def err_invalid_neon_type_code : Error<
1071310718
"incompatible constant for this __builtin_neon function">;
1071410719
def err_argument_invalid_range : Error<

clang/lib/CodeGen/CGBuiltin.cpp

+56
Original file line numberDiff line numberDiff line change
@@ -19470,6 +19470,62 @@ Value *CodeGenFunction::EmitHLSLBuiltinExpr(unsigned BuiltinID,
1947019470
return nullptr;
1947119471

1947219472
switch (BuiltinID) {
19473+
case Builtin::BI__builtin_hlsl_adduint64: {
19474+
Value *OpA = EmitScalarExpr(E->getArg(0));
19475+
Value *OpB = EmitScalarExpr(E->getArg(1));
19476+
QualType Arg0Ty = E->getArg(0)->getType();
19477+
uint64_t NumElements = Arg0Ty->castAs<VectorType>()->getNumElements();
19478+
assert(Arg0Ty == E->getArg(1)->getType() &&
19479+
"AddUint64 operand types must match");
19480+
assert(Arg0Ty->hasIntegerRepresentation() &&
19481+
"AddUint64 operands must have an integer representation");
19482+
assert((NumElements == 2 || NumElements == 4) &&
19483+
"AddUint64 operands must have 2 or 4 elements");
19484+
19485+
llvm::Value *LowA;
19486+
llvm::Value *HighA;
19487+
llvm::Value *LowB;
19488+
llvm::Value *HighB;
19489+
19490+
// Obtain low and high words of inputs A and B
19491+
if (NumElements == 2) {
19492+
LowA = Builder.CreateExtractElement(OpA, (uint64_t)0, "LowA");
19493+
HighA = Builder.CreateExtractElement(OpA, (uint64_t)1, "HighA");
19494+
LowB = Builder.CreateExtractElement(OpB, (uint64_t)0, "LowB");
19495+
HighB = Builder.CreateExtractElement(OpB, (uint64_t)1, "HighB");
19496+
} else {
19497+
LowA = Builder.CreateShuffleVector(OpA, ArrayRef<int>{0, 2}, "LowA");
19498+
HighA = Builder.CreateShuffleVector(OpA, ArrayRef<int>{1, 3}, "HighA");
19499+
LowB = Builder.CreateShuffleVector(OpB, ArrayRef<int>{0, 2}, "LowB");
19500+
HighB = Builder.CreateShuffleVector(OpB, ArrayRef<int>{1, 3}, "HighB");
19501+
}
19502+
19503+
// Use an uadd_with_overflow to compute the sum of low words and obtain a
19504+
// carry value
19505+
llvm::Value *Carry;
19506+
llvm::Value *LowSum = EmitOverflowIntrinsic(
19507+
*this, llvm::Intrinsic::uadd_with_overflow, LowA, LowB, Carry);
19508+
llvm::Value *ZExtCarry =
19509+
Builder.CreateZExt(Carry, HighA->getType(), "CarryZExt");
19510+
19511+
// Sum the high words and the carry
19512+
llvm::Value *HighSum = Builder.CreateAdd(HighA, HighB, "HighSum");
19513+
llvm::Value *HighSumPlusCarry =
19514+
Builder.CreateAdd(HighSum, ZExtCarry, "HighSumPlusCarry");
19515+
19516+
if (NumElements == 4) {
19517+
return Builder.CreateShuffleVector(LowSum, HighSumPlusCarry,
19518+
ArrayRef<int>{0, 2, 1, 3},
19519+
"hlsl.AddUint64");
19520+
}
19521+
19522+
llvm::Value *Result = PoisonValue::get(OpA->getType());
19523+
Result = Builder.CreateInsertElement(Result, LowSum, (uint64_t)0,
19524+
"hlsl.AddUint64.upto0");
19525+
Result = Builder.CreateInsertElement(Result, HighSumPlusCarry, (uint64_t)1,
19526+
"hlsl.AddUint64");
19527+
return Result;
19528+
}
1947319529
case Builtin::BI__builtin_hlsl_resource_getpointer: {
1947419530
Value *HandleOp = EmitScalarExpr(E->getArg(0));
1947519531
Value *IndexOp = EmitScalarExpr(E->getArg(1));

clang/lib/Headers/hlsl/hlsl_alias_intrinsics.h

+21
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,27 @@ float3 acos(float3);
174174
_HLSL_BUILTIN_ALIAS(__builtin_elementwise_acos)
175175
float4 acos(float4);
176176

177+
//===----------------------------------------------------------------------===//
178+
// AddUint64 builtins
179+
//===----------------------------------------------------------------------===//
180+
181+
/// \fn T AddUint64(T a, T b)
182+
/// \brief Implements unsigned 64-bit integer addition using pairs of unsigned
183+
/// 32-bit integers.
184+
/// \param x [in] The first unsigned 32-bit integer pair(s)
185+
/// \param y [in] The second unsigned 32-bit integer pair(s)
186+
///
187+
/// This function takes one or two pairs (low, high) of unsigned 32-bit integer
188+
/// values and returns pairs (low, high) of unsigned 32-bit integer
189+
/// values representing the result of unsigned 64-bit integer addition.
190+
191+
_HLSL_AVAILABILITY(shadermodel, 6.0)
192+
_HLSL_BUILTIN_ALIAS(__builtin_hlsl_adduint64)
193+
uint32_t2 AddUint64(uint32_t2, uint32_t2);
194+
_HLSL_AVAILABILITY(shadermodel, 6.0)
195+
_HLSL_BUILTIN_ALIAS(__builtin_hlsl_adduint64)
196+
uint32_t4 AddUint64(uint32_t4, uint32_t4);
197+
177198
//===----------------------------------------------------------------------===//
178199
// all builtins
179200
//===----------------------------------------------------------------------===//

clang/lib/Sema/SemaHLSL.cpp

+58
Original file line numberDiff line numberDiff line change
@@ -2086,6 +2086,18 @@ static bool CheckAllArgsHaveFloatRepresentation(Sema *S, CallExpr *TheCall) {
20862086
checkAllFloatTypes);
20872087
}
20882088

2089+
static bool CheckUnsignedIntRepresentations(Sema *S, CallExpr *TheCall) {
2090+
auto checkUnsignedInteger = [](clang::QualType PassedType) -> bool {
2091+
clang::QualType BaseType =
2092+
PassedType->isVectorType()
2093+
? PassedType->getAs<clang::VectorType>()->getElementType()
2094+
: PassedType;
2095+
return !BaseType->isUnsignedIntegerType();
2096+
};
2097+
return CheckAllArgTypesAreCorrect(S, TheCall, S->Context.UnsignedIntTy,
2098+
checkUnsignedInteger);
2099+
}
2100+
20892101
static bool CheckFloatOrHalfRepresentations(Sema *S, CallExpr *TheCall) {
20902102
auto checkFloatorHalf = [](clang::QualType PassedType) -> bool {
20912103
clang::QualType BaseType =
@@ -2277,6 +2289,52 @@ static bool CheckResourceHandle(
22772289
// returning an ExprError
22782290
bool SemaHLSL::CheckBuiltinFunctionCall(unsigned BuiltinID, CallExpr *TheCall) {
22792291
switch (BuiltinID) {
2292+
case Builtin::BI__builtin_hlsl_adduint64: {
2293+
if (SemaRef.checkArgCount(TheCall, 2))
2294+
return true;
2295+
if (CheckVectorElementCallArgs(&SemaRef, TheCall))
2296+
return true;
2297+
if (CheckUnsignedIntRepresentations(&SemaRef, TheCall))
2298+
return true;
2299+
2300+
// CheckVectorElementCallArgs(...) guarantees both args are the same type.
2301+
assert(TheCall->getArg(0)->getType() == TheCall->getArg(1)->getType() &&
2302+
"Both args must be of the same type");
2303+
2304+
// ensure both args are vectors
2305+
auto *VTy = TheCall->getArg(0)->getType()->getAs<VectorType>();
2306+
if (!VTy) {
2307+
SemaRef.Diag(TheCall->getBeginLoc(), diag::err_vec_builtin_non_vector)
2308+
<< TheCall->getDirectCallee() << /*all*/ 1;
2309+
return true;
2310+
}
2311+
2312+
// ensure arg integers are 32-bits
2313+
uint64_t ElementBitCount = getASTContext()
2314+
.getTypeSizeInChars(VTy->getElementType())
2315+
.getQuantity() *
2316+
8;
2317+
if (ElementBitCount != 32) {
2318+
SemaRef.Diag(TheCall->getBeginLoc(),
2319+
diag::err_integer_incorrect_bit_count)
2320+
<< 32 << ElementBitCount;
2321+
return true;
2322+
}
2323+
2324+
// ensure both args are vectors of total bit size of a multiple of 64
2325+
int NumElementsArg = VTy->getNumElements();
2326+
if (NumElementsArg != 2 && NumElementsArg != 4) {
2327+
SemaRef.Diag(TheCall->getBeginLoc(), diag::err_vector_incorrect_bit_count)
2328+
<< 1 /*a multiple of*/ << 64 << NumElementsArg * ElementBitCount;
2329+
return true;
2330+
}
2331+
2332+
ExprResult A = TheCall->getArg(0);
2333+
QualType ArgTyA = A.get()->getType();
2334+
// return type is the same as the input type
2335+
TheCall->setType(ArgTyA);
2336+
break;
2337+
}
22802338
case Builtin::BI__builtin_hlsl_resource_getpointer: {
22812339
if (SemaRef.checkArgCount(TheCall, 2) ||
22822340
CheckResourceHandle(&SemaRef, TheCall, 0) ||
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// NOTE: Assertions have been autogenerated by utils/update_cc_test_checks.py UTC_ARGS: --version 5
2+
// RUN: %clang_cc1 -finclude-default-header -triple dxil-pc-shadermodel6.3-library %s \
3+
// RUN: -emit-llvm -disable-llvm-passes -o - | \
4+
// RUN: FileCheck %s --check-prefixes=CHECK
5+
6+
7+
// CHECK-LABEL: define noundef <2 x i32> @_Z20test_AddUint64_uint2Dv2_jS_(
8+
// CHECK-SAME: <2 x i32> noundef [[A:%.*]], <2 x i32> noundef [[B:%.*]]) #[[ATTR0:[0-9]+]] {
9+
// CHECK-NEXT: [[ENTRY:.*:]]
10+
// CHECK-NEXT: [[A_ADDR:%.*]] = alloca <2 x i32>, align 8
11+
// CHECK-NEXT: [[B_ADDR:%.*]] = alloca <2 x i32>, align 8
12+
// CHECK-NEXT: store <2 x i32> [[A]], ptr [[A_ADDR]], align 8
13+
// CHECK-NEXT: store <2 x i32> [[B]], ptr [[B_ADDR]], align 8
14+
// CHECK-NEXT: [[TMP0:%.*]] = load <2 x i32>, ptr [[A_ADDR]], align 8
15+
// CHECK-NEXT: [[TMP1:%.*]] = load <2 x i32>, ptr [[B_ADDR]], align 8
16+
// CHECK-NEXT: [[LOWA:%.*]] = extractelement <2 x i32> [[TMP0]], i64 0
17+
// CHECK-NEXT: [[HIGHA:%.*]] = extractelement <2 x i32> [[TMP0]], i64 1
18+
// CHECK-NEXT: [[LOWB:%.*]] = extractelement <2 x i32> [[TMP1]], i64 0
19+
// CHECK-NEXT: [[HIGHB:%.*]] = extractelement <2 x i32> [[TMP1]], i64 1
20+
// CHECK-NEXT: [[TMP2:%.*]] = call { i32, i1 } @llvm.uadd.with.overflow.i32(i32 [[LOWA]], i32 [[LOWB]])
21+
// CHECK-NEXT: [[TMP3:%.*]] = extractvalue { i32, i1 } [[TMP2]], 1
22+
// CHECK-NEXT: [[TMP4:%.*]] = extractvalue { i32, i1 } [[TMP2]], 0
23+
// CHECK-NEXT: [[CARRYZEXT:%.*]] = zext i1 [[TMP3]] to i32
24+
// CHECK-NEXT: [[HIGHSUM:%.*]] = add i32 [[HIGHA]], [[HIGHB]]
25+
// CHECK-NEXT: [[HIGHSUMPLUSCARRY:%.*]] = add i32 [[HIGHSUM]], [[CARRYZEXT]]
26+
// CHECK-NEXT: [[HLSL_ADDUINT64_UPTO0:%.*]] = insertelement <2 x i32> poison, i32 [[TMP4]], i64 0
27+
// CHECK-NEXT: [[HLSL_ADDUINT64:%.*]] = insertelement <2 x i32> [[HLSL_ADDUINT64_UPTO0]], i32 [[HIGHSUMPLUSCARRY]], i64 1
28+
// CHECK-NEXT: ret <2 x i32> [[HLSL_ADDUINT64]]
29+
//
30+
uint2 test_AddUint64_uint2(uint2 a, uint2 b) {
31+
return AddUint64(a, b);
32+
}
33+
34+
// CHECK-LABEL: define noundef <4 x i32> @_Z20test_AddUint64_uint4Dv4_jS_(
35+
// CHECK-SAME: <4 x i32> noundef [[A:%.*]], <4 x i32> noundef [[B:%.*]]) #[[ATTR0]] {
36+
// CHECK-NEXT: [[ENTRY:.*:]]
37+
// CHECK-NEXT: [[A_ADDR:%.*]] = alloca <4 x i32>, align 16
38+
// CHECK-NEXT: [[B_ADDR:%.*]] = alloca <4 x i32>, align 16
39+
// CHECK-NEXT: store <4 x i32> [[A]], ptr [[A_ADDR]], align 16
40+
// CHECK-NEXT: store <4 x i32> [[B]], ptr [[B_ADDR]], align 16
41+
// CHECK-NEXT: [[TMP0:%.*]] = load <4 x i32>, ptr [[A_ADDR]], align 16
42+
// CHECK-NEXT: [[TMP1:%.*]] = load <4 x i32>, ptr [[B_ADDR]], align 16
43+
// CHECK-NEXT: [[LOWA:%.*]] = shufflevector <4 x i32> [[TMP0]], <4 x i32> poison, <2 x i32> <i32 0, i32 2>
44+
// CHECK-NEXT: [[HIGHA:%.*]] = shufflevector <4 x i32> [[TMP0]], <4 x i32> poison, <2 x i32> <i32 1, i32 3>
45+
// CHECK-NEXT: [[LOWB:%.*]] = shufflevector <4 x i32> [[TMP1]], <4 x i32> poison, <2 x i32> <i32 0, i32 2>
46+
// CHECK-NEXT: [[HIGHB:%.*]] = shufflevector <4 x i32> [[TMP1]], <4 x i32> poison, <2 x i32> <i32 1, i32 3>
47+
// CHECK-NEXT: [[TMP2:%.*]] = call { <2 x i32>, <2 x i1> } @llvm.uadd.with.overflow.v2i32(<2 x i32> [[LOWA]], <2 x i32> [[LOWB]])
48+
// CHECK-NEXT: [[TMP3:%.*]] = extractvalue { <2 x i32>, <2 x i1> } [[TMP2]], 1
49+
// CHECK-NEXT: [[TMP4:%.*]] = extractvalue { <2 x i32>, <2 x i1> } [[TMP2]], 0
50+
// CHECK-NEXT: [[CARRYZEXT:%.*]] = zext <2 x i1> [[TMP3]] to <2 x i32>
51+
// CHECK-NEXT: [[HIGHSUM:%.*]] = add <2 x i32> [[HIGHA]], [[HIGHB]]
52+
// CHECK-NEXT: [[HIGHSUMPLUSCARRY:%.*]] = add <2 x i32> [[HIGHSUM]], [[CARRYZEXT]]
53+
// CHECK-NEXT: [[HLSL_ADDUINT64:%.*]] = shufflevector <2 x i32> [[TMP4]], <2 x i32> [[HIGHSUMPLUSCARRY]], <4 x i32> <i32 0, i32 2, i32 1, i32 3>
54+
// CHECK-NEXT: ret <4 x i32> [[HLSL_ADDUINT64]]
55+
//
56+
uint4 test_AddUint64_uint4(uint4 a, uint4 b) {
57+
return AddUint64(a, b);
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// RUN: %clang_cc1 -finclude-default-header -triple dxil-pc-shadermodel6.6-library %s -fnative-half-type -emit-llvm-only -disable-llvm-passes -verify
2+
3+
uint2 test_too_few_arg() {
4+
return __builtin_hlsl_adduint64();
5+
// expected-error@-1 {{too few arguments to function call, expected 2, have 0}}
6+
}
7+
8+
uint4 test_too_many_arg(uint4 a) {
9+
return __builtin_hlsl_adduint64(a, a, a);
10+
// expected-error@-1 {{too many arguments to function call, expected 2, have 3}}
11+
}
12+
13+
uint2 test_mismatched_arg_types(uint2 a, uint4 b) {
14+
return __builtin_hlsl_adduint64(a, b);
15+
// expected-error@-1 {{all arguments to '__builtin_hlsl_adduint64' must have the same type}}
16+
}
17+
18+
uint2 test_bad_num_arg_elements(uint3 a, uint3 b) {
19+
return __builtin_hlsl_adduint64(a, b);
20+
// expected-error@-1 {{incorrect number of bits in vector operand (expected a multiple of 64 bits, have 96)}}
21+
}
22+
23+
uint2 test_scalar_arg_type(uint a) {
24+
return __builtin_hlsl_adduint64(a, a);
25+
// expected-error@-1 {{all arguments to '__builtin_hlsl_adduint64' must be vectors}}
26+
}
27+
28+
uint2 test_uint64_args(uint16_t2 a) {
29+
return __builtin_hlsl_adduint64(a, a);
30+
// expected-error@-1 {{incorrect number of bits in integer (expected 32 bits, have 16)}}
31+
}
32+
33+
uint2 test_signed_integer_args(int2 a, int2 b) {
34+
return __builtin_hlsl_adduint64(a, b);
35+
// expected-error@-1 {{passing 'int2' (aka 'vector<int, 2>') to parameter of incompatible type '__attribute__((__vector_size__(2 * sizeof(unsigned int)))) unsigned int' (vector of 2 'unsigned int' values)}}
36+
}
37+
38+
struct S {
39+
uint2 a;
40+
};
41+
42+
uint2 test_incorrect_arg_type(S a) {
43+
return __builtin_hlsl_adduint64(a, a);
44+
// expected-error@-1 {{passing 'S' to parameter of incompatible type 'unsigned int'}}
45+
}
46+

llvm/lib/Target/DirectX/DXIL.td

+11
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def HandleTy : DXILOpParamType;
5656
def ResBindTy : DXILOpParamType;
5757
def ResPropsTy : DXILOpParamType;
5858
def SplitDoubleTy : DXILOpParamType;
59+
def BinaryWithCarryTy : DXILOpParamType;
5960

6061
class DXILOpClass;
6162

@@ -744,6 +745,16 @@ def UMin : DXILOp<40, binary> {
744745
let attributes = [Attributes<DXIL1_0, [ReadNone]>];
745746
}
746747

748+
def UAddc : DXILOp<44, binaryWithCarryOrBorrow > {
749+
let Doc = "unsigned add of 32-bit operand with the carry";
750+
let intrinsics = [IntrinSelect<int_uadd_with_overflow>];
751+
let arguments = [OverloadTy, OverloadTy];
752+
let result = BinaryWithCarryTy;
753+
let overloads = [Overloads<DXIL1_0, [Int32Ty]>];
754+
let stages = [Stages<DXIL1_0, [all_stages]>];
755+
let attributes = [Attributes<DXIL1_0, [ReadNone]>];
756+
}
757+
747758
def FMad : DXILOp<46, tertiary> {
748759
let Doc = "Floating point arithmetic multiply/add operation. fmad(m,a,b) = m "
749760
"* a + b.";

llvm/lib/Target/DirectX/DXILOpBuilder.cpp

+10
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,14 @@ static StructType *getSplitDoubleType(LLVMContext &Context) {
253253
return StructType::create({Int32Ty, Int32Ty}, "dx.types.splitdouble");
254254
}
255255

256+
static StructType *getBinaryWithCarryType(LLVMContext &Context) {
257+
if (auto *ST = StructType::getTypeByName(Context, "dx.types.i32c"))
258+
return ST;
259+
Type *Int32Ty = Type::getInt32Ty(Context);
260+
Type *Int1Ty = Type::getInt1Ty(Context);
261+
return StructType::create({Int32Ty, Int1Ty}, "dx.types.i32c");
262+
}
263+
256264
static Type *getTypeFromOpParamType(OpParamType Kind, LLVMContext &Ctx,
257265
Type *OverloadTy) {
258266
switch (Kind) {
@@ -308,6 +316,8 @@ static Type *getTypeFromOpParamType(OpParamType Kind, LLVMContext &Ctx,
308316
return getResPropsType(Ctx);
309317
case OpParamType::SplitDoubleTy:
310318
return getSplitDoubleType(Ctx);
319+
case OpParamType::BinaryWithCarryTy:
320+
return getBinaryWithCarryType(Ctx);
311321
}
312322
llvm_unreachable("Invalid parameter kind");
313323
return nullptr;

0 commit comments

Comments
 (0)