Skip to content

Light up remaining SIMD intrinsics on WASM#130850

Merged
tannergooding merged 26 commits into
dotnet:mainfrom
tannergooding:tannergooding-wasm-simd-lightup
Jul 16, 2026
Merged

Light up remaining SIMD intrinsics on WASM#130850
tannergooding merged 26 commits into
dotnet:mainfrom
tannergooding:tannergooding-wasm-simd-lightup

Conversation

@tannergooding

Copy link
Copy Markdown
Member

This lights up the remaining Vector128/Vector SIMD intrinsics that were previously NYI_WASM_SIMD/TODO-WASM-SIMD on WASM by wiring them through the existing cross-platform gtNewSimd*Node builders 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 shared gtNewSimdNarrowWithSaturationNode).

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 / ToScalar
  • Scalar (single-element) shifts
  • CreateScalar / CreateScalarUnsafe
  • Vector2 / Vector3 conversions

Shuffle

  • Constant-index Shuffle
  • Variable-index Shuffle / ShuffleNative
  • Emit i8x16.shuffle directly for a constant, in-range Shuffle

Cross-lane / arithmetic

  • Reverse, Sum, Zip/Unzip, Concat, CreateAlternatingSequence
  • Dot (imported as Sum(Mul(...)), matching the fallback other archs already use)
  • WidenUpper (float), Narrow, NarrowWithSaturation
  • MinMax (scalar) — reshaped to a V128 operation like ARM64

Optimization / folding

  • Promoted SIMD field access lowering (now that GetElement/WithElement are supported)
  • ExtractMostSignificantBits constant folding
  • ConditionalSelect constant folding — on WASM the node is still NI_Vector_ConditionalSelect at both fold sites (VN and gtFoldExprHWIntrinsic run before Lowering, where it is rewritten to PackedSimd.BitwiseSelect), so the existing xarch fold body applies unchanged

Intentionally deferred / out of scope

  • A handful of intrinsics remain a software fallback because WASM has no corresponding native instruction: longdouble conversions, Fma, and variable per-lane ShiftLeft. The TODO-WASM-SIMD comments for these were reworded to document why rather than implying they're simply unimplemented.
  • Codegen ABI / V128 materialization (SIMD values currently coming through as i32 in 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.

tannergooding and others added 21 commits July 15, 2026 19:37
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>
Copilot AI review requested due to automatic review settings July 16, 2026 06:54
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 16, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@tannergooding

Copy link
Copy Markdown
Member Author

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.shuffle fast-path).
  • Expand PackedSimdTests.cs with 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/MethodImplOptions is used later in this file, but System.Runtime.CompilerServices isn'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

Comment thread src/coreclr/jit/hwintrinsiclist.h
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 1

Comment thread src/tests/JIT/HardwareIntrinsics/Wasm/PackedSimd/PackedSimdTests.cs
tannergooding and others added 2 commits July 16, 2026 06:17
`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>
Copilot AI review requested due to automatic review settings July 16, 2026 13:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 1

Comment thread src/coreclr/jit/gentree.cpp Outdated
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>
Copilot AI review requested due to automatic review settings July 16, 2026 14:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new

Comment thread src/coreclr/jit/gentree.h
Comment thread src/coreclr/jit/hwintrinsiccodegenwasm.cpp
Comment thread src/coreclr/jit/lowerwasm.cpp
Comment thread src/coreclr/jit/gentree.cpp
@adamperlin

Copy link
Copy Markdown
Contributor

Overall looks good to me, I just had a few small questions / comment suggestions.

@tannergooding
tannergooding merged commit 34d8d66 into dotnet:main Jul 16, 2026
146 checks passed
@tannergooding
tannergooding deleted the tannergooding-wasm-simd-lightup branch July 16, 2026 23:53
@tannergooding

Copy link
Copy Markdown
Member Author

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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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]

tannergooding added a commit that referenced this pull request Jul 17, 2026
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>
tannergooding added a commit that referenced this pull request Jul 17, 2026
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>
lewing added a commit to lewing/runtime that referenced this pull request Jul 20, 2026
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.
@github-actions github-actions Bot mentioned this pull request Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants