Skip to content

Commit b8a814e

Browse files
authored
[HLSL] Add support for user semantics (#153424)
This commit adds support for HLSL input semantics. User semantics are all semantics not starting with `SV_`. Those semantics ends up with a Location assignment in SPIR-V. Note: user semantics means Location, but the opposite is not true. Depending on the stage, some system semantics can rely on a Location index. This is not implemented in this PR.
1 parent 831a8b5 commit b8a814e

File tree

14 files changed

+360
-21
lines changed

14 files changed

+360
-21
lines changed

clang/include/clang/Basic/Attr.td

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5017,6 +5017,10 @@ def HLSLUnparsedSemantic : HLSLAnnotationAttr {
50175017
let Documentation = [InternalOnly];
50185018
}
50195019

5020+
def HLSLUserSemantic : HLSLSemanticAttr</* Indexable= */ 1> {
5021+
let Documentation = [InternalOnly];
5022+
}
5023+
50205024
def HLSLSV_Position : HLSLSemanticAttr</* Indexable= */ 1> {
50215025
let Documentation = [HLSLSV_PositionDocs];
50225026
}

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13184,6 +13184,7 @@ def err_hlsl_semantic_indexing_not_supported
1318413184
: Error<"semantic %0 does not allow indexing">;
1318513185
def err_hlsl_init_priority_unsupported : Error<
1318613186
"initializer priorities are not supported in HLSL">;
13187+
def err_hlsl_semantic_index_overlap : Error<"semantic index overlap %0">;
1318713188

1318813189
def warn_hlsl_user_defined_type_missing_member: Warning<"binding type '%select{t|u|b|s|c}0' only applies to types containing %select{SRV resources|UAV resources|constant buffer resources|sampler state|numeric types}0">, InGroup<LegacyConstantRegisterBinding>;
1318913190
def err_hlsl_binding_type_mismatch: Error<"binding type '%select{t|u|b|s|c}0' only applies to %select{SRV resources|UAV resources|constant buffer resources|sampler state|numeric variables in the global scope}0">;

clang/include/clang/Sema/SemaHLSL.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
#include "clang/Basic/DiagnosticSema.h"
2121
#include "clang/Basic/SourceLocation.h"
2222
#include "clang/Sema/SemaBase.h"
23+
#include "llvm/ADT/DenseMap.h"
2324
#include "llvm/ADT/SmallVector.h"
25+
#include "llvm/ADT/StringSet.h"
2426
#include "llvm/TargetParser/Triple.h"
2527
#include <initializer_list>
2628

@@ -259,9 +261,11 @@ class SemaHLSL : public SemaBase {
259261
HLSLSemanticAttr *createSemantic(const SemanticInfo &Semantic,
260262
DeclaratorDecl *TargetDecl);
261263
bool determineActiveSemanticOnScalar(FunctionDecl *FD, DeclaratorDecl *D,
262-
SemanticInfo &ActiveSemantic);
264+
SemanticInfo &ActiveSemantic,
265+
llvm::StringSet<> &ActiveInputSemantics);
263266
bool determineActiveSemantic(FunctionDecl *FD, DeclaratorDecl *D,
264-
SemanticInfo &ActiveSemantic);
267+
SemanticInfo &ActiveSemantic,
268+
llvm::StringSet<> &ActiveInputSemantics);
265269

266270
void processExplicitBindingsOnDecl(VarDecl *D);
267271

clang/lib/CodeGen/CGHLSLRuntime.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,16 @@ static void addSPIRVBuiltinDecoration(llvm::GlobalVariable *GV,
549549
GV->addMetadata("spirv.Decorations", *Decoration);
550550
}
551551

552+
static void addLocationDecoration(llvm::GlobalVariable *GV, unsigned Location) {
553+
LLVMContext &Ctx = GV->getContext();
554+
IRBuilder<> B(GV->getContext());
555+
MDNode *Operands =
556+
MDNode::get(Ctx, {ConstantAsMetadata::get(B.getInt32(/* Location */ 30)),
557+
ConstantAsMetadata::get(B.getInt32(Location))});
558+
MDNode *Decoration = MDNode::get(Ctx, {Operands});
559+
GV->addMetadata("spirv.Decorations", *Decoration);
560+
}
561+
552562
static llvm::Value *createSPIRVBuiltinLoad(IRBuilder<> &B, llvm::Module &M,
553563
llvm::Type *Ty, const Twine &Name,
554564
unsigned BuiltInID) {
@@ -562,6 +572,69 @@ static llvm::Value *createSPIRVBuiltinLoad(IRBuilder<> &B, llvm::Module &M,
562572
return B.CreateLoad(Ty, GV);
563573
}
564574

575+
static llvm::Value *createSPIRVLocationLoad(IRBuilder<> &B, llvm::Module &M,
576+
llvm::Type *Ty, unsigned Location,
577+
StringRef Name) {
578+
auto *GV = new llvm::GlobalVariable(
579+
M, Ty, /* isConstant= */ true, llvm::GlobalValue::ExternalLinkage,
580+
/* Initializer= */ nullptr, /* Name= */ Name, /* insertBefore= */ nullptr,
581+
llvm::GlobalVariable::GeneralDynamicTLSModel,
582+
/* AddressSpace */ 7, /* isExternallyInitialized= */ true);
583+
GV->setVisibility(llvm::GlobalValue::HiddenVisibility);
584+
addLocationDecoration(GV, Location);
585+
return B.CreateLoad(Ty, GV);
586+
}
587+
588+
llvm::Value *
589+
CGHLSLRuntime::emitSPIRVUserSemanticLoad(llvm::IRBuilder<> &B, llvm::Type *Type,
590+
HLSLSemanticAttr *Semantic,
591+
std::optional<unsigned> Index) {
592+
Twine BaseName = Twine(Semantic->getAttrName()->getName());
593+
Twine VariableName = BaseName.concat(Twine(Index.value_or(0)));
594+
595+
unsigned Location = SPIRVLastAssignedInputSemanticLocation;
596+
597+
// DXC completely ignores the semantic/index pair. Location are assigned from
598+
// the first semantic to the last.
599+
llvm::ArrayType *AT = dyn_cast<llvm::ArrayType>(Type);
600+
unsigned ElementCount = AT ? AT->getNumElements() : 1;
601+
SPIRVLastAssignedInputSemanticLocation += ElementCount;
602+
return createSPIRVLocationLoad(B, CGM.getModule(), Type, Location,
603+
VariableName.str());
604+
}
605+
606+
llvm::Value *
607+
CGHLSLRuntime::emitDXILUserSemanticLoad(llvm::IRBuilder<> &B, llvm::Type *Type,
608+
HLSLSemanticAttr *Semantic,
609+
std::optional<unsigned> Index) {
610+
Twine BaseName = Twine(Semantic->getAttrName()->getName());
611+
Twine VariableName = BaseName.concat(Twine(Index.value_or(0)));
612+
613+
// DXIL packing rules etc shall be handled here.
614+
// FIXME: generate proper sigpoint, index, col, row values.
615+
// FIXME: also DXIL loads vectors element by element.
616+
SmallVector<Value *> Args{B.getInt32(4), B.getInt32(0), B.getInt32(0),
617+
B.getInt8(0),
618+
llvm::PoisonValue::get(B.getInt32Ty())};
619+
620+
llvm::Intrinsic::ID IntrinsicID = llvm::Intrinsic::dx_load_input;
621+
llvm::Value *Value = B.CreateIntrinsic(/*ReturnType=*/Type, IntrinsicID, Args,
622+
nullptr, VariableName);
623+
return Value;
624+
}
625+
626+
llvm::Value *CGHLSLRuntime::emitUserSemanticLoad(
627+
IRBuilder<> &B, llvm::Type *Type, const clang::DeclaratorDecl *Decl,
628+
HLSLSemanticAttr *Semantic, std::optional<unsigned> Index) {
629+
if (CGM.getTarget().getTriple().isSPIRV())
630+
return emitSPIRVUserSemanticLoad(B, Type, Semantic, Index);
631+
632+
if (CGM.getTarget().getTriple().isDXIL())
633+
return emitDXILUserSemanticLoad(B, Type, Semantic, Index);
634+
635+
llvm_unreachable("Unsupported target for user-semantic load.");
636+
}
637+
565638
llvm::Value *CGHLSLRuntime::emitSystemSemanticLoad(
566639
IRBuilder<> &B, llvm::Type *Type, const clang::DeclaratorDecl *Decl,
567640
Attr *Semantic, std::optional<unsigned> Index) {
@@ -626,6 +699,9 @@ CGHLSLRuntime::handleScalarSemanticLoad(IRBuilder<> &B, const FunctionDecl *FD,
626699
std::optional<unsigned> Index = std::nullopt;
627700
if (Semantic->isSemanticIndexExplicit())
628701
Index = Semantic->getSemanticIndex();
702+
703+
if (isa<HLSLUserSemanticAttr>(Semantic))
704+
return emitUserSemanticLoad(B, Type, Decl, Semantic, Index);
629705
return emitSystemSemanticLoad(B, Type, Decl, Semantic, Index);
630706
}
631707

clang/lib/CodeGen/CGHLSLRuntime.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,25 @@ class CGHLSLRuntime {
200200
llvm::GlobalVariable *BufGV);
201201
void initializeBufferFromBinding(const HLSLBufferDecl *BufDecl,
202202
llvm::GlobalVariable *GV);
203+
void initializeBufferFromBinding(const HLSLBufferDecl *BufDecl,
204+
llvm::GlobalVariable *GV,
205+
HLSLResourceBindingAttr *RBA);
206+
207+
llvm::Value *emitSPIRVUserSemanticLoad(llvm::IRBuilder<> &B, llvm::Type *Type,
208+
HLSLSemanticAttr *Semantic,
209+
std::optional<unsigned> Index);
210+
llvm::Value *emitDXILUserSemanticLoad(llvm::IRBuilder<> &B, llvm::Type *Type,
211+
HLSLSemanticAttr *Semantic,
212+
std::optional<unsigned> Index);
213+
llvm::Value *emitUserSemanticLoad(llvm::IRBuilder<> &B, llvm::Type *Type,
214+
const clang::DeclaratorDecl *Decl,
215+
HLSLSemanticAttr *Semantic,
216+
std::optional<unsigned> Index);
217+
203218
llvm::Triple::ArchType getArch();
204219

205220
llvm::DenseMap<const clang::RecordType *, llvm::TargetExtType *> LayoutTypes;
221+
unsigned SPIRVLastAssignedInputSemanticLocation = 0;
206222
};
207223

208224
} // namespace CodeGen

clang/lib/Sema/SemaHLSL.cpp

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,10 @@ HLSLSemanticAttr *SemaHLSL::createSemantic(const SemanticInfo &Info,
775775
DeclaratorDecl *TargetDecl) {
776776
std::string SemanticName = Info.Semantic->getAttrName()->getName().upper();
777777

778+
if (dyn_cast<HLSLUserSemanticAttr>(Info.Semantic))
779+
return createSemanticAttr<HLSLUserSemanticAttr>(*Info.Semantic, TargetDecl,
780+
Info.Index);
781+
778782
if (SemanticName == "SV_DISPATCHTHREADID") {
779783
return createSemanticAttr<HLSLSV_DispatchThreadIDAttr>(
780784
*Info.Semantic, TargetDecl, Info.Index);
@@ -797,9 +801,10 @@ HLSLSemanticAttr *SemaHLSL::createSemantic(const SemanticInfo &Info,
797801
return nullptr;
798802
}
799803

800-
bool SemaHLSL::determineActiveSemanticOnScalar(FunctionDecl *FD,
801-
DeclaratorDecl *D,
802-
SemanticInfo &ActiveSemantic) {
804+
bool SemaHLSL::determineActiveSemanticOnScalar(
805+
FunctionDecl *FD, DeclaratorDecl *D, SemanticInfo &ActiveSemantic,
806+
llvm::StringSet<> &ActiveInputSemantics) {
807+
803808
if (ActiveSemantic.Semantic == nullptr) {
804809
ActiveSemantic.Semantic = D->getAttr<HLSLSemanticAttr>();
805810
if (ActiveSemantic.Semantic &&
@@ -818,11 +823,31 @@ bool SemaHLSL::determineActiveSemanticOnScalar(FunctionDecl *FD,
818823

819824
checkSemanticAnnotation(FD, D, A);
820825
FD->addAttr(A);
826+
827+
unsigned Location = ActiveSemantic.Index.value_or(0);
828+
829+
const ConstantArrayType *AT = dyn_cast<ConstantArrayType>(D->getType());
830+
unsigned ElementCount = AT ? AT->getZExtSize() : 1;
831+
ActiveSemantic.Index = Location + ElementCount;
832+
833+
Twine BaseName = Twine(ActiveSemantic.Semantic->getAttrName()->getName());
834+
for (unsigned I = 0; I < ElementCount; ++I) {
835+
Twine VariableName = BaseName.concat(Twine(Location + I));
836+
837+
auto [_, Inserted] = ActiveInputSemantics.insert(VariableName.str());
838+
if (!Inserted) {
839+
Diag(D->getLocation(), diag::err_hlsl_semantic_index_overlap)
840+
<< VariableName.str();
841+
return false;
842+
}
843+
}
844+
821845
return true;
822846
}
823847

824-
bool SemaHLSL::determineActiveSemantic(FunctionDecl *FD, DeclaratorDecl *D,
825-
SemanticInfo &ActiveSemantic) {
848+
bool SemaHLSL::determineActiveSemantic(
849+
FunctionDecl *FD, DeclaratorDecl *D, SemanticInfo &ActiveSemantic,
850+
llvm::StringSet<> &ActiveInputSemantics) {
826851
if (ActiveSemantic.Semantic == nullptr) {
827852
ActiveSemantic.Semantic = D->getAttr<HLSLSemanticAttr>();
828853
if (ActiveSemantic.Semantic &&
@@ -833,12 +858,13 @@ bool SemaHLSL::determineActiveSemantic(FunctionDecl *FD, DeclaratorDecl *D,
833858
const Type *T = D->getType()->getUnqualifiedDesugaredType();
834859
const RecordType *RT = dyn_cast<RecordType>(T);
835860
if (!RT)
836-
return determineActiveSemanticOnScalar(FD, D, ActiveSemantic);
861+
return determineActiveSemanticOnScalar(FD, D, ActiveSemantic,
862+
ActiveInputSemantics);
837863

838864
const RecordDecl *RD = RT->getDecl();
839865
for (FieldDecl *Field : RD->fields()) {
840866
SemanticInfo Info = ActiveSemantic;
841-
if (!determineActiveSemantic(FD, Field, Info)) {
867+
if (!determineActiveSemantic(FD, Field, Info, ActiveInputSemantics)) {
842868
Diag(Field->getLocation(), diag::note_hlsl_semantic_used_here) << Field;
843869
return false;
844870
}
@@ -911,12 +937,14 @@ void SemaHLSL::CheckEntryPoint(FunctionDecl *FD) {
911937
llvm_unreachable("Unhandled environment in triple");
912938
}
913939

940+
llvm::StringSet<> ActiveInputSemantics;
914941
for (ParmVarDecl *Param : FD->parameters()) {
915942
SemanticInfo ActiveSemantic;
916943
ActiveSemantic.Semantic = nullptr;
917944
ActiveSemantic.Index = std::nullopt;
918945

919-
if (!determineActiveSemantic(FD, Param, ActiveSemantic)) {
946+
if (!determineActiveSemantic(FD, Param, ActiveSemantic,
947+
ActiveInputSemantics)) {
920948
Diag(Param->getLocation(), diag::note_previous_decl) << Param;
921949
FD->setInvalidDecl();
922950
}
@@ -947,6 +975,8 @@ void SemaHLSL::checkSemanticAnnotation(FunctionDecl *EntryPoint,
947975
return;
948976
DiagnoseAttrStageMismatch(SemanticAttr, ST, {llvm::Triple::Pixel});
949977
break;
978+
case attr::HLSLUserSemantic:
979+
return;
950980
default:
951981
llvm_unreachable("Unknown SemanticAttr");
952982
}
@@ -1766,7 +1796,7 @@ void SemaHLSL::handleSemanticAttr(Decl *D, const ParsedAttr &AL) {
17661796
if (AL.getAttrName()->getName().starts_with_insensitive("SV_"))
17671797
diagnoseSystemSemanticAttr(D, AL, Index);
17681798
else
1769-
Diag(AL.getLoc(), diag::err_hlsl_unknown_semantic) << AL;
1799+
D->addAttr(createSemanticAttr<HLSLUserSemanticAttr>(AL, nullptr, Index));
17701800
}
17711801

17721802
void SemaHLSL::handlePackOffsetAttr(Decl *D, const ParsedAttr &AL) {

clang/test/CodeGenHLSL/semantics/DispatchThreadID.hlsl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,3 @@ void foo(uint Idx : SV_DispatchThreadID) {}
2424
[shader("compute")]
2525
[numthreads(8,8,1)]
2626
void bar(uint2 Idx : SV_DispatchThreadID) {}
27-
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// RUN: %clang_cc1 -triple spirv-unknown-vulkan-vertex -x hlsl -emit-llvm -finclude-default-header -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,CHECK-SPIRV -DTARGET=spv
2+
// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.3-vertex -x hlsl -emit-llvm -finclude-default-header -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,CHECK-DXIL -DTARGET=dx
3+
4+
// CHECK-SPIRV-DAG: @AAA0 = external hidden thread_local addrspace(7) externally_initialized constant float, !spirv.Decorations ![[#METADATA_0:]]
5+
// CHECK-SPIRV-DAG: @B0 = external hidden thread_local addrspace(7) externally_initialized constant i32, !spirv.Decorations ![[#METADATA_2:]]
6+
// CHECK-SPIRV-DAG: @CC0 = external hidden thread_local addrspace(7) externally_initialized constant <2 x float>, !spirv.Decorations ![[#METADATA_4:]]
7+
8+
9+
// FIXME: replace `float2 c` with a matrix when available.
10+
void main(float a : AAA, int b : B, float2 c : CC) {
11+
float tmp = a + b + c.x + c.y;
12+
}
13+
// CHECK-SPIRV: define internal spir_func void @_Z4mainfiDv2_f(float noundef nofpclass(nan inf) %a, i32 noundef %b, <2 x float> noundef nofpclass(nan inf) %c) #0 {
14+
15+
// CHECK: define void @main()
16+
17+
// CHECK-DXIL: %AAA0 = call float @llvm.dx.load.input.f32(i32 4, i32 0, i32 0, i8 0, i32 poison)
18+
// CHECK-DXIL: %B0 = call i32 @llvm.dx.load.input.i32(i32 4, i32 0, i32 0, i8 0, i32 poison)
19+
// CHECK-DXIL %CC0 = call <2 x float> @llvm.dx.load.input.v2f32(i32 4, i32 0, i32 0, i8 0, i32 poison)
20+
// CHECK-DXIL: call void @_Z4mainfiDv2_f(float %AAA0, i32 %B0, <2 x float> %CC0)
21+
22+
// CHECK-SPIRV: %[[#AAA0:]] = load float, ptr addrspace(7) @AAA0, align 4
23+
// CHECK-SPIRV: %[[#B0:]] = load i32, ptr addrspace(7) @B0, align 4
24+
// CHECK-SPIRV: %[[#CC0:]] = load <2 x float>, ptr addrspace(7) @CC0, align 8
25+
// CHECK-SPIRV: call spir_func void @_Z4mainfiDv2_f(float %[[#AAA0]], i32 %[[#B0]], <2 x float> %[[#CC0]]) [ "convergencectrl"(token %0) ]
26+
27+
28+
// CHECK-SPIRV-DAG: ![[#METADATA_0]] = !{![[#METADATA_1:]]}
29+
// CHECK-SPIRV-DAG: ![[#METADATA_2]] = !{![[#METADATA_3:]]}
30+
// CHECK-SPIRV-DAG: ![[#METADATA_4]] = !{![[#METADATA_5:]]}
31+
32+
// CHECK-SPIRV-DAG: ![[#METADATA_1]] = !{i32 30, i32 0}
33+
// CHECK-SPIRV-DAG: ![[#METADATA_3]] = !{i32 30, i32 1}
34+
// CHECK-SPIRV-DAG: ![[#METADATA_5]] = !{i32 30, i32 2}
35+
// | `- Location index
36+
// `-> Decoration "Location"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// RUN: %clang_cc1 -triple spirv-linux-vulkan-library -x hlsl -emit-llvm -finclude-default-header -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,CHECK-SPIRV -DTARGET=spv
2+
// RUN: %clang_cc1 -triple dxil-px-shadermodel6.3-library -x hlsl -emit-llvm -finclude-default-header -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,CHECK-DXIL -DTARGET=dx
3+
4+
struct S0 {
5+
float4 position[2];
6+
float4 color;
7+
};
8+
9+
// CHECK: %struct.S0 = type { [2 x <4 x float>], <4 x float> }
10+
11+
// CHECK-SPIRV: @A0 = external hidden thread_local addrspace(7) externally_initialized constant [2 x <4 x float>], !spirv.Decorations ![[#MD_0:]]
12+
// CHECK-SPIRV: @A2 = external hidden thread_local addrspace(7) externally_initialized constant <4 x float>, !spirv.Decorations ![[#MD_2:]]
13+
14+
// CHECK: define void @main0()
15+
// CHECK-DXIL: %A0 = call [2 x <4 x float>] @llvm.dx.load.input.a2v4f32(i32 4, i32 0, i32 0, i8 0, i32 poison)
16+
// CHECK-DXIL: %[[#TMP0:]] = insertvalue %struct.S0 poison, [2 x <4 x float>] %A0, 0
17+
// CHECK-DXIL: %A2 = call <4 x float> @llvm.dx.load.input.v4f32(i32 4, i32 0, i32 0, i8 0, i32 poison)
18+
// CHECK-DXIL: %[[#TMP1:]] = insertvalue %struct.S0 %[[#TMP0]], <4 x float> %A2, 1
19+
20+
// CHECK-SPIRV: %[[#A0:]] = load [2 x <4 x float>], ptr addrspace(7) @A0, align 16
21+
// CHECK-SPIRV: %[[#TMP0:]] = insertvalue %struct.S0 poison, [2 x <4 x float>] %[[#A0]], 0
22+
// CHECK-SPIRV: %[[#A2:]] = load <4 x float>, ptr addrspace(7) @A2, align 16
23+
// CHECK-SPIRV: %[[#TMP1:]] = insertvalue %struct.S0 %[[#TMP0]], <4 x float> %[[#A2]], 1
24+
25+
// CHECK: %[[#ARG:]] = alloca %struct.S0, align 16
26+
// CHECK: store %struct.S0 %[[#TMP1]], ptr %[[#ARG]], align 16
27+
// CHECK-DXIL: call void @{{.*}}main0{{.*}}(ptr %[[#ARG]])
28+
// CHECK-SPIRV: call spir_func void @{{.*}}main0{{.*}}(ptr %[[#ARG]])
29+
[shader("pixel")]
30+
void main0(S0 p : A) {
31+
float tmp = p.position[0] + p.position[1] + p.color;
32+
}
33+
34+
// CHECK-SPIRV: ![[#MD_0]] = !{![[#MD_1:]]}
35+
// CHECK-SPIRV: ![[#MD_1]] = !{i32 30, i32 0}
36+
// CHECK-SPIRV: ![[#MD_2]] = !{![[#MD_3:]]}
37+
// CHECK-SPIRV: ![[#MD_3]] = !{i32 30, i32 2}

0 commit comments

Comments
 (0)