Skip to content

Materialize Vector128<T> as a wasm v128 in the codegen ABI - #130866

Merged
tannergooding merged 15 commits into
dotnet:mainfrom
tannergooding:tannergooding-wasm-simd-abi-codegen
Jul 22, 2026
Merged

Materialize Vector128<T> as a wasm v128 in the codegen ABI#130866
tannergooding merged 15 commits into
dotnet:mainfrom
tannergooding:tannergooding-wasm-simd-abi-codegen

Conversation

@tannergooding

@tannergooding tannergooding commented Jul 16, 2026

Copy link
Copy Markdown
Member

Today several wasm codegen paths bail to the interpreter (NYI_WASM_SIMD) because a 128-bit SIMD value arrives as an i32 (a by-ref pointer) rather than a real v128 on the wasm value stack. Emitting a v128.* op against an i32 operand produces an invalid module, so the JIT defensively bails. This is the end-to-end blocker for SIMD actually executing on wasm.

This change treats Vector128<T> as the wasm v128 ABI primitive and materializes it by value:

  • getWasmLowering maps Vector128<T> to CORINFO_WASM_TYPE_V128, and ToJitType maps that to TYP_SIMD16.
  • The five codegen sites that previously bailed now emit real v128 values: SIMD parameter homing, SIMD16 local field load, store-indirect, call argument, and local load.
  • RaiseSignature can round-trip the 'V' signature char back to a concrete v128 type.

Why only Vector128<T>. It is the single SIMD type the JIT recognizes as TYP_SIMD16 on wasm. In getBaseTypeAndSizeOfSIMDType, Vector128<T> (16 bytes) is not target-gated, where-as Vector64<T> is TARGET_ARM64-only and Vector256<T>/Vector512<T> are TARGET_XARCH-only, so on wasm those fall through to TYP_UNDEF and are handled as regular structs. The System.Numerics vectors (Vector2/Vector3/Vector4, Vector<T>, Plane, Quaternion) are multi-field, so getWasmLowering classifies them as passed by reference (they arrive as TYP_BYREF, not varTypeIsSIMD). The net effect is that the only by-value SIMD value reaching these codegen paths is a Vector128<T> in a real v128 register, so the previous bails are no longer needed and no new gate is required. The other SIMD types keep their pre-change ABI and are deferred to follow-ups.

Because every 'V' in a lowered signature is now exactly 16 bytes, there is no mixed-width offset ambiguity in the signature round-trip.


Testing. Adds an R2R test (WasmSimdModule) exercising the v128 calling convention — parameter, return, through-local, store-indirect, and call-argument — using Vector128<int> without any SIMD arithmetic intrinsics, so only the ABI/materialization paths are covered. Locally, ILCompiler.ReadyToRun.Tests (filter Wasm) passes 2/2; wasm SIMD only executes in CI, which is the point of this draft.

Opening as draft for full CI validation.

Note

This PR description was drafted with GitHub Copilot.

Vector128<T> is the one SIMD type the JIT recognizes as TYP_SIMD16 on wasm
(the others are TARGET_ARM64/TARGET_XARCH-only or multi-field structs passed
by reference), so treat it as the wasm v128 ABI primitive. The lowering now
maps Vector128<T> to CORINFO_WASM_TYPE_V128 -> TYP_SIMD16, and the codegen
paths that previously bailed to the interpreter (SIMD parameter, SIMD16 local
field load, store-indirect, call argument, and local load) emit real v128
values instead of producing an invalid module.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 11:44
@github-actions github-actions Bot added the area-crossgen2-coreclr only use for closed issues 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.

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

Updates the wasm ABI lowering and wasm JIT codegen so Vector128<T> is treated as a real wasm v128 value (instead of arriving as an i32 byref), enabling SIMD values to flow through key codegen paths without bailing to interpreter.

Changes:

  • Teach wasm lowering/signature logic to classify Vector128<T> as v128 and round-trip 'V' signature encodings.
  • Update wasm JIT ABI/type mapping and remove several NYI_WASM_SIMD bailouts now that v128 is materialized by-value.
  • Add an R2R wasm test module and a new xUnit test suite entry to exercise the v128 calling convention plumbing.
Show a summary per file
File Description
src/coreclr/tools/Common/JitInterface/WasmLowering.cs Recognize Vector128<T> as v128, emit 'V' in signature strings, and enable signature round-tripping.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs Plumbs WasmValueType.V128 into the JIT-facing CorInfoWasmType lowering.
src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs Adds caching for a representative v128 type to support 'V' signature raising.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs Adds argument materialization support for v128 in interpreter-to-R2R thunks.
src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs New test-case source to exercise v128 param/return/local/store/call-arg flows.
src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs Adds a new test that compiles the wasm SIMD test case into a .wasm R2R artifact.
src/coreclr/jit/targetwasm.cpp Maps CORINFO_WASM_TYPE_V128 to TYP_SIMD16 for wasm ABI classification.
src/coreclr/jit/codegenwasm.cpp Removes several SIMD-related NYI bailouts in prolog/loads/stores/calls now that v128 is supported.

Copilot's findings

  • Files reviewed: 8/8 changed files
  • Comments generated: 2

Comment thread src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs
Strengthen the WasmSimdModule test to assert that all four methods (Echo,
ThroughLocal, Store, CallEcho) are present in the compiled output rather than
just one, so a regression in any single ABI/materialization path is caught.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 13:03

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: 8/8 changed files
  • Comments generated: 1

Comment thread src/coreclr/tools/Common/JitInterface/WasmLowering.cs Outdated
GetSignature already computes loweredParamType via LowerToAbiType, so pass it to
LowerType directly instead of re-lowering paramType, avoiding a redundant field
walk for single-field structs and matching the return-type lowering pattern.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 13:29

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: 8/8 changed files
  • Comments generated: 2

Comment thread src/coreclr/tools/Common/JitInterface/WasmLowering.cs
Comment thread src/coreclr/jit/codegenwasm.cpp
The V128 branch already has loweredParamType (guaranteed to be the Vector128<T>
that lowered to 'V'), so cache it directly instead of the outer paramType. This
keeps the cached representative consistent with the type that produced the 'V'
encoding for single-field struct wrappers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 14:08

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: 8/8 changed files
  • Comments generated: 1

Comment thread src/coreclr/tools/Common/JitInterface/WasmLowering.cs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 14:42

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: 8/8 changed files
  • Comments generated: 1

Comment thread src/coreclr/tools/Common/JitInterface/WasmLowering.cs
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 14:51

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: 8/8 changed files
  • Comments generated: 1

Decode each method's wasm function signature and verify the Vector128<int>
parameter (and return, where applicable) materializes as v128 (0x7B) rather
than a by-ref i32, so a regression to the generic struct ABI is caught.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 15:37
# Conflicts:
#	src/coreclr/jit/codegenwasm.cpp
Copilot AI review requested due to automatic review settings July 18, 2026 16:50

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

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
  "version": 5,
  "last_dispatched_commit": "7ffe7916ffb20d3db9d718d372e6bd0fffd2e0af",
  "last_dispatched_base_ref": "main",
  "last_dispatched_base_sha": "46ec2c1bce94b5cc262049195c5b5b50a64708e9",
  "last_reviewed_commit": "7ffe7916ffb20d3db9d718d372e6bd0fffd2e0af",
  "last_reviewed_base_ref": "main",
  "last_reviewed_base_sha": "46ec2c1bce94b5cc262049195c5b5b50a64708e9",
  "last_recorded_worker_run_id": "29688140671",
  "review_attempt_commit": "",
  "review_attempt_base_ref": "",
  "review_attempt_count": 0,
  "max_review_attempts": 5,
  "review_history_format": "holistic-review-disclosure-v1",
  "review_history": [
    {
      "commit": "7ffe7916ffb20d3db9d718d372e6bd0fffd2e0af",
      "review_id": 4730810927
    }
  ]
}

@github-actions github-actions Bot 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.

Holistic Review

Motivation: On wasm, several codegen paths currently bail to the interpreter (NYI_WASM_SIMD) because a 128-bit SIMD value arrives as an i32 by-ref pointer rather than a real v128 on the wasm value stack. Emitting a v128.* op against an i32 operand yields an invalid module, so the JIT defensively bails. This blocks SIMD from actually executing on wasm. The motivation is clear, correct, and well-justified.

Approach: The PR treats Vector128<T> as the wasm v128 ABI primitive and materializes it by value. getWasmLowering/LowerType map Vector128<T> (primitive-numeric T only) to CORINFO_WASM_TYPE_V128; WasmClassifier::ToJitType maps that to TYP_SIMD16; and the five codegen sites that previously bailed (SIMD parameter homing, SIMD16 local field load, store-indirect, call argument, and local load) are removed so real v128 values flow through. RaiseSignature round-trips the 'V' signature char via a newly cached representative v128 type. The IsPrimitiveNumeric gate in the three ARM64 ComputeValueTypeShapeCharacteristics sites is refactored into a shared VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType helper, reused by the new wasm check. The scoping rationale for restricting to Vector128<T> (other SIMD types remain multi-field/by-ref or are target-gated to non-wasm) is sound and preserves existing ABI for those types.

Summary: This is a focused, correct step that removes the interpreter bails and wires Vector128<T> through the wasm v128 ABI by value. The IsSupportedVectorBaseType refactor is behavior-preserving for the existing ARM64 sites (it currently just wraps IsPrimitiveNumeric) while centralizing the definition. The 'V' round-trip via CachedV128Type is safe because every lowered 'V' is exactly a 16-byte v128, so the JIT's element-type-agnostic TYP_SIMD16 mapping makes the specific cached instantiation immaterial; the caching uses the same non-atomic ??= pattern as the existing empty-struct/struct-by-size caches. The added R2R test (WasmSimdModule) validates the ABI paths (parameter, return, through-local, store-indirect, call-argument) by asserting the emitted wasm valtype is 0x7B (v128), which is a meaningful regression guard against reverting to the by-ref i32 ABI. No arithmetic intrinsics are exercised, matching the stated scope. I found no correctness, security, or convention issues in the changed lines. Verdict: LGTM. As the author notes, this is a draft pending full CI (wasm SIMD only executes in CI), which is the appropriate gate before merge.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 65.4 AIC · ⌖ 10.6 AIC · ⊞ 10K

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.
# Conflicts:
#	src/coreclr/jit/codegenwasm.cpp
Copilot AI review requested due to automatic review settings July 21, 2026 03:41

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: 11/11 changed files
  • Comments generated: 0 new

@davidwrighton davidwrighton left a comment

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.

I'd like to understand the rationale behind some choices, and push back on the not handling the wrapper struct scenario.

Comment thread src/coreclr/tools/Common/JitInterface/WasmLowering.cs Outdated
Comment thread src/coreclr/tools/Common/JitInterface/WasmLowering.cs Outdated
A 128-bit Vector<T> uses the same v128 ABI as Vector128<T>, and a single-field
struct wrapping either lowers to the v128 primitive, matching emscripten's
isSingleElementStruct handling. Multi-field aggregates keep the generic by-ref
ABI since the wasm C ABI has no HFA/HVA concept.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 21, 2026 19:59

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

Comments suppressed due to low confidence (1)

src/coreclr/tools/Common/JitInterface/WasmLowering.cs:154

  • LowerType checks IsWasmV128Type(type) before lowering, but LowerToAbiType can now lower a non-intrinsic wrapper struct (single-field wrapper around v128) to a v128 ABI type. In that case abiType becomes Vector128<T>/Vector<T> and the subsequent switch (abiType.UnderlyingType.Category) will hit the default path and throw, even though the ABI lowering succeeded. Adding a post-lowering v128 check keeps LowerType consistent with LowerToAbiType and makes it safe to call with wrapper structs.
            TypeDesc abiType = LowerToAbiType(type);

            if (abiType == null)
            {
                return pointerType;
            }

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

lewing added a commit to lewing/runtime that referenced this pull request Jul 21, 2026
…Shuffle (Adam's shuffle-mask fix)

Fixes the invalid i8x16.shuffle immediate (out-of-range lane) that made the
SIMD composite fail wasm validation. Resolved 2 conflicts vs dotnet#130866/dotnet#131000:
- codegenwasm.cpp: dropped redundant ins local (superseded by dotnet#131000 store restructure)
- lowerwasm.cpp: took shuffle case + IsCnsVec() immediate handling from dotnet#130991
@tannergooding
tannergooding enabled auto-merge (squash) July 21, 2026 22:50
@tannergooding
tannergooding merged commit 8fa5c76 into dotnet:main Jul 22, 2026
133 of 135 checks passed
@tannergooding
tannergooding deleted the tannergooding-wasm-simd-abi-codegen branch July 22, 2026 00:40
tannergooding added a commit that referenced this pull request Jul 27, 2026
…st (#131429)

Fixes #131339 -- though not in the way the issue proposed; see the note
at the bottom.

`RaiseSignature` resolved the `'V'` (v128) signature char to
`CompilerTypeSystemContext.CachedV128Type`, i.e. literally whichever
v128 type lowering happened to encounter first, written racily from
parallel compilation. `'V'` is fully determined by the wasm ABI (16
bytes, 16-byte aligned), so raising now resolves a fixed canonical
`Vector128<byte>` instead. Any v128 type round-trips `'V'` identically,
so `RaiseSignature`'s own round-trip assert still holds, and the raised
signature no longer depends on compilation order.

The 16-byte alignment `Debug.Assert` that guarded this invariant moved
from `CacheV128Type` into `IsWasmV128Type`. `CacheV128Type` only ever
saw the first cached type, where-as `IsWasmV128Type` has three call
sites and so covers every v128 type.

This also drops the `?? throw new InvalidOperationException(...)` on the
`'V'` path -- that failure mode is simply gone, since raising no longer
needs lowering to have run first.

----------

`ILCompiler.ReadyToRun.Tests` gains `WasmArgumentLayoutTests`: 20 cases
driving crossgen2's type system and `GCRefMapBuilder.BuildArgIterator`
directly, so they need neither a wasm JIT nor a runtime to execute
against.

- `WasmV128TypesAre16ByteAligned` -- 12 cases over
`Vector128<T>`/`Vector<T>` x 6 element types.
- `WasmV128ArgumentsStartOn16ByteBoundaries` -- `static void M(long,
TVector, ref int)` must lay out as `[0, 16, 32]`.
- `OtherSimdWidthsAreNotV128` -- `Vector64/256/512<T>` must fall back to
the generic struct ABI.
- `RaisingV128SignatureIsIndependentOfLoweringOrder` -- lowers a
different vector type first, then asserts the raised `'V'` is unchanged
and that its offsets match the original signature's.

These also cover the `Vector<T>` alignment fix from #131328, and I
verified they bite: with that fix reverted, 7 cases fail -- the six
`Vector<T>` alignment cases report 8 instead of 16, and
`WasmV128ArgumentsStartOn16ByteBoundaries` for `Vector<T>` reports `[0,
8, 24]` instead of `[0, 16, 32]`.

----------

On #131339 itself. The issue asks for a test asserting `Vector128<T>` is
16-byte aligned, on the premise that this is what #131328 fixed. That is
not correct -- `Vector128<T>` gets alignment 16 from
`VectorFieldLayoutAlgorithm` on every architecture except ARM and was
untouched by that fix. I confirmed empirically that the proposed test
passes unchanged against a compiler with #131328 reverted, so it would
have been a no-op. The actual regression was
`System.Numerics.Vector<T>`, which kept the 8-byte alignment from its
metadata layout. The tests here cover both.

The issue also says the test was deferred for lack of a CI-runnable
home. That premise is stale: #130866 added the `WasmSimdModule` wasm
crossgen2 suite to `ILCompiler.ReadyToRun.Tests` three days before the
issue was filed, and that is where these tests live.

----------

Also hoisted the `System.Private.CoreLib.dll` resolution out of
`R2RTestRunner` into `TestPaths.SystemPrivateCoreLibPath` so the new
tests share the existing runtime-pack to CoreCLR-artifacts fallback.
Without it they would hard-fail in partial builds that skip
`libs.pretest`, e.g. a bare `clr.toolstests`.

Out of scope, to be filed separately:
`CacheStructBySize`/`GetCachedStructOfSize` has the same
first-wins-cache shape for the `'S<N>'` encoding, and there it is a live
miscompile rather than a latent hazard. `Guid` and `Int128` are both 16
bytes and both encode `S16`, but their clamped alignments are 8 and 16,
so the shared thunk signature `vlS16ip` gets two different frame
layouts. Fixing that means changing the on-disk encoding across
crossgen2, `src/coreclr/vm/wasm/helpers.cpp`, `WasmAppBuilder`, and the
R2R format doc, so it does not belong here.

Local test run: `ILCompiler.ReadyToRun.Tests` is 58 total / 1 failed.
The failure is `CompositeManifestAssemblyMvidsArePaddedWhenPdbPresent`,
an `ArgumentNullException` out of the native PDB COM writer in my Debug
layout. Confirmed pre-existing -- it reproduces identically with this
branch's changes stashed.

CC. @lewing @adamperlin -- ready for review.

> [!NOTE]
> This PR description was drafted by Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-crossgen2-coreclr only use for closed issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants