Skip to content

[SPIRV] Add reads from image buffer for shaders. #115178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions llvm/docs/SPIRVUsage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,14 @@ SPIR-V backend, along with their descriptions and argument details.
If `arraySize > 1`, then the binding represents an array of resources\
of the given size, and the handle for the resource at the given index is returned.\
If the index is possibly non-uniform, then `isUniformIndex` must get set to true.
* - `int_spv_typeBufferLoad`
- Scalar or vector
- `[spirv.Image ImageBuffer, 32-bit Integer coordinate]`
- Loads a value from a Vulkan image buffer at the given coordinate. The \
image buffer data is assumed to be stored as a 4-element vector. If the \
return type is a scalar, then the first element of the vector is \
returned. If the return type is an n-element vector, then the first \
n-elements of the 4-element vector are returned.

.. _spirv-builtin-functions:

Expand Down
6 changes: 6 additions & 0 deletions llvm/include/llvm/IR/IntrinsicsSPIRV.td
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,10 @@ let TargetPrefix = "spv" in {
[IntrNoMem]>;
def int_spv_firstbituhigh : DefaultAttrsIntrinsic<[LLVMScalarOrSameVectorWidth<0, llvm_i32_ty>], [llvm_anyint_ty], [IntrNoMem]>;
def int_spv_firstbitshigh : DefaultAttrsIntrinsic<[LLVMScalarOrSameVectorWidth<0, llvm_i32_ty>], [llvm_anyint_ty], [IntrNoMem]>;

// Read a value from the image buffer. It does not translate directly to a
// single OpImageRead because the result type is not necessarily a 4 element
// vector.
def int_spv_typedBufferLoad
: DefaultAttrsIntrinsic<[llvm_any_ty], [llvm_any_ty, llvm_i32_ty]>;
}
23 changes: 23 additions & 0 deletions llvm/lib/Target/SPIRV/SPIRVGlobalRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,11 @@ SPIRVGlobalRegistry::getSPIRVTypeForVReg(Register VReg,
return nullptr;
}

SPIRVType *SPIRVGlobalRegistry::getResultType(Register VReg) {
MachineInstr *Instr = getVRegDef(CurMF->getRegInfo(), VReg);
return getSPIRVTypeForVReg(Instr->getOperand(1).getReg());
}

SPIRVType *SPIRVGlobalRegistry::getOrCreateSPIRVType(
const Type *Ty, MachineIRBuilder &MIRBuilder,
SPIRV::AccessQualifier::AccessQualifier AccessQual, bool EmitIR) {
Expand Down Expand Up @@ -1126,6 +1131,24 @@ SPIRVGlobalRegistry::getScalarOrVectorComponentCount(SPIRVType *Type) const {
: 1;
}

SPIRVType *
SPIRVGlobalRegistry::getScalarOrVectorComponentType(Register VReg) const {
return getScalarOrVectorComponentType(getSPIRVTypeForVReg(VReg));
}

SPIRVType *
SPIRVGlobalRegistry::getScalarOrVectorComponentType(SPIRVType *Type) const {
if (!Type)
return nullptr;
Register ScalarReg = Type->getOpcode() == SPIRV::OpTypeVector
? Type->getOperand(1).getReg()
: Type->getOperand(0).getReg();
SPIRVType *ScalarType = getSPIRVTypeForVReg(ScalarReg);
assert(isScalarOrVectorOfType(Type->getOperand(0).getReg(),
ScalarType->getOpcode()));
return ScalarType;
}

unsigned
SPIRVGlobalRegistry::getScalarOrVectorBitWidth(const SPIRVType *Type) const {
assert(Type && "Invalid Type pointer");
Expand Down
9 changes: 9 additions & 0 deletions llvm/lib/Target/SPIRV/SPIRVGlobalRegistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ class SPIRVGlobalRegistry {
SPIRVType *getSPIRVTypeForVReg(Register VReg,
const MachineFunction *MF = nullptr) const;

// Return the result type of the instruction defining the register.
SPIRVType *getResultType(Register VReg);

// Whether the given VReg has a SPIR-V type mapped to it yet.
bool hasSPIRVTypeForVReg(Register VReg) const {
return getSPIRVTypeForVReg(VReg) != nullptr;
Expand Down Expand Up @@ -388,6 +391,12 @@ class SPIRVGlobalRegistry {
unsigned getScalarOrVectorComponentCount(Register VReg) const;
unsigned getScalarOrVectorComponentCount(SPIRVType *Type) const;

// Return the component type in a vector if the argument is associated with
// a vector type. Returns the argument itself for other types, and nullptr
// for a missing type.
SPIRVType *getScalarOrVectorComponentType(Register VReg) const;
SPIRVType *getScalarOrVectorComponentType(SPIRVType *Type) const;

// For vectors or scalars of booleans, integers and floats, return the scalar
// type's bitwidth. Otherwise calls llvm_unreachable().
unsigned getScalarOrVectorBitWidth(const SPIRVType *Type) const;
Expand Down
103 changes: 103 additions & 0 deletions llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "SPIRVTargetMachine.h"
#include "SPIRVUtils.h"
#include "llvm/ADT/APFloat.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/CodeGen/GlobalISel/GIMatchTableExecutorImpl.h"
#include "llvm/CodeGen/GlobalISel/GenericMachineInstrs.h"
#include "llvm/CodeGen/GlobalISel/InstructionSelector.h"
Expand Down Expand Up @@ -267,6 +268,9 @@ class SPIRVInstructionSelector : public InstructionSelector {
void selectHandleFromBinding(Register &ResVReg, const SPIRVType *ResType,
MachineInstr &I) const;

void selectReadImageIntrinsic(Register &ResVReg, const SPIRVType *ResType,
MachineInstr &I) const;

// Utilities
Register buildI32Constant(uint32_t Val, MachineInstr &I,
const SPIRVType *ResType = nullptr) const;
Expand All @@ -291,6 +295,9 @@ class SPIRVInstructionSelector : public InstructionSelector {
uint32_t Binding, uint32_t ArraySize,
Register IndexReg, bool IsNonUniform,
MachineIRBuilder MIRBuilder) const;
SPIRVType *widenTypeToVec4(const SPIRVType *Type, MachineInstr &I) const;
void extractSubvector(Register &ResVReg, const SPIRVType *ResType,
Register &ReadReg, MachineInstr &InsertionPoint) const;
};

} // end anonymous namespace
Expand Down Expand Up @@ -2809,6 +2816,10 @@ bool SPIRVInstructionSelector::selectIntrinsic(Register ResVReg,
selectHandleFromBinding(ResVReg, ResType, I);
return true;
}
case Intrinsic::spv_typedBufferLoad: {
selectReadImageIntrinsic(ResVReg, ResType, I);
return true;
}
default: {
std::string DiagMsg;
raw_string_ostream OS(DiagMsg);
Expand Down Expand Up @@ -2845,6 +2856,83 @@ void SPIRVInstructionSelector::selectHandleFromBinding(Register &ResVReg,
.addUse(VarReg);
}

void SPIRVInstructionSelector::selectReadImageIntrinsic(
Register &ResVReg, const SPIRVType *ResType, MachineInstr &I) const {

// If the load of the image is in a different basic block, then
// this will generate invalid code. A proper solution is to move
// the OpLoad from selectHandleFromBinding here. However, to do
// that we will need to change the return type of the intrinsic.
// We will do that when we can, but for now trying to move forward with other
// issues.
Register ImageReg = I.getOperand(2).getReg();
assert(MRI->getVRegDef(ImageReg)->getParent() == I.getParent() &&
"The image must be loaded in the same basic block as its use.");

uint64_t ResultSize = GR.getScalarOrVectorComponentCount(ResType);
if (ResultSize == 4) {
BuildMI(*I.getParent(), I, I.getDebugLoc(), TII.get(SPIRV::OpImageRead))
.addDef(ResVReg)
.addUse(GR.getSPIRVTypeID(ResType))
.addUse(ImageReg)
.addUse(I.getOperand(3).getReg());
return;
}

SPIRVType *ReadType = widenTypeToVec4(ResType, I);
Register ReadReg = MRI->createVirtualRegister(GR.getRegClass(ReadType));
BuildMI(*I.getParent(), I, I.getDebugLoc(), TII.get(SPIRV::OpImageRead))
.addDef(ReadReg)
.addUse(GR.getSPIRVTypeID(ReadType))
.addUse(ImageReg)
.addUse(I.getOperand(3).getReg());

if (ResultSize == 1) {
BuildMI(*I.getParent(), I, I.getDebugLoc(),
TII.get(SPIRV::OpCompositeExtract))
.addDef(ResVReg)
.addUse(GR.getSPIRVTypeID(ResType))
.addUse(ReadReg)
.addImm(0);
return;
}
extractSubvector(ResVReg, ResType, ReadReg, I);
}

void SPIRVInstructionSelector::extractSubvector(
Register &ResVReg, const SPIRVType *ResType, Register &ReadReg,
MachineInstr &InsertionPoint) const {
SPIRVType *InputType = GR.getResultType(ReadReg);
uint64_t InputSize = GR.getScalarOrVectorComponentCount(InputType);
uint64_t ResultSize = GR.getScalarOrVectorComponentCount(ResType);
assert(InputSize > 1 && "The input must be a vector.");
assert(ResultSize > 1 && "The result must be a vector.");
assert(ResultSize < InputSize &&
"Cannot extract more element than there are in the input.");
SmallVector<Register> ComponentRegisters;
SPIRVType *ScalarType = GR.getScalarOrVectorComponentType(ResType);
const TargetRegisterClass *ScalarRegClass = GR.getRegClass(ScalarType);
for (uint64_t I = 0; I < ResultSize; I++) {
Register ComponentReg = MRI->createVirtualRegister(ScalarRegClass);
BuildMI(*InsertionPoint.getParent(), InsertionPoint,
InsertionPoint.getDebugLoc(), TII.get(SPIRV::OpCompositeExtract))
.addDef(ComponentReg)
.addUse(ScalarType->getOperand(0).getReg())
.addUse(ReadReg)
.addImm(I);
ComponentRegisters.emplace_back(ComponentReg);
}

MachineInstrBuilder MIB = BuildMI(*InsertionPoint.getParent(), InsertionPoint,
InsertionPoint.getDebugLoc(),
TII.get(SPIRV::OpCompositeConstruct))
.addDef(ResVReg)
.addUse(GR.getSPIRVTypeID(ResType));

for (Register ComponentReg : ComponentRegisters)
MIB.addUse(ComponentReg);
}

Register SPIRVInstructionSelector::buildPointerToResource(
const SPIRVType *ResType, uint32_t Set, uint32_t Binding,
uint32_t ArraySize, Register IndexReg, bool IsNonUniform,
Expand Down Expand Up @@ -3347,6 +3435,21 @@ bool SPIRVInstructionSelector::selectSpvThreadId(Register ResVReg,
return MIB.constrainAllUses(TII, TRI, RBI);
}

SPIRVType *SPIRVInstructionSelector::widenTypeToVec4(const SPIRVType *Type,
MachineInstr &I) const {
MachineIRBuilder MIRBuilder(I);
if (Type->getOpcode() != SPIRV::OpTypeVector)
return GR.getOrCreateSPIRVVectorType(Type, 4, MIRBuilder);

uint64_t VectorSize = Type->getOperand(2).getImm();
if (VectorSize == 4)
return Type;

Register ScalarTypeReg = Type->getOperand(1).getReg();
const SPIRVType *ScalarType = GR.getSPIRVTypeForVReg(ScalarTypeReg);
return GR.getOrCreateSPIRVVectorType(ScalarType, 4, MIRBuilder);
}

namespace llvm {
InstructionSelector *
createSPIRVInstructionSelector(const SPIRVTargetMachine &TM,
Expand Down
22 changes: 21 additions & 1 deletion llvm/lib/Target/SPIRV/SPIRVModuleAnalysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@ void RequirementHandler::initAvailableCapabilitiesForOpenCL(
// Add the min requirements for different OpenCL and SPIR-V versions.
addAvailableCaps({Capability::Addresses, Capability::Float16Buffer,
Capability::Kernel, Capability::Vector16,
Capability::Groups, Capability::GenericPointer});
Capability::Groups, Capability::GenericPointer,
Capability::StorageImageReadWithoutFormat});
if (ST.hasOpenCLFullProfile())
addAvailableCaps({Capability::Int64, Capability::Int64Atomics});
if (ST.hasOpenCLImageSupport()) {
Expand Down Expand Up @@ -719,6 +720,10 @@ void RequirementHandler::initAvailableCapabilitiesForVulkan(
Capability::UniformTexelBufferArrayNonUniformIndexingEXT,
Capability::StorageTexelBufferArrayNonUniformIndexingEXT});
}

// Became core in Vulkan 1.3
if (ST.isAtLeastSPIRVVer(VersionTuple(1, 6)))
addAvailableCaps({Capability::StorageImageReadWithoutFormat});
}

} // namespace SPIRV
Expand Down Expand Up @@ -1005,6 +1010,13 @@ void addOpAccessChainReqs(const MachineInstr &Instr,
}
}

static bool isImageTypeWithUnknownFormat(SPIRVType *TypeInst) {
if (TypeInst->getOpcode() != SPIRV::OpTypeImage)
return false;
assert(TypeInst->getOperand(7).isImm() && "The image format must be an imm.");
return TypeInst->getOperand(7).getImm() == 0;
}

static void AddDotProductRequirements(const MachineInstr &MI,
SPIRV::RequirementHandler &Reqs,
const SPIRVSubtarget &ST) {
Expand Down Expand Up @@ -1411,6 +1423,14 @@ void addInstrRequirements(const MachineInstr &MI,
case SPIRV::OpUDot:
AddDotProductRequirements(MI, Reqs, ST);
break;
case SPIRV::OpImageRead: {
Register ImageReg = MI.getOperand(2).getReg();
SPIRVType *TypeDef = ST.getSPIRVGlobalRegistry()->getResultType(ImageReg);
if (isImageTypeWithUnknownFormat(TypeDef))
Reqs.addCapability(SPIRV::Capability::StorageImageReadWithoutFormat);
break;
}

default:
break;
}
Expand Down
66 changes: 66 additions & 0 deletions llvm/test/CodeGen/SPIRV/hlsl-resources/BufferLoad.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv-vulkan-library %s -o - | FileCheck %s
; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-vulkan-library %s -o - -filetype=obj | spirv-val %}

; CHECK-NOT: OpCapability StorageImageReadWithoutFormat

; CHECK-DAG: OpDecorate [[IntBufferVar:%[0-9]+]] DescriptorSet 16
; CHECK-DAG: OpDecorate [[IntBufferVar]] Binding 7

; CHECK-DAG: [[int:%[0-9]+]] = OpTypeInt 32 0
; CHECK-DAG: [[zero:%[0-9]+]] = OpConstant [[int]] 0
; CHECK-DAG: [[v4_int:%[0-9]+]] = OpTypeVector [[int]] 4
; CHECK-DAG: [[v2_int:%[0-9]+]] = OpTypeVector [[int]] 2
; CHECK-DAG: [[RWBufferTypeInt:%[0-9]+]] = OpTypeImage [[int]] Buffer 2 0 0 2 R32i {{$}}
; CHECK-DAG: [[IntBufferPtrType:%[0-9]+]] = OpTypePointer UniformConstant [[RWBufferTypeInt]]
; CHECK-DAG: [[IntBufferVar]] = OpVariable [[IntBufferPtrType]] UniformConstant

; CHECK: {{%[0-9]+}} = OpFunction {{%[0-9]+}} DontInline {{%[0-9]+}}
; CHECK-NEXT: OpLabel
define void @RWBufferLoad_Vec4_I32() #0 {
; CHECK: [[buffer:%[0-9]+]] = OpLoad [[RWBufferTypeInt]] [[IntBufferVar]]
%buffer0 = call target("spirv.Image", i32, 5, 2, 0, 0, 2, 24)
@llvm.spv.handle.fromBinding.tspirv.Image_i32_5_2_0_0_2_24(
i32 16, i32 7, i32 1, i32 0, i1 false)

; CHECK: OpImageRead [[v4_int]] [[buffer]] [[zero]]
%data0 = call <4 x i32> @llvm.spv.typedBufferLoad(
target("spirv.Image", i32, 5, 2, 0, 0, 2, 24) %buffer0, i32 0)

ret void
}

; CHECK: {{%[0-9]+}} = OpFunction {{%[0-9]+}} DontInline {{%[0-9]+}}
; CHECK-NEXT: OpLabel
define void @RWBufferLoad_I32() #0 {
; CHECK: [[buffer:%[0-9]+]] = OpLoad [[RWBufferTypeInt]] [[IntBufferVar]]
%buffer1 = call target("spirv.Image", i32, 5, 2, 0, 0, 2, 24)
@llvm.spv.handle.fromBinding.tspirv.Image_i32_5_2_0_0_2_24(
i32 16, i32 7, i32 1, i32 0, i1 false)

; CHECK: [[V:%[0-9]+]] = OpImageRead [[v4_int]] [[buffer]] [[zero]]
; CHECK: OpCompositeExtract [[int]] [[V]] 0
%data1 = call i32 @llvm.spv.typedBufferLoad(
target("spirv.Image", i32, 5, 2, 0, 0, 2, 24) %buffer1, i32 0)

ret void
}

; CHECK: {{%[0-9]+}} = OpFunction {{%[0-9]+}} DontInline {{%[0-9]+}}
; CHECK-NEXT: OpLabel
define void @RWBufferLoad_Vec2_I32() #0 {
; CHECK: [[buffer:%[0-9]+]] = OpLoad [[RWBufferTypeInt]] [[IntBufferVar]]
%buffer0 = call target("spirv.Image", i32, 5, 2, 0, 0, 2, 24)
@llvm.spv.handle.fromBinding.tspirv.Image_i32_5_2_0_0_2_24(
i32 16, i32 7, i32 1, i32 0, i1 false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we have a similar test, but descriptor# and binding# are changed between the 2 handles to make sure we generate 2 resources with 2 binding numbers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is really a test for handle.fromBinding. I believe I have that covered in existing testing:

; CHECK-DAG: OpDecorate [[IntBufferVar:%[0-9]+]] DescriptorSet 16
; CHECK-DAG: OpDecorate [[IntBufferVar]] Binding 7
; CHECK-DAG: OpDecorate [[FloatBufferVar:%[0-9]+]] DescriptorSet 16
; CHECK-DAG: OpDecorate [[FloatBufferVar]] Binding 7

and

; CHECK-DAG: OpDecorate [[Var:%[0-9]+]] DescriptorSet 3
; CHECK-DAG: OpDecorate [[Var]] Binding 4

Is that enough, or do you need more?


; CHECK: [[V:%[0-9]+]] = OpImageRead [[v4_int]] [[buffer]] [[zero]]
; CHECK: [[e0:%[0-9]+]] = OpCompositeExtract [[int]] [[V]] 0
; CHECK: [[e1:%[0-9]+]] = OpCompositeExtract [[int]] [[V]] 1
; CHECK: OpCompositeConstruct [[v2_int]] [[e0]] [[e1]]
%data0 = call <2 x i32> @llvm.spv.typedBufferLoad(
target("spirv.Image", i32, 5, 2, 0, 0, 2, 24) %buffer0, i32 0)

ret void
}

attributes #0 = { convergent noinline norecurse "frame-pointer"="all" "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" "no-trapping-math"="true" "stack-protector-buffer-size"="8" }
30 changes: 30 additions & 0 deletions llvm/test/CodeGen/SPIRV/hlsl-resources/UnknownBufferLoad.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv1.6-vulkan1.3-library %s -o - | FileCheck %s
; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val %}

; CHECK: OpCapability StorageImageReadWithoutFormat
; CHECK-DAG: OpDecorate [[IntBufferVar:%[0-9]+]] DescriptorSet 16
; CHECK-DAG: OpDecorate [[IntBufferVar]] Binding 7

; CHECK-DAG: [[int:%[0-9]+]] = OpTypeInt 32 0
; CHECK-DAG: [[zero:%[0-9]+]] = OpConstant [[int]] 0
; CHECK-DAG: [[v4_int:%[0-9]+]] = OpTypeVector [[int]] 4
; CHECK-DAG: [[RWBufferTypeInt:%[0-9]+]] = OpTypeImage [[int]] Buffer 2 0 0 2 Unknown {{$}}
; CHECK-DAG: [[IntBufferPtrType:%[0-9]+]] = OpTypePointer UniformConstant [[RWBufferTypeInt]]
; CHECK-DAG: [[IntBufferVar]] = OpVariable [[IntBufferPtrType]] UniformConstant

; CHECK: {{%[0-9]+}} = OpFunction {{%[0-9]+}} DontInline {{%[0-9]+}}
; CHECK-NEXT: OpLabel
define void @RWBufferLoad_Vec4_I32() #0 {
; CHECK: [[buffer:%[0-9]+]] = OpLoad [[RWBufferTypeInt]] [[IntBufferVar]]
%buffer0 = call target("spirv.Image", i32, 5, 2, 0, 0, 2, 0)
@llvm.spv.handle.fromBinding.tspirv.Image_f32_5_2_0_0_2_0(
i32 16, i32 7, i32 1, i32 0, i1 false)

; CHECK: OpImageRead [[v4_int]] [[buffer]] [[zero]]
%data0 = call <4 x i32> @llvm.spv.typedBufferLoad(
target("spirv.Image", i32, 5, 2, 0, 0, 2, 0) %buffer0, i32 0)

ret void
}

attributes #0 = { convergent noinline norecurse "frame-pointer"="all" "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" "no-trapping-math"="true" "stack-protector-buffer-size"="8" }
2 changes: 2 additions & 0 deletions llvm/test/CodeGen/SPIRV/read_image.ll
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
; RUN: llc -O0 -mtriple=spirv64-unknown-unknown %s -o - | FileCheck %s --check-prefix=CHECK-SPIRV

; CHECK-SPIRV: OpCapability StorageImageReadWithoutFormat

; CHECK-SPIRV: %[[#IntTy:]] = OpTypeInt
; CHECK-SPIRV: %[[#IVecTy:]] = OpTypeVector %[[#IntTy]]
; CHECK-SPIRV: %[[#FloatTy:]] = OpTypeFloat
Expand Down
1 change: 1 addition & 0 deletions llvm/test/CodeGen/SPIRV/transcoding/OpImageReadMS.ll
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
; RUN: llc -O0 -mtriple=spirv32-unknown-unknown %s -o - | FileCheck %s --check-prefix=CHECK-SPIRV

; CHECK-SPIRV: OpCapability StorageImageReadWithoutFormat
; CHECK-SPIRV: %[[#]] = OpImageRead %[[#]] %[[#]] %[[#]] Sample %[[#]]

define spir_kernel void @sample_test(target("spirv.Image", void, 1, 0, 0, 1, 0, 0, 0) %source, i32 %sampler, <4 x float> addrspace(1)* nocapture %results) {
Expand Down
Loading