From 55d579bc06ba377425d48b3a8815e93d7edcb575 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 13:23:48 -0700 Subject: [PATCH 1/4] Implement wasm codegen for sub-16 SIMD load/store 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. Emit the split lane loads/stores instead of bailing out via the ins_Load/ins_Store NYIs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 133 ++++++++++++++++++++++++++++--- src/coreclr/jit/instr.cpp | 4 + src/coreclr/jit/lowerwasm.cpp | 10 ++- src/coreclr/jit/regallocwasm.cpp | 18 +++++ 4 files changed, 154 insertions(+), 11 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index b82bf33b8c34c8..9e162d5e83da98 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2597,8 +2597,15 @@ void CodeGen::genCodeForLclFld(GenTreeLclFld* tree) NYI_WASM_SIMD("SIMD16 local field load"); } - GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); - GetEmitter()->emitIns_S(ins_Load(type), emitTypeSize(tree), tree->GetLclNum(), tree->GetLclOffs()); + if (type == TYP_SIMD12) + { + genLoadLclTypeSimd12(tree); + } + else + { + GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); + GetEmitter()->emitIns_S(ins_Load(type), emitTypeSize(tree), tree->GetLclNum(), tree->GetLclOffs()); + } WasmProduceReg(tree); } @@ -2621,8 +2628,15 @@ void CodeGen::genCodeForLclVar(GenTreeLclVar* tree) { var_types type = varDsc->GetRegisterType(tree); - GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); - GetEmitter()->emitIns_S(ins_Load(type), emitTypeSize(type), tree->GetLclNum(), 0); + if (type == TYP_SIMD12) + { + genLoadLclTypeSimd12(tree); + } + else + { + GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); + GetEmitter()->emitIns_S(ins_Load(type), emitTypeSize(type), tree->GetLclNum(), 0); + } WasmProduceReg(tree); } else @@ -2695,6 +2709,90 @@ void CodeGen::genCodeForFrameSize(GenTree* tree) WasmProduceReg(tree); } +//------------------------------------------------------------------------ +// genLoadLclTypeSimd12: Load a TYP_SIMD12 (i.e. Vector3) local into a v128. +// +// Arguments: +// tree - the GT_LCL_FLD or GT_LCL_VAR node +// +// Notes: +// Vector3 has no native wasm valtype, so it lives as a v128 with the low 12 bytes +// populated. The frame address is pushed twice: v128.load64_zero fills lanes 0-1 +// (zeroing the rest) and v128.load32_lane fills lane 2 from bytes 8-11. +// +void CodeGen::genLoadLclTypeSimd12(GenTreeLclVarCommon* tree) +{ + bool fpBased; + unsigned frameOffset = (unsigned)(m_compiler->lvaFrameAddress(tree->GetLclNum(), &fpBased) + tree->GetLclOffs()); + unsigned fpIndex = GetFramePointerRegIndex(); + emitter* emit = GetEmitter(); + + emit->emitIns_I(INS_local_get, EA_PTRSIZE, fpIndex); + emit->emitIns_I(INS_local_get, EA_PTRSIZE, fpIndex); + emit->emitIns_I(INS_v128_load64_zero, EA_8BYTE, frameOffset); + emit->emitIns_MemargLane(INS_v128_load32_lane, EA_4BYTE, frameOffset + 8, 2); +} + +//------------------------------------------------------------------------ +// genLoadIndTypeSimd12: Load a TYP_SIMD12 (i.e. Vector3) value through an indirection. +// +// Arguments: +// tree - the GT_IND node +// +// Notes: +// One copy of the address is already on the value stack (from genConsumeAddress) and +// is multiply-used, so the trailing v128.load32_lane can re-push it for the upper 4 bytes. +// +void CodeGen::genLoadIndTypeSimd12(GenTreeIndir* tree) +{ + emitter* emit = GetEmitter(); + + emit->emitIns_I(INS_local_get, EA_PTRSIZE, WasmRegToIndex(GetMultiUseOperandReg(tree->Addr()))); + emit->emitIns_I(INS_v128_load64_zero, EA_8BYTE, 0); + emit->emitIns_MemargLane(INS_v128_load32_lane, EA_4BYTE, 8, 2); +} + +//------------------------------------------------------------------------ +// genStoreIndTypeSimd12: Store a TYP_SIMD12 (i.e. Vector3) value through an indirection. +// +// Arguments: +// tree - the GT_STOREIND node +// +// Notes: +// On entry the value stack holds [addr, value]. The value is teed into an internal v128 +// local so it survives the low-8 store; the address is then re-materialized to store the +// upper 4 bytes via a lane store - re-emitting the frame pointer for a LCL_ADDR, or +// re-pushing the multiply-used address register otherwise. +// +void CodeGen::genStoreIndTypeSimd12(GenTreeStoreInd* tree) +{ + emitter* emit = GetEmitter(); + GenTree* addr = tree->Addr(); + + InternalRegs* regs = internalRegisters.GetAll(tree); + assert(regs->Count() == 1); + regNumber valReg = regs->Extract(); + + emit->emitIns_I(INS_local_tee, EA_16BYTE, WasmRegToIndex(valReg)); // [addr, value] + emit->emitIns_MemargLane(INS_v128_store64_lane, EA_8BYTE, 0, 0); // [] + + if (addr->OperIs(GT_LCL_ADDR)) + { + bool fpBased; + unsigned frameOffset = (unsigned)(m_compiler->lvaFrameAddress(addr->AsLclVarCommon()->GetLclNum(), &fpBased) + + addr->AsLclVarCommon()->GetLclOffs()); + emit->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); // [fp] + emit->emitIns_I(INS_local_get, EA_16BYTE, WasmRegToIndex(valReg)); // [fp, value] + emit->emitIns_MemargLane(INS_v128_store32_lane, EA_4BYTE, frameOffset + 8, 2); // [] + } + else + { + emit->emitIns_I(INS_local_get, EA_PTRSIZE, WasmRegToIndex(GetMultiUseOperandReg(addr))); // [addr] + emit->emitIns_I(INS_local_get, EA_16BYTE, WasmRegToIndex(valReg)); // [addr, value] + emit->emitIns_MemargLane(INS_v128_store32_lane, EA_4BYTE, 8, 2); // [] + } +} + //------------------------------------------------------------------------ // genCodeForIndir: Produce code for a GT_IND node. // @@ -2705,8 +2803,7 @@ void CodeGen::genCodeForIndir(GenTreeIndir* tree) { assert(tree->OperIs(GT_IND)); - var_types type = tree->TypeGet(); - instruction ins = ins_Load(type); + var_types type = tree->TypeGet(); genConsumeAddress(tree->Addr()); @@ -2718,7 +2815,14 @@ void CodeGen::genCodeForIndir(GenTreeIndir* tree) // TODO-WASM: Memory barriers - GetEmitter()->emitIns_I(ins, emitActualTypeSize(type), 0); + if (type == TYP_SIMD12) + { + genLoadIndTypeSimd12(tree); + } + else + { + GetEmitter()->emitIns_I(ins_Load(type), emitActualTypeSize(type), 0); + } WasmProduceReg(tree); } @@ -2762,11 +2866,22 @@ void CodeGen::genCodeForStoreInd(GenTreeStoreInd* tree) // module. Bail until SIMD16 store is properly supported. NYI_WASM_SIMD("SIMD16 store indirect"); } - instruction ins = ins_Store(type); // TODO-WASM: Memory barriers - GetEmitter()->emitIns_I(ins, emitActualTypeSize(type), 0); + if (type == TYP_SIMD8) + { + // stack: [addr, value] -> store the low 8 bytes. + GetEmitter()->emitIns_MemargLane(INS_v128_store64_lane, EA_8BYTE, 0, 0); + } + else if (type == TYP_SIMD12) + { + genStoreIndTypeSimd12(tree); + } + else + { + GetEmitter()->emitIns_I(ins_Store(type), emitActualTypeSize(type), 0); + } } genUpdateLife(tree); diff --git a/src/coreclr/jit/instr.cpp b/src/coreclr/jit/instr.cpp index 28ca9242ba3cd4..5b5269007f844c 100644 --- a/src/coreclr/jit/instr.cpp +++ b/src/coreclr/jit/instr.cpp @@ -2070,6 +2070,10 @@ instruction CodeGenInterface::ins_Load(var_types srcType, bool aligned /*=false* case TYP_DOUBLE: return INS_f64_load; #if defined(FEATURE_SIMD) + case TYP_SIMD8: + // SIMD8 (Vector2) lives as a v128 with the low 8 bytes populated. SIMD12 (Vector3) is + // handled at the callers since it needs a trailing lane load for the upper 4 bytes. + return INS_v128_load64_zero; case TYP_SIMD16: return INS_v128_load; #endif diff --git a/src/coreclr/jit/lowerwasm.cpp b/src/coreclr/jit/lowerwasm.cpp index 18227b108bf8d2..341e4ae4be4391 100644 --- a/src/coreclr/jit/lowerwasm.cpp +++ b/src/coreclr/jit/lowerwasm.cpp @@ -164,9 +164,13 @@ GenTree* Lowering::LowerStoreLoc(GenTreeLclVarCommon* storeLoc) // GenTree* Lowering::LowerStoreIndir(GenTreeStoreInd* node) { - if ((node->gtFlags & GTF_IND_NONFAULTING) == 0) + if (((node->gtFlags & GTF_IND_NONFAULTING) == 0) || + (node->TypeIs(TYP_SIMD12) && !node->Addr()->OperIs(GT_LCL_ADDR))) { // We need to be able to null check the address, and that requires multiple uses of the address operand. + // SIMD12 stores also re-materialize the address for the trailing lane store, so force it there as well - + // unless the address is a re-materializable LCL_ADDR (the local-to-stack store rewrite), which codegen + // re-emits directly. SetMultiplyUsed(node->Addr() DEBUGARG("LowerStoreIndir faulting Addr")); } @@ -459,8 +463,10 @@ void Lowering::ContainCheckIndir(GenTreeIndir* indirNode) return; } - if (indirNode->OperIs(GT_IND) && ((indirNode->gtFlags & GTF_IND_NONFAULTING) == 0)) + if (indirNode->OperIs(GT_IND) && + (((indirNode->gtFlags & GTF_IND_NONFAULTING) == 0) || indirNode->TypeIs(TYP_SIMD12))) { + // SIMD12 loads re-materialize the address for the trailing lane load, so force it there regardless. SetMultiplyUsed(indirNode->Addr() DEBUGARG("ContainCheckIndir faulting load Addr")); } diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index 860f8d8dbd0dc6..2599d53996432c 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -662,6 +662,14 @@ void WasmRegAlloc::CollectReferencesForIndir(GenTreeIndir* node) { GenTree* const addr = node->Addr(); ConsumeTemporaryRegForOperand(addr DEBUGARG("indirection address")); + + if (node->OperIs(GT_STOREIND) && node->TypeIs(TYP_SIMD12)) + { + // The SIMD12 store stashes the v128 value so it can re-push it for the trailing lane store. + regNumber internalReg = RequestInternalRegister(node, TYP_SIMD16); + regNumber releasedReg = ReleaseTemporaryRegister(WasmRegToType(internalReg)); + assert(releasedReg == internalReg); + } } //------------------------------------------------------------------------ @@ -800,6 +808,16 @@ void WasmRegAlloc::RewriteLocalStackStore(GenTreeLclVarCommon* lclNode) LIR::ReadOnlyRange storeRange(store, store); m_compiler->GetLowering()->LowerRange(m_currentBlock, storeRange); + if (store->OperIs(GT_STOREIND) && store->TypeIs(TYP_SIMD12)) + { + // genStoreIndTypeSimd12 tees the value into a v128 temporary to split the store into an 8-byte and a + // 4-byte lane store. The main collection walk does not revisit this freshly-introduced node, so request + // that internal register here. The re-materializable LCL_ADDR address needs no temporary. + regNumber internalReg = RequestInternalRegister(store, TYP_SIMD16); + regNumber releasedReg = ReleaseTemporaryRegister(WasmRegToType(internalReg)); + assert(releasedReg == internalReg); + } + // FIXME-WASM: Should we be doing this here? // CollectReferencesForNode(store); } From cb4c11bf103333cedf78b3f0df854da7e496ff11 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 15:03:57 -0700 Subject: [PATCH 2/4] Use a signed frame offset for the simd12 lane ops Casting the frame offset to unsigned before passing it as a wasm memarg defeated the emitter's offset >= 0 invariant, silently turning an unexpected negative offset into a large positive one. Compute a signed offset and noway_assert it, matching emitter::emitIns_S. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 9e162d5e83da98..147085c420219b 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2722,10 +2722,11 @@ void CodeGen::genCodeForFrameSize(GenTree* tree) // void CodeGen::genLoadLclTypeSimd12(GenTreeLclVarCommon* tree) { - bool fpBased; - unsigned frameOffset = (unsigned)(m_compiler->lvaFrameAddress(tree->GetLclNum(), &fpBased) + tree->GetLclOffs()); - unsigned fpIndex = GetFramePointerRegIndex(); - emitter* emit = GetEmitter(); + bool fpBased; + int frameOffset = m_compiler->lvaFrameAddress(tree->GetLclNum(), &fpBased) + (int)tree->GetLclOffs(); + noway_assert(frameOffset >= 0); // WASM address modes are unsigned. + unsigned fpIndex = GetFramePointerRegIndex(); + emitter* emit = GetEmitter(); emit->emitIns_I(INS_local_get, EA_PTRSIZE, fpIndex); emit->emitIns_I(INS_local_get, EA_PTRSIZE, fpIndex); @@ -2778,11 +2779,12 @@ void CodeGen::genStoreIndTypeSimd12(GenTreeStoreInd* tree) if (addr->OperIs(GT_LCL_ADDR)) { - bool fpBased; - unsigned frameOffset = (unsigned)(m_compiler->lvaFrameAddress(addr->AsLclVarCommon()->GetLclNum(), &fpBased) + - addr->AsLclVarCommon()->GetLclOffs()); - emit->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); // [fp] - emit->emitIns_I(INS_local_get, EA_16BYTE, WasmRegToIndex(valReg)); // [fp, value] + bool fpBased; + int frameOffset = m_compiler->lvaFrameAddress(addr->AsLclVarCommon()->GetLclNum(), &fpBased) + + (int)addr->AsLclVarCommon()->GetLclOffs(); + noway_assert(frameOffset >= 0); // WASM address modes are unsigned. + emit->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); // [fp] + emit->emitIns_I(INS_local_get, EA_16BYTE, WasmRegToIndex(valReg)); // [fp, value] emit->emitIns_MemargLane(INS_v128_store32_lane, EA_4BYTE, frameOffset + 8, 2); // [] } else From 0475c6638117f12541b735f2cc5e4a45795f24dd Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 15:14:14 -0700 Subject: [PATCH 3/4] Reword the simd12 load note to not reference genConsumeAddress Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 147085c420219b..629fd5ba35711e 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2741,8 +2741,8 @@ void CodeGen::genLoadLclTypeSimd12(GenTreeLclVarCommon* tree) // tree - the GT_IND node // // Notes: -// One copy of the address is already on the value stack (from genConsumeAddress) and -// is multiply-used, so the trailing v128.load32_lane can re-push it for the upper 4 bytes. +// The address is left on the value stack by prior codegen and is multiply-used, so the +// trailing v128.load32_lane can re-push it for the upper 4 bytes. // void CodeGen::genLoadIndTypeSimd12(GenTreeIndir* tree) { From b3f4fd66d8ae178435471f7958591fb5b4772b55 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 15:34:34 -0700 Subject: [PATCH 4/4] Assert fpBased and clarify multiply-use reasons for simd12 Assert the wasm frame address is FP-based before re-emitting the frame pointer in the simd12 lane helpers, matching the block-copy codegen. Update the multiply-use DEBUGARG reasons since simd12 now forces it regardless of faulting. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 8 +++++--- src/coreclr/jit/lowerwasm.cpp | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 629fd5ba35711e..abb0807982ce18 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2725,6 +2725,7 @@ void CodeGen::genLoadLclTypeSimd12(GenTreeLclVarCommon* tree) bool fpBased; int frameOffset = m_compiler->lvaFrameAddress(tree->GetLclNum(), &fpBased) + (int)tree->GetLclOffs(); noway_assert(frameOffset >= 0); // WASM address modes are unsigned. + assert(fpBased); unsigned fpIndex = GetFramePointerRegIndex(); emitter* emit = GetEmitter(); @@ -2782,9 +2783,10 @@ void CodeGen::genStoreIndTypeSimd12(GenTreeStoreInd* tree) bool fpBased; int frameOffset = m_compiler->lvaFrameAddress(addr->AsLclVarCommon()->GetLclNum(), &fpBased) + (int)addr->AsLclVarCommon()->GetLclOffs(); - noway_assert(frameOffset >= 0); // WASM address modes are unsigned. - emit->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); // [fp] - emit->emitIns_I(INS_local_get, EA_16BYTE, WasmRegToIndex(valReg)); // [fp, value] + noway_assert(frameOffset >= 0); // WASM address modes are unsigned. + assert(fpBased); + emit->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); // [fp] + emit->emitIns_I(INS_local_get, EA_16BYTE, WasmRegToIndex(valReg)); // [fp, value] emit->emitIns_MemargLane(INS_v128_store32_lane, EA_4BYTE, frameOffset + 8, 2); // [] } else diff --git a/src/coreclr/jit/lowerwasm.cpp b/src/coreclr/jit/lowerwasm.cpp index 341e4ae4be4391..c55f76236afa90 100644 --- a/src/coreclr/jit/lowerwasm.cpp +++ b/src/coreclr/jit/lowerwasm.cpp @@ -171,7 +171,7 @@ GenTree* Lowering::LowerStoreIndir(GenTreeStoreInd* node) // SIMD12 stores also re-materialize the address for the trailing lane store, so force it there as well - // unless the address is a re-materializable LCL_ADDR (the local-to-stack store rewrite), which codegen // re-emits directly. - SetMultiplyUsed(node->Addr() DEBUGARG("LowerStoreIndir faulting Addr")); + SetMultiplyUsed(node->Addr() DEBUGARG("LowerStoreIndir Addr (null check or simd12 lane store)")); } ContainCheckStoreIndir(node); @@ -467,7 +467,7 @@ void Lowering::ContainCheckIndir(GenTreeIndir* indirNode) (((indirNode->gtFlags & GTF_IND_NONFAULTING) == 0) || indirNode->TypeIs(TYP_SIMD12))) { // SIMD12 loads re-materialize the address for the trailing lane load, so force it there regardless. - SetMultiplyUsed(indirNode->Addr() DEBUGARG("ContainCheckIndir faulting load Addr")); + SetMultiplyUsed(indirNode->Addr() DEBUGARG("ContainCheckIndir load Addr (null check or simd12 lane load)")); } // TODO-WASM-CQ: contain suitable LEAs here. Take note of the fact that for this to be correct we must prove the