RISC-V and LoongArch64 IR JITs: Vec4 vmin, vmax, vslt, vsge and vi2f

The ops from 108e66f2d1, so both now get moreVec4Ops. With LSX they're
vector code: vfpu_minmax_key() of each lane picked by VBITSEL, a compare
ANDed with 1.0, and VFFINT with a scale. RISC-V and LoongArch without LSX
do them lane by lane, vslt and vsge as a compare into a GPR rather than
through fpcond.

Vec4VfpuMultiDot stays ARM64-only, so the frontend still doesn't emit it
for these.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-15 15:17:50 +02:00
1 parent 59b6b4de78
commit a80cea2b9e
4 files changed
+131

No files matched your search

+3
View File
@@ -81,6 +81,9 @@ IRJit::IRJit(MIPSState *mipsState, bool actualJit) : frontend_(mipsState->HasDef
#if PPSSPP_ARCH(ARM64)
opts.vfpuMultiDot = true;
opts.moreVec4Ops = true;
#elif PPSSPP_ARCH(RISCV64) || PPSSPP_ARCH(LOONGARCH64)
opts.vfpuMultiDot = !actualJit;
opts.moreVec4Ops = true;
#else
opts.vfpuMultiDot = !actualJit;
opts.moreVec4Ops = !actualJit;
@@ -346,6 +346,91 @@ void LoongArch64JitBackend::CompIR_VecArith(IRInst inst) {
FABS_S(regs_.F(inst.dest + i), regs_.F(inst.src1 + i));
break;
case IROp::Vec4Min:
case IROp::Vec4Max:
if (cpu_info.LOONGARCH_LSX) {
// vfpu_minmax_key() of each lane of both, compared as signed, picks the lanes. A tie
// takes src2.
const LoongArch64Reg temp = EncodeRegToV(regs_.MapWithFPRTemp(inst));
const LoongArch64Reg a = regs_.V(inst.src1), b = regs_.V(inst.src2);
const LoongArch64Reg keyA = EncodeRegToV(SCRATCHF1), keyB = EncodeRegToV(SCRATCHF2);
auto key = [&](LoongArch64Reg k, LoongArch64Reg src) {
VSRAI_W(temp, src, 31);
VSRLI_W(temp, temp, 1);
VXOR_V(k, src, temp);
// Zero if the exponent is.
VSLLI_W(temp, src, 1);
VSRLI_W(temp, temp, 24);
VSEQI_W(temp, temp, 0);
VANDN_V(k, temp, k);
};
key(keyA, a);
key(keyB, b);
if (inst.op == IROp::Vec4Min)
VSLT_W(keyA, keyA, keyB);
else
VSLT_W(keyA, keyB, keyA);
VBITSEL_V(regs_.V(inst.dest), b, a, keyA);
} else {
// As FMin and FMax, lane by lane. The lanes line up, so overlap doesn't matter.
for (int i = 0; i < 4; ++i) {
IRInst lane{ inst.op == IROp::Vec4Min ? IROp::FMin : IROp::FMax };
lane.dest = inst.dest + i;
lane.src1 = inst.src1 + i;
lane.src2 = inst.src2 + i;
CompIR_FCondAssign(lane);
}
}
break;
case IROp::Vec4Slt:
case IROp::Vec4Sge:
// 1.0 where src1 < src2 (vslt) or src1 >= src2 (vsge), 0.0 otherwise, NaN included.
regs_.Map(inst);
if (cpu_info.LOONGARCH_LSX) {
if (inst.op == IROp::Vec4Slt)
VFCMP_CLT_S(EncodeRegToV(SCRATCHF1), regs_.V(inst.src1), regs_.V(inst.src2));
else
VFCMP_CLE_S(EncodeRegToV(SCRATCHF1), regs_.V(inst.src2), regs_.V(inst.src1));
LU12I_W(SCRATCH1, 0x3F800000 >> 12);
VREPLGR2VR_W(EncodeRegToV(SCRATCHF2), SCRATCH1);
VAND_V(regs_.V(inst.dest), EncodeRegToV(SCRATCHF1), EncodeRegToV(SCRATCHF2));
} else {
QuickFLI(32, SCRATCHF1, 1.0f, SCRATCH1);
MOVGR2FR_W(SCRATCHF2, R_ZERO);
for (int i = 0; i < 4; ++i) {
if (inst.op == IROp::Vec4Slt)
FCMP_COND_S(FCC0, regs_.F(inst.src1 + i), regs_.F(inst.src2 + i), LoongArch64Fcond::CLT);
else
FCMP_COND_S(FCC0, regs_.F(inst.src2 + i), regs_.F(inst.src1 + i), LoongArch64Fcond::CLE);
FSEL(regs_.F(inst.dest + i), SCRATCHF2, SCRATCHF1, FCC0);
}
}
break;
case IROp::Vec4CvtScaledSW:
regs_.Map(inst);
if (cpu_info.LOONGARCH_LSX) {
VFFINT_S_W(regs_.V(inst.dest), regs_.V(inst.src1));
if ((inst.src2 & 0x1F) != 0) {
float scale = 1.0f / (1UL << (inst.src2 & 0x1F));
u32 bits;
memcpy(&bits, &scale, 4);
LI(SCRATCH1, (s32)bits);
VREPLGR2VR_W(EncodeRegToV(SCRATCHF1), SCRATCH1);
VFMUL_S(regs_.V(inst.dest), regs_.V(inst.dest), EncodeRegToV(SCRATCHF1));
}
} else {
if ((inst.src2 & 0x1F) != 0)
QuickFLI(32, SCRATCHF1, 1.0f / (1UL << (inst.src2 & 0x1F)), SCRATCH1);
for (int i = 0; i < 4; ++i) {
FFINT_S_W(regs_.F(inst.dest + i), regs_.F(inst.src1 + i));
if ((inst.src2 & 0x1F) != 0)
FMUL_S(regs_.F(inst.dest + i), regs_.F(inst.dest + i), SCRATCHF1);
}
}
break;
default:
INVALIDOP;
break;
+39
View File
@@ -283,6 +283,45 @@ void RiscVJitBackend::CompIR_VecArith(IRInst inst) {
FABS(32, regs_.F(inst.dest + i), regs_.F(inst.src1 + i));
break;
case IROp::Vec4Min:
case IROp::Vec4Max:
// As FMin and FMax, lane by lane. The lanes line up, so overlap doesn't matter.
for (int i = 0; i < 4; ++i) {
IRInst lane{ inst.op == IROp::Vec4Min ? IROp::FMin : IROp::FMax };
lane.dest = inst.dest + i;
lane.src1 = inst.src1 + i;
lane.src2 = inst.src2 + i;
CompIR_FCondAssign(lane);
}
break;
case IROp::Vec4Slt:
case IROp::Vec4Sge:
// FLT and FLE are false for NaN, as both want.
regs_.Map(inst);
for (int i = 0; i < 4; ++i) {
if (inst.op == IROp::Vec4Slt)
FLT(32, SCRATCH1, regs_.F(inst.src1 + i), regs_.F(inst.src2 + i));
else
FLE(32, SCRATCH1, regs_.F(inst.src2 + i), regs_.F(inst.src1 + i));
FCVT(FConv::S, FConv::W, regs_.F(inst.dest + i), SCRATCH1);
}
break;
case IROp::Vec4CvtScaledSW:
{
const RiscVReg scaleReg = regs_.MapWithFPRTemp(inst);
if ((inst.src2 & 0x1F) != 0)
QuickFLI(32, scaleReg, 1.0f / (1UL << (inst.src2 & 0x1F)), SCRATCH1);
for (int i = 0; i < 4; ++i) {
FMV(FMv::X, FMv::W, SCRATCH1, regs_.F(inst.src1 + i));
FCVT(FConv::S, FConv::W, regs_.F(inst.dest + i), SCRATCH1);
if ((inst.src2 & 0x1F) != 0)
FMUL(32, regs_.F(inst.dest + i), regs_.F(inst.dest + i), scaleReg);
}
break;
}
default:
INVALIDOP;
break;
+4
View File
@@ -280,6 +280,10 @@ against the reference on `VFPUDot`'s inputs. Whether the RVV version beats the s
hardware hasn't been measured: it moves values between the vector and scalar units three times,
which costs more on some cores than on others.
Neither JIT has `Vec4VfpuMultiDot`, so the frontend leaves runs of dots as single
`Vec4VfpuDot`s for them (`IROptions::vfpuMultiDot`). Both have the Vec4 vmin, vmax, vslt, vsge and
vi2f ops (`IROptions::moreVec4Ops`), lane by lane on RISC-V and on LoongArch without LSX.
Before the accurate dot product, the JIT gave the same end state as the IR interpreter in 20000
VFPU-heavy cases (`vvvf`), 20000 mixed and 10000 integer/memory ones, on RISC-V with and without
the extensions and on LoongArch without LSX. What was left were host NaNs out of a horizontal op