IR: Vec4 vmin, vmax, vslt, vsge and vi2f

New IR ops do them four lanes at a time where the backend has them, the
ARM64 JIT and the IR interpreter so far. vmin and vmax compare by
vfpu_minmax_key() in NEON, which took about twelve instructions a lane
through the integer registers before; vslt and vsge are a compare and an
AND with 1.0.

vi2f becomes one SCVTF with fractional bits.

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 11:02:17 +02:00
1 parent e33f38afa6
commit 108e66f2d1
8 files changed
+146 -1

No files matched your search

+47
View File
@@ -93,6 +93,53 @@ void Arm64JitBackend::CompIR_VecArith(IRInst inst) {
fp_.FABS(32, regs_.FQ(inst.dest), regs_.FQ(inst.src1));
break;
case IROp::Vec4Min:
case IROp::Vec4Max:
{
// vfpu_minmax_key() of each lane of both, compared as signed, picks the lanes. A tie
// takes src2. Q0-Q3 are temps the register cache never hands out.
regs_.Map(inst);
const ARM64Reg a = regs_.FQ(inst.src1), b = regs_.FQ(inst.src2);
MOVI2R(SCRATCH1, 0x7F800000);
fp_.DUP(32, Q3, SCRATCH1);
auto key = [&](ARM64Reg dest, ARM64Reg src) {
fp_.SSHR(32, Q2, src, 31);
fp_.USHR(32, Q2, Q2, 1);
fp_.EOR(dest, src, Q2);
fp_.CMTST(32, Q2, src, Q3);
fp_.AND(dest, dest, Q2);
};
key(Q0, a);
key(Q1, b);
if (inst.op == IROp::Vec4Min)
fp_.CMGT(32, Q0, Q1, Q0);
else
fp_.CMGT(32, Q0, Q0, Q1);
fp_.BSL(Q0, a, b);
fp_.MOV(regs_.FQ(inst.dest), Q0);
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 (inst.op == IROp::Vec4Slt)
fp_.FCMGT(32, Q0, regs_.FQ(inst.src2), regs_.FQ(inst.src1));
else
fp_.FCMGE(32, Q0, regs_.FQ(inst.src1), regs_.FQ(inst.src2));
fp_.FMOV(32, Q1, 0x70); // 1.0
fp_.AND(regs_.FQ(inst.dest), Q0, Q1);
break;
case IROp::Vec4CvtScaledSW:
regs_.Map(inst);
if ((inst.src2 & 0x1F) == 0)
fp_.SCVTF(32, regs_.FQ(inst.dest), regs_.FQ(inst.src1));
else
fp_.SCVTF(32, regs_.FQ(inst.dest), regs_.FQ(inst.src1), inst.src2 & 0x1F);
break;
default:
INVALIDOP;
break;
+18 -1
View File
@@ -1198,7 +1198,7 @@ namespace MIPSComp {
case VecDo3Op::VMAX:
case VecDo3Op::VSGE:
case VecDo3Op::VSLT:
allowSIMD = false;
allowSIMD = opts.moreVec4Ops;
break;
case VecDo3Op::INVALID: // Can't happen, but to avoid compiler warnings
break;
@@ -1234,6 +1234,18 @@ namespace MIPSComp {
case VecDo3Op::VMUL: // d[i] = s[i] * t[i]; break; //vmul
opFunc = IROp::Vec4Mul;
break;
case VecDo3Op::VMIN:
opFunc = IROp::Vec4Min;
break;
case VecDo3Op::VMAX:
opFunc = IROp::Vec4Max;
break;
case VecDo3Op::VSLT:
opFunc = IROp::Vec4Slt;
break;
case VecDo3Op::VSGE:
opFunc = IROp::Vec4Sge;
break;
default:
// Leave it Nop, disabled below.
break;
@@ -1500,6 +1512,11 @@ namespace MIPSComp {
u8 sregs[4], dregs[4], outregs[4];
GetVectorRegsPrefixS(sregs, sz, _VS);
GetVectorRegsPrefixD(dregs, sz, _VD);
if (opts.moreVec4Ops && IsVec4(sz, dregs) && IsVec4(sz, sregs)) {
ir.Write(IROp::Vec4CvtScaledSW, dregs[0], sregs[0], imm);
ApplyPrefixD(dregs, sz, _VD);
return;
}
// Through temps if a lane would be written before a later one reads it.
const bool direct = IsOverlapSafeAllowS(n, dregs, n, sregs);
for (int i = 0; i < n; i++)
+5
View File
@@ -162,6 +162,11 @@ static const IRMeta irMeta[] = {
{ IROp::Vec4VfpuMultiDot, "Vec4VfpuMultiDot", "VMVc" },
{ IROp::Vec4Neg, "Vec4Neg", "VV" },
{ IROp::Vec4Abs, "Vec4Abs", "VV" },
{ IROp::Vec4Min, "Vec4Min", "VVV" },
{ IROp::Vec4Max, "Vec4Max", "VVV" },
{ IROp::Vec4Slt, "Vec4Slt", "VVV" },
{ IROp::Vec4Sge, "Vec4Sge", "VVV" },
{ IROp::Vec4CvtScaledSW, "Vec4CvtScaledSW", "VVI" },
// Pack/Unpack
{ IROp::Vec2Unpack16To31, "Vec2Unpack16To31", "2F" }, // Note that the result is shifted down by 1, hence 31
+9
View File
@@ -191,6 +191,13 @@ enum class IROp : uint8_t {
Vec4VfpuMultiDot, // Four of them against one vector, see IRMultiDot.
Vec4Neg,
Vec4Abs,
// vmin/vmax (by vfpu_minmax_key, a tie taking src2), vslt and vsge (1.0 or 0.0, 0.0 for NaN).
Vec4Min,
Vec4Max,
Vec4Slt,
Vec4Sge,
// vi2f: each lane's int to float, divided by 2^src2.
Vec4CvtScaledSW,
// vx2i
Vec2Unpack16To31, // Note that the result is shifted down by 1, hence 31
@@ -448,6 +455,8 @@ struct IROptions {
bool vfpuMultiDot;
// Compile on past conditional branches (see IRFrontend::CanContinuePastBranch).
bool continueBranches;
// The backend has Vec4Min, Vec4Max, Vec4Slt, Vec4Sge and Vec4CvtScaledSW.
bool moreVec4Ops;
};
const IRMeta *GetIRMeta(IROp op);
+55
View File
@@ -171,6 +171,11 @@ u32 IRInterpret(MIPSState *mips, const IRInst *inst) {
dispatch[(int)IROp::FCmpVfpuAggregate] = &&L_FCmpVfpuAggregate;
dispatch[(int)IROp::FCmovVfpuCC] = &&L_FCmovVfpuCC;
dispatch[(int)IROp::Vec4Dot] = &&L_Vec4Dot;
dispatch[(int)IROp::Vec4Min] = &&L_Vec4Min;
dispatch[(int)IROp::Vec4Max] = &&L_Vec4Max;
dispatch[(int)IROp::Vec4Slt] = &&L_Vec4Slt;
dispatch[(int)IROp::Vec4Sge] = &&L_Vec4Sge;
dispatch[(int)IROp::Vec4CvtScaledSW] = &&L_Vec4CvtScaledSW;
dispatch[(int)IROp::Vec4VfpuDot] = &&L_Vec4VfpuDot;
dispatch[(int)IROp::Vec4VfpuMultiDot] = &&L_Vec4VfpuMultiDot;
dispatch[(int)IROp::FSin] = &&L_FSin;
@@ -583,6 +588,56 @@ u32 IRInterpret(MIPSState *mips, const IRInst *inst) {
IR_NEXT;
}
IR_CASE(Vec4Min)
{
float result[4];
for (int i = 0; i < 4; i++) {
const u32 a = mips->fi[inst->src1 + i], b = mips->fi[inst->src2 + i];
result[i] = vfpu_minmax_key(a) < vfpu_minmax_key(b) ? mips->f[inst->src1 + i] : mips->f[inst->src2 + i];
}
memcpy(&mips->f[inst->dest], result, sizeof(result));
IR_NEXT;
}
IR_CASE(Vec4Max)
{
float result[4];
for (int i = 0; i < 4; i++) {
const u32 a = mips->fi[inst->src1 + i], b = mips->fi[inst->src2 + i];
result[i] = vfpu_minmax_key(a) > vfpu_minmax_key(b) ? mips->f[inst->src1 + i] : mips->f[inst->src2 + i];
}
memcpy(&mips->f[inst->dest], result, sizeof(result));
IR_NEXT;
}
IR_CASE(Vec4Slt)
{
float result[4];
for (int i = 0; i < 4; i++)
result[i] = mips->f[inst->src1 + i] < mips->f[inst->src2 + i] ? 1.0f : 0.0f;
memcpy(&mips->f[inst->dest], result, sizeof(result));
IR_NEXT;
}
IR_CASE(Vec4Sge)
{
float result[4];
for (int i = 0; i < 4; i++)
result[i] = mips->f[inst->src1 + i] >= mips->f[inst->src2 + i] ? 1.0f : 0.0f;
memcpy(&mips->f[inst->dest], result, sizeof(result));
IR_NEXT;
}
IR_CASE(Vec4CvtScaledSW)
{
float result[4];
const float scale = 1.0f / (1UL << (inst->src2 & 0x1F));
for (int i = 0; i < 4; i++)
result[i] = (float)mips->fs[inst->src1 + i] * scale;
memcpy(&mips->f[inst->dest], result, sizeof(result));
IR_NEXT;
}
IR_CASE(Vec4Abs)
{
#if PPSSPP_ARCH(SSE2)
+2
View File
@@ -80,8 +80,10 @@ IRJit::IRJit(MIPSState *mipsState, bool actualJit) : frontend_(mipsState->HasDef
// The IR interpreter, and the backends with a routine for it.
#if PPSSPP_ARCH(ARM64)
opts.vfpuMultiDot = true;
opts.moreVec4Ops = true;
#else
opts.vfpuMultiDot = !actualJit;
opts.moreVec4Ops = !actualJit;
#endif
opts.continueBranches = true;
frontend_.SetOptions(opts);
+5
View File
@@ -422,6 +422,11 @@ void IRNativeBackend::CompileIRInst(IRInst inst) {
case IROp::Vec4Scale:
case IROp::Vec4Neg:
case IROp::Vec4Abs:
case IROp::Vec4Min:
case IROp::Vec4Max:
case IROp::Vec4Slt:
case IROp::Vec4Sge:
case IROp::Vec4CvtScaledSW:
CompIR_VecArith(inst);
break;
+5
View File
@@ -854,6 +854,11 @@ bool PropagateConstants(const IRWriter &in, IRWriter &out, const IROptions &opts
case IROp::Vec4Blend:
case IROp::Vec4Neg:
case IROp::Vec4Abs:
case IROp::Vec4Min:
case IROp::Vec4Max:
case IROp::Vec4Slt:
case IROp::Vec4Sge:
case IROp::Vec4CvtScaledSW:
case IROp::Vec4Pack31To8:
case IROp::Vec4Pack32To8:
case IROp::Vec2Pack32To16: