From: Daniel Golle Subject: [PATCH] JavaScriptCore: add RISCV64 MacroAssembler primitives required by BBQJIT Enabling BBQJIT on RISCV64 exposes a set of MacroAssembler primitives that WasmBBQJIT.cpp / WasmBBQJIT64.cpp / WasmBBQJIT32_64.cpp call but that MacroAssemblerRISCV64.h does not yet provide. Add them in four groups: * Scalar BBQJIT primitives, fully implemented for RISCV64: - floatMin / floatMax / doubleMin / doubleMax via fmin.s/fmax.s and fmin.d/fmax.d (the F/D extensions in rv64gc). - addLeftShift64 via slli + add. - multiplyAddZeroExtend32 via mulw + 32-bit mask + add. - rotateLeft32 / rotateLeft64 (immediate and variable shift) via shift + shift + or; rv64gc has no rotate, the Zbb rol* family is not in the baseline. - div32 / uDiv32 / div64 / uDiv64 via the RISC-V M extension (divw / divuw / div / divu); 32-bit results are masked back to 32 bits. - multiplySub32 / multiplySub64 via mul + sub. - convertUInt32ToFloat via fcvt.s.wu. * 8/16-bit and Address->BaseIndex transfer overloads. transfer{8,16} were not defined for any addressing mode, and transfer{32,64,Vector} only had Address->Address and BaseIndex->BaseIndex variants. BBQJIT calls all of these for wasm struct copy / memory init code. Each is the same pattern as the existing transfer32: load to a scratch register, store from it. * SIMD vector noop stubs (~44 methods). The RISC-V V vector extension is not part of the OpenWrt rv64gc baseline, and there is no SIMD codegen for RISCV64 in MacroAssemblerRISCV64.h. Options::useWasmSIMD is forced off on RISCV64, so the BBQJIT SIMD codepaths are unreachable; matching the existing pattern in this file, these are templated empty noops via MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD. vectorExtractLane / vectorReplaceLane cannot use the existing forwarding-reference noop macro: their first argument is a SIMDInfo bit-field (SIMDLane / SIMDSignMode) and bit-fields cannot bind to Args&&. They are defined as separate by-value templated overloads. The SIMDLane / SIMDSignMode type names are not visible in this header without pulling in , which we avoid for pure stubs, so the by-value template avoids naming the types altogether. * Wasm atomic hard-fault stubs (36 methods: loadLinkAcq{8,16,32,64}, storeCondRel{8,16,32,64}, branchAtomicStrongCAS{8,16,32,64}, atomicStrongCAS{8,16,32,64}, atomicXchg{,Add,Clear,Or,Xor}{8,16,32,64}). The RISC-V A extension is present in rv64gc, but the corresponding AMO / LR / SC instruction emitters have not been added to RISCV64Assembler.h. Wasm threads / shared memory is gated off at runtime via useSharedArrayBuffer = false (the default), which keeps wasm atomic opcodes off the JIT codepath, so these stubs are unreachable. A new MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD macro variant is added so an accidentally-reached atomic codepath traps loudly rather than silently miscompiling. This is the minimum surface needed to make BBQJIT on RISCV64 link and run non-SIMD, non-atomic wasm modules. The two stub groups (atomic and SIMD) are intentionally flagged in their comments so they are easy to find when implementing the real codegen in follow-up work. Signed-off-by: Daniel Golle --- a/Source/JavaScriptCore/assembler/MacroAssemblerRISCV64.h +++ b/Source/JavaScriptCore/assembler/MacroAssemblerRISCV64.h @@ -36,6 +36,16 @@ template void methodName(Args&&...) { } #define MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD_WITH_RETURN(methodName, returnType) \ template returnType methodName(Args&&...) { return { }; } +// Atomic / FP-minmax / SIMD methods needed by BBQJIT for which no native +// RISC-V code generation exists yet. These are deliberately runtime-fatal +// rather than silent noops: wasm shared memory and SIMD are gated off on +// RISCV64 (useSharedArrayBuffer / useWasmSIMD), so the BBQJIT codegen for +// atomic and SIMD wasm opcodes must never run. If something does reach +// these, we want to know with a loud crash, not a silent miscompilation. +#define MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(methodName) \ + template void methodName(Args&&...) { RELEASE_ASSERT_NOT_REACHED(); } +#define MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD_WITH_RETURN(methodName, returnType) \ + template returnType methodName(Args&&...) { RELEASE_ASSERT_NOT_REACHED(); return { }; } namespace JSC { @@ -837,6 +847,123 @@ public: MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(rotateRight32); MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(rotateRight64); + // Scalar BBQJIT primitives: fused shift/add and rotate-left, used by the + // wasm bytecode-to-machine-code path. RISC-V's baseline rv64gc has no + // rotate instruction (Zbb's rolw/rol would do it in one), so synthesize + // via shift + shift + or. + void addLeftShift64(RegisterID n, RegisterID m, TrustedImm32 amount, RegisterID d) + { + auto temp = temps(); + m_assembler.slliInsn(temp.data(), m, uint32_t(amount.m_value & 63)); + m_assembler.addInsn(d, n, temp.data()); + } + + void multiplyAddZeroExtend32(RegisterID mulLeft, RegisterID mulRight, RegisterID summand, RegisterID dest) + { + auto temp = temps(); + m_assembler.mulwInsn(temp.data(), mulLeft, mulRight); + m_assembler.maskRegister<32>(temp.data()); + m_assembler.addInsn(dest, summand, temp.data()); + } + + void rotateLeft32(RegisterID src, TrustedImm32 imm, RegisterID dest) + { + int32_t shift = imm.m_value & 31; + if (!shift) { + if (src != dest) + move(src, dest); + m_assembler.maskRegister<32>(dest); + return; + } + auto temp = temps(); + m_assembler.slliwInsn(temp.data(), src, uint32_t(shift)); + m_assembler.srliwInsn(temp.memory(), src, uint32_t(32 - shift)); + m_assembler.orInsn(dest, temp.data(), temp.memory()); + m_assembler.maskRegister<32>(dest); + } + + void rotateLeft32(RegisterID src, RegisterID shift, RegisterID dest) + { + auto temp = temps(); + m_assembler.addiInsn(temp.data(), RISCV64Registers::zero, Imm::I<32>()); + m_assembler.subInsn(temp.data(), temp.data(), shift); + m_assembler.sllwInsn(temp.memory(), src, shift); + m_assembler.srlwInsn(temp.data(), src, temp.data()); + m_assembler.orInsn(dest, temp.memory(), temp.data()); + m_assembler.maskRegister<32>(dest); + } + + void rotateLeft64(RegisterID src, TrustedImm32 imm, RegisterID dest) + { + int32_t shift = imm.m_value & 63; + if (!shift) { + if (src != dest) + move(src, dest); + return; + } + auto temp = temps(); + m_assembler.slliInsn(temp.data(), src, uint32_t(shift)); + m_assembler.srliInsn(temp.memory(), src, uint32_t(64 - shift)); + m_assembler.orInsn(dest, temp.data(), temp.memory()); + } + + void rotateLeft64(RegisterID src, RegisterID shift, RegisterID dest) + { + auto temp = temps(); + m_assembler.addiInsn(temp.data(), RISCV64Registers::zero, Imm::I<64>()); + m_assembler.subInsn(temp.data(), temp.data(), shift); + m_assembler.sllInsn(temp.memory(), src, shift); + m_assembler.srlInsn(temp.data(), src, temp.data()); + m_assembler.orInsn(dest, temp.memory(), temp.data()); + } + + // Integer divide / modulo via the RISC-V M extension (in rv64gc). + // The 32-bit forms produce a sign-extended result; mask to 32 bits. + void div32(RegisterID dividend, RegisterID divisor, RegisterID dest) + { + m_assembler.divwInsn(dest, dividend, divisor); + m_assembler.maskRegister<32>(dest); + } + + void uDiv32(RegisterID dividend, RegisterID divisor, RegisterID dest) + { + m_assembler.divuwInsn(dest, dividend, divisor); + m_assembler.maskRegister<32>(dest); + } + + void div64(RegisterID dividend, RegisterID divisor, RegisterID dest) + { + m_assembler.divInsn(dest, dividend, divisor); + } + + void uDiv64(RegisterID dividend, RegisterID divisor, RegisterID dest) + { + m_assembler.divuInsn(dest, dividend, divisor); + } + + // dest = minuend - (mulLeft * mulRight). Used by wasm i32/i64 rem + // (rem == lhs - (lhs/rhs) * rhs). RISC-V has no fused mul-sub. + void multiplySub32(RegisterID mulLeft, RegisterID mulRight, RegisterID minuend, RegisterID dest) + { + auto temp = temps(); + m_assembler.mulwInsn(temp.data(), mulLeft, mulRight); + m_assembler.subwInsn(dest, minuend, temp.data()); + m_assembler.maskRegister<32>(dest); + } + + void multiplySub64(RegisterID mulLeft, RegisterID mulRight, RegisterID minuend, RegisterID dest) + { + auto temp = temps(); + m_assembler.mulInsn(temp.data(), mulLeft, mulRight); + m_assembler.subInsn(dest, minuend, temp.data()); + } + + // uint32 -> single-precision float, fcvt.s.wu (one instruction). + void convertUInt32ToFloat(RegisterID src, FPRegisterID dest) + { + m_assembler.fcvtInsn(dest, src); + } + void load8(Address address, RegisterID dest) { auto resolution = resolveAddress(address, lazyTemp()); @@ -1425,6 +1552,73 @@ public: transfer64(src, dest); } + // 8- and 16-bit mem-to-mem transfers, and Address->BaseIndex + // variants of the existing widths, needed by BBQJIT (wasm + // struct copy / memory init etc.). Implemented in the same shape + // as the existing transfer32 / transfer64: load to a scratch + // register, store from it. + void transfer8(Address src, Address dest) + { + auto temp = temps(); + load8(src, temp.data()); + store8(temp.data(), dest); + } + + void transfer8(BaseIndex src, BaseIndex dest) + { + auto temp = temps(); + load8(src, temp.data()); + store8(temp.data(), dest); + } + + void transfer8(Address src, BaseIndex dest) + { + auto temp = temps(); + load8(src, temp.data()); + store8(temp.data(), dest); + } + + void transfer16(Address src, Address dest) + { + auto temp = temps(); + load16(src, temp.data()); + store16(temp.data(), dest); + } + + void transfer16(BaseIndex src, BaseIndex dest) + { + auto temp = temps(); + load16(src, temp.data()); + store16(temp.data(), dest); + } + + void transfer16(Address src, BaseIndex dest) + { + auto temp = temps(); + load16(src, temp.data()); + store16(temp.data(), dest); + } + + void transfer32(Address src, BaseIndex dest) + { + auto temp = temps(); + load32(src, temp.data()); + store32(temp.data(), dest); + } + + void transfer64(Address src, BaseIndex dest) + { + auto temp = temps(); + load64(src, temp.data()); + store64(temp.data(), dest); + } + + void transferVector(Address src, BaseIndex dest) + { + loadVector(src, fpTempRegister); + storeVector(fpTempRegister, dest); + } + void storePair32(RegisterID src1, RegisterID src2, RegisterID dest) { storePair32(src1, src2, dest, TrustedImm32(0)); @@ -2150,6 +2344,116 @@ public: MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorMulSat); MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorDotProduct); MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorSwizzle); + // Additional vector noop stubs needed by BBQJIT (kept unreachable via + // useWasmSIMD = false on RISCV64; see Options.cpp). + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(compareFloatingPointVectorUnordered); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(moveZeroToVector); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorAbsInt64); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorConvertLowSignedInt32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorConvertLowUnsignedInt32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorConvertUnsigned); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorExtaddPairwiseUnsignedInt16); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorExtractPair); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorHorizontalAdd); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorLoad8Splat); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorSshl); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorSshr8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorUshl); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorUshr8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorSwizzle2); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorTruncSatSignedFloat64); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorTruncSatUnsignedFloat32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorTruncSatUnsignedFloat64); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorUnsignedMax); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorUnsignedMin); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorUnzipEven); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorZipUpper); + + // Wasm atomics: the RISC-V A extension is available (the OpenWrt -march + // baseline is rv64gc, i.e. includes A), but the AMO/LR/SC instruction + // emitters in RISCV64Assembler.h have not been added yet. Stub the + // BBQJIT atomic API with hard-fault unimplemented methods: at runtime + // wasm shared memory is gated off via useSharedArrayBuffer = false, so + // wasm atomic opcodes are unreachable, and these stubs only ever exist + // for compile-time completeness. Filling these in (and adding the + // matching RISCV64Assembler.h emitters) is a follow-up that unlocks the + // wasm threads proposal on RISCV64. + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(loadLinkAcq8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(loadLinkAcq16); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(loadLinkAcq32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(loadLinkAcq64); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(storeCondRel8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(storeCondRel16); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(storeCondRel32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(storeCondRel64); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD_WITH_RETURN(branchAtomicStrongCAS8, Jump); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD_WITH_RETURN(branchAtomicStrongCAS16, Jump); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD_WITH_RETURN(branchAtomicStrongCAS32, Jump); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD_WITH_RETURN(branchAtomicStrongCAS64, Jump); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchg8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchg16); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchg32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchg64); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgAdd8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgAdd16); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgAdd32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgAdd64); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgClear8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgClear16); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgClear32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgClear64); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgOr8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgOr16); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgOr32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgOr64); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgXor8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgXor16); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgXor32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicXchgXor64); + // atomicStrongCAS{N}: the non-branching CAS overloads used by BBQJIT + // when the caller only needs success/failure in resultGPR (rather + // than a JIT-emitted branch). Same runtime-unreachable rationale as + // branchAtomicStrongCAS{N} above. + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicStrongCAS8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicStrongCAS16); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicStrongCAS32); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_UNIMPLEMENTED_METHOD(atomicStrongCAS64); + // Additional SIMD vector noop stubs uncovered by enabling BBQJIT. + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorSplat); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorUshl8); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorSshr); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorUshr); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorMulLow); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorMulHigh); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorFusedMulAdd); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorFusedNegMulAdd); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorLoad16Splat); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorLoad32Splat); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorLoad64Splat); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorLoad8Lane); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorLoad16Lane); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorLoad32Lane); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorLoad64Lane); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorStore8Lane); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorStore16Lane); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorStore32Lane); + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(vectorStore64Lane); + // vectorExtractLane / vectorReplaceLane: by-value templated stubs. + // The MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD form uses + // Args&& forwarding references; binding a SIMDInfo::lane / + // SIMDInfo::signMode bit-field to a non-const reference is + // ill-formed, so use by-value template parameters instead. + // (The SIMD type names are not visible in this header without + // pulling in , which we avoid for + // pure stubs.) Same unreachability rationale as the other SIMD + // stubs: useWasmSIMD = false on RISCV64. + template + void vectorExtractLane(T1, T2, T3, T4) { } + template + void vectorExtractLane(T1, T2, T3, T4, T5) { } + template + void vectorReplaceLane(T1, T2, T3, T4) { } + MACRO_ASSEMBLER_RISCV64_TEMPLATED_NOOP_METHOD(move128ToVector); template static CodePtr readCallTarget(CodeLocationCall call) @@ -3520,6 +3824,26 @@ public: m_assembler.fsgnjxInsn<64>(dest, src, src); } + void floatMin(FPRegisterID op1, FPRegisterID op2, FPRegisterID dest) + { + m_assembler.fminInsn<32>(dest, op1, op2); + } + + void floatMax(FPRegisterID op1, FPRegisterID op2, FPRegisterID dest) + { + m_assembler.fmaxInsn<32>(dest, op1, op2); + } + + void doubleMin(FPRegisterID op1, FPRegisterID op2, FPRegisterID dest) + { + m_assembler.fminInsn<64>(dest, op1, op2); + } + + void doubleMax(FPRegisterID op1, FPRegisterID op2, FPRegisterID dest) + { + m_assembler.fmaxInsn<64>(dest, op1, op2); + } + void ceilFloat(FPRegisterID src, FPRegisterID dest) { roundFP<32, RISCV64Assembler::FPRoundingMode::RUP>(src, dest);