//===-- SPIRVLegalizePointerCast.cpp ----------------------*- C++ -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // The LLVM IR has multiple legal patterns we cannot lower to Logical SPIR-V. // This pass modifies such loads to have an IR we can directly lower to valid // logical SPIR-V. // OpenCL can avoid this because they rely on ptrcast, which is not supported // by logical SPIR-V. // // This pass relies on the assign_ptr_type intrinsic to deduce the type of the // pointed values, must replace all occurences of `ptrcast`. This is why // unhandled cases are reported as unreachable: we MUST cover all cases. // // 1. Loading the first element of an array // // %array = [10 x i32] // %value = load i32, ptr %array // // LLVM can skip the GEP instruction, and only request loading the first 4 // bytes. In logical SPIR-V, we need an OpAccessChain to access the first // element. This pass will add a getelementptr instruction before the load. // // // 2. Implicit downcast from load // // %1 = getelementptr <4 x i32>, ptr %vec4, i64 0 // %2 = load <3 x i32>, ptr %1 // // The pointer in the GEP instruction is only used for offset computations, // but it doesn't NEED to match the pointed type. OpAccessChain however // requires this. Also, LLVM loads define the bitwidth of the load, not the // pointer. In this example, we can guess %vec4 is a vec4 thanks to the GEP // instruction basetype, but we only want to load the first 3 elements, hence // do a partial load. In logical SPIR-V, this is not legal. What we must do // is load the full vector (basetype), extract 3 elements, and recombine them // to form a 3-element vector. // //===----------------------------------------------------------------------===// #include "SPIRVLegalizePointerCast.h" #include "SPIRV.h" #include "SPIRVSubtarget.h" #include "SPIRVTargetMachine.h" #include "SPIRVUtils.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/IntrinsicInst.h" #include "llvm/IR/Intrinsics.h" #include "llvm/IR/IntrinsicsSPIRV.h" #include "llvm/Transforms/Utils/Cloning.h" #include "llvm/Transforms/Utils/LowerMemIntrinsics.h" using namespace llvm; namespace { class SPIRVLegalizePointerCastImpl { // Builds the `spv_assign_type` assigning |Ty| to |Value| at the current // builder position. void buildAssignType(IRBuilder<> &B, Type *Ty, Value *Arg) { Value *OfType = PoisonValue::get(Ty); CallInst *AssignCI = buildIntrWithMD(Intrinsic::spv_assign_type, {Arg->getType()}, OfType, Arg, {}, B); GR->addAssignPtrTypeInstr(Arg, AssignCI); } static FixedVectorType *makeVectorFromTotalBits(Type *ElemTy, TypeSize TotalBits) { unsigned ElemBits = ElemTy->getScalarSizeInBits(); assert(ElemBits && TotalBits % ElemBits == 0 && "TotalBits must be divisible by element bit size"); return FixedVectorType::get(ElemTy, TotalBits / ElemBits); } Value *resizeVectorBitsWithShuffle(IRBuilder<> &B, Value *V, FixedVectorType *DstTy) { auto *SrcTy = cast(V->getType()); assert(SrcTy->getElementType() == DstTy->getElementType() && "shuffle resize expects identical element types"); const unsigned NumNeeded = DstTy->getNumElements(); const unsigned NumSource = SrcTy->getNumElements(); SmallVector Mask(NumNeeded); for (unsigned I = 0; I < NumNeeded; ++I) Mask[I] = (I < NumSource) ? static_cast(I) : -1; Value *Resized = B.CreateShuffleVector(V, V, Mask); buildAssignType(B, DstTy, Resized); return Resized; } // Loads parts of the vector of type |SourceType| from the pointer |Source| // and create a new vector of type |TargetType|. |TargetType| must be a vector // type. // Returns the loaded value. Value *loadVectorFromVector(IRBuilder<> &B, FixedVectorType *SourceType, FixedVectorType *TargetType, Value *Source) { LoadInst *NewLoad = B.CreateLoad(SourceType, Source); buildAssignType(B, SourceType, NewLoad); Value *AssignValue = NewLoad; if (TargetType->getElementType() != SourceType->getElementType()) { const DataLayout &DL = B.GetInsertBlock()->getModule()->getDataLayout(); TypeSize TargetTypeSize = DL.getTypeSizeInBits(TargetType); TypeSize SourceTypeSize = DL.getTypeSizeInBits(SourceType); Value *BitcastSrcVal = NewLoad; FixedVectorType *BitcastSrcTy = cast(BitcastSrcVal->getType()); FixedVectorType *BitcastDstTy = TargetType; if (TargetTypeSize != SourceTypeSize) { unsigned TargetElemBits = TargetType->getElementType()->getScalarSizeInBits(); if (SourceTypeSize % TargetElemBits == 0) { // No Resize needed. Same total bits as source, but use target element // type. BitcastDstTy = makeVectorFromTotalBits(TargetType->getElementType(), SourceTypeSize); } else { // Resize source to target total bitwidth using source element type. BitcastSrcTy = makeVectorFromTotalBits(SourceType->getElementType(), TargetTypeSize); BitcastSrcVal = resizeVectorBitsWithShuffle(B, NewLoad, BitcastSrcTy); } } AssignValue = B.CreateIntrinsic(Intrinsic::spv_bitcast, {BitcastDstTy, BitcastSrcTy}, {BitcastSrcVal}); buildAssignType(B, BitcastDstTy, AssignValue); if (BitcastDstTy == TargetType) return AssignValue; } assert(TargetType->getNumElements() < SourceType->getNumElements()); SmallVector Mask(/* Size= */ TargetType->getNumElements()); for (unsigned I = 0; I < TargetType->getNumElements(); ++I) Mask[I] = I; Value *Output = B.CreateShuffleVector(AssignValue, AssignValue, Mask); buildAssignType(B, TargetType, Output); return Output; } // Returns true if |FromTy| has a memory layout compatible with loading or // storing |ToTy|. bool isCompatibleMemoryLayout(Type *ToTy, Type *FromTy) { if (ToTy == FromTy) return true; auto *SVT = dyn_cast(FromTy); auto *DVT = dyn_cast(ToTy); if (SVT && DVT) return true; auto *SAT = dyn_cast(FromTy); if (SAT && DVT) { if (SAT->getElementType() == DVT->getElementType()) return true; if (auto *MAT = dyn_cast(SAT->getElementType())) if (MAT->getElementType() == DVT->getElementType()) return true; } return false; } // Traverses the aggregate type to find the first sub-type that matches // the TargetElemType's memory layout, optionally emitting a GEP intrinsic. std::optional> getPointerToFirstCompatibleType(IRBuilder<> &B, Value *BasePtr, Type *PointerType, Type *TargetElemType, bool IsInBounds) { Type *CurrentTy = GR->findDeducedElementType(BasePtr); assert(CurrentTy && "Could not deduce aggregate type"); SmallVector Args{/* isInBounds= */ B.getInt1(IsInBounds), BasePtr}; Args.push_back(B.getInt32(0)); // Pointer offset while (!isCompatibleMemoryLayout(TargetElemType, CurrentTy)) { if (auto *ST = dyn_cast(CurrentTy)) { if (ST->getNumElements() == 0) return std::nullopt; CurrentTy = ST->getTypeAtIndex(0u); } else if (auto *AT = dyn_cast(CurrentTy)) { CurrentTy = AT->getElementType(); } else if (auto *VT = dyn_cast(CurrentTy)) { CurrentTy = VT->getElementType(); } else { return std::nullopt; } Args.push_back(B.getInt32(0)); } Value *GEP = BasePtr; if (Args.size() > 3) { std::array Types = {PointerType, BasePtr->getType()}; GEP = B.CreateIntrinsic(Intrinsic::spv_gep, {Types}, {Args}); GR->buildAssignPtr(B, CurrentTy, GEP); } return std::make_pair(GEP, CurrentTy); } // Builds a legalized load from a pointer, drilling down through // memory layouts to find a compatible type. Load flags will be // copied from |BadLoad|, which should be the load being legalized. Value *buildLegalizedLoad(IRBuilder<> &B, Type *ElementType, Value *Source, LoadInst *BadLoad) { auto ResultOpt = getPointerToFirstCompatibleType( B, Source, BadLoad->getPointerOperandType(), ElementType, false); assert(ResultOpt && "Failed to load from aggregate: " "Could not find compatible memory layout."); auto [GEP, CurrentTy] = *ResultOpt; auto *SAT = dyn_cast(CurrentTy); auto *SVT = dyn_cast(CurrentTy); auto *DVT = dyn_cast(ElementType); auto *MAT = SAT ? dyn_cast(SAT->getElementType()) : nullptr; if (ElementType == CurrentTy) { LoadInst *LI = B.CreateLoad(ElementType, GEP); LI->setAlignment(BadLoad->getAlign()); buildAssignType(B, ElementType, LI); return LI; } if (SVT && DVT) return loadVectorFromVector(B, SVT, DVT, GEP); if (SAT && DVT && SAT->getElementType() == DVT->getElementType()) return loadVectorFromArray(B, DVT, GEP); if (MAT && DVT && MAT->getElementType() == DVT->getElementType()) return loadVectorFromMatrixArray(B, DVT, GEP, MAT); llvm_unreachable("Failed to load from aggregate."); } Value * buildVectorFromLoadedElements(IRBuilder<> &B, FixedVectorType *TargetType, SmallVector &LoadedElements) { // <1 x T> shares the SPIR-V type with T, so emitting OpCompositeInsert on // a scalar would be invalid. Bridge with spv_bitcast instead. if (TargetType->getNumElements() == 1) { Value *Scalar = LoadedElements[0]; Value *NewVector = B.CreateIntrinsic( Intrinsic::spv_bitcast, {TargetType, Scalar->getType()}, {Scalar}); buildAssignType(B, TargetType, NewVector); return NewVector; } // Build the vector from the loaded elements. Value *NewVector = PoisonValue::get(TargetType); buildAssignType(B, TargetType, NewVector); for (unsigned I = 0, E = TargetType->getNumElements(); I < E; ++I) { Value *Index = B.getInt32(I); SmallVector Types = {TargetType, TargetType, TargetType->getElementType(), Index->getType()}; SmallVector Args = {NewVector, LoadedElements[I], Index}; NewVector = B.CreateIntrinsic(Intrinsic::spv_insertelt, {Types}, {Args}); buildAssignType(B, TargetType, NewVector); } return NewVector; } // Loads elements from a matrix with an array of vector memory layout and // constructs a vector. Value *loadVectorFromMatrixArray(IRBuilder<> &B, FixedVectorType *TargetType, Value *Source, FixedVectorType *ArrElemVecTy) { Type *TargetElemTy = TargetType->getElementType(); unsigned ScalarsPerArrayElement = ArrElemVecTy->getNumElements(); // Load each element of the array. SmallVector LoadedElements; std::array Types = {Source->getType(), Source->getType()}; for (unsigned I = 0, E = TargetType->getNumElements(); I < E; ++I) { unsigned ArrayIndex = I / ScalarsPerArrayElement; unsigned ElementIndexInArrayElem = I % ScalarsPerArrayElement; // Create a GEP to access the i-th element of the array. std::array Args = { B.getInt1(/*Inbounds=*/false), Source, B.getInt32(0), ConstantInt::get(B.getInt32Ty(), ArrayIndex)}; auto *ElementPtr = B.CreateIntrinsic(Intrinsic::spv_gep, {Types}, {Args}); GR->buildAssignPtr(B, ArrElemVecTy, ElementPtr); Value *LoadVec = B.CreateLoad(ArrElemVecTy, ElementPtr); buildAssignType(B, ArrElemVecTy, LoadVec); LoadedElements.push_back(makeExtractElement(B, TargetElemTy, LoadVec, ElementIndexInArrayElem)); } return buildVectorFromLoadedElements(B, TargetType, LoadedElements); } // Loads elements from an array and constructs a vector. Value *loadVectorFromArray(IRBuilder<> &B, FixedVectorType *TargetType, Value *Source) { // Load each element of the array. SmallVector LoadedElements; std::array Types = {Source->getType(), Source->getType()}; for (unsigned I = 0, E = TargetType->getNumElements(); I < E; ++I) { // Create a GEP to access the i-th element of the array. std::array Args = {B.getInt1(/*Inbounds=*/false), Source, B.getInt32(0), ConstantInt::get(B.getInt32Ty(), I)}; auto *ElementPtr = B.CreateIntrinsic(Intrinsic::spv_gep, {Types}, {Args}); GR->buildAssignPtr(B, TargetType->getElementType(), ElementPtr); // Load the value from the element pointer. Value *Load = B.CreateLoad(TargetType->getElementType(), ElementPtr); buildAssignType(B, TargetType->getElementType(), Load); LoadedElements.push_back(Load); } return buildVectorFromLoadedElements(B, TargetType, LoadedElements); } // Stores elements from a vector into a matrix (an array of vectors). void storeMatrixArrayFromVector(IRBuilder<> &B, Value *SrcVector, Value *DstArrayPtr, ArrayType *ArrTy, Align Alignment) { auto *SrcVecTy = cast(SrcVector->getType()); auto *ArrElemVecTy = cast(ArrTy->getElementType()); Type *ElemTy = ArrElemVecTy->getElementType(); unsigned ScalarsPerArrayElement = ArrElemVecTy->getNumElements(); unsigned SrcNumElements = SrcVecTy->getNumElements(); assert( SrcNumElements % ScalarsPerArrayElement == 0 && "Source vector size must be a multiple of array element vector size"); std::array Types = {DstArrayPtr->getType(), DstArrayPtr->getType()}; for (unsigned I = 0; I < SrcNumElements; I += ScalarsPerArrayElement) { unsigned ArrayIndex = I / ScalarsPerArrayElement; // Create a GEP to access the array element. std::array Args = { B.getInt1(/*Inbounds=*/false), DstArrayPtr, B.getInt32(0), ConstantInt::get(B.getInt32Ty(), ArrayIndex)}; auto *ElementPtr = B.CreateIntrinsic(Intrinsic::spv_gep, {Types}, {Args}); GR->buildAssignPtr(B, ArrElemVecTy, ElementPtr); // Extract scalar elements from the source vector for this array slot. SmallVector Elements; for (unsigned J = 0; J < ScalarsPerArrayElement; ++J) Elements.push_back(makeExtractElement(B, ElemTy, SrcVector, I + J)); // Build a vector from the extracted elements and store it. Value *Vec = buildVectorFromLoadedElements(B, ArrElemVecTy, Elements); StoreInst *SI = B.CreateStore(Vec, ElementPtr); SI->setAlignment(Alignment); } } // Stores elements from a vector into an array. void storeArrayFromVector(IRBuilder<> &B, Value *SrcVector, Value *DstArrayPtr, ArrayType *ArrTy, Align Alignment) { auto *VecTy = cast(SrcVector->getType()); Type *ElemTy = ArrTy->getElementType(); // Ensure the element types of the array and vector are the same. assert(VecTy->getElementType() == ElemTy && "Element types of array and vector must be the same."); std::array Types = {DstArrayPtr->getType(), DstArrayPtr->getType()}; for (unsigned I = 0, E = VecTy->getNumElements(); I < E; ++I) { // Create a GEP to access the i-th element of the array. std::array Args = {B.getInt1(/*Inbounds=*/false), DstArrayPtr, B.getInt32(0), ConstantInt::get(B.getInt32Ty(), I)}; auto *ElementPtr = B.CreateIntrinsic(Intrinsic::spv_gep, {Types}, {Args}); GR->buildAssignPtr(B, ElemTy, ElementPtr); // Extract the element from the vector and store it. Value *Element = E == 1 ? SrcVector : makeExtractElement(B, ElemTy, SrcVector, I); StoreInst *SI = B.CreateStore(Element, ElementPtr); SI->setAlignment(Alignment); } } // Replaces the load instruction to get rid of the ptrcast used as source // operand. void transformLoad(IRBuilder<> &B, LoadInst *LI, Value *CastedOperand, Value *OriginalOperand) { Type *ToTy = GR->findDeducedElementType(CastedOperand); B.SetInsertPoint(LI); Value *Output = buildLegalizedLoad(B, ToTy, OriginalOperand, LI); GR->replaceAllUsesWith(LI, Output, /* DeleteOld= */ true); DeadInstructions.push_back(LI); } // Creates an spv_insertelt instruction (equivalent to llvm's insertelement). Value *makeInsertElement(IRBuilder<> &B, Value *Vector, Value *Element, unsigned Index) { Type *Int32Ty = Type::getInt32Ty(B.getContext()); SmallVector Types = {Vector->getType(), Vector->getType(), Element->getType(), Int32Ty}; SmallVector Args = {Vector, Element, B.getInt32(Index)}; Instruction *NewI = B.CreateIntrinsic(Intrinsic::spv_insertelt, {Types}, {Args}); buildAssignType(B, Vector->getType(), NewI); return NewI; } // Creates an spv_extractelt instruction (equivalent to llvm's // extractelement). Value *makeExtractElement(IRBuilder<> &B, Type *ElementType, Value *Vector, unsigned Index) { Type *Int32Ty = Type::getInt32Ty(B.getContext()); SmallVector Types = {ElementType, Vector->getType(), Int32Ty}; SmallVector Args = {Vector, B.getInt32(Index)}; Instruction *NewI = B.CreateIntrinsic(Intrinsic::spv_extractelt, {Types}, {Args}); buildAssignType(B, ElementType, NewI); return NewI; } // Stores the given Src vector operand into the Dst vector, adjusting the size // if required. Value *storeVectorFromVector(IRBuilder<> &B, Value *Src, Value *Dst, Align Alignment) { FixedVectorType *SrcType = cast(Src->getType()); FixedVectorType *DstType = cast(GR->findDeducedElementType(Dst)); auto dstNumElements = DstType->getNumElements(); auto srcNumElements = SrcType->getNumElements(); // if the element type differs, it is a bitcast. if (DstType->getElementType() != SrcType->getElementType()) { // Support bitcast between vectors of different sizes only if // the total bitwidth is the same. [[maybe_unused]] auto dstBitWidth = DstType->getElementType()->getScalarSizeInBits() * dstNumElements; [[maybe_unused]] auto srcBitWidth = SrcType->getElementType()->getScalarSizeInBits() * srcNumElements; assert(dstBitWidth == srcBitWidth && "Unsupported bitcast between vectors of different sizes."); Src = B.CreateIntrinsic(Intrinsic::spv_bitcast, {DstType, SrcType}, {Src}); buildAssignType(B, DstType, Src); SrcType = DstType; StoreInst *SI = B.CreateStore(Src, Dst); SI->setAlignment(Alignment); return SI; } assert(DstType->getNumElements() >= SrcType->getNumElements()); LoadInst *LI = B.CreateLoad(DstType, Dst); LI->setAlignment(Alignment); Value *OldValues = LI; buildAssignType(B, OldValues->getType(), OldValues); Value *NewValues = Src; for (unsigned I = 0; I < SrcType->getNumElements(); ++I) { Value *Element = makeExtractElement(B, SrcType->getElementType(), NewValues, I); OldValues = makeInsertElement(B, OldValues, Element, I); } StoreInst *SI = B.CreateStore(OldValues, Dst); SI->setAlignment(Alignment); return SI; } // Builds a legalized store to a pointer, drilling down through // memory layouts to find a compatible type. void buildLegalizedStore(IRBuilder<> &B, Value *Src, Value *Dst, Align Alignment) { auto ResultOpt = getPointerToFirstCompatibleType(B, Dst, Dst->getType(), Src->getType(), true); assert(ResultOpt && "Failed to store to aggregate: " "Could not find compatible memory layout."); auto [GEP, CurrentTy] = *ResultOpt; auto *DAT = dyn_cast(CurrentTy); auto *DVT = dyn_cast(CurrentTy); auto *SVT = dyn_cast(Src->getType()); auto *DMAT = DAT ? dyn_cast(DAT->getElementType()) : nullptr; if (Src->getType() == CurrentTy) { StoreInst *SI = B.CreateStore(Src, GEP); SI->setAlignment(Alignment); return; } if (DVT && SVT) { storeVectorFromVector(B, Src, GEP, Alignment); return; } if (DAT && SVT && SVT->getElementType() == DAT->getElementType()) { storeArrayFromVector(B, Src, GEP, DAT, Alignment); return; } if (DMAT && SVT && DMAT->getElementType() == SVT->getElementType()) { storeMatrixArrayFromVector(B, Src, GEP, DAT, Alignment); return; } llvm_unreachable("Failed to store to aggregate."); } // Transforms a store instruction (or SPV intrinsic) using a ptrcast as // operand into a valid logical SPIR-V store with no ptrcast. void transformStore(IRBuilder<> &B, Instruction *BadStore, Value *Src, Value *Dst, Align Alignment) { B.SetInsertPoint(BadStore); buildLegalizedStore(B, Src, Dst, Alignment); DeadInstructions.push_back(BadStore); } void legalizePointerCast(IntrinsicInst *II) { Value *CastedOperand = II; Value *OriginalOperand = II->getOperand(0); IRBuilder<> B(II->getContext()); std::vector Users; for (Use &U : II->uses()) Users.push_back(U.getUser()); for (Value *User : Users) { if (LoadInst *LI = dyn_cast(User)) { transformLoad(B, LI, CastedOperand, OriginalOperand); continue; } if (StoreInst *SI = dyn_cast(User)) { transformStore(B, SI, SI->getValueOperand(), OriginalOperand, SI->getAlign()); continue; } if (IntrinsicInst *Intrin = dyn_cast(User)) { if (Intrin->getIntrinsicID() == Intrinsic::spv_assign_ptr_type) { DeadInstructions.push_back(Intrin); continue; } if (Intrin->getIntrinsicID() == Intrinsic::spv_gep) { GR->replaceAllUsesWith(CastedOperand, OriginalOperand, /* DeleteOld= */ false); continue; } if (Intrin->getIntrinsicID() == Intrinsic::spv_store) { Align Alignment; if (ConstantInt *C = dyn_cast(Intrin->getOperand(3))) Alignment = Align(C->getZExtValue()); transformStore(B, Intrin, Intrin->getArgOperand(0), OriginalOperand, Alignment); continue; } } llvm_unreachable("Unsupported ptrcast user. Please fix."); } DeadInstructions.push_back(II); } public: SPIRVLegalizePointerCastImpl(const SPIRVTargetMachine &TM) : TM(TM) {} bool run(Function &F) { const SPIRVSubtarget &ST = TM.getSubtarget(F); GR = ST.getSPIRVGlobalRegistry(); DeadInstructions.clear(); std::vector WorkList; for (auto &BB : F) { for (auto &I : BB) { auto *II = dyn_cast(&I); if (II && II->getIntrinsicID() == Intrinsic::spv_ptrcast) WorkList.push_back(II); } } for (IntrinsicInst *II : WorkList) legalizePointerCast(II); for (Instruction *I : DeadInstructions) I->eraseFromParent(); return DeadInstructions.size() != 0; } private: const SPIRVTargetMachine &TM; SPIRVGlobalRegistry *GR = nullptr; std::vector DeadInstructions; }; class SPIRVLegalizePointerCastLegacy : public FunctionPass { public: static char ID; SPIRVLegalizePointerCastLegacy(const SPIRVTargetMachine &TM) : FunctionPass(ID), TM(TM) {} bool runOnFunction(Function &F) override { return SPIRVLegalizePointerCastImpl(TM).run(F); } private: const SPIRVTargetMachine &TM; }; } // namespace PreservedAnalyses SPIRVLegalizePointerCast::run(Function &F, FunctionAnalysisManager &AM) { return SPIRVLegalizePointerCastImpl(TM).run(F) ? PreservedAnalyses::none() : PreservedAnalyses::all(); } char SPIRVLegalizePointerCastLegacy::ID = 0; INITIALIZE_PASS(SPIRVLegalizePointerCastLegacy, "spirv-legalize-pointer-cast", "SPIRV legalize pointer cast pass", false, false) FunctionPass *llvm::createSPIRVLegalizePointerCastPass(SPIRVTargetMachine *TM) { return new SPIRVLegalizePointerCastLegacy(*TM); }