Files
ppsspp/Core/MIPS/ARM64/Arm64IRAsm.cpp
T
Henrik RydgårdandClaude Opus 5.5 533baffb24 IR: Compute runs of VFPU dot products four at a time
A new IR op, Vec4VfpuMultiDot, does four vfpu_dot()s against one vector,
the other operands being the rows or Vec4s of a 4x4 block of registers.
The frontend emits it for vmmul and vtfm/vhtfm, and for runs of vdot that
fit it, like a matrix times a vector written out.

On ARM64 it calls a routine that has the dots in lanes rather than the
terms, so nothing is horizontal and nothing goes through the integer
registers. The block comes in with LD1, or LD4 for the transposed form,
when none of it is in host registers. The IR interpreter runs it through
vfpu_dot(); the other backends don't get it yet.

The CPU fuzzer gets runs of vdot shaped for it, and finds no difference
between the cores on them.

There's a routine per number of terms, so vdot.t runs don't pay for a
fourth. The vector's significands are worked out once and multiplied by
element, and the rounding at the end is UCVTF's in round-to-nearest,
which is what games run in; the integer version stays for the other
modes. The routine saves nothing, and the call site keeps only those
registers it clobbers that hold guest state.

JitDotBench, ns per dot: vtfm4.q 3.40, vmmul.q 3.12, a run of vdot.t
2.83, against 4.6 for single dots.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 11:02:17 +02:00

315 lines
11 KiB
C++

// Copyright (c) 2023- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include "ppsspp_config.h"
// In other words, PPSSPP_ARCH(ARM64) || DISASM_ALL.
#if PPSSPP_ARCH(ARM64) || (PPSSPP_PLATFORM(WINDOWS) && !defined(__LIBRETRO__))
#include "Common/Log.h"
#include "Core/CoreTiming.h"
#include "Core/MemMap.h"
#include "Core/MIPS/ARM64/Arm64VfpuDot.h"
#include "Core/MIPS/ARM64/Arm64IRJit.h"
#include "Core/MIPS/ARM64/Arm64IRRegCache.h"
#include "Core/MIPS/JitCommon/JitCommon.h"
#include "Core/MIPS/JitCommon/JitState.h"
#include "Core/Core.h"
namespace MIPSComp {
using namespace Arm64Gen;
using namespace Arm64IRJitConstants;
static const bool enableDebug = false;
static const bool enableDisasm = false;
static void ShowPC(void *membase, void *jitbase) {
static int count = 0;
if (currentMIPS) {
u32 downcount = currentMIPS->downcount;
ERROR_LOG(Log::JIT, "[%08x] ShowPC Downcount : %08x %d %p %p", currentMIPS->pc, downcount, count, membase, jitbase);
} else {
ERROR_LOG(Log::JIT, "Universe corrupt?");
}
//if (count > 2000)
// exit(0);
count++;
}
void Arm64JitBackend::GenerateFixedCode(MIPSState *mipsState) {
// This will be used as a writable scratch area, always 32-bit accessible.
const u8 *start = AlignCodePage();
if (DebugProfilerEnabled()) {
ProtectMemoryPages(start, GetMemoryProtectPageSize(), MEM_PROT_READ | MEM_PROT_WRITE);
hooks_.profilerPC = (uint32_t *)GetWritableCodePtr();
Write32(0);
hooks_.profilerStatus = (IRProfilerStatus *)GetWritableCodePtr();
Write32(0);
}
const u8 *disasmStart = AlignCodePage();
BeginWrite(GetMemoryProtectPageSize());
if (jo.useStaticAlloc) {
saveStaticRegisters_ = AlignCode16();
STR(INDEX_UNSIGNED, DOWNCOUNTREG, CTXREG, offsetof(MIPSState, downcount));
regs_.EmitSaveStaticRegisters();
RET();
loadStaticRegisters_ = AlignCode16();
regs_.EmitLoadStaticRegisters();
LDR(INDEX_UNSIGNED, DOWNCOUNTREG, CTXREG, offsetof(MIPSState, downcount));
RET();
} else {
saveStaticRegisters_ = nullptr;
loadStaticRegisters_ = nullptr;
}
restoreRoundingMode_ = AlignCode16();
{
MRS(SCRATCH2_64, FIELD_FPCR);
// We are not in flush-to-zero mode outside the JIT, so let's turn it off.
uint32_t mask = ~(4 << 22);
// Assume we're always in round-to-nearest mode beforehand.
mask &= ~(3 << 22);
ANDI2R(SCRATCH2, SCRATCH2, mask);
_MSR(FIELD_FPCR, SCRATCH2_64);
RET();
}
applyRoundingMode_ = AlignCode16();
{
LDR(INDEX_UNSIGNED, SCRATCH1, CTXREG, offsetof(MIPSState, fcr31));
ANDI2R(SCRATCH2, SCRATCH1, 3);
FixupBranch skip1 = TBZ(SCRATCH1, 24);
ADDI2R(SCRATCH2, SCRATCH2, 4);
SetJumpTarget(skip1);
// We can skip if the rounding mode is nearest (0) and flush is not set.
// (as restoreRoundingMode cleared it out anyway)
FixupBranch skip = CBZ(SCRATCH2);
// MIPS Rounding Mode: ARM Rounding Mode
// 0: Round nearest 0
// 1: Round to zero 3
// 2: Round up (ceil) 1
// 3: Round down (floor) 2
ANDI2R(SCRATCH1, SCRATCH2, 3);
CMPI2R(SCRATCH1, 1);
FixupBranch skipadd = B(CC_NEQ);
ADDI2R(SCRATCH2, SCRATCH2, 2);
SetJumpTarget(skipadd);
FixupBranch skipsub = B(CC_LE);
SUBI2R(SCRATCH2, SCRATCH2, 1);
SetJumpTarget(skipsub);
// Actually change the system FPCR register
MRS(SCRATCH1_64, FIELD_FPCR);
// Clear both flush-to-zero and rounding before re-setting them.
ANDI2R(SCRATCH1, SCRATCH1, ~((4 | 3) << 22));
ORR(SCRATCH1, SCRATCH1, SCRATCH2, ArithOption(SCRATCH2, ST_LSL, 22));
_MSR(FIELD_FPCR, SCRATCH1_64);
SetJumpTarget(skip);
RET();
}
updateRoundingMode_ = AlignCode16();
{
LDR(INDEX_UNSIGNED, SCRATCH1, CTXREG, offsetof(MIPSState, fcr31));
// Set SCRATCH2 to FZ:RM (FZ is bit 24, and RM are lowest 2 bits.)
ANDI2R(SCRATCH2, SCRATCH1, 3);
FixupBranch skip = TBZ(SCRATCH1, 24);
ADDI2R(SCRATCH2, SCRATCH2, 4);
SetJumpTarget(skip);
// Update currentRoundingFunc_ with the right convertS0ToSCRATCH1_ func.
MOVP2R(SCRATCH1_64, convertS0ToSCRATCH1_);
LSL(SCRATCH2, SCRATCH2, 3);
LDR(SCRATCH2_64, SCRATCH1_64, SCRATCH2);
MOVP2R(SCRATCH1_64, &currentRoundingFunc_);
STR(INDEX_UNSIGNED, SCRATCH2_64, SCRATCH1_64, 0);
RET();
}
hooks_.enterDispatcher = (IRNativeFuncNoArg)AlignCode16();
uint32_t regs_to_save = Arm64Gen::ALL_CALLEE_SAVED;
uint32_t regs_to_save_fp = Arm64Gen::ALL_CALLEE_SAVED_FP;
fp_.ABI_PushRegisters(regs_to_save, regs_to_save_fp);
// Fixed registers, these are always kept when in Jit context.
MOVP2R(MEMBASEREG, Memory::base);
MOVP2R(CTXREG, mipsState);
MOVP2R(JITBASEREG, GetBasePtr());
LoadStaticRegisters();
WriteDebugProfilerStatus(IRProfilerStatus::IN_JIT);
MovFromPC(SCRATCH1);
WriteDebugPC(SCRATCH1);
outerLoopPCInSCRATCH1_ = GetCodePtr();
MovToPC(SCRATCH1);
outerLoop_ = GetCodePtr();
SaveStaticRegisters(); // Advance can change the downcount, so must save/restore
RestoreRoundingMode(true);
WriteDebugProfilerStatus(IRProfilerStatus::TIMER_ADVANCE);
QuickCallFunctionR(SCRATCH1_64, &CoreTiming::Advance, CTXREG);
WriteDebugProfilerStatus(IRProfilerStatus::IN_JIT);
ApplyRoundingMode(true);
LoadStaticRegisters();
dispatcherCheckCoreState_ = GetCodePtr();
MOVP2R(SCRATCH1_64, &coreState);
LDR(INDEX_UNSIGNED, SCRATCH1, SCRATCH1_64, 0);
FixupBranch badCoreState = CBNZ(SCRATCH1);
// Check downcount.
TBNZ(DOWNCOUNTREG, 31, outerLoop_);
FixupBranch skipToRealDispatch = B();
dispatcherPCInSCRATCH1_ = GetCodePtr();
MovToPC(SCRATCH1);
hooks_.dispatcher = GetCodePtr();
FixupBranch bail = TBNZ(DOWNCOUNTREG, 31);
SetJumpTarget(skipToRealDispatch);
dispatcherNoCheck_ = GetCodePtr();
// Debug
if (enableDebug) {
MOV(W0, DOWNCOUNTREG);
MOV(X1, MEMBASEREG);
MOV(X2, JITBASEREG);
QuickCallFunction(SCRATCH1_64, &ShowPC);
}
MovFromPC(SCRATCH1);
WriteDebugPC(SCRATCH1);
ANDI2R(SCRATCH1, SCRATCH1, Memory::BLOCK_SHADOW_ENTRY_MASK);
MOVP2R(SCRATCH2_64, Memory::blockShadow);
hooks_.dispatchFetch = GetCodePtr();
LDR(SCRATCH1, SCRATCH2_64, SCRATCH1_64);
// The entry is the block's offset into the code space, or 0 for none.
FixupBranch skipJump = CBZ(SCRATCH1);
ADD(SCRATCH1_64, JITBASEREG, SCRATCH1_64);
BR(SCRATCH1_64);
SetJumpTarget(skipJump);
// No block found, let's jit. We don't need to save static regs, they're all callee saved.
// A replacement hook's block shadow entry jumps here too (GetBlockShadowHookValue()). JitAt
// replaces it with the new block's entry, and destroying that block puts the hook value back.
hooks_.blockShadowHook = GetCodePtr();
RestoreRoundingMode(true);
WriteDebugProfilerStatus(IRProfilerStatus::COMPILING);
QuickCallFunctionR(SCRATCH1_64, &MIPSComp::JitAt, CTXREG);
WriteDebugProfilerStatus(IRProfilerStatus::IN_JIT);
ApplyRoundingMode(true);
// Let's just dispatch again, we'll enter the block since we know it's there.
B(dispatcherNoCheck_);
SetJumpTarget(bail);
MOVP2R(SCRATCH1_64, &coreState);
LDR(INDEX_UNSIGNED, SCRATCH1, SCRATCH1_64, 0);
CBZ(SCRATCH1, outerLoop_);
const uint8_t *quitLoop = GetCodePtr();
SetJumpTarget(badCoreState);
WriteDebugProfilerStatus(IRProfilerStatus::NOT_RUNNING);
SaveStaticRegisters();
RestoreRoundingMode(true);
fp_.ABI_PopRegisters(regs_to_save, regs_to_save_fp);
RET();
hooks_.crashHandler = GetCodePtr();
MOVP2R(SCRATCH1_64, &coreState);
MOVI2R(SCRATCH2, CORE_RUNTIME_ERROR);
STR(INDEX_UNSIGNED, SCRATCH2, SCRATCH1_64, 0);
B(quitLoop);
// Generate some integer conversion funcs.
// MIPS order!
static const RoundingMode roundModes[8] = { ROUND_N, ROUND_Z, ROUND_P, ROUND_M, ROUND_N, ROUND_Z, ROUND_P, ROUND_M };
for (size_t i = 0; i < ARRAY_SIZE(roundModes); ++i) {
convertS0ToSCRATCH1_[i] = AlignCode16();
// Invert 0x80000000 -> 0x7FFFFFFF for the NAN result.
fp_.MVNI(32, EncodeRegToDouble(SCRATCHF2), 0x80, 24);
fp_.FCMP(S0, S0); // Detect NaN
fp_.FCVTS(S0, S0, roundModes[i]);
fp_.FCSEL(S0, S0, SCRATCHF2, CC_VC);
RET();
}
vfpuDotSpecial_ = GenerateArm64VfpuDotSpecial(this, &fp_, CTXREG);
vfpuDot_ = GenerateArm64VfpuDotCall(this, &fp_, CTXREG, vfpuDotSpecial_);
for (int terms = 2; terms <= 4; terms++)
vfpuMultiDot_[terms] = GenerateArm64VfpuMultiDot(this, &fp_, CTXREG, terms);
// Leave this at the end, add more stuff above.
if (enableDisasm) {
std::vector<std::string> lines = DisassembleArm64(disasmStart, (int)(GetCodePtr() - disasmStart));
for (auto s : lines) {
INFO_LOG(Log::JIT, "%s", s.c_str());
}
}
// Let's spare the pre-generated code from unprotect-reprotect.
AlignCodePage();
jitStartOffset_ = (int)(GetCodePtr() - start);
// Don't forget to zap the instruction cache! This must stay at the end of this function.
FlushIcache();
EndWrite();
// Update our current cached rounding mode func, too.
UpdateFCR31(mipsState);
}
void Arm64JitBackend::GetFixedCodeLabels(std::vector<IRNativeFixedCodeLabel> &labels) const {
labels.push_back({ outerLoop_, "outerLoop", false });
labels.push_back({ outerLoopPCInSCRATCH1_, "outerLoopPCInSCRATCH1", false });
labels.push_back({ dispatcherCheckCoreState_, "dispatcherCheckCoreState", false });
labels.push_back({ dispatcherPCInSCRATCH1_, "dispatcherPCInSCRATCH1", false });
labels.push_back({ dispatcherNoCheck_, "dispatcherNoCheck", false });
labels.push_back({ restoreRoundingMode_, "restoreRoundingMode", true });
labels.push_back({ applyRoundingMode_, "applyRoundingMode", true });
labels.push_back({ vfpuDot_, "vfpuDot", true });
labels.push_back({ updateRoundingMode_, "updateRoundingMode", true });
labels.push_back({ saveStaticRegisters_, "saveStaticRegisters", true });
labels.push_back({ loadStaticRegisters_, "loadStaticRegisters", true });
labels.push_back({ vfpuDotSpecial_, "vfpuDotSpecial", true });
labels.push_back({ vfpuMultiDot_[2], "vfpuMultiDot2", true });
labels.push_back({ vfpuMultiDot_[3], "vfpuMultiDot3", true });
labels.push_back({ vfpuMultiDot_[4], "vfpuMultiDot4", true });
for (int i = 0; i < 8; ++i)
labels.push_back({ convertS0ToSCRATCH1_[i], "convertS0ToSCRATCH1", true });
}
} // namespace MIPSComp
#endif