Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1050151
Add PackedSimd load/store mappings to table
adamperlin Jul 14, 2026
45f618e
Fix OperIsMemory{Load,Store} for PackedSimd operations
adamperlin Jul 15, 2026
7fd077a
Implement SIMD element-wise load/store codegen
adamperlin Jul 15, 2026
00c8430
Merge branch 'main' into adamperlin/wasm-simd-loads-stores
adamperlin Jul 15, 2026
f9504c5
Properly lower PackedSimd.Shuffle with non-constant operands
adamperlin Jul 17, 2026
8d07863
Enable null checks for PackedSimd loads/stores
adamperlin Jul 17, 2026
0df646a
Apply suggestions from code review
adamperlin Jul 17, 2026
6ddbd32
Add PackedSimdTests to exercise null check for loads/stores
adamperlin Jul 17, 2026
b9e3468
Merge branch 'main' into adamperlin/wasm-simd-loads-stores
adamperlin Jul 17, 2026
f1f45ce
jit-format
adamperlin Jul 17, 2026
842bb15
Merge branch 'main' into adamperlin/wasm-basic-shuffle-codegen
adamperlin Jul 17, 2026
d53c1cc
Add header comment
adamperlin Jul 17, 2026
7fd3953
Merge branch 'main' into adamperlin/wasm-simd-loads-stores
adamperlin Jul 17, 2026
e78535b
Fix segfault in release: move side effecting statement out of assert()
adamperlin Jul 18, 2026
bdbba8c
jit-format
adamperlin Jul 20, 2026
6709418
Merge branch 'main' into adamperlin/wasm-simd-loads-stores
adamperlin Jul 20, 2026
27bf104
Apply suggestions from code review
adamperlin Jul 20, 2026
66691cd
Merge branch 'adamperlin/wasm-simd-loads-stores' into adamperlin/wasm…
adamperlin Jul 20, 2026
246ed1e
Merge branch 'main' into adamperlin/wasm-basic-shuffle-codegen
adamperlin Jul 21, 2026
726aa2b
Fix immediate operand position unset bug for shuffle
adamperlin Jul 21, 2026
067c299
More review feedback
adamperlin Jul 21, 2026
3b00dc3
More review feedback: handle shuffle masks that aren't in bounds
adamperlin Jul 22, 2026
437223f
Merge remote-tracking branch 'upstream/main' into adamperlin/wasm-bas…
adamperlin Jul 22, 2026
d925633
jit-format
adamperlin Jul 22, 2026
4ed524f
Revert accidental diff
adamperlin Jul 22, 2026
2656ed4
Fix bad stack ordering in shuffle fallback codegen
adamperlin Jul 22, 2026
506a771
Apply suggestions from code review
adamperlin Jul 22, 2026
a546d72
Separate out fetching PackedSimd.Shuffle immediate from GetImmOp() fo…
adamperlin Jul 23, 2026
d684659
Merge branch 'main' into adamperlin/wasm-basic-shuffle-codegen
adamperlin Jul 24, 2026
5fb29db
Emit range check around PackedSimd.Shuffle mask
adamperlin Jul 24, 2026
1ccc347
jit-format
adamperlin Jul 24, 2026
90adaee
Adjust comments
adamperlin Jul 24, 2026
20ce696
Reset state of PackedSimdTests.cs
adamperlin Jul 25, 2026
b03f851
Cleanup and copilot feedback
adamperlin Jul 25, 2026
48ec254
Fix comment
adamperlin Jul 25, 2026
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
5 changes: 5 additions & 0 deletions src/coreclr/jit/codegen.h
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ class CodeGen final : public CodeGenInterface
void genEmitBeginBlock(WasmValueType blockType = WasmValueType::Invalid);
void genEmitEndBlock();
void genEmitFunctionEnd(bool emitTerminalUnreachable = true);
#ifdef FEATURE_SIMD
void genEmitSimdRangeCheck(GenTreeBoundsChk* boundsCheck);
#endif

#endif
Comment thread
adamperlin marked this conversation as resolved.

void genEmitStartBlock(BasicBlock* block);
Expand Down Expand Up @@ -1089,6 +1093,7 @@ class CodeGen final : public CodeGenInterface

#if defined(TARGET_WASM)
void genHWIntrinsicJumpTableFallback(GenTreeHWIntrinsic* node, HWIntrinsic info);
void genHWIntrinsicShuffleRngCheck(GenTreeHWIntrinsic* node, HWIntrinsic info);
#endif
Comment thread
adamperlin marked this conversation as resolved.

#endif // FEATURE_HW_INTRINSICS
Expand Down
14 changes: 13 additions & 1 deletion src/coreclr/jit/codegenwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2272,7 +2272,19 @@ void CodeGen::genRangeCheck(GenTree* tree)
assert(tree->OperIs(GT_BOUNDS_CHECK));
GenTreeBoundsChk* boundsCheck = tree->AsBoundsChk();
genConsumeOperands(boundsCheck);
GetEmitter()->emitIns(INS_I_ge_u);
#ifdef FEATURE_SIMD
if (varTypeIsSIMD(boundsCheck->GetIndex()->TypeGet()))
{
GetEmitter()->emitIns(INS_i8x16_splat);
GetEmitter()->emitIns(INS_i8x16_ge_u);
GetEmitter()->emitIns(INS_v128_any_true);
}
else
#endif
{
GetEmitter()->emitIns(INS_I_ge_u);
}
Comment thread
adamperlin marked this conversation as resolved.

genJumpToThrowHlpBlk(boundsCheck->gtThrowKind);
}

Expand Down
11 changes: 7 additions & 4 deletions src/coreclr/jit/hwintrinsiccodegenwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
//
void CodeGen::genHWIntrinsic(GenTreeHWIntrinsic* node)
{
// emitIns_Lane
// emitIns_Memarg_Lane

const HWIntrinsic info(node);
genConsumeMultiOpOperands(node);

Expand All @@ -40,7 +37,13 @@ void CodeGen::genHWIntrinsic(GenTreeHWIntrinsic* node)
{
case HW_Category_SIMD:
{
if ((info.id == NI_PackedSimd_Swizzle) && node->Op(2)->isContained())
if (info.id == NI_PackedSimd_Shuffle)
{
JITDUMP("Lowered to a native shuffle instruction, so the mask is encoded as an immediate");
assert(node->Op(3)->isContained());
GetEmitter()->emitIns_V128Imm(ins, node->Op(3)->AsVecCon()->gtSimdVal.u8);
}
Comment thread
adamperlin marked this conversation as resolved.
else if ((info.id == NI_PackedSimd_Swizzle) && node->Op(2)->isContained())
{
// A constant, fully in-range mask was lowered to an immediate i8x16.shuffle.
// prior codegen left the source on the value stack once (the mask
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/lower.h
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ class Lowering final : public Phase
#elif defined(TARGET_WASM)
GenTree* LowerHWIntrinsicCompareUnsignedLong(GenTreeHWIntrinsic* node);
GenTree* LowerHWIntrinsicWithImm(GenTreeHWIntrinsic* node);
GenTree* LowerHWIntrinsicNativeShuffle(GenTreeHWIntrinsic* node);
void LowerHWIntrinsicSwizzle(GenTreeHWIntrinsic* node);
#endif // !TARGET_XARCH && !TARGET_ARM64
GenTree* InsertNewSimdCreateScalarUnsafeNode(var_types type,
Expand Down
176 changes: 176 additions & 0 deletions src/coreclr/jit/lowerwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,11 @@ GenTree* Lowering::LowerHWIntrinsic(GenTreeHWIntrinsic* node)
return LowerHWIntrinsicWithImm(node);
}

case NI_PackedSimd_Shuffle:
{
return LowerHWIntrinsicNativeShuffle(node);
}

case NI_PackedSimd_LoadScalarAndSplatVector128:
case NI_PackedSimd_LoadScalarVector128:
case NI_PackedSimd_LoadWideningVector128:
Expand Down Expand Up @@ -962,6 +967,8 @@ GenTree* Lowering::LowerHWIntrinsic(GenTreeHWIntrinsic* node)
GenTree* Lowering::LowerHWIntrinsicWithImm(GenTreeHWIntrinsic* node)
{
GenTree* immOp = node->GetImmOp();
assert(varTypeIsIntegral(immOp->TypeGet()) && "Immediate operand must be an integral type");

if (!immOp->IsCnsIntOrI())
{
// This node has a non-constant immediate operand, so it will need a jump table
Expand Down Expand Up @@ -1316,6 +1323,168 @@ GenTree* Lowering::LowerHWIntrinsicCreate(GenTreeHWIntrinsic* node)
return LowerNode(node);
}

// --------------------------------------------------------------------------------
// LowerHWIntrinsicNativeShuffle: Lowers a PackedSimd Shuffle call with a possibly non-constant mask
//
// Arguments:
// node - The hardware intrinsic node.
//
// Notes:
// If the shuffle mask is a constant vector, it can be contained as an immediate and emitted. Otherwise,
// the shuffle is rewritten into two swizzles for the upper and lower input vectors and combined into the final result.
GenTree* Lowering::LowerHWIntrinsicNativeShuffle(GenTreeHWIntrinsic* node)
{
assert(node->GetHWIntrinsicId() == NI_PackedSimd_Shuffle);

GenTree* op1 = node->Op(1);
GenTree* shuffleMask = node->Op(3);
var_types resultType = node->TypeGet();
Comment thread
adamperlin marked this conversation as resolved.

// No extra work to do if the shuffle is an in-range constant vector, it can be contained as an immediate and
// emitted.
if (shuffleMask->IsCnsVec())
{
const simd_t& mask = shuffleMask->AsVecCon()->gtSimdVal;
bool allInRange = true;
for (int i = 0; i < 16; i++)
{
if (mask.u8[i] >= 32)
{
allInRange = false;
break;
}
}
if (allInRange)
Comment thread
adamperlin marked this conversation as resolved.
{
ContainCheckHWIntrinsic(node);
return node->gtNext;
}
}

// If the shuffle mask is not a constant vector or the mask is not in range, we will need to rewrite the shuffle
Comment thread
adamperlin marked this conversation as resolved.
// into a BOUNDS_CHECK around the mask, and two swizzles: 1 to handle elements from the first vector, and 1 to
// handle elements from the second vector. The two swizzles will then be or'd together to produce the final result.
// We will be constructing IR like the following:
// op1 = ...
// op2 = ...
// /--* op2
// /--* LCL_VAR op2Tmp
// STORE_LCL_VAR op2Tmp
// originalMask = ...
// /--* originalMask
// /--* LCL_VAR originalMaskTmp
// STORE_LCL_VAR originalMaskTmp
// m0 = * LCL_VAR originalMaskTmp
// b0 = * CNS_INT int 31
// /--* m0 simd
// +--* b0 int
// GT_BOUNDS_CHECK RNG_CHK_FAIL
Comment thread
adamperlin marked this conversation as resolved.
// m1 = * LCL_VAR originalMaskTmp
// /--* op1 simd
// +--* m1 simd
// tmp1 = * HWINTRINSIC simd byte PackedSimd.Swizzle
// op2Reload = * LCL_VAR op2tmp
// m2 = * LCL_VAR originalMaskTmp
// upperBnd = * CNS_VEC simd byte <0x10, 0x10, ...>
// /--* m2 simd
// +--* upperBnd simd
// upperMask = * HWINTRINSIC simd byte PackedSimd.Subtract
// /--* op2Reload simd
// +--* upperMask simd
// tmp3 = * HWINTRINSIC simd byte PackedSimd.Swizzle
// /--* tmp1 simd
// +--* tmp3 simd
// res = * HWINTRINSIC simd byte PackedSimd.Or
//
// This is roughly equivalent to the following C#:
// ...
// if (GreaterThanAny(originalMask, 31)) { throw new ArrayIndexOutOfBoundsException(); }
// tmp1 = PackedSimd.Swizzle(op1, originalMask);
// tmp2 = PackedSimd.Swizzle(op2, PackedSimd.Subtract(originalMask, PackedSimd.Splat(0x10)));
// result = PackedSimd.Or(tmp1, tmp2);
//...

// op2 needs to be moved, replace with a local, but don't immediately reload
LIR::Use op2Use(BlockRange(), &node->Op(2), node);
op2Use.ReplaceWithLclVar(m_compiler);
GenTree* op2Reload = node->Op(2);
BlockRange().Remove(op2Reload);

// Shuffle mask will be used several times, replace with a local
LIR::Use shuffleMaskUse(BlockRange(), &node->Op(3), node);
unsigned int shuffleMaskTmp = shuffleMaskUse.ReplaceWithLclVar(m_compiler);
GenTree* maskReloadForChk = node->Op(3);

// Insert a bounds check for the shuffle mask. A bounds check against a simd value will be handled by codegen as a
// GreaterThanAny(vec, bound) type operation.
GenTree* bound = m_compiler->gtNewIconNode(32);
BlockRange().InsertBefore(node, bound);
LowerNode(bound);

GenTree* boundsCheck = new (m_compiler, GT_BOUNDS_CHECK) GenTreeBoundsChk(maskReloadForChk, bound, SCK_RNGCHK_FAIL);
Comment thread
adamperlin marked this conversation as resolved.
Outdated
BlockRange().InsertBefore(node, boundsCheck);
LowerNode(boundsCheck);

// Do a swizzle of the first vector with the original shuffle mask (now loaded from a local), which will produce a
// vector with the elements from the first vector in the correct order, and zero's for all elements which correspond
// to the second vector.
GenTree* maskReload1 = m_compiler->gtNewLclVarNode(shuffleMaskTmp, shuffleMask->TypeGet());
BlockRange().InsertBefore(node, maskReload1);
LowerNode(maskReload1);

GenTree* swizzle1 =
m_compiler->gtNewSimdHWIntrinsicNode(resultType, op1, maskReload1, NI_PackedSimd_Swizzle, TYP_BYTE, 16);
BlockRange().InsertBefore(node, swizzle1);
LowerNode(swizzle1);

// Re-load op2
BlockRange().InsertBefore(node, op2Reload);
LowerNode(op2Reload);

// Re-load the original shuffle mask
GenTreeLclVar* maskReload2 = m_compiler->gtNewLclVarNode(shuffleMaskTmp, shuffleMask->TypeGet());
BlockRange().InsertBefore(node, maskReload2);
LowerNode(maskReload2);

// Create constant upper bound vector of <16, 16, ..., 16>
GenTreeVecCon* upperBound = m_compiler->gtNewVconNode(shuffleMask->TypeGet());
upperBound->EvaluateBroadcastInPlace(TYP_BYTE, static_cast<int64_t>(16));
BlockRange().InsertBefore(node, upperBound);
LowerNode(upperBound);

// Use the above to subtract 16 from each mask element to mark each element which corresponds to the lower vector as
// unused, leading to a zero in the result of the swizzle.
GenTree* upperMask = m_compiler->gtNewSimdHWIntrinsicNode(shuffleMask->TypeGet(), maskReload2, upperBound,
NI_PackedSimd_Subtract, TYP_BYTE, 16);
BlockRange().InsertBefore(node, upperMask);
LowerNode(upperMask);

GenTree* swizzle2 =
m_compiler->gtNewSimdHWIntrinsicNode(resultType, op2Reload, upperMask, NI_PackedSimd_Swizzle, TYP_BYTE, 16);
BlockRange().InsertBefore(node, swizzle2);
LowerNode(swizzle2);

// Since we've left zero's for all the elements which correspond to the upper vector, we can just or the two
// swizzles together to get the final result.
GenTreeHWIntrinsic* result =
m_compiler->gtNewSimdHWIntrinsicNode(resultType, swizzle1, swizzle2, NI_PackedSimd_Or, TYP_BYTE, 16);
BlockRange().InsertBefore(node, result);

LIR::Use use;
if (BlockRange().TryGetUse(node, &use))
{
use.ReplaceWith(result);
}
else
{
result->SetUnusedValue();
}

BlockRange().Remove(node);

return LowerNode(result);
}

//----------------------------------------------------------------------------------------------
// ContainCheckHWIntrinsic: Perform containment analysis for a hardware intrinsic node.
//
Expand All @@ -1333,4 +1502,11 @@ void Lowering::ContainCheckHWIntrinsic(GenTreeHWIntrinsic* node)
MakeSrcContained(node, immOp);
}
}
else if (intrinsicId == NI_PackedSimd_Shuffle && node->Op(3)->IsCnsVec())
{
// Shuffle is a special case where the mask is a constant vector immediate
// which must be contained. If the mask is non-constant we will re-write the shuffle into a fallback equivalent
// operation. (see LowerHWIntrinsicNativeShuffle).
MakeSrcContained(node, node->Op(3));
}
}
30 changes: 30 additions & 0 deletions src/coreclr/jit/stacklevelsetter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,40 @@ void StackLevelSetter::SetThrowHelperBlocks(GenTree* node, BasicBlock* block)
case GT_HWINTRINSIC:
{
HWIntrinsicCategory category = HWIntrinsicInfo::lookupCategory(node->AsHWIntrinsic()->GetHWIntrinsicId());
NamedIntrinsic intrinsicId = node->AsHWIntrinsic()->GetHWIntrinsicId();
if (category == HW_Category_MemoryLoad || category == HW_Category_MemoryStore)
{
SetThrowHelperBlock(SCK_NULL_CHECK, block);
}
else if (intrinsicId == NI_PackedSimd_Shuffle)
{
GenTreeHWIntrinsic* hwIntrinsic = node->AsHWIntrinsic();
if (hwIntrinsic->Op(3)->IsCnsVec())
{
Comment thread
adamperlin marked this conversation as resolved.
Outdated
// If we have an out of range constant shuffle mask, we will need to throw a range check exception.
const simd_t& mask = hwIntrinsic->Op(3)->AsVecCon()->gtSimdVal;
bool allInRange = true;

for (int i = 0; i < 16; i++)
{
if (mask.u8[i] >= 32)
{
allInRange = false;
break;
}
}

if (!allInRange)
{
SetThrowHelperBlock(SCK_RNGCHK_FAIL, block);
}
}
else
{
// Non-constant shuffle mask needs a range check
SetThrowHelperBlock(SCK_RNGCHK_FAIL, block);
}
}
}
break;

Expand Down
40 changes: 28 additions & 12 deletions src/tests/JIT/HardwareIntrinsics/Wasm/PackedSimd/PackedSimdTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ public sealed class PackedSimdTests
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(PackedSimd))]
public static unsafe void PackedSimdIsSupported()
{
MethodInfo? methodInfo = typeof(PackedSimd).GetProperty(nameof(PackedSimd.IsSupported))?.GetGetMethod();
Assert.NotNull(methodInfo);
Assert.Equal(PackedSimd.IsSupported, methodInfo.Invoke(null, null));
// MethodInfo? methodInfo = typeof(PackedSimd).GetProperty(nameof(PackedSimd.IsSupported))?.GetGetMethod();
// Assert.NotNull(methodInfo);
// Assert.Equal(PackedSimd.IsSupported, methodInfo.Invoke(null, null));
Comment thread
adamperlin marked this conversation as resolved.
Outdated
Assert.Equal(PackedSimd.IsSupported, Vector128.IsHardwareAccelerated);
Assert.True(PackedSimd.IsSupported);
}
Expand Down Expand Up @@ -1074,15 +1074,15 @@ public static unsafe void LoadScalarAndSplatTest()
Assert.Equal(Vector128.Create(3.14f, 3.14f, 3.14f, 3.14f), floatSplat);
}

[Fact]
public static unsafe void LoadStoreNullCheckTest()
{
Assert.Throws<NullReferenceException>(() => LoadScalarAndSplatVector128(null));
Assert.Throws<NullReferenceException>(() => LoadScalarVector128(null));
Assert.Throws<NullReferenceException>(() => LoadWideningVector128(null));
Assert.Throws<NullReferenceException>(() => LoadScalarAndInsert(null, 2));
Assert.Throws<NullReferenceException>(() => StoreSelectedScalar(null, 2));
}
// [Fact]
// public static unsafe void LoadStoreNullCheckTest()
// {
// Assert.Throws<NullReferenceException>(() => LoadScalarAndSplatVector128(null));
// Assert.Throws<NullReferenceException>(() => LoadScalarVector128(null));
// Assert.Throws<NullReferenceException>(() => LoadWideningVector128(null));
// Assert.Throws<NullReferenceException>(() => LoadScalarAndInsert(null, 2));
// Assert.Throws<NullReferenceException>(() => StoreSelectedScalar(null, 2));
// }

[MethodImpl(MethodImplOptions.NoInlining)]
private static unsafe Vector128<int> LoadScalarAndSplatVector128(int* address)
Expand Down Expand Up @@ -1560,4 +1560,20 @@ public static unsafe void BitmaskTest()
Assert.Equal(v128emsb_s, (uint)bitmask_s);
Assert.Equal(v128emsb_i, (uint)bitmask_i);
}

[Fact]
public static void ShuffleTest()
{
var v1 = Vector128.Create((byte)1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16);
var v2 = Vector128.Create((byte)17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32);

var shuffled = PackedSimd.Shuffle(v1.AsByte(), v2.AsByte(),
Vector128.Create((byte)0, 17, 2, 19, 4, 21, 6, 23, 8, 25, 10, 27, 12, 29, 14, 31));

var expect = Vector128.Create((byte)1, 18, 3, 20, 5, 22, 7, 24,
9, 26, 11, 28, 13, 30, 15, 32);
Assert.Equal(expect, shuffled);
}
}
Comment thread
adamperlin marked this conversation as resolved.
Outdated
Loading