ppsspp/Core/MIPS/IR/IRRegCache.cpp
2023-09-24 17:31:25 -07:00

1268 lines
37 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/.
#ifndef offsetof
#include <cstddef>
#endif
#include <cstring>
#include "Common/Log.h"
#include "Common/LogReporting.h"
#include "Core/MemMap.h"
#include "Core/MIPS/IR/IRAnalysis.h"
#include "Core/MIPS/IR/IRRegCache.h"
#include "Core/MIPS/IR/IRInst.h"
#include "Core/MIPS/IR/IRJit.h"
#include "Core/MIPS/JitCommon/JitState.h"
void IRImmRegCache::Flush(IRReg rd) {
if (isImm_[rd]) {
if (rd == 0) {
return;
}
_assert_((rd > 0 && rd < 32) || (rd >= IRTEMP_0 && rd < IRREG_VFPU_CTRL_BASE));
ir_->WriteSetConstant(rd, immVal_[rd]);
isImm_[rd] = false;
}
}
void IRImmRegCache::Discard(IRReg rd) {
if (rd == 0) {
return;
}
isImm_[rd] = false;
}
IRImmRegCache::IRImmRegCache(IRWriter *ir) : ir_(ir) {
memset(&isImm_, 0, sizeof(isImm_));
memset(&immVal_, 0, sizeof(immVal_));
isImm_[0] = true;
ir_ = ir;
}
void IRImmRegCache::FlushAll() {
for (int i = 1; i < TOTAL_MAPPABLE_IRREGS; ) {
if (isImm_[i]) {
Flush(i);
}
// Most of the time, lots are not. This speeds it up a lot.
bool *next = (bool *)memchr(&isImm_[i], 1, TOTAL_MAPPABLE_IRREGS - i);
if (!next)
break;
i = (int)(next - &isImm_[0]);
}
}
void IRImmRegCache::MapIn(IRReg rd) {
Flush(rd);
}
void IRImmRegCache::MapDirty(IRReg rd) {
Discard(rd);
}
void IRImmRegCache::MapInIn(IRReg rs, IRReg rt) {
Flush(rs);
Flush(rt);
}
void IRImmRegCache::MapInInIn(IRReg rd, IRReg rs, IRReg rt) {
Flush(rd);
Flush(rs);
Flush(rt);
}
void IRImmRegCache::MapDirtyIn(IRReg rd, IRReg rs) {
if (rs != rd) {
Discard(rd);
}
Flush(rs);
}
void IRImmRegCache::MapDirtyInIn(IRReg rd, IRReg rs, IRReg rt) {
if (rs != rd && rt != rd) {
Discard(rd);
}
Flush(rs);
Flush(rt);
}
IRNativeRegCacheBase::IRNativeRegCacheBase(MIPSComp::JitOptions *jo)
: jo_(jo) {}
void IRNativeRegCacheBase::Start(MIPSComp::IRBlock *irBlock) {
if (!initialReady_) {
SetupInitialRegs();
initialReady_ = true;
}
memcpy(nr, nrInitial_, sizeof(nr[0]) * config_.totalNativeRegs);
memcpy(mr, mrInitial_, sizeof(mr));
int numStatics;
const StaticAllocation *statics = GetStaticAllocations(numStatics);
for (int i = 0; i < numStatics; i++) {
nr[statics[i].nr].mipsReg = statics[i].mr;
nr[statics[i].nr].pointerified = statics[i].pointerified && jo_->enablePointerify;
nr[statics[i].nr].normalized32 = statics[i].normalized32;
mr[statics[i].mr].loc = statics[i].loc;
mr[statics[i].mr].nReg = statics[i].nr;
mr[statics[i].mr].isStatic = true;
// Lock it until the very end.
mr[statics[i].mr].spillLockIRIndex = irBlock->GetNumInstructions();
}
irBlock_ = irBlock;
irIndex_ = 0;
}
void IRNativeRegCacheBase::SetupInitialRegs() {
_assert_msg_(config_.totalNativeRegs > 0, "totalNativeRegs was never set by backend");
// Everything else is initialized in the struct.
mrInitial_[MIPS_REG_ZERO].loc = MIPSLoc::IMM;
mrInitial_[MIPS_REG_ZERO].imm = 0;
}
bool IRNativeRegCacheBase::IsGPRInRAM(IRReg gpr) {
_dbg_assert_(IsValidGPR(gpr));
return mr[gpr].loc == MIPSLoc::MEM;
}
bool IRNativeRegCacheBase::IsFPRInRAM(IRReg fpr) {
_dbg_assert_(IsValidFPR(fpr));
return mr[fpr + 32].loc == MIPSLoc::MEM;
}
bool IRNativeRegCacheBase::IsGPRMapped(IRReg gpr) {
_dbg_assert_(IsValidGPR(gpr));
return mr[gpr].loc == MIPSLoc::REG || mr[gpr].loc == MIPSLoc::REG_IMM;
}
bool IRNativeRegCacheBase::IsFPRMapped(IRReg fpr) {
_dbg_assert_(IsValidFPR(fpr));
return mr[fpr + 32].loc == MIPSLoc::FREG || mr[fpr + 32].loc == MIPSLoc::VREG;
}
int IRNativeRegCacheBase::GetFPRLaneCount(IRReg fpr) {
if (!IsFPRMapped(fpr))
return 0;
if (mr[fpr + 32].lane == -1)
return 1;
IRReg base = fpr + 32 - mr[fpr + 32].lane;
int c = 1;
for (int i = 1; i < 4; ++i) {
if (mr[base + i].nReg != mr[base].nReg || mr[base + i].loc != mr[base].loc)
return c;
if (mr[base + i].lane != i)
return c;
c++;
}
return c;
}
int IRNativeRegCacheBase::GetFPRLane(IRReg fpr) {
_dbg_assert_(IsValidFPR(fpr));
if (mr[fpr + 32].loc == MIPSLoc::FREG || mr[fpr + 32].loc == MIPSLoc::VREG) {
int l = mr[fpr + 32].lane;
return l == -1 ? 0 : l;
}
return -1;
}
bool IRNativeRegCacheBase::IsGPRMappedAsPointer(IRReg gpr) {
_dbg_assert_(IsValidGPR(gpr));
if (mr[gpr].loc == MIPSLoc::REG) {
return nr[mr[gpr].nReg].pointerified;
} else if (mr[gpr].loc == MIPSLoc::REG_IMM) {
_assert_msg_(!nr[mr[gpr].nReg].pointerified, "Really shouldn't be pointerified here");
} else if (mr[gpr].loc == MIPSLoc::REG_AS_PTR) {
return true;
}
return false;
}
bool IRNativeRegCacheBase::IsGPRMappedAsStaticPointer(IRReg gpr) {
if (IsGPRMappedAsPointer(gpr)) {
return mr[gpr].isStatic;
}
return false;
}
bool IRNativeRegCacheBase::IsGPRImm(IRReg gpr) {
_dbg_assert_(IsValidGPR(gpr));
if (gpr == MIPS_REG_ZERO)
return true;
return mr[gpr].loc == MIPSLoc::IMM || mr[gpr].loc == MIPSLoc::REG_IMM;
}
bool IRNativeRegCacheBase::IsGPR2Imm(IRReg base) {
return IsGPRImm(base) && IsGPRImm(base + 1);
}
uint32_t IRNativeRegCacheBase::GetGPRImm(IRReg gpr) {
_dbg_assert_(IsValidGPR(gpr));
if (gpr == MIPS_REG_ZERO)
return 0;
if (mr[gpr].loc != MIPSLoc::IMM && mr[gpr].loc != MIPSLoc::REG_IMM) {
_assert_msg_(mr[gpr].loc == MIPSLoc::IMM || mr[gpr].loc == MIPSLoc::REG_IMM, "GPR %d not in an imm", gpr);
}
return mr[gpr].imm;
}
uint64_t IRNativeRegCacheBase::GetGPR2Imm(IRReg base) {
return (uint64_t)GetGPRImm(base) | ((uint64_t)GetGPRImm(base + 1) << 32);
}
void IRNativeRegCacheBase::SetGPRImm(IRReg gpr, uint32_t immVal) {
_dbg_assert_(IsValidGPR(gpr));
if (gpr == MIPS_REG_ZERO && immVal != 0) {
ERROR_LOG_REPORT(JIT, "Trying to set immediate %08x to r0", immVal);
return;
}
if (mr[gpr].loc == MIPSLoc::REG_IMM && mr[gpr].imm == immVal) {
// Already have that value, let's keep it in the reg.
return;
}
if (mr[gpr].nReg != -1) {
// Zap existing value if cached in a reg.
_assert_msg_(mr[gpr].lane == -1, "Should not be a multilane reg");
DiscardNativeReg(mr[gpr].nReg);
}
mr[gpr].loc = MIPSLoc::IMM;
mr[gpr].imm = immVal;
}
void IRNativeRegCacheBase::SetGPR2Imm(IRReg base, uint64_t immVal) {
_dbg_assert_(IsValidGPRNoZero(base));
uint32_t imm0 = (uint32_t)(immVal & 0xFFFFFFFF);
uint32_t imm1 = (uint32_t)(immVal >> 32);
if (IsGPRImm(base) && IsGPRImm(base + 1) && GetGPRImm(base) == imm0 && GetGPRImm(base + 1) == imm1) {
// Already set to this, don't bother.
return;
}
if (mr[base].nReg != -1) {
// Zap existing value if cached in a reg.
DiscardNativeReg(mr[base].nReg);
if (mr[base + 1].nReg != -1)
DiscardNativeReg(mr[base + 1].nReg);
}
mr[base].loc = MIPSLoc::IMM;
mr[base].imm = imm0;
mr[base + 1].loc = MIPSLoc::IMM;
mr[base + 1].imm = imm1;
}
void IRNativeRegCacheBase::SpillLockGPR(IRReg r1, IRReg r2, IRReg r3, IRReg r4) {
_dbg_assert_(IsValidGPR(r1));
_dbg_assert_(r2 == IRREG_INVALID || IsValidGPR(r2));
_dbg_assert_(r3 == IRREG_INVALID || IsValidGPR(r3));
_dbg_assert_(r4 == IRREG_INVALID || IsValidGPR(r4));
SetSpillLockIRIndex(r1, r2, r3, r4, 0, irIndex_);
}
void IRNativeRegCacheBase::SpillLockFPR(IRReg r1, IRReg r2, IRReg r3, IRReg r4) {
_dbg_assert_(IsValidFPR(r1));
_dbg_assert_(r2 == IRREG_INVALID || IsValidFPR(r2));
_dbg_assert_(r3 == IRREG_INVALID || IsValidFPR(r3));
_dbg_assert_(r4 == IRREG_INVALID || IsValidFPR(r4));
SetSpillLockIRIndex(r1, r2, r3, r4, 32, irIndex_);
}
void IRNativeRegCacheBase::ReleaseSpillLockGPR(IRReg r1, IRReg r2, IRReg r3, IRReg r4) {
_dbg_assert_(IsValidGPR(r1));
_dbg_assert_(r2 == IRREG_INVALID || IsValidGPR(r2));
_dbg_assert_(r3 == IRREG_INVALID || IsValidGPR(r3));
_dbg_assert_(r4 == IRREG_INVALID || IsValidGPR(r4));
SetSpillLockIRIndex(r1, r2, r3, r4, 0, -1);
}
void IRNativeRegCacheBase::ReleaseSpillLockFPR(IRReg r1, IRReg r2, IRReg r3, IRReg r4) {
_dbg_assert_(IsValidFPR(r1));
_dbg_assert_(r2 == IRREG_INVALID || IsValidFPR(r2));
_dbg_assert_(r3 == IRREG_INVALID || IsValidFPR(r3));
_dbg_assert_(r4 == IRREG_INVALID || IsValidFPR(r4));
SetSpillLockIRIndex(r1, r2, r3, r4, 32, -1);
}
void IRNativeRegCacheBase::SetSpillLockIRIndex(IRReg r1, IRReg r2, IRReg r3, IRReg r4, int offset, int index) {
if (!mr[r1 + offset].isStatic)
mr[r1 + offset].spillLockIRIndex = index;
if (r2 != IRREG_INVALID && !mr[r2 + offset].isStatic)
mr[r2 + offset].spillLockIRIndex = index;
if (r3 != IRREG_INVALID && !mr[r3 + offset].isStatic)
mr[r3 + offset].spillLockIRIndex = index;
if (r4 != IRREG_INVALID && !mr[r4 + offset].isStatic)
mr[r4 + offset].spillLockIRIndex = index;
}
void IRNativeRegCacheBase::SetSpillLockIRIndex(IRReg r1, int index) {
if (!mr[r1].isStatic)
mr[r1].spillLockIRIndex = index;
}
void IRNativeRegCacheBase::MarkGPRDirty(IRReg gpr, bool andNormalized32) {
_assert_(IsGPRMapped(gpr));
if (!IsGPRMapped(gpr))
return;
IRNativeReg nreg = mr[gpr].nReg;
nr[nreg].isDirty = true;
nr[nreg].normalized32 = andNormalized32;
// If reg is written to, pointerification is assumed lost.
nr[nreg].pointerified = false;
if (mr[gpr].loc == MIPSLoc::REG_AS_PTR || mr[gpr].loc == MIPSLoc::REG_IMM) {
mr[gpr].loc = MIPSLoc::REG;
mr[gpr].imm = -1;
}
_dbg_assert_(mr[gpr].loc == MIPSLoc::REG);
}
void IRNativeRegCacheBase::MarkGPRAsPointerDirty(IRReg gpr) {
_assert_(IsGPRMappedAsPointer(gpr));
if (!IsGPRMappedAsPointer(gpr))
return;
#ifdef MASKED_PSP_MEMORY
if (mr[gpr].loc == MIPSLoc::REG_AS_PTR) {
_assert_msg_(false, "MarkGPRAsPointerDirty is not possible when using MASKED_PSP_MEMORY");
}
#endif
IRNativeReg nreg = mr[gpr].nReg;
_dbg_assert_(!nr[nreg].normalized32);
nr[nreg].isDirty = true;
// Stays pointerified or REG_AS_PTR.
}
IRNativeReg IRNativeRegCacheBase::AllocateReg(MIPSLoc type, MIPSMap flags) {
_dbg_assert_(type == MIPSLoc::REG || type == MIPSLoc::FREG || type == MIPSLoc::VREG);
IRNativeReg nreg = FindFreeReg(type, flags);
if (nreg != -1)
return nreg;
// Still nothing. Let's spill a reg and goto 10.
bool clobbered;
IRNativeReg bestToSpill = FindBestToSpill(type, flags, true, &clobbered);
if (bestToSpill == -1) {
bestToSpill = FindBestToSpill(type, flags, false, &clobbered);
}
if (bestToSpill != -1) {
if (clobbered) {
DiscardNativeReg(bestToSpill);
} else {
FlushNativeReg(bestToSpill);
}
// Now one must be free.
return FindFreeReg(type, flags);
}
// Uh oh, we have all of them spilllocked....
ERROR_LOG_REPORT(JIT, "Out of spillable registers in block PC %08x, index %d", irBlock_->GetOriginalStart(), irIndex_);
_assert_(bestToSpill != -1);
return -1;
}
IRNativeReg IRNativeRegCacheBase::FindFreeReg(MIPSLoc type, MIPSMap flags) const {
int allocCount = 0, base = 0;
const int *allocOrder = GetAllocationOrder(type, flags, allocCount, base);
for (int i = 0; i < allocCount; i++) {
IRNativeReg nreg = IRNativeReg(allocOrder[i] - base);
if (nr[nreg].mipsReg == IRREG_INVALID && nr[nreg].tempLockIRIndex < irIndex_) {
return nreg;
}
}
return -1;
}
bool IRNativeRegCacheBase::IsGPRClobbered(IRReg gpr) const {
_dbg_assert_(IsValidGPR(gpr));
return IsRegClobbered(MIPSLoc::REG, gpr);
}
bool IRNativeRegCacheBase::IsFPRClobbered(IRReg fpr) const {
_dbg_assert_(IsValidFPR(fpr));
return IsRegClobbered(MIPSLoc::FREG, fpr + 32);
}
IRUsage IRNativeRegCacheBase::GetNextRegUsage(const IRSituation &info, MIPSLoc type, IRReg r) const {
if (type == MIPSLoc::REG)
return IRNextGPRUsage(r, info);
else if (type == MIPSLoc::FREG || type == MIPSLoc::VREG)
return IRNextFPRUsage(r - 32, info);
_assert_msg_(false, "Unknown spill allocation type");
return IRUsage::UNKNOWN;
}
bool IRNativeRegCacheBase::IsRegClobbered(MIPSLoc type, IRReg r) const {
static const int UNUSED_LOOKAHEAD_OPS = 30;
IRSituation info;
info.lookaheadCount = UNUSED_LOOKAHEAD_OPS;
// We look starting one ahead, unlike spilling. We want to know if it clobbers later.
info.currentIndex = irIndex_ + 1;
info.instructions = irBlock_->GetInstructions();
info.numInstructions = irBlock_->GetNumInstructions();
// Make sure we're on the first one if this is multi-lane.
IRReg first = r;
if (mr[r].lane != -1)
first -= mr[r].lane;
IRUsage usage = GetNextRegUsage(info, type, first);
if (usage == IRUsage::CLOBBERED) {
// If multiple mips regs use this native reg (i.e. vector, HI/LO), check each.
bool canClobber = true;
for (IRReg m = first + 1; mr[m].nReg == mr[first].nReg && m < IRREG_INVALID && canClobber; ++m)
canClobber = GetNextRegUsage(info, type, m) == IRUsage::CLOBBERED;
return canClobber;
}
return false;
}
bool IRNativeRegCacheBase::IsRegRead(MIPSLoc type, IRReg first) const {
static const int UNUSED_LOOKAHEAD_OPS = 30;
IRSituation info;
info.lookaheadCount = UNUSED_LOOKAHEAD_OPS;
// We look starting one ahead, unlike spilling.
info.currentIndex = irIndex_ + 1;
info.instructions = irBlock_->GetInstructions();
info.numInstructions = irBlock_->GetNumInstructions();
// Note: this intentionally doesn't look at the full reg, only the lane.
IRUsage usage = GetNextRegUsage(info, type, first);
return usage == IRUsage::READ;
}
IRNativeReg IRNativeRegCacheBase::FindBestToSpill(MIPSLoc type, MIPSMap flags, bool unusedOnly, bool *clobbered) const {
int allocCount = 0, base = 0;
const int *allocOrder = GetAllocationOrder(type, flags, allocCount, base);
static const int UNUSED_LOOKAHEAD_OPS = 30;
IRSituation info;
info.lookaheadCount = UNUSED_LOOKAHEAD_OPS;
info.currentIndex = irIndex_;
info.instructions = irBlock_->GetInstructions();
info.numInstructions = irBlock_->GetNumInstructions();
*clobbered = false;
for (int i = 0; i < allocCount; i++) {
IRNativeReg nreg = IRNativeReg(allocOrder[i] - base);
if (nr[nreg].mipsReg != IRREG_INVALID && mr[nr[nreg].mipsReg].spillLockIRIndex >= irIndex_)
continue;
if (nr[nreg].tempLockIRIndex >= irIndex_)
continue;
// As it's in alloc-order, we know it's not static so we don't need to check for that.
IRReg mipsReg = nr[nreg].mipsReg;
IRUsage usage = GetNextRegUsage(info, type, mipsReg);
// Awesome, a clobbered reg. Let's use it?
if (usage == IRUsage::CLOBBERED) {
// If multiple mips regs use this native reg (i.e. vector, HI/LO), check each.
// Note: mipsReg points to the lowest numbered IRReg.
bool canClobber = true;
for (IRReg m = mipsReg + 1; mr[m].nReg == nreg && m < IRREG_INVALID && canClobber; ++m)
canClobber = GetNextRegUsage(info, type, m) == IRUsage::CLOBBERED;
// Okay, if all can be clobbered, we're good to go.
if (canClobber) {
*clobbered = true;
return nreg;
}
}
// Not awesome. A used reg. Let's try to avoid spilling.
if (!unusedOnly || usage == IRUsage::UNUSED) {
// TODO: Use age or something to choose which register to spill?
// TODO: Spill dirty regs first? or opposite?
*clobbered = mipsReg == MIPS_REG_ZERO;
return nreg;
}
}
return -1;
}
bool IRNativeRegCacheBase::IsNativeRegCompatible(IRNativeReg nreg, MIPSLoc type, MIPSMap flags, int lanes) {
int allocCount = 0, base = 0;
const int *allocOrder = GetAllocationOrder(type, flags, allocCount, base);
for (int i = 0; i < allocCount; i++) {
IRNativeReg allocReg = IRNativeReg(allocOrder[i] - base);
if (allocReg == nreg)
return true;
}
return false;
}
bool IRNativeRegCacheBase::TransferNativeReg(IRNativeReg nreg, IRNativeReg dest, MIPSLoc type, IRReg first, int lanes, MIPSMap flags) {
// To be overridden if the backend supports transfers.
return false;
}
void IRNativeRegCacheBase::DiscardNativeReg(IRNativeReg nreg) {
_assert_msg_(nreg >= 0 && nreg < config_.totalNativeRegs, "DiscardNativeReg on invalid register %d", nreg);
if (nr[nreg].mipsReg != IRREG_INVALID) {
int8_t lanes = 0;
for (IRReg m = nr[nreg].mipsReg; mr[m].nReg == nreg && m < IRREG_INVALID; ++m)
lanes++;
if (mr[nr[nreg].mipsReg].isStatic) {
_assert_(nr[nreg].mipsReg != MIPS_REG_ZERO);
int numStatics;
const StaticAllocation *statics = GetStaticAllocations(numStatics);
// If it's not currently marked as in a reg, throw it away.
for (IRReg m = nr[nreg].mipsReg; m < nr[nreg].mipsReg + lanes; ++m) {
_assert_msg_(mr[m].isStatic, "Reg in lane %d mismatched static status", m - nr[nreg].mipsReg);
for (int i = 0; i < numStatics; i++) {
if (m == statics[i].mr)
mr[m].loc = statics[i].loc;
}
}
} else {
for (IRReg m = nr[nreg].mipsReg; m < nr[nreg].mipsReg + lanes; ++m) {
mr[m].loc = MIPSLoc::MEM;
mr[m].nReg = -1;
mr[m].imm = 0;
mr[m].lane = -1;
_assert_msg_(!mr[m].isStatic, "Reg in lane %d mismatched static status", m - nr[nreg].mipsReg);
}
nr[nreg].mipsReg = IRREG_INVALID;
}
}
// Even for a static reg, we assume this means it's not pointerified anymore.
nr[nreg].pointerified = false;
nr[nreg].isDirty = false;
nr[nreg].normalized32 = false;
}
void IRNativeRegCacheBase::FlushNativeReg(IRNativeReg nreg) {
_assert_msg_(nreg >= 0 && nreg < config_.totalNativeRegs, "FlushNativeReg on invalid register %d", nreg);
if (nr[nreg].mipsReg == IRREG_INVALID || nr[nreg].mipsReg == MIPS_REG_ZERO) {
// Nothing to do, reg not mapped or mapped to fixed zero.
_dbg_assert_(!nr[nreg].isDirty);
return;
}
_dbg_assert_(!mr[nr[nreg].mipsReg].isStatic);
if (mr[nr[nreg].mipsReg].isStatic) {
ERROR_LOG(JIT, "Cannot FlushNativeReg a statically mapped register");
return;
}
// Multiple mipsRegs may match this if a vector or HI/LO, etc.
bool isDirty = nr[nreg].isDirty;
int8_t lanes = 0;
for (IRReg m = nr[nreg].mipsReg; mr[m].nReg == nreg && m < IRREG_INVALID; ++m) {
_assert_(!mr[m].isStatic);
// If we're flushing a native reg, better not be partially in mem or an imm.
_assert_(mr[m].loc != MIPSLoc::MEM && mr[m].loc != MIPSLoc::IMM);
lanes++;
}
if (isDirty) {
IRReg first = nr[nreg].mipsReg;
if (mr[first].loc == MIPSLoc::REG_AS_PTR) {
// We assume this can't be multiple lanes. Maybe some gather craziness?
_assert_(lanes == 1);
AdjustNativeRegAsPtr(nreg, false);
mr[first].loc = MIPSLoc::REG;
}
StoreNativeReg(nreg, first, lanes);
}
for (int8_t i = 0; i < lanes; ++i) {
auto &mreg = mr[nr[nreg].mipsReg + i];
mreg.nReg = -1;
// Note that it loses its imm status, because imms are always dirty.
mreg.loc = MIPSLoc::MEM;
mreg.imm = 0;
mreg.lane = -1;
}
nr[nreg].mipsReg = IRREG_INVALID;
nr[nreg].isDirty = false;
nr[nreg].pointerified = false;
nr[nreg].normalized32 = false;
}
void IRNativeRegCacheBase::DiscardReg(IRReg mreg) {
if (mr[mreg].isStatic) {
DiscardNativeReg(mr[mreg].nReg);
return;
}
switch (mr[mreg].loc) {
case MIPSLoc::IMM:
if (mreg != MIPS_REG_ZERO) {
mr[mreg].loc = MIPSLoc::MEM;
mr[mreg].imm = 0;
}
break;
case MIPSLoc::REG:
case MIPSLoc::REG_AS_PTR:
case MIPSLoc::REG_IMM:
case MIPSLoc::FREG:
case MIPSLoc::VREG:
DiscardNativeReg(mr[mreg].nReg);
break;
case MIPSLoc::MEM:
// Already discarded.
break;
}
mr[mreg].spillLockIRIndex = -1;
}
void IRNativeRegCacheBase::FlushReg(IRReg mreg) {
_assert_msg_(!mr[mreg].isStatic, "Cannot flush static reg %d", mreg);
switch (mr[mreg].loc) {
case MIPSLoc::IMM:
// IMM is always "dirty".
StoreRegValue(mreg, mr[mreg].imm);
mr[mreg].loc = MIPSLoc::MEM;
mr[mreg].nReg = -1;
mr[mreg].imm = 0;
break;
case MIPSLoc::REG:
case MIPSLoc::REG_IMM:
case MIPSLoc::REG_AS_PTR:
case MIPSLoc::FREG:
case MIPSLoc::VREG:
// Might be in a native reg with multiple IR regs, flush together.
FlushNativeReg(mr[mreg].nReg);
break;
case MIPSLoc::MEM:
// Already there, nothing to do.
break;
}
}
void IRNativeRegCacheBase::FlushAll(bool gprs, bool fprs) {
// Note: make sure not to change the registers when flushing.
// Branching code may expect the native reg to retain its value.
if (!mr[MIPS_REG_ZERO].isStatic && mr[MIPS_REG_ZERO].nReg != -1)
DiscardNativeReg(mr[MIPS_REG_ZERO].nReg);
for (int i = 1; i < TOTAL_MAPPABLE_IRREGS; i++) {
IRReg mipsReg = (IRReg)i;
if (!fprs && i >= 32 && IsValidFPR(mipsReg))
continue;
if (!gprs && IsValidGPR(mipsReg))
continue;
if (mr[i].isStatic) {
IRNativeReg nreg = mr[i].nReg;
// Cannot leave any IMMs in registers, not even MIPSLoc::REG_IMM.
// Can confuse the regalloc later if this flush is mid-block
// due to an interpreter fallback that changes the register.
if (mr[i].loc == MIPSLoc::IMM) {
SetNativeRegValue(mr[i].nReg, mr[i].imm);
_assert_(IsValidGPR(mipsReg));
mr[i].loc = MIPSLoc::REG;
nr[nreg].pointerified = false;
} else if (mr[i].loc == MIPSLoc::REG_IMM) {
// The register already contains the immediate.
if (nr[nreg].pointerified) {
ERROR_LOG(JIT, "RVREG_IMM but pointerified. Wrong.");
nr[nreg].pointerified = false;
}
mr[i].loc = MIPSLoc::REG;
} else if (mr[i].loc == MIPSLoc::REG_AS_PTR) {
AdjustNativeRegAsPtr(mr[i].nReg, false);
mr[i].loc = MIPSLoc::REG;
}
_assert_(mr[i].nReg != -1);
} else if (mr[i].loc != MIPSLoc::MEM) {
FlushReg(mipsReg);
}
}
int count = 0;
const StaticAllocation *allocs = GetStaticAllocations(count);
for (int i = 0; i < count; i++) {
if (!fprs && allocs[i].loc != MIPSLoc::FREG && allocs[i].loc != MIPSLoc::VREG)
continue;
if (!gprs && allocs[i].loc != MIPSLoc::REG)
continue;
if (allocs[i].pointerified && !nr[allocs[i].nr].pointerified && jo_->enablePointerify) {
// Re-pointerify
if (mr[allocs[i].mr].loc == MIPSLoc::REG_IMM)
mr[allocs[i].mr].loc = MIPSLoc::REG;
_dbg_assert_(mr[allocs[i].mr].loc == MIPSLoc::REG);
AdjustNativeRegAsPtr(allocs[i].nr, true);
nr[allocs[i].nr].pointerified = true;
} else if (!allocs[i].pointerified) {
// If this register got pointerified on the way, mark it as not.
// This is so that after save/reload (like in an interpreter fallback),
// it won't be regarded as such, as it may no longer be.
nr[allocs[i].nr].pointerified = false;
}
}
// Sanity check
for (int i = 0; i < config_.totalNativeRegs; i++) {
if (nr[i].mipsReg != IRREG_INVALID && !mr[nr[i].mipsReg].isStatic) {
ERROR_LOG_REPORT(JIT, "Flush fail: nr[%i].mipsReg=%i", i, nr[i].mipsReg);
}
}
}
void IRNativeRegCacheBase::Map(const IRInst &inst) {
Mapping mapping[3];
MappingFromInst(inst, mapping);
ApplyMapping(mapping, 3);
CleanupMapping(mapping, 3);
}
void IRNativeRegCacheBase::MapWithExtra(const IRInst &inst, std::vector<Mapping> extra) {
extra.resize(extra.size() + 3);
MappingFromInst(inst, &extra[extra.size() - 3]);
ApplyMapping(extra.data(), (int)extra.size());
CleanupMapping(extra.data(), (int)extra.size());
}
IRNativeReg IRNativeRegCacheBase::MapWithTemp(const IRInst &inst, MIPSLoc type) {
Mapping mapping[3];
MappingFromInst(inst, mapping);
ApplyMapping(mapping, 3);
// Grab a temp while things are spill locked.
IRNativeReg temp = AllocateReg(type, MIPSMap::INIT);
CleanupMapping(mapping, 3);
return temp;
}
void IRNativeRegCacheBase::ApplyMapping(const Mapping *mapping, int count) {
for (int i = 0; i < count; ++i) {
SetSpillLockIRIndex(mapping[i].reg, irIndex_);
if (!config_.mapFPUSIMD && mapping[i].type != 'G') {
for (int j = 1; j < mapping[i].lanes; ++j)
SetSpillLockIRIndex(mapping[i].reg + j, irIndex_);
}
}
auto isNoinit = [](MIPSMap f) {
return (f & MIPSMap::NOINIT) == MIPSMap::NOINIT;
};
auto mapRegs = [&](int i) {
MIPSLoc type = MIPSLoc::MEM;
switch (mapping[i].type) {
case 'G': type = MIPSLoc::REG; break;
case 'F': type = MIPSLoc::FREG; break;
case 'V': type = MIPSLoc::VREG; break;
case '_':
// Ignored intentionally.
return;
default:
_assert_msg_(false, "Unexpected type: %c", mapping[i].type);
return;
}
bool mapSIMD = config_.mapFPUSIMD || mapping[i].type == 'G';
MIPSMap flags = mapping[i].flags;
for (int j = 0; j < count; ++j) {
if (mapping[j].type == mapping[i].type && mapping[j].reg == mapping[i].reg && i != j) {
_assert_msg_(!mapSIMD || mapping[j].lanes == mapping[i].lanes, "Lane aliasing not supported yet");
if (!isNoinit(mapping[j].flags) && isNoinit(flags)) {
flags = (flags & MIPSMap::BACKEND_MASK) | MIPSMap::DIRTY;
}
}
}
if (mapSIMD) {
MapNativeReg(type, mapping[i].reg, mapping[i].lanes, flags);
return;
}
for (int j = 0; j < mapping[i].lanes; ++j)
MapNativeReg(type, mapping[i].reg + j, 1, flags);
};
auto mapFilteredRegs = [&](auto pred) {
for (int i = 0; i < count; ++i) {
if (pred(mapping[i].flags))
mapRegs(i);
}
};
// Do two passes: with backend special flags, and without.
mapFilteredRegs([](MIPSMap flags) {
return (flags & MIPSMap::BACKEND_MASK) != MIPSMap::INIT;
});
mapFilteredRegs([](MIPSMap flags) {
return (flags & MIPSMap::BACKEND_MASK) == MIPSMap::INIT;
});
}
void IRNativeRegCacheBase::CleanupMapping(const Mapping *mapping, int count) {
for (int i = 0; i < count; ++i) {
SetSpillLockIRIndex(mapping[i].reg, -1);
if (!config_.mapFPUSIMD && mapping[i].type != 'G') {
for (int j = 1; j < mapping[i].lanes; ++j)
SetSpillLockIRIndex(mapping[i].reg + j, -1);
}
}
// Sanity check. If these don't pass, we may have Vec overlap issues or etc.
for (int i = 0; i < count; ++i) {
if (mapping[i].reg != IRREG_INVALID) {
auto &mreg = mr[mapping[i].reg];
_dbg_assert_(mreg.nReg != -1);
if (mapping[i].type == 'G') {
_dbg_assert_(mreg.loc == MIPSLoc::REG || mreg.loc == MIPSLoc::REG_AS_PTR || mreg.loc == MIPSLoc::REG_IMM);
} else if (mapping[i].type == 'F') {
_dbg_assert_(mreg.loc == MIPSLoc::FREG);
} else if (mapping[i].type == 'V') {
_dbg_assert_(mreg.loc == MIPSLoc::VREG);
}
if (mapping[i].lanes != 1 && (config_.mapFPUSIMD || mapping[i].type == 'G')) {
_dbg_assert_(mreg.lane == 0);
_dbg_assert_(mr[mapping[i].reg + mapping[i].lanes - 1].lane == mapping[i].lanes - 1);
_dbg_assert_(mreg.nReg == mr[mapping[i].reg + mapping[i].lanes - 1].nReg);
} else {
_dbg_assert_(mreg.lane == -1);
}
}
}
}
void IRNativeRegCacheBase::MappingFromInst(const IRInst &inst, Mapping mapping[3]) {
mapping[0].reg = inst.dest;
mapping[1].reg = inst.src1;
mapping[2].reg = inst.src2;
const IRMeta *m = GetIRMeta(inst.op);
for (int i = 0; i < 3; ++i) {
switch (m->types[i]) {
case 'G':
mapping[i].type = 'G';
_assert_msg_(IsValidGPR(mapping[i].reg), "G was not valid GPR?");
break;
case 'F':
mapping[i].reg += 32;
mapping[i].type = 'F';
_assert_msg_(IsValidFPR(mapping[i].reg - 32), "F was not valid FPR?");
break;
case 'V':
case '2':
mapping[i].reg += 32;
mapping[i].type = config_.mapUseVRegs ? 'V' : 'F';
mapping[i].lanes = m->types[i] == 'V' ? 4 : (m->types[i] == '2' ? 2 : 1);
_assert_msg_(IsValidFPR(mapping[i].reg - 32), "%c was not valid FPR?", m->types[i]);
break;
case 'T':
mapping[i].type = 'G';
_assert_msg_(mapping[i].reg < VFPU_CTRL_MAX, "T was not valid VFPU CTRL?");
mapping[i].reg += IRREG_VFPU_CTRL_BASE;
break;
case '\0':
case '_':
case 'C':
case 'I':
case 'v':
case 's':
case 'm':
mapping[i].type = '_';
mapping[i].reg = IRREG_INVALID;
mapping[i].lanes = 0;
break;
default:
_assert_msg_(mapping[i].reg == IRREG_INVALID, "Unexpected register type %c", m->types[i]);
break;
}
}
if (mapping[0].type != '_') {
if ((m->flags & IRFLAG_SRC3DST) != 0)
mapping[0].flags = MIPSMap::DIRTY;
else if ((m->flags & IRFLAG_SRC3) != 0)
mapping[0].flags = MIPSMap::INIT;
else
mapping[0].flags = MIPSMap::NOINIT;
}
}
IRNativeReg IRNativeRegCacheBase::MapNativeReg(MIPSLoc type, IRReg first, int lanes, MIPSMap flags) {
_assert_msg_(first != IRREG_INVALID, "Cannot map invalid register");
_assert_msg_(lanes >= 1 && lanes <= 4, "Cannot map %d lanes", lanes);
if (first == IRREG_INVALID || lanes < 0)
return -1;
// Let's see if it's already mapped or we need a new reg.
IRNativeReg nreg = mr[first].nReg;
if (mr[first].isStatic) {
_assert_msg_(nreg != -1, "MapIRReg on static without an nReg?");
} else {
switch (mr[first].loc) {
case MIPSLoc::REG_IMM:
case MIPSLoc::REG_AS_PTR:
case MIPSLoc::REG:
if (type != MIPSLoc::REG) {
nreg = AllocateReg(type, flags);
} else if (!IsNativeRegCompatible(nreg, type, flags, lanes)) {
// If it's not compatible, we'll need to reallocate.
if (TransferNativeReg(nreg, -1, type, first, lanes, flags)) {
nreg = mr[first].nReg;
} else {
FlushNativeReg(nreg);
nreg = AllocateReg(type, flags);
}
}
break;
case MIPSLoc::FREG:
case MIPSLoc::VREG:
if (type != mr[first].loc) {
nreg = AllocateReg(type, flags);
} else if (!IsNativeRegCompatible(nreg, type, flags, lanes)) {
if (TransferNativeReg(nreg, -1, type, first, lanes, flags)) {
nreg = mr[first].nReg;
} else {
FlushNativeReg(nreg);
nreg = AllocateReg(type, flags);
}
}
break;
case MIPSLoc::IMM:
case MIPSLoc::MEM:
nreg = AllocateReg(type, flags);
break;
}
}
if (nreg != -1) {
// This will handle already mapped and new mappings.
MapNativeReg(type, nreg, first, lanes, flags);
}
return nreg;
}
void IRNativeRegCacheBase::MapNativeReg(MIPSLoc type, IRNativeReg nreg, IRReg first, int lanes, MIPSMap flags) {
// First, try to clean up any lane mismatches.
// It must either be in the same nreg and lane count, or not in an nreg.
for (int i = 0; i < lanes; ++i) {
auto &mreg = mr[first + i];
if (mreg.nReg != -1) {
// How many lanes is it currently in?
int oldlanes = 0;
for (IRReg m = nr[mreg.nReg].mipsReg; mr[m].nReg == mreg.nReg && m < IRREG_INVALID; ++m)
oldlanes++;
// We may need to flush if it goes outside or we're initing.
int oldlane = mreg.lane == -1 ? 0 : mreg.lane;
bool mismatch = oldlanes != lanes || oldlane != i;
if (mismatch) {
_assert_msg_(!mreg.isStatic, "Cannot MapNativeReg a static reg mismatch");
if ((flags & MIPSMap::NOINIT) != MIPSMap::NOINIT) {
// If we need init, we have to flush mismatches.
if (!TransferNativeReg(mreg.nReg, nreg, type, first, lanes, flags)) {
// TODO: We may also be motivated to have multiple read-only "views" or an IRReg.
// For example Vec4Scale v0..v3, v0..v3, v3
FlushNativeReg(mreg.nReg);
}
// The mismatch has been "resolved" now.
mismatch = false;
} else if (oldlanes != 1) {
// Even if we don't care about the current contents, we can't discard outside.
bool extendsBefore = oldlane > i;
bool extendsAfter = i + oldlanes - oldlane > lanes;
if (extendsBefore || extendsAfter) {
// Usually, this is 4->1. Check for clobber.
bool clobbered = false;
if (lanes == 1) {
IRSituation info;
info.lookaheadCount = 16;
info.currentIndex = irIndex_;
info.instructions = irBlock_->GetInstructions();
info.numInstructions = irBlock_->GetNumInstructions();
IRReg basefpr = first - oldlane - 32;
clobbered = true;
for (int l = 0; l < oldlanes; ++l) {
// Ignore the one we're modifying.
if (l == oldlane)
continue;
if (IRNextFPRUsage(basefpr + l, info) != IRUsage::CLOBBERED) {
clobbered = false;
break;
}
}
}
if (clobbered)
DiscardNativeReg(mreg.nReg);
else
FlushNativeReg(mreg.nReg);
// That took care of the mismatch, either by clobber or flush.
mismatch = false;
}
}
}
// If it's still in a different reg, either discard or possibly transfer.
if (mreg.nReg != -1 && (mreg.nReg != nreg || mismatch)) {
_assert_msg_(!mreg.isStatic, "Cannot MapNativeReg a static reg to a new reg");
if ((flags & MIPSMap::NOINIT) != MIPSMap::NOINIT) {
// We better not be trying to map to a different nreg if it's in one now.
// This might happen on some sort of transfer...
if (!TransferNativeReg(mreg.nReg, nreg, type, first, lanes, flags))
FlushNativeReg(mreg.nReg);
} else {
DiscardNativeReg(mreg.nReg);
}
}
}
// If somehow this is an imm and mapping to a multilane native reg (HI/LO?), we store it.
// TODO: Could check the others are imm and be smarter, but seems an unlikely case.
if (mreg.loc == MIPSLoc::IMM && lanes > 1) {
if ((flags & MIPSMap::NOINIT) != MIPSMap::NOINIT)
StoreRegValue(first + i, mreg.imm);
mreg.loc = MIPSLoc::MEM;
if (!mreg.isStatic)
mreg.nReg = -1;
mreg.imm = 0;
}
}
// Double check: everything should be in the same loc for multilane now.
for (int i = 1; i < lanes; ++i) {
_assert_(mr[first + i].loc == mr[first].loc);
}
bool markDirty = (flags & MIPSMap::DIRTY) == MIPSMap::DIRTY;
if (mr[first].nReg != nreg) {
nr[nreg].isDirty = markDirty;
nr[nreg].pointerified = false;
nr[nreg].normalized32 = false;
}
// Alright, now to actually map.
if ((flags & MIPSMap::NOINIT) != MIPSMap::NOINIT) {
if (first == MIPS_REG_ZERO) {
_assert_msg_(lanes == 1, "Cannot use MIPS_REG_ZERO in multilane");
SetNativeRegValue(nreg, 0);
mr[first].loc = MIPSLoc::REG_IMM;
mr[first].imm = 0;
} else {
// Note: we checked above, everything is in the same loc if multilane.
switch (mr[first].loc) {
case MIPSLoc::IMM:
_assert_msg_(lanes == 1, "Not handling multilane imm here");
SetNativeRegValue(nreg, mr[first].imm);
// IMM is always dirty unless static.
if (!mr[first].isStatic)
nr[nreg].isDirty = true;
// If we are mapping dirty, it means we're gonna overwrite.
// So the imm value is no longer valid.
if ((flags & MIPSMap::DIRTY) == MIPSMap::DIRTY)
mr[first].loc = MIPSLoc::REG;
else
mr[first].loc = MIPSLoc::REG_IMM;
break;
case MIPSLoc::REG_IMM:
// If it's not dirty, we can keep it.
_assert_msg_(type == MIPSLoc::REG, "Should have flushed this reg already");
if ((flags & MIPSMap::DIRTY) == MIPSMap::DIRTY || lanes != 1)
mr[first].loc = MIPSLoc::REG;
for (int i = 1; i < lanes; ++i)
mr[first + i].loc = type;
break;
case MIPSLoc::REG_AS_PTR:
_assert_msg_(lanes == 1, "Should have flushed before getting here");
_assert_msg_(type == MIPSLoc::REG, "Should have flushed this reg already");
#ifndef MASKED_PSP_MEMORY
AdjustNativeRegAsPtr(nreg, false);
#endif
for (int i = 0; i < lanes; ++i)
mr[first + i].loc = type;
#ifdef MASKED_PSP_MEMORY
LoadNativeReg(nreg, first, lanes);
#endif
break;
case MIPSLoc::REG:
case MIPSLoc::FREG:
case MIPSLoc::VREG:
// Might be flipping from FREG -> VREG or something.
_assert_msg_(type == mr[first].loc, "Should have flushed this reg already");
for (int i = 0; i < lanes; ++i)
mr[first + i].loc = type;
break;
case MIPSLoc::MEM:
for (int i = 0; i < lanes; ++i)
mr[first + i].loc = type;
LoadNativeReg(nreg, first, lanes);
break;
}
}
} else {
for (int i = 0; i < lanes; ++i)
mr[first + i].loc = type;
}
for (int i = 0; i < lanes; ++i) {
mr[first + i].nReg = nreg;
mr[first + i].lane = lanes == 1 ? -1 : i;
}
nr[nreg].mipsReg = first;
if (markDirty) {
nr[nreg].isDirty = true;
nr[nreg].pointerified = false;
nr[nreg].normalized32 = false;
_assert_(first != MIPS_REG_ZERO);
}
}
IRNativeReg IRNativeRegCacheBase::MapNativeRegAsPointer(IRReg gpr) {
_dbg_assert_(IsValidGPRNoZero(gpr));
// Already mapped.
if (mr[gpr].loc == MIPSLoc::REG_AS_PTR) {
return mr[gpr].nReg;
}
// Cannot use if somehow multilane.
if (mr[gpr].nReg != -1 && mr[gpr].lane != -1) {
FlushNativeReg(mr[gpr].nReg);
}
IRNativeReg nreg = mr[gpr].nReg;
if (mr[gpr].loc != MIPSLoc::REG && mr[gpr].loc != MIPSLoc::REG_IMM) {
nreg = MapNativeReg(MIPSLoc::REG, gpr, 1, MIPSMap::INIT);
}
if (mr[gpr].loc == MIPSLoc::REG || mr[gpr].loc == MIPSLoc::REG_IMM) {
// If there was an imm attached, discard it.
mr[gpr].loc = MIPSLoc::REG;
mr[gpr].imm = 0;
#ifdef MASKED_PSP_MEMORY
if (nr[mr[gpr].nReg].isDirty) {
StoreNativeReg(mr[gpr].nReg, gpr, 1);
nr[mr[gpr].nReg].isDirty = false;
}
#endif
if (!jo_->enablePointerify) {
AdjustNativeRegAsPtr(nreg, true);
mr[gpr].loc = MIPSLoc::REG_AS_PTR;
} else if (!nr[nreg].pointerified) {
AdjustNativeRegAsPtr(nreg, true);
nr[nreg].pointerified = true;
}
} else {
ERROR_LOG(JIT, "MapNativeRegAsPointer: MapNativeReg failed to allocate a register?");
}
return nreg;
}
void IRNativeRegCacheBase::AdjustNativeRegAsPtr(IRNativeReg nreg, bool state) {
// This isn't necessary to implement if REG_AS_PTR is unsupported entirely.
_assert_msg_(false, "AdjustNativeRegAsPtr unimplemented");
}
int IRNativeRegCacheBase::GetMipsRegOffset(IRReg r) {
_dbg_assert_(IsValidGPR(r) || (r >= 32 && IsValidFPR(r - 32)));
return r * 4;
}
bool IRNativeRegCacheBase::IsValidGPR(IRReg r) const {
// See MIPSState for these offsets.
// Don't allow FPU regs, VFPU regs, or VFPU temps here.
if (r >= 32 && IsValidFPR(r - 32))
return false;
// Don't allow nextPC, etc. since it's probably a mistake.
if (r > IRREG_FPCOND && r != IRREG_LLBIT)
return false;
// Don't allow PC either.
if (r == 241)
return false;
return true;
}
bool IRNativeRegCacheBase::IsValidGPRNoZero(IRReg r) const {
return IsValidGPR(r) && r != MIPS_REG_ZERO;
}
bool IRNativeRegCacheBase::IsValidFPR(IRReg r) const {
// FPR parameters are off by 32 within the MIPSState object.
if (r >= TOTAL_MAPPABLE_IRREGS - 32)
return false;
// See MIPSState for these offsets.
int index = r + 32;
// Allow FPU or VFPU regs here.
if (index >= 32 && index < 32 + 32 + 128)
return true;
// Also allow VFPU temps.
if (index >= 224 && index < 224 + 16)
return true;
// Nothing else is allowed for the FPU side.
return false;
}