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
tannergooding added a commit that referenced this pull request Jul 18, 2026
`Vector2` (`TYP_SIMD8`) and `Vector3` (`TYP_SIMD12`) have no native wasm
valtype, so they live as a `v128` with the low 8/12 bytes populated.
Previously the wasm JIT bailed out via the `ins_Load`/`ins_Store` NYIs
for these types; this implements the split lane load/store sequences
instead.

The emitted sequences are:

- **simd8 load**: `v128.load64_zero 0`
- **simd8 store**: `v128.store64_lane 0, lane 0`
- **simd12 load**: `local.get addr; v128.load64_zero 0; v128.load32_lane
8, lane 2`
- **simd12 store**: tee the value into a `v128` temporary,
`v128.store64_lane 0, lane 0` for the low 8 bytes, then re-materialize
the address and `v128.store32_lane 8, lane 2` for the upper 4 bytes

`v128.load64_zero` fills lanes 0-1 (zeroing the rest); the trailing lane
store/load handles bytes 8-11 for the `Vector3` case.

----------

The `TYP_SIMD12` address is forced multiply-used (loads and heap stores
re-materialize it for the trailing lane op). The local-to-stack store
rewrite (`RewriteLocalStackStore`) produces a `STOREIND(LCL_ADDR,
value)` whose address is a re-materializable `GT_LCL_ADDR`, so that case
is excluded from multiply-use and codegen re-emits the frame pointer
directly. Because that synthesized `STOREIND` is not revisited by the
main collection walk, its internal `v128` tee register is requested in
`RewriteLocalStackStore`.

----------

Measured on the corelib crossgen2 browser SuperPMI collection (27,540
contexts): hard asserts drop from **403 to 30**, eliminating **373**
`SIMD8`/`SIMD12` load/store asserts with no regressions. The residual 30
are the pre-existing `NYIRAW` oper catch-all, unrelated to this change.

Full effectiveness requires #130866 (which removes the shadowing
SIMD-ABI bailouts for SIMD params/locals/stores/call-args); standalone,
this change already clears the 373 asserts above on that collection.

> [!NOTE]
> This PR description was drafted with the assistance of GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
# 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
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