diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 58c0018687da2e..75b6403dec8fb9 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -103,18 +103,6 @@ void CodeGen::genMarkLabelsForCodegen() // void CodeGen::genBeginFnProlog() { - // SIMD (Vector2/3/4, Vector128) parameters are lowered to i32 in the wasm signature, so any - // vector operation performed on them produces an invalid module (e.g. a v128/f64 op with - // an i32 operand). Bail such methods to the interpreter until SIMD parameters are - // properly supported in the wasm calling convention. - for (unsigned lclNum = 0; lclNum < m_compiler->info.compArgsCount; lclNum++) - { - if (varTypeIsSIMD(m_compiler->lvaGetDesc(lclNum)->TypeGet())) - { - NYI_WASM_SIMD("SIMD parameter"); - } - } - GetEmitter()->emitIns(INS_code_size); FuncInfoDsc* const func = m_compiler->funGetFunc(ROOT_FUNC_IDX); @@ -2592,11 +2580,6 @@ void CodeGen::genCodeForLclFld(GenTreeLclFld* tree) LclVarDsc* varDsc = m_compiler->lvaGetDesc(tree); var_types type = tree->TypeGet(); - if (type == TYP_SIMD16) - { - NYI_WASM_SIMD("SIMD16 local field load"); - } - if (type == TYP_SIMD12) { genLoadLclTypeSimd12(tree); @@ -2862,8 +2845,7 @@ void CodeGen::genCodeForStoreInd(GenTreeStoreInd* tree) } else // A normal store, not a WriteBarrier store { - var_types type = tree->TypeGet(); - instruction ins = ins_Store(type); + var_types type = tree->TypeGet(); // TODO-WASM: Memory barriers @@ -2974,13 +2956,6 @@ void CodeGen::genCallInstruction(GenTreeCall* call) assert(seg.IsPassedInRegister()); WasmValueType wvt = WasmRegToType(seg.GetRegister()); assert(wvt < WasmValueType::Count); - if (wvt == WasmValueType::V128) - { - // Passing a 16-byte SIMD value by value through a call is not yet correctly - // implemented: the argument is materialized as an i32 (by-ref) while the call - // signature requires v128, producing an invalid module. Bail for now. - NYI_WASM_SIMD("SIMD16 call argument"); - } typeStack.Push((CorInfoWasmType)emitter::GetWasmValueTypeCode(wvt)); } } @@ -3905,10 +3880,6 @@ void CodeGen::genLoadLocalIntoReg(regNumber targetReg, unsigned lclNum) { LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); var_types type = varDsc->GetRegisterType(); - if (type == TYP_SIMD16) - { - NYI_WASM_SIMD("SIMD16 local load"); - } GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); GetEmitter()->emitIns_S(ins_Load(type), emitTypeSize(type), lclNum, 0); diff --git a/src/coreclr/jit/targetwasm.cpp b/src/coreclr/jit/targetwasm.cpp index 0a9db2bf8cd5b6..94392952e15bf6 100644 --- a/src/coreclr/jit/targetwasm.cpp +++ b/src/coreclr/jit/targetwasm.cpp @@ -46,8 +46,7 @@ var_types WasmClassifier::ToJitType(CorInfoWasmType wasmType) case CORINFO_WASM_TYPE_F64: return TYP_DOUBLE; case CORINFO_WASM_TYPE_V128: - // TODO-WASM: Simd support - unreached(); + return TYP_SIMD16; default: unreached(); } diff --git a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs index f3a270d255af18..bd6ec1ef7b184a 100644 --- a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs +++ b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs @@ -12,6 +12,21 @@ public partial class CompilerTypeSystemContext private readonly object _structCacheLock = new object(); private readonly Dictionary _structsBySize = new Dictionary(); private volatile TypeDesc _cachedEmptyStruct; + private volatile TypeDesc _cachedV128Type; + + /// + /// Gets the first SIMD v128 type encountered during lowering, or null if none has been seen. + /// Used by RaiseSignature to produce a roundtrippable type for the 'V' encoding. + /// + public TypeDesc CachedV128Type => _cachedV128Type; + + /// + /// Caches a SIMD v128 type discovered during lowering. Only the first one is retained. + /// + public void CacheV128Type(TypeDesc type) + { + _cachedV128Type ??= type; + } /// /// Gets the first empty struct type encountered during lowering, or null if none has been seen. diff --git a/src/coreclr/tools/Common/Compiler/VectorFieldLayoutAlgorithm.cs b/src/coreclr/tools/Common/Compiler/VectorFieldLayoutAlgorithm.cs index 74481988dadf25..21226021f29657 100644 --- a/src/coreclr/tools/Common/Compiler/VectorFieldLayoutAlgorithm.cs +++ b/src/coreclr/tools/Common/Compiler/VectorFieldLayoutAlgorithm.cs @@ -147,7 +147,7 @@ public override bool ComputeIsUnsafeValueType(DefType type) public override ValueTypeShapeCharacteristics ComputeValueTypeShapeCharacteristics(DefType type) { if (type.Context.Target.Architecture == TargetArchitecture.ARM64 && - type.Instantiation[0].IsPrimitiveNumeric) + IsSupportedVectorBaseType(type.Instantiation[0])) { return type.InstanceFieldSize.AsInt switch { @@ -170,5 +170,14 @@ public static bool IsVectorType(DefType type) type.Name == "Vector256`1"u8 || type.Name == "Vector512`1"u8); } + + /// + /// Determines whether is supported as the base (element) + /// type of an intrinsic vector, mirroring the set the JIT recognizes as a SIMD base type. + /// + public static bool IsSupportedVectorBaseType(TypeDesc elementType) + { + return elementType.IsPrimitiveNumeric; + } } } diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index b25f8d0d683983..329abbf03b7f78 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -3907,6 +3907,8 @@ private CorInfoWasmType getWasmLowering(CORINFO_CLASS_STRUCT_* structHnd) return CorInfoWasmType.CORINFO_WASM_TYPE_F32; case WasmValueType.F64: return CorInfoWasmType.CORINFO_WASM_TYPE_F64; + case WasmValueType.V128: + return CorInfoWasmType.CORINFO_WASM_TYPE_V128; default: ThrowHelper.ThrowInvalidProgramException(); return CorInfoWasmType.CORINFO_WASM_TYPE_I32; // unreachable diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index ddbb03db80bdff..58447a357d4151 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -38,6 +38,12 @@ public static MethodSignature GetStringCtorActualSignature(MethodSignature signa public static TypeDesc LowerToAbiType(TypeDesc type) { + // Vector128 and a 128-bit Vector are wasm v128 ABI primitives passed by value. + if (IsWasmV128Type(type)) + { + return type; + } + if (!(type.IsValueType && !type.IsPrimitive)) { return type; @@ -65,6 +71,10 @@ public static TypeDesc LowerToAbiType(TypeDesc type) if (numIntroducedFields != 1) { + // Multi-field aggregates (including a homogeneous 2x v128) use the generic by-ref + // struct ABI; the wasm C ABI has no HFA/HVA concept. Only emscripten's opt-in + // experimental multivalue ABI expands these into per-field registers, which we + // don't target. return null; } @@ -78,6 +88,13 @@ public static TypeDesc LowerToAbiType(TypeDesc type) type = firstFieldElementType; + // A single-field wrapper struct around a v128 lowers to the v128 primitive, matching + // emscripten, which passes a struct wrapping a v128 as a v128. + if (IsWasmV128Type(type)) + { + return type; + } + if (type.IsValueType && !type.IsPrimitive) { continue; @@ -87,10 +104,47 @@ public static TypeDesc LowerToAbiType(TypeDesc type) } } + /// + /// Determines whether a type is passed and returned by value as a wasm v128, matching + /// the SIMD types the JIT recognizes as TYP_SIMD16 on wasm. This is + /// and a 128-bit + /// , in both cases only when T is a supported + /// primitive numeric base type. Other SIMD types (Vector2/3/4, Vector64/256/512<T>, ...) + /// and non-primitive instantiations (e.g. the shared __Canon form) are not ABI + /// primitives and continue to use the generic struct ABI. + /// + private static bool IsWasmV128Type(TypeDesc type) + { + if (!type.IsIntrinsic || + type.Instantiation.Length != 1 || + !VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType(type.Instantiation[0])) + { + return false; + } + + // Vector128 is always a 16-byte v128. + if (Internal.TypeSystem.Interop.InteropTypes.IsSystemRuntimeIntrinsicsVector128T(type.Context, type)) + { + return true; + } + + // Vector is target-sized, so it is only a v128 when the target's maximum SIMD width is + // 128-bit (i.e. it is exactly 16 bytes). This matches the JIT recognizing it as TYP_SIMD16 + // via getVectorTByteLength() and keeps the ABI correct should wasm later gain wider vectors. + return type is DefType vectorOfT && + VectorOfTFieldLayoutAlgorithm.IsVectorOfTType(vectorOfT) && + type.GetElementSize().AsInt == 16; + } + public static WasmValueType LowerType(TypeDesc type) { WasmValueType pointerType = (type.Context.Target.PointerSize == 4) ? WasmValueType.I32 : WasmValueType.I64; + if (IsWasmV128Type(type)) + { + return WasmValueType.V128; + } + TypeDesc abiType = LowerToAbiType(type); if (abiType == null) @@ -165,7 +219,8 @@ public static WasmValueType LowerType(TypeDesc type) 'l' => context.GetWellKnownType(WellKnownType.Int64), 'f' => context.GetWellKnownType(WellKnownType.Single), 'd' => context.GetWellKnownType(WellKnownType.Double), - 'V' => throw new NotSupportedException("SIMD types are not supported in this version of the compiler"), + 'V' => ((CompilerTypeSystemContext)context).CachedV128Type + ?? throw new InvalidOperationException("Encountered 'V' in signature but no v128 type was cached during lowering"), _ => throw new InvalidOperationException($"Unknown signature char: {c}") }; @@ -347,7 +402,12 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag } else { - sigBuilder.Append(WasmValueTypeToSigChar(LowerType(loweredReturnType))); + WasmValueType returnWasmType = LowerType(loweredReturnType); + if (returnWasmType == WasmValueType.V128) + { + ((CompilerTypeSystemContext)returnType.Context).CacheV128Type(loweredReturnType); + } + sigBuilder.Append(WasmValueTypeToSigChar(returnWasmType)); } // Reserve space for potential implicit this, stack pointer parameter, portable entrypoint parameter, @@ -423,7 +483,11 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag } else { - WasmValueType paramWasmType = LowerType(paramType); + WasmValueType paramWasmType = LowerType(loweredParamType); + if (paramWasmType == WasmValueType.V128) + { + ((CompilerTypeSystemContext)paramType.Context).CacheV128Type(loweredParamType); + } sigBuilder.Append(WasmValueTypeToSigChar(paramWasmType)); result.Add(paramWasmType); } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/VectorOfTFieldLayoutAlgorithm.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/VectorOfTFieldLayoutAlgorithm.cs index 0bb36a84800e56..1a6f8ae2cd8df4 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/VectorOfTFieldLayoutAlgorithm.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/VectorOfTFieldLayoutAlgorithm.cs @@ -78,7 +78,7 @@ public override bool ComputeContainsByRefs(DefType type) public override ValueTypeShapeCharacteristics ComputeValueTypeShapeCharacteristics(DefType type) { if (type.Context.Target.Architecture == TargetArchitecture.ARM64 && - type.Instantiation[0].IsPrimitiveNumeric) + VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType(type.Instantiation[0])) { return type.InstanceFieldSize.AsInt switch { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index e29a57c61cccc2..09e98fcc6a088b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection.PortableExecutable; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; using ILCompiler.Reflection.ReadyToRun; @@ -120,6 +121,87 @@ static void Validate(ReadyToRunReader reader) } } + [Fact] + public void WasmSimdModule() + { + var wasmSimdModule = new CompiledAssembly + { + AssemblyName = nameof(WasmSimdModule), + SourceResourceNames = ["Webcil/WasmSimdModule.cs"], + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(WasmSimdModule), + [ + new(nameof(WasmSimdModule), [new CrossgenAssembly(wasmSimdModule)]) + { + OutputFileExtension = ".wasm", + AdditionalArgs = + { + "--targetarch", + "wasm", + "--targetos", + "browser", + }, + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + var webcilReader = Assert.IsType(reader.CompositeReader); + Assert.True(webcilReader.IsWasmWrapped); + Assert.Equal(WasmMachine.Wasm32, reader.Machine); + + List methods = R2RAssert.GetAllMethods(reader); + + // Each method's compiled body must actually use the wasm v128 (0x7B) valtype for its + // Vector128 parameter, and for its return when it returns one. A regression that + // reverts to the by-ref i32 ABI would produce no v128 in the signature at all. + const byte WasmV128 = 0x7B; + + // (method name, expects v128 return). All take a v128-classified value by value (a + // Vector128, a 128-bit Vector, or a single-field struct wrapping one); Store + // returns void (its 'ref Vector128' destination is an i32 pointer). + foreach ((string name, bool expectsV128Return) in + new[] + { + ("Echo", true), ("ThroughLocal", true), ("Store", false), ("CallEcho", true), + ("EchoVectorT", true), ("CallEchoVectorT", true), + ("EchoWrapped", true), ("CallEchoWrapped", true), + ("EchoWrappedVectorT", true), ("CallEchoWrappedVectorT", true), + }) + { + ReadyToRunMethod method = Assert.Single( + methods, m => m.SignatureString.Contains($".{name}(", StringComparison.Ordinal)); + + WebcilImageReader.WasmFunctionInfo body = ResolveWasmBody(reader, webcilReader, method); + + Assert.True( + body.ParamTypes.Count(b => b == WasmV128) == 1, + $"'{name}' should have exactly one wasm v128 parameter; params were {Format(body.ParamTypes)}."); + Assert.True( + body.ResultTypes.Contains(WasmV128) == expectsV128Return, + $"'{name}' v128 return expectation was {expectsV128Return}; results were {Format(body.ResultTypes)}."); + } + + static string Format(IReadOnlyList valTypes) => + $"[{string.Join(",", valTypes.Select(b => $"0x{b:X2}"))}]"; + } + + static WebcilImageReader.WasmFunctionInfo ResolveWasmBody( + ReadyToRunReader reader, WebcilImageReader webcilReader, ReadyToRunMethod method) + { + uint tableIndex = checked(reader.WasmMinFunctionTableIndex + (uint)method.EntryPointRuntimeFunctionId); + int functionIndex = webcilReader.GetFunctionIndexFromTableIndex(tableIndex); + Assert.True(functionIndex >= 0, $"Could not resolve wasm table index {tableIndex} to a function body."); + + WebcilImageReader.WasmFunctionInfo? body = webcilReader.GetWasmFunctionBody(functionIndex); + Assert.True(body is not null, $"Wasm function body {functionIndex} was not found."); + return body.Value; + } + } + [Fact] public void RuntimeFunctionsSectionSizeExcludesSentinel() { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs new file mode 100644 index 00000000000000..77518d9ee5336d --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace Webcil; + +// Exercises the wasm v128 calling convention (SIMD passed/returned/stored by value) +// without relying on any SIMD arithmetic intrinsics, so only the ABI/materialization +// paths are covered. +public static class WasmSimdModule +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector128 Echo(Vector128 value) + { + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector128 ThroughLocal(Vector128 value) + { + Vector128 local = value; + return local; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Store(Vector128 value, ref Vector128 destination) + { + destination = value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector128 CallEcho(Vector128 value) + { + return Echo(value); + } + + // Vector is 16 bytes on wasm (128-bit vectors), so it uses the same v128 ABI as Vector128. + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector EchoVectorT(Vector value) + { + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector CallEchoVectorT(Vector value) + { + return EchoVectorT(value); + } + + // A single-field struct wrapping a v128 is itself passed/returned as a v128, matching emscripten. + public struct WrappedVector128 + { + public Vector128 Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static WrappedVector128 EchoWrapped(WrappedVector128 value) + { + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static WrappedVector128 CallEchoWrapped(WrappedVector128 value) + { + return EchoWrapped(value); + } + + // The same unwrapping applies to a struct wrapping a 128-bit Vector. + public struct WrappedVectorT + { + public Vector Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static WrappedVectorT EchoWrappedVectorT(WrappedVectorT value) + { + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static WrappedVectorT CallEchoWrappedVectorT(WrappedVectorT value) + { + return EchoWrappedVectorT(value); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs index 0a88512554eebc..7e597bf5218819 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs @@ -209,6 +209,9 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr case WasmValueType.F64: expressions.Add(F64.Load((ulong)interpOffsets[i])); break; + case WasmValueType.V128: + expressions.Add(V128.Load((ulong)interpOffsets[i])); + break; default: throw new Exception("Unexpected wasm type for interpreter-to-R2R arg"); } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs index 568488eda1f9ec..edf32f30df371a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs @@ -329,7 +329,7 @@ public override ComputedStaticFieldLayout ComputeStaticFieldLayout(DefType type, public override ValueTypeShapeCharacteristics ComputeValueTypeShapeCharacteristics(DefType type) { if (type.Context.Target.Architecture == TargetArchitecture.ARM64 && - type.Instantiation[0].IsPrimitiveNumeric) + VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType(type.Instantiation[0])) { return type.InstanceFieldSize.AsInt switch {