Light up remaining SIMD intrinsics on WASM#130850
Conversation
Import them as the platform-neutral NI_Vector_GetElement/ToScalar/WithElement nodes so constant folding and value numbering apply, then rewrite to NI_PackedSimd_ExtractScalar/ReplaceScalar during lowering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
op_LeftShift/op_RightShift/op_UnsignedRightShift map directly to the PackedSimd ShiftLeft/ShiftRightArithmetic/ShiftRightLogical instructions, which take a scalar i32 amount. The intrinsic-id selection was already wired in GetHWIntrinsicIdForBinOp; this removes the unreachable import guards and the constant-folding NYI (the scalar shift amount is handled by the generic broadcast path). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CreateScalar already lowers to a zero vector plus ReplaceScalar via the shared LowerHWIntrinsicCreate path, so it just needed the import guard removed. CreateScalarUnsafe leaves the upper elements undefined, so it lowers directly to a single PackedSimd Splat rather than requiring a zero-fill. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mirror the xarch handling of AsVector128Unsafe/AsVector2/AsVector3 by making them real SpecialImport|SpecialCodeGen SIMD nodes rather than InvalidNodeId helpers. All SIMD widths occupy a full v128 on the wasm value stack, so the reinterpret is a pure no-op passthrough in codegen -- the same shape as a same-size GT_BITCAST. This unblocks the AsVector128(Vector2/Vector3) importer paths, which now rewrite through AsVector128Unsafe plus WithElement to zero the upper lanes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mirror the arm64 constant path: build a byte-granular index vector (out-of-range elements set to 0xFF) and emit NI_PackedSimd_Swizzle, whose i8x16_swizzle semantics match arm64's VectorTableLookup. The importer only takes constant-index, non-native Vector.Shuffle; variable indices and ShuffleNative fall back to the managed implementation until the variable path is implemented. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Implement the WASM gtNewSimdShuffleVariableNode by mirroring arm64: for non-byte element types, expand each element index into its run of byte indices (ShiftLeft to scale, Swizzle-broadcast the low byte, then OR the intra-element offsets) and perform a single i8x16.swizzle. Out-of-range element indices are normalized to zero by the shared masking step for elementSize > 1; byte indices are zeroed directly by Swizzle. This removes the importer guard so both Shuffle (constant and variable) and ShuffleNative flow through, and treats byte ShuffleNative as deterministic since Swizzle always zeroes out-of-range byte indices. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A constant-index Vector Shuffle lowers to NI_PackedSimd_Swizzle with a constant byte-index mask. When every mask lane is in range, rewrite it to an immediate i8x16.shuffle so the underlying wasm engine can pattern-match the compile-time-known permutation into an optimal native sequence instead of a generic runtime table lookup. i8x16.shuffle selects from two vectors, so the single source is reused as both operands: it is marked multiply-used (materialized into a local) and codegen emits the extra local.get. Out-of-range masks keep using i8x16.swizzle, whose native index >= 16 -> 0 behavior the shuffle form cannot reproduce without a zero operand. Behavior is unchanged; ConstantShuffleTest already covers both the in-range (shuffle) and out-of-range (swizzle) paths across all element widths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reverse expands to a constant Shuffle, which is now supported on WASM, so the importer guard can simply be removed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WASM is V128-only with no native horizontal reduction, so reduce via a shuffle + add tree reading lane 0. Increasing strides keep the grouping identical to the software Sum(lower) + Sum(upper) fallback, preserving floating-point determinism. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WASM has no native interleave/deinterleave and the results draw from two vectors, so scatter each operand's elements into disjoint lanes with a single-source shuffle (out-of-range indices zero-fill the gaps) and OR the results together. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Concat draws each result half from a different operand, which a single-source shuffle can't express directly. Factor the two-scatter + OR mechanism already used by Zip/Unzip into a shared gtNewSimdWasmTwoSourceShuffleNode helper that takes a single logical selector array, and express Concat, Zip, and Unzip in terms of it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The non-constant path builds two broadcasts and zips them, which WASM now supports, so the importer no longer needs to bail out for non-constant operands. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WASM has no native horizontal reduction, so import Dot as Sum(left * right), reusing the same fallback xarch already uses for the cases its Dot lowering can't handle. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WASM only exposes f64x2.promote_low_f32x4, so move the upper two floats into the low lanes with a shuffle and then promote them. The integer widening forms were already handled by the sign/zero-extend widening instructions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Integer narrowing reuses the two-source shuffle helper at byte granularity to gather each wide element's low bytes; double->float narrowing demotes both operands and interleaves their low pairs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Factor the portable clamp-then-narrow fallback out of the xarch importer into a shared gtNewSimdNarrowWithSaturationNode builder and route both xarch's fallback and WASM through it. WASM's native narrow_u treats inputs as signed, so it can't be used for the unsigned cases, and there is no long->int narrow; the clamp-based fallback is correct for every base type. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WASM SIMD has no i64/u64 to f64 (or reverse) lane conversions, no per-lane variable shift, and no fused-multiply-add, so ConvertToDouble, ConvertToInt64, ConvertToUInt64 (and their Native variants), variable ShiftLeft, and FusedMultiplyAdd correctly fall back to the managed software path. Reword the stale TODO comments to explain this rather than implying an intrinsic is missing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Now that GetElement and WithElement are supported on WASM, remove the lclmorph guard that forced promoted SIMD field accesses to a LclFld. Vector2/3/4 float fields and the embedded Vector3 of a 16-byte value (e.g. Plane.Normal) now lower through GetElement/WithElement instead of memory-based field loads and stores. The SIMD-halves path stays gated to xarch/arm64, which is unreachable on WASM since it has no SIMD types wider than 16 bytes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The value-number folding of ExtractMostSignificantBits / PackedSimd.Bitmask on a constant vector was gated behind FEATURE_MASKED_HW_INTRINSICS, so WASM hit an NYI. The computation (EvaluateExtractMSB plus simdmask_t) is portable, so share it for WASM instead, folding a constant mask extraction to an integer constant. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 5 pipeline(s). 10 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
|
CC. @dotnet/jit-contrib, @adamperlin. Should be the rest of the WASM SIMD lightup here. ABI work and some minor codegen dispatch handling to be done in a followup |
There was a problem hiding this comment.
Pull request overview
This PR expands WASM SIMD support by importing/lowering a broad set of Vector128/Vector intrinsics through existing cross-platform SIMD node builders, adding WASM-specific lowering/codegen where needed (notably shuffle/swizzle), and adding/expanding coverage in the WASM PackedSimd JIT tests.
Changes:
- Enable additional WASM lowering/import paths for
Vector*intrinsics (element access, scalar create, shuffle, reductions, concat/zip/unzip, narrow/widen, etc.), including VN / fold support for a subset. - Add WASM-specific implementations for shuffle-related nodes (constant shuffle via swizzle; variable shuffle via byte-index expansion; constant swizzle →
i8x16.shufflefast-path). - Expand
PackedSimdTests.cswith targeted regression tests for the newly enabled intrinsics and fold paths.
Show a summary per file
| File | Description |
|---|---|
| src/tests/JIT/HardwareIntrinsics/Wasm/PackedSimd/PackedSimdTests.cs | Adds regression coverage for newly enabled Vector128/Vector intrinsics on WASM (element ops, shifts, shuffle, reductions, min/max scalar behavior, folding). |
| src/coreclr/jit/valuenum.cpp | Enables VN evaluation for ExtractMostSignificantBits and ConditionalSelect on WASM. |
| src/coreclr/jit/regallocwasm.cpp | Adjusts temp-local consumption for the swizzle→shuffle lowering path (source reuse). |
| src/coreclr/jit/lowerwasm.cpp | Adds lowering for CreateScalarUnsafe, GetElement/ToScalar/WithElement, and a swizzle constant-mask fast-path to i8x16.shuffle. |
| src/coreclr/jit/lower.h | Declares the new WASM lowering helper (LowerHWIntrinsicSwizzle). |
| src/coreclr/jit/lclmorph.cpp | Enables SIMD field get/set morphing on WASM now that GetElement/WithElement are supported. |
| src/coreclr/jit/importercalls.cpp | Enables scalar min/max import for WASM via SIMD builders (removes prior WASM TODO blocks). |
| src/coreclr/jit/hwintrinsiclist.h | Reclassifies AsVector128Unsafe/AsVector2/AsVector3 for non-xarch/non-arm64 to allow WASM handling. |
| src/coreclr/jit/hwintrinsiccodegenwasm.cpp | Emits i8x16.shuffle for the contained-constant swizzle fast-path; implements no-op codegen for SIMD-width reinterprets. |
| src/coreclr/jit/hwintrinsic.cpp | Removes/rewrites WASM TODO gates for several Vector* intrinsics (enables import paths and documents true ISA gaps). |
| src/coreclr/jit/gentree.cpp | Implements multiple WASM SIMD builders (narrow, narrow-with-saturation fallback, shuffle, sum reduction, widen upper float, concat/zip/unzip via two-source shuffle emulation). |
| src/coreclr/jit/compiler.h | Adds declarations for newly introduced SIMD builder helpers used by the importer/lowering. |
Copilot's findings
Comments suppressed due to low confidence (1)
src/tests/JIT/HardwareIntrinsics/Wasm/PackedSimd/PackedSimdTests.cs:10
MethodImpl/MethodImplOptionsis used later in this file, butSystem.Runtime.CompilerServicesisn't imported, which will break compilation for this test project.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Wasm;
using Xunit;
- Files reviewed: 12/12 changed files
- Comments generated: 1
ReplaceWith byte-copies the source node, including the MultiOp `m_operands` pointer. When the source stores its operands inline, that pointer aliases the source's own storage, so after the copy the destination's `m_operands` dangles into the soon-to-be-destroyed source. Re-point it into the destination's own inline storage; a heap operand array lives outside the source and is left untouched. Latent for all targets; exposed on WASM where ToScalar is grown from one to two operands during lowering, tripping the `oldOperands == inlineOperands` assert. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`AsMultiOp` only exists when hardware intrinsics are enabled, so the unconditional call broke targets without them (e.g. arm). `OperIsMultiOp` is only ever true for HW-intrinsic nodes, so nothing needs relocating there. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`Opaque<T>` uses `[MethodImpl(MethodImplOptions.NoInlining)]`, which needs `System.Runtime.CompilerServices`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The prior remark claimed i8x16.shuffle has no codegen path for its immediates, but PackedSimd.Shuffle does emit it. The real limitation is that the general shuffle IR is single-source, so the two-source permute is decomposed to stay in the foldable representation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Overall looks good to me, I just had a few small questions / comment suggestions. |
|
Handling the 3 minor comment asks in a follow-up to avoid blocking the product changes. |
|
|
||
| Vector128<short> sh1 = Opaque(Vector128.Create((short)0x0100, 0x0302, 0x0504, 0x0706, 0x0908, 0x0B0A, 0x0D0C, 0x0F0E)); | ||
| Vector128<short> sh2 = Opaque(Vector128.Create(unchecked((short)0x1110), 0x1312, 0x1514, 0x1716, 0x1918, 0x1B1A, 0x1D1C, 0x1F1E)); | ||
| Assert.Equal(Vector128.Create((byte)0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E), Vector128.Narrow(sh1, sh2)); |
There was a problem hiding this comment.
outerloop build break:
2026-07-17T09:32:35.0505605Z /__w/1/s/src/tests/JIT/HardwareIntrinsics/Wasm/PackedSimd/PackedSimdTests.cs(678,22): error CS1503: Argument 1: cannot convert from 'System.Runtime.Intrinsics.Vector128<byte>' to 'System.DateTime' [/__w/1/s/src/tests/JIT/HardwareIntrinsics/Wasm/PackedSimd/PackedSimdTests_r.csproj] [/__w/1/s/src/tests/build.proj]
2026-07-17T09:32:35.0511496Z /__w/1/s/src/tests/JIT/HardwareIntrinsics/Wasm/PackedSimd/PackedSimdTests.cs(678,142): error CS1503: Argument 2: cannot convert from 'System.Runtime.Intrinsics.Vector128<sbyte>' to 'System.DateTime' [/__w/1/s/src/tests/JIT/HardwareIntrinsics/Wasm/PackedSimd/PackedSimdTests_r.csproj] [/__w/1/s/src/tests/build.proj]
Post-merge follow-up to #130850, addressing three review comments from @adamperlin. - `hwintrinsiccodegenwasm.cpp`: reword the contained-mask Swizzle->`i8x16.shuffle` remark. On Wasm `genConsumeMultiOpOperands` only updates liveness and doesn't touch the value stack, so the comment now says "prior codegen left the source on the value stack once" rather than attributing it to `genConsumeMultiOpOperands`. ---------- - `gentree.cpp` (`TARGET_WASM` Shuffle-lowering branch): add a lead-in comment describing the element->byte-index expansion -- each element-granular selector expands into `elementSize` consecutive byte indices for `i8x16.swizzle`, and an out-of-range selector becomes `0xFF` bytes so swizzle's native `index >= 16 -> 0` behavior zero-fills that element. Dropped the now-redundant inline else-block comment. The third comment (`lowerwasm.cpp`, feeding a constant-zero second operand to `i8x16.shuffle` instead of the source twice) is addressed in the thread rather than in code: a `local.get` of the existing source is smaller than materializing a 16-byte `v128.const` zero, and it can't remove the special case since the Swizzle node is 2-operand (source, mask->immediate) with no second-vector slot. Comment-only; no functional change. > [!NOTE] > This PR description was drafted by Copilot on behalf of @tannergooding. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The wasm `HardwareIntrinsics` tests (`src/tests/JIT/HardwareIntrinsics/Wasm`) are gated by `HWITestsWasmOnly` -> `CLRTestTargetUnsupported` unless `TargetArchitecture == wasm` (plus an `EnableWasmHWIntrinsicsTests` opt-in). As a result, none of the existing PR-triggered intrinsics pipelines ever compile them: - `runtime-coreclr hardware-intrinsics` (`src/coreclr/jit/**`, x86/x64/arm/osx) passes without building the wasm tree. - `hardware-intrinsics-arm64` (`src/coreclr/jit/*arm64*`) is skipped for wasm-only changes. So a break in the wasm intrinsic tests only surfaces in a rolling/outerloop browser-wasm build, which is what required the #130962 follow-up to #130850. ---------- This adds a wasm counterpart mirroring `hardware-intrinsics-arm64.yml`: - `eng/pipelines/coreclr/hardware-intrinsics-wasm.yml` -- PR-triggered, filtered on `src/coreclr/jit/*wasm*` (covers `hwintrinsic*wasm*`, `lowerwasm.cpp`, `regallocwasm.cpp`) plus `src/tests/JIT/HardwareIntrinsics/Wasm/**`. Runs a `browser_wasm` leg with `/p:EnableWasmHWIntrinsicsTests=true -tree:JIT/HardwareIntrinsics/Wasm`. - `eng/pipelines/coreclr/templates/jit-hardware-intrinsics-wasm.yml` -- modeled on the existing `wasi-wasm-coreclr-runtime-tests.yml` (`global-build-job`, `runtimeFlavor: coreclr`, `-s clr+libs+packs`). It is build-only (`sendToHelix: false`) since browser/V8 can''t execute these tests yet -- enough to catch compile breaks like #130962. Like `hardware-intrinsics-arm64.yml`, the new pipeline still needs an Azure DevOps definition registered against the YAML to appear as a PR check. > [!NOTE] > This PR description was drafted by Copilot. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Restore the main SIMD-enabled ISA config (compiler.cpp compSetProcessor adds
PackedSimd + Vector128; InstructionSetHelpers adds simd128) that the prototype
had previously disabled, and drop the prototype-stale NYI_WASM_SIMD stubs for
NI_PackedSimd_{ExtractScalar,ReplaceScalar,shifts,StoreSelectedScalar} that were
shadowing the real implementations from dotnet#130850. Keeps the WasmBase
LeadingZeroCount/TrailingZeroCount cases. SIMD-enabled crossgen relies on the
JitWasmSimdNyiToR2RUnsupported punt flag for the remaining v128-materialization
gap (dotnet#130866) until that lands.
This lights up the remaining
Vector128/VectorSIMD intrinsics that were previouslyNYI_WASM_SIMD/TODO-WASM-SIMDon WASM by wiring them through the existing cross-platformgtNewSimd*Nodebuilders and importer paths, reusing WASM's V128 primitives (PackedSimd). WASM has no V64, so — like xarch — these start at V128. Where the shape mirrored an existing architecture, the logic is shared rather than duplicated (e.g. a common two-source shuffle builder and a sharedgtNewSimdNarrowWithSaturationNode).Each commit is self-contained and covers one intrinsic (or a tightly related group), with a regression test added to
PackedSimdTests.cs.Element access & scalars
GetElement/WithElement/ToScalarCreateScalar/CreateScalarUnsafeVector2/Vector3conversionsShuffle
ShuffleShuffle/ShuffleNativei8x16.shuffledirectly for a constant, in-rangeShuffleCross-lane / arithmetic
Reverse,Sum,Zip/Unzip,Concat,CreateAlternatingSequenceDot(imported asSum(Mul(...)), matching the fallback other archs already use)WidenUpper(float),Narrow,NarrowWithSaturationMinMax(scalar) — reshaped to a V128 operation like ARM64Optimization / folding
GetElement/WithElementare supported)ExtractMostSignificantBitsconstant foldingConditionalSelectconstant folding — on WASM the node is stillNI_Vector_ConditionalSelectat both fold sites (VN andgtFoldExprHWIntrinsicrun before Lowering, where it is rewritten toPackedSimd.BitwiseSelect), so the existing xarch fold body applies unchangedIntentionally deferred / out of scope
long↔doubleconversions,Fma, and variable per-laneShiftLeft. TheTODO-WASM-SIMDcomments for these were reworded to document why rather than implying they're simply unimplemented.i32in a few codegen paths) and the remaining codegen dispatch gaps are not part of this PR — that's the end-to-end enablement work and will be handled separately.CC. @dotnet/jit-contrib
Note
This PR description was drafted with GitHub Copilot.