Materialize Vector128<T> as a wasm v128 in the codegen ABI - #130866
Materialize Vector128<T> as a wasm v128 in the codegen ABI#130866tannergooding merged 15 commits into
Conversation
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>
|
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. |
There was a problem hiding this comment.
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>asv128and round-trip'V'signature encodings. - Update wasm JIT ABI/type mapping and remove several
NYI_WASM_SIMDbailouts now thatv128is 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
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>
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>
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>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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>
# Conflicts: # src/coreclr/jit/codegenwasm.cpp
|
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
}
]
} |
There was a problem hiding this comment.
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
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
davidwrighton
left a comment
There was a problem hiding this comment.
I'd like to understand the rationale behind some choices, and push back on the not handling the wrapper struct scenario.
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>
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (1)
src/coreclr/tools/Common/JitInterface/WasmLowering.cs:154
LowerTypechecksIsWasmV128Type(type)before lowering, butLowerToAbiTypecan now lower a non-intrinsic wrapper struct (single-field wrapper around v128) to a v128 ABI type. In that caseabiTypebecomesVector128<T>/Vector<T>and the subsequentswitch (abiType.UnderlyingType.Category)will hit the default path and throw, even though the ABI lowering succeeded. Adding a post-lowering v128 check keepsLowerTypeconsistent withLowerToAbiTypeand 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
…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
…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>
Today several wasm codegen paths bail to the interpreter (
NYI_WASM_SIMD) because a 128-bit SIMD value arrives as ani32(a by-ref pointer) rather than a realv128on the wasm value stack. Emitting av128.*op against ani32operand 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 wasmv128ABI primitive and materializes it by value:getWasmLoweringmapsVector128<T>toCORINFO_WASM_TYPE_V128, andToJitTypemaps that toTYP_SIMD16.v128values: SIMD parameter homing, SIMD16 local field load, store-indirect, call argument, and local load.RaiseSignaturecan 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 asTYP_SIMD16on wasm. IngetBaseTypeAndSizeOfSIMDType,Vector128<T>(16 bytes) is not target-gated,where-asVector64<T>isTARGET_ARM64-only andVector256<T>/Vector512<T>areTARGET_XARCH-only, so on wasm those fall through toTYP_UNDEFand are handled as regular structs. TheSystem.Numericsvectors (Vector2/Vector3/Vector4,Vector<T>,Plane,Quaternion) are multi-field, sogetWasmLoweringclassifies them as passed by reference (they arrive asTYP_BYREF, notvarTypeIsSIMD). The net effect is that the only by-value SIMD value reaching these codegen paths is aVector128<T>in a realv128register, 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 — usingVector128<int>without any SIMD arithmetic intrinsics, so only the ABI/materialization paths are covered. Locally,ILCompiler.ReadyToRun.Tests(filterWasm) 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.