Skip to content

Commit 07530bd

Browse files
implement OpIAddCarry, OpISubBorrow; improve type inference; fix access to erased from parent after visit instructions; fix validation of pointer types for frexp and lgamma_r; improve parsing of builtin names
1 parent cc22d56 commit 07530bd

File tree

10 files changed

+410
-37
lines changed

10 files changed

+410
-37
lines changed

llvm/lib/Target/SPIRV/SPIRVBuiltins.cpp

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,14 @@ std::string lookupBuiltinNameHelper(StringRef DemangledCall) {
190190
// Check if the extracted name contains type information between angle
191191
// brackets. If so, the builtin is an instantiated template - needs to have
192192
// the information after angle brackets and return type removed.
193-
if (BuiltinName.find('<') && BuiltinName.back() == '>') {
194-
BuiltinName = BuiltinName.substr(0, BuiltinName.find('<'));
193+
std::size_t Pos1 = BuiltinName.rfind('<');
194+
if (Pos1 != std::string::npos && BuiltinName.back() == '>') {
195+
std::size_t Pos2 = BuiltinName.rfind(' ', Pos1);
196+
if (Pos2 == std::string::npos)
197+
Pos2 = 0;
198+
else
199+
++Pos2;
200+
BuiltinName = BuiltinName.substr(Pos2, Pos1 - Pos2);
195201
BuiltinName = BuiltinName.substr(BuiltinName.find_last_of(' ') + 1);
196202
}
197203

@@ -461,9 +467,11 @@ static Register buildBuiltinVariableLoad(
461467
SPIRVGlobalRegistry *GR, SPIRV::BuiltIn::BuiltIn BuiltinValue, LLT LLType,
462468
Register Reg = Register(0), bool isConst = true, bool hasLinkageTy = true) {
463469
Register NewRegister =
464-
MIRBuilder.getMRI()->createVirtualRegister(&SPIRV::iIDRegClass);
465-
MIRBuilder.getMRI()->setType(NewRegister,
466-
LLT::pointer(0, GR->getPointerSize()));
470+
MIRBuilder.getMRI()->createVirtualRegister(&SPIRV::pIDRegClass);
471+
MIRBuilder.getMRI()->setType(
472+
NewRegister,
473+
LLT::pointer(storageClassToAddressSpace(SPIRV::StorageClass::Function),
474+
GR->getPointerSize()));
467475
SPIRVType *PtrType = GR->getOrCreateSPIRVPointerType(
468476
VariableType, MIRBuilder, SPIRV::StorageClass::Input);
469477
GR->assignSPIRVTypeToVReg(PtrType, NewRegister, MIRBuilder.getMF());
@@ -1556,6 +1564,55 @@ static bool generateWaveInst(const SPIRV::IncomingCall *Call,
15561564
/* isConst= */ false, /* hasLinkageTy= */ false);
15571565
}
15581566

1567+
// We expect a builtin
1568+
// Name(ptr sret([RetType]) %result, Type %operand1, Type %operand1)
1569+
// where %result is a pointer to where the result of the builtin execution
1570+
// is to be stored, and generate the following instructions:
1571+
// Res = Opcode RetType Operand1 Operand1
1572+
// OpStore RetVariable Res
1573+
static bool generateICarryBorrowInst(const SPIRV::IncomingCall *Call,
1574+
MachineIRBuilder &MIRBuilder,
1575+
SPIRVGlobalRegistry *GR) {
1576+
const SPIRV::DemangledBuiltin *Builtin = Call->Builtin;
1577+
unsigned Opcode =
1578+
SPIRV::lookupNativeBuiltin(Builtin->Name, Builtin->Set)->Opcode;
1579+
1580+
Register SRetReg = Call->Arguments[0];
1581+
SPIRVType *PtrRetType = GR->getSPIRVTypeForVReg(SRetReg);
1582+
SPIRVType *RetType = GR->getPointeeType(PtrRetType);
1583+
if (!RetType)
1584+
report_fatal_error("The first parameter must be a pointer");
1585+
if (RetType->getOpcode() != SPIRV::OpTypeStruct)
1586+
report_fatal_error("Expected struct type result for the arithmetic with "
1587+
"overflow builtins");
1588+
1589+
SPIRVType *OpType1 = GR->getSPIRVTypeForVReg(Call->Arguments[1]);
1590+
SPIRVType *OpType2 = GR->getSPIRVTypeForVReg(Call->Arguments[2]);
1591+
if (!OpType1 || !OpType2 || OpType1 != OpType2)
1592+
report_fatal_error("Operands must have the same type");
1593+
if (OpType1->getOpcode() == SPIRV::OpTypeVector)
1594+
switch (Opcode) {
1595+
case SPIRV::OpIAddCarryS:
1596+
Opcode = SPIRV::OpIAddCarryV;
1597+
break;
1598+
case SPIRV::OpISubBorrowS:
1599+
Opcode = SPIRV::OpISubBorrowV;
1600+
break;
1601+
}
1602+
1603+
MachineRegisterInfo *MRI = MIRBuilder.getMRI();
1604+
Register ResReg = MRI->createGenericVirtualRegister(LLT::scalar(64));
1605+
MRI->setRegClass(ResReg, &SPIRV::iIDRegClass);
1606+
GR->assignSPIRVTypeToVReg(RetType, ResReg, MIRBuilder.getMF());
1607+
MIRBuilder.buildInstr(Opcode)
1608+
.addDef(ResReg)
1609+
.addUse(GR->getSPIRVTypeID(RetType))
1610+
.addUse(Call->Arguments[1])
1611+
.addUse(Call->Arguments[2]);
1612+
MIRBuilder.buildInstr(SPIRV::OpStore).addUse(SRetReg).addUse(ResReg);
1613+
return true;
1614+
}
1615+
15591616
static bool generateGetQueryInst(const SPIRV::IncomingCall *Call,
15601617
MachineIRBuilder &MIRBuilder,
15611618
SPIRVGlobalRegistry *GR) {
@@ -2511,6 +2568,8 @@ std::optional<bool> lowerBuiltin(const StringRef DemangledCall,
25112568
return generateDotOrFMulInst(Call.get(), MIRBuilder, GR);
25122569
case SPIRV::Wave:
25132570
return generateWaveInst(Call.get(), MIRBuilder, GR);
2571+
case SPIRV::ICarryBorrow:
2572+
return generateICarryBorrowInst(Call.get(), MIRBuilder, GR);
25142573
case SPIRV::GetQuery:
25152574
return generateGetQueryInst(Call.get(), MIRBuilder, GR);
25162575
case SPIRV::ImageSizeQuery:

llvm/lib/Target/SPIRV/SPIRVBuiltins.td

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def KernelClock : BuiltinGroup;
6363
def CastToPtr : BuiltinGroup;
6464
def Construct : BuiltinGroup;
6565
def CoopMatr : BuiltinGroup;
66+
def ICarryBorrow : BuiltinGroup;
6667

6768
//===----------------------------------------------------------------------===//
6869
// Class defining a demangled builtin record. The information in the record
@@ -628,6 +629,10 @@ defm : DemangledNativeBuiltin<"barrier", OpenCL_std, Barrier, 1, 3, OpControlBar
628629
defm : DemangledNativeBuiltin<"work_group_barrier", OpenCL_std, Barrier, 1, 3, OpControlBarrier>;
629630
defm : DemangledNativeBuiltin<"__spirv_ControlBarrier", OpenCL_std, Barrier, 3, 3, OpControlBarrier>;
630631

632+
// ICarryBorrow builtin record:
633+
defm : DemangledNativeBuiltin<"__spirv_IAddCarry", OpenCL_std, ICarryBorrow, 3, 3, OpIAddCarryS>;
634+
defm : DemangledNativeBuiltin<"__spirv_ISubBorrow", OpenCL_std, ICarryBorrow, 3, 3, OpISubBorrowS>;
635+
631636
// cl_intel_split_work_group_barrier
632637
defm : DemangledNativeBuiltin<"intel_work_group_barrier_arrive", OpenCL_std, Barrier, 1, 2, OpControlBarrierArriveINTEL>;
633638
defm : DemangledNativeBuiltin<"__spirv_ControlBarrierArriveINTEL", OpenCL_std, Barrier, 3, 3, OpControlBarrierArriveINTEL>;

llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ class SPIRVEmitIntrinsics
7676
SPIRV::InstructionSet::InstructionSet InstrSet;
7777

7878
// a register of Instructions that don't have a complete type definition
79-
SmallPtrSet<Value *, 8> UncompleteTypeInfo;
80-
SmallVector<Instruction *> PostprocessWorklist;
79+
DenseMap<Value *, unsigned> UncompleteTypeInfo;
80+
SmallVector<Value *> PostprocessWorklist;
8181

8282
// well known result types of builtins
8383
enum WellKnownTypes { Event };
@@ -147,6 +147,7 @@ class SPIRVEmitIntrinsics
147147
std::unordered_set<Function *> &FVisited);
148148
void replaceWithPtrcasted(Instruction *CI, Type *NewElemTy, Type *KnownElemTy,
149149
CallInst *AssignCI);
150+
void replaceAllUsesWith(Value *Src, Value *Dest, bool DeleteOld = true);
150151

151152
bool runOnFunction(Function &F);
152153
bool postprocessTypes();
@@ -272,6 +273,27 @@ static inline void reportFatalOnTokenType(const Instruction *I) {
272273
false);
273274
}
274275

276+
void SPIRVEmitIntrinsics::replaceAllUsesWith(Value *Src, Value *Dest,
277+
bool DeleteOld) {
278+
Src->replaceAllUsesWith(Dest);
279+
// Update deduced type records
280+
GR->updateIfExistDeducedElementType(Src, Dest, DeleteOld);
281+
GR->updateIfExistAssignPtrTypeInstr(Src, Dest, DeleteOld);
282+
// Update uncomplete type records if any
283+
auto It = UncompleteTypeInfo.find(Src);
284+
if (It == UncompleteTypeInfo.end())
285+
return;
286+
if (DeleteOld) {
287+
unsigned Pos = It->second;
288+
UncompleteTypeInfo.erase(Src);
289+
UncompleteTypeInfo[Dest] = Pos;
290+
PostprocessWorklist[Pos] = Dest;
291+
} else {
292+
UncompleteTypeInfo[Dest] = PostprocessWorklist.size();
293+
PostprocessWorklist.push_back(Dest);
294+
}
295+
}
296+
275297
static bool IsKernelArgInt8(Function *F, StoreInst *SI) {
276298
return SI && F->getCallingConv() == CallingConv::SPIR_KERNEL &&
277299
isPointerTy(SI->getValueOperand()->getType()) &&
@@ -434,7 +456,7 @@ void SPIRVEmitIntrinsics::maybeAssignPtrType(Type *&Ty, Value *Op, Type *RefTy,
434456
if (!UnknownElemTypeI8)
435457
return;
436458
if (auto *I = dyn_cast<Instruction>(Op)) {
437-
UncompleteTypeInfo.insert(I);
459+
UncompleteTypeInfo[I] = PostprocessWorklist.size();
438460
PostprocessWorklist.push_back(I);
439461
}
440462
}
@@ -640,7 +662,7 @@ Type *SPIRVEmitIntrinsics::deduceElementType(Value *I, bool UnknownElemTypeI8) {
640662
if (!UnknownElemTypeI8)
641663
return nullptr;
642664
if (auto *Instr = dyn_cast<Instruction>(I)) {
643-
UncompleteTypeInfo.insert(Instr);
665+
UncompleteTypeInfo[Instr] = PostprocessWorklist.size();
644666
PostprocessWorklist.push_back(Instr);
645667
}
646668
return IntegerType::getInt8Ty(I->getContext());
@@ -1062,7 +1084,7 @@ Instruction *SPIRVEmitIntrinsics::visitSwitchInst(SwitchInst &I) {
10621084
{I.getOperand(0)->getType()}, {Args});
10631085
// remove switch to avoid its unneeded and undesirable unwrap into branches
10641086
// and conditions
1065-
I.replaceAllUsesWith(NewI);
1087+
replaceAllUsesWith(&I, NewI);
10661088
I.eraseFromParent();
10671089
// insert artificial and temporary instruction to preserve valid CFG,
10681090
// it will be removed after IR translation pass
@@ -1084,7 +1106,7 @@ Instruction *SPIRVEmitIntrinsics::visitGetElementPtrInst(GetElementPtrInst &I) {
10841106
for (auto &Op : I.operands())
10851107
Args.push_back(Op);
10861108
auto *NewI = B.CreateIntrinsic(Intrinsic::spv_gep, {Types}, {Args});
1087-
I.replaceAllUsesWith(NewI);
1109+
replaceAllUsesWith(&I, NewI);
10881110
I.eraseFromParent();
10891111
return NewI;
10901112
}
@@ -1099,7 +1121,7 @@ Instruction *SPIRVEmitIntrinsics::visitBitCastInst(BitCastInst &I) {
10991121
// such bitcasts do not provide sufficient information, should be just skipped
11001122
// here, and handled in insertPtrCastOrAssignTypeInstr.
11011123
if (isPointerTy(I.getType())) {
1102-
I.replaceAllUsesWith(Source);
1124+
replaceAllUsesWith(&I, Source);
11031125
I.eraseFromParent();
11041126
return nullptr;
11051127
}
@@ -1108,7 +1130,7 @@ Instruction *SPIRVEmitIntrinsics::visitBitCastInst(BitCastInst &I) {
11081130
SmallVector<Value *> Args(I.op_begin(), I.op_end());
11091131
auto *NewI = B.CreateIntrinsic(Intrinsic::spv_bitcast, {Types}, {Args});
11101132
std::string InstName = I.hasName() ? I.getName().str() : "";
1111-
I.replaceAllUsesWith(NewI);
1133+
replaceAllUsesWith(&I, NewI);
11121134
I.eraseFromParent();
11131135
NewI->setName(InstName);
11141136
return NewI;
@@ -1333,7 +1355,7 @@ Instruction *SPIRVEmitIntrinsics::visitInsertElementInst(InsertElementInst &I) {
13331355
SmallVector<Value *> Args(I.op_begin(), I.op_end());
13341356
auto *NewI = B.CreateIntrinsic(Intrinsic::spv_insertelt, {Types}, {Args});
13351357
std::string InstName = I.hasName() ? I.getName().str() : "";
1336-
I.replaceAllUsesWith(NewI);
1358+
replaceAllUsesWith(&I, NewI);
13371359
I.eraseFromParent();
13381360
NewI->setName(InstName);
13391361
return NewI;
@@ -1348,7 +1370,7 @@ SPIRVEmitIntrinsics::visitExtractElementInst(ExtractElementInst &I) {
13481370
SmallVector<Value *, 2> Args = {I.getVectorOperand(), I.getIndexOperand()};
13491371
auto *NewI = B.CreateIntrinsic(Intrinsic::spv_extractelt, {Types}, {Args});
13501372
std::string InstName = I.hasName() ? I.getName().str() : "";
1351-
I.replaceAllUsesWith(NewI);
1373+
replaceAllUsesWith(&I, NewI);
13521374
I.eraseFromParent();
13531375
NewI->setName(InstName);
13541376
return NewI;
@@ -1384,7 +1406,7 @@ Instruction *SPIRVEmitIntrinsics::visitExtractValueInst(ExtractValueInst &I) {
13841406
Args.push_back(B.getInt32(Op));
13851407
auto *NewI =
13861408
B.CreateIntrinsic(Intrinsic::spv_extractv, {I.getType()}, {Args});
1387-
I.replaceAllUsesWith(NewI);
1409+
replaceAllUsesWith(&I, NewI);
13881410
I.eraseFromParent();
13891411
return NewI;
13901412
}
@@ -1445,7 +1467,7 @@ Instruction *SPIRVEmitIntrinsics::visitAllocaInst(AllocaInst &I) {
14451467
{PtrTy, ArraySize->getType()}, {ArraySize})
14461468
: B.CreateIntrinsic(Intrinsic::spv_alloca, {PtrTy}, {});
14471469
std::string InstName = I.hasName() ? I.getName().str() : "";
1448-
I.replaceAllUsesWith(NewI);
1470+
replaceAllUsesWith(&I, NewI);
14491471
I.eraseFromParent();
14501472
NewI->setName(InstName);
14511473
return NewI;
@@ -1615,7 +1637,7 @@ void SPIRVEmitIntrinsics::processInstrAfterVisit(Instruction *I,
16151637
auto *NewOp =
16161638
buildIntrWithMD(Intrinsic::spv_track_constant,
16171639
{II->getType(), II->getType()}, t->second, I, {}, B);
1618-
I->replaceAllUsesWith(NewOp);
1640+
replaceAllUsesWith(I, NewOp, false);
16191641
NewOp->setArgOperand(0, I);
16201642
}
16211643
bool IsPhi = isa<PHINode>(I), BPrepared = false;

llvm/lib/Target/SPIRV/SPIRVGlobalRegistry.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ class SPIRVGlobalRegistry {
180180
auto It = AssignPtrTypeInstr.find(Val);
181181
return It == AssignPtrTypeInstr.end() ? nullptr : It->second;
182182
}
183+
// - Find a record and update its key or add a new record, if found.
184+
void updateIfExistAssignPtrTypeInstr(Value *OldVal, Value *NewVal,
185+
bool DeleteOld) {
186+
if (CallInst *CI = findAssignPtrTypeInstr(OldVal)) {
187+
if (DeleteOld)
188+
AssignPtrTypeInstr.erase(OldVal);
189+
AssignPtrTypeInstr[NewVal] = CI;
190+
}
191+
}
183192

184193
// A registry of mutated values
185194
// (see `SPIRVPrepareFunctions::removeAggregateTypesFromSignature()`):
@@ -214,6 +223,15 @@ class SPIRVGlobalRegistry {
214223
auto It = DeducedElTys.find(Val);
215224
return It == DeducedElTys.end() ? nullptr : It->second;
216225
}
226+
// - Find a record and update its key or add a new record, if found.
227+
void updateIfExistDeducedElementType(Value *OldVal, Value *NewVal,
228+
bool DeleteOld) {
229+
if (Type *Ty = findDeducedElementType(OldVal)) {
230+
if (DeleteOld)
231+
DeducedElTys.erase(OldVal);
232+
DeducedElTys[NewVal] = Ty;
233+
}
234+
}
217235
// - Add a record to the map of deduced composite types.
218236
void addDeducedCompositeType(Value *Val, Type *Ty) {
219237
DeducedNestedTys[Val] = Ty;

llvm/lib/Target/SPIRV/SPIRVISelLowering.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,11 @@ void SPIRVTargetLowering::finalizeLowering(MachineFunction &MF) const {
473473
MI.getOperand(2).getImm() != SPIRV::InstructionSet::OpenCL_std)
474474
continue;
475475
switch (MI.getOperand(3).getImm()) {
476+
case SPIRV::OpenCLExtInst::frexp:
477+
case SPIRV::OpenCLExtInst::lgamma_r:
476478
case SPIRV::OpenCLExtInst::remquo: {
477-
// The last operand must be of a pointer to the return type.
479+
// The last operand must be of a pointer to i32 or vector of i32
480+
// values.
478481
MachineIRBuilder MIB(MI);
479482
SPIRVType *Int32Type = GR.getOrCreateSPIRVIntegerType(32, MIB);
480483
SPIRVType *RetType = MRI->getVRegDef(MI.getOperand(1).getReg());
@@ -487,8 +490,6 @@ void SPIRVTargetLowering::finalizeLowering(MachineFunction &MF) const {
487490
Int32Type, RetType->getOperand(2).getImm(), MIB));
488491
} break;
489492
case SPIRV::OpenCLExtInst::fract:
490-
case SPIRV::OpenCLExtInst::frexp:
491-
case SPIRV::OpenCLExtInst::lgamma_r:
492493
case SPIRV::OpenCLExtInst::modf:
493494
case SPIRV::OpenCLExtInst::sincos:
494495
// The last operand must be of a pointer to the base type represented

llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3051,22 +3051,9 @@ bool SPIRVInstructionSelector::selectFrameIndex(Register ResVReg,
30513051
MachineInstr &I) const {
30523052
// Change order of instructions if needed: all OpVariable instructions in a
30533053
// function must be the first instructions in the first block
3054-
MachineFunction *MF = I.getParent()->getParent();
3055-
MachineBasicBlock *MBB = &MF->front();
3056-
auto It = MBB->SkipPHIsAndLabels(MBB->begin()), E = MBB->end();
3057-
bool IsHeader = false;
3058-
unsigned Opcode;
3059-
for (; It != E && It != I; ++It) {
3060-
Opcode = It->getOpcode();
3061-
if (Opcode == SPIRV::OpFunction || Opcode == SPIRV::OpFunctionParameter) {
3062-
IsHeader = true;
3063-
} else if (IsHeader &&
3064-
!(Opcode == SPIRV::ASSIGN_TYPE || Opcode == SPIRV::OpLabel)) {
3065-
++It;
3066-
break;
3067-
}
3068-
}
3069-
return BuildMI(*MBB, It, It->getDebugLoc(), TII.get(SPIRV::OpVariable))
3054+
auto It = getOpVariableMBBIt(I);
3055+
return BuildMI(*It->getParent(), It, It->getDebugLoc(),
3056+
TII.get(SPIRV::OpVariable))
30703057
.addDef(ResVReg)
30713058
.addUse(GR.getSPIRVTypeID(ResType))
30723059
.addImm(static_cast<uint32_t>(SPIRV::StorageClass::Function))

llvm/lib/Target/SPIRV/SPIRVUtils.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,26 @@ void buildOpSpirvDecorations(Register Reg, MachineIRBuilder &MIRBuilder,
162162
}
163163
}
164164

165+
MachineBasicBlock::iterator getOpVariableMBBIt(MachineInstr &I) {
166+
MachineFunction *MF = I.getParent()->getParent();
167+
MachineBasicBlock *MBB = &MF->front();
168+
MachineBasicBlock::iterator It = MBB->SkipPHIsAndLabels(MBB->begin()),
169+
E = MBB->end();
170+
bool IsHeader = false;
171+
unsigned Opcode;
172+
for (; It != E && It != I; ++It) {
173+
Opcode = It->getOpcode();
174+
if (Opcode == SPIRV::OpFunction || Opcode == SPIRV::OpFunctionParameter) {
175+
IsHeader = true;
176+
} else if (IsHeader &&
177+
!(Opcode == SPIRV::ASSIGN_TYPE || Opcode == SPIRV::OpLabel)) {
178+
++It;
179+
break;
180+
}
181+
}
182+
return It;
183+
}
184+
165185
SPIRV::StorageClass::StorageClass
166186
addressSpaceToStorageClass(unsigned AddrSpace, const SPIRVSubtarget &STI) {
167187
switch (AddrSpace) {

llvm/lib/Target/SPIRV/SPIRVUtils.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#include "MCTargetDesc/SPIRVBaseInfo.h"
1717
#include "llvm/Analysis/LoopInfo.h"
18+
#include "llvm/CodeGen/MachineBasicBlock.h"
1819
#include "llvm/IR/Dominators.h"
1920
#include "llvm/IR/IRBuilder.h"
2021
#include "llvm/IR/TypedPointerType.h"
@@ -139,6 +140,10 @@ void buildOpDecorate(Register Reg, MachineInstr &I, const SPIRVInstrInfo &TII,
139140
void buildOpSpirvDecorations(Register Reg, MachineIRBuilder &MIRBuilder,
140141
const MDNode *GVarMD);
141142

143+
// Return a valid position for the OpVariable instruction inside a function,
144+
// i.e., at the beginning of the first block of the function.
145+
MachineBasicBlock::iterator getOpVariableMBBIt(MachineInstr &I);
146+
142147
// Convert a SPIR-V storage class to the corresponding LLVM IR address space.
143148
// TODO: maybe the following two functions should be handled in the subtarget
144149
// to allow for different OpenCL vs Vulkan handling.

0 commit comments

Comments
 (0)