Files
ppsspp/Core/MIPS/Interpreter.cpp
Henrik RydgårdandClaude Opus 5 bd505b7f1c Interpreter: implement the FPU divide-by-zero exception
div.s now maintains fcr31's Cause.Z and (when the trap is masked) sticky
Flag.Z bits, in the standard MIPS bit positions. Only a finite non-zero
dividend counts, so 0/0, inf/0 and NaN operands are excluded per IEEE 754.

When the guest has the trap unmasked, the new Core_FPUException() reports it
with the usual module suffix and MIPS call stack, and fd is left unwritten as
hardware would. That's gated behind a new developer setting, off by default:
PSP threads start with fcr31 = 0x00000e00, i.e. three of the traps already
enabled, and games divide by zero without meaning anything by it. The fcr31
bits are updated either way, so what the game reads back doesn't depend on
the setting.

Interpreter only - the JITs are unchanged, and none of this is reachable
under them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9fKXvYBrnqtp1QQGaGWVv
2026-08-29 23:40:48 +02:00

1339 lines
38 KiB
C++

// Copyright (c) 2012- 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 <cmath>
#include "Common/Data/Convert/SmallDataConvert.h"
#include "Common/Math/math_util.h"
#include "Common/BitSet.h"
#include "Common/BitScan.h"
#include "Common/CommonTypes.h"
#include "Core/Config.h"
#include "Core/Core.h"
#include "Core/CoreTiming.h"
#include "Core/MemMap.h"
#include "Core/MIPS/MIPS.h"
#include "Core/MIPS/MIPSCodeUtils.h"
#include "Core/MIPS/Interpreter.h"
#include "Core/MIPS/MIPSTables.h"
#include "Core/Reporting.h"
#include "Core/HLE/HLE.h"
#include "Core/HLE/HLETables.h"
#include "Core/HLE/ReplaceTables.h"
#include "Core/HW/GpioMMIO.h"
#define R(i) (mips->r[i])
#define F(i) (mips->f[i])
#define FI(i) (mips->fi[i])
#define FsI(i) (mips->fs[i])
#define PC (mips->pc)
#define _SIMM16_SHL2 ((u32)(s32)(s16)(op & 0xFFFF) << 2)
#define _RS ((op>>21) & 0x1F)
#define _RT ((op>>16) & 0x1F)
#define _RD ((op>>11) & 0x1F)
#define _FS ((op>>11) & 0x1F)
#define _FT ((op>>16) & 0x1F)
#define _FD ((op>>6 ) & 0x1F)
#define _POS ((op>>6 ) & 0x1F)
#define _SIZE ((op>>11) & 0x1F)
#define HI mips->hi
#define LO mips->lo
static inline void DelayBranchTo(MIPSState *mips, u32 where) {
if (!Memory::IsValidAddress(where) || (where & 3) != 0) {
Core_ExecException(where, PC, ExecExceptionType::JUMP);
}
PC += 4;
mips->nextPC = where;
mips->inDelaySlot = true;
}
static inline void SkipLikely(MIPSState *mips) {
MIPSInfo delaySlot = MIPSGetInfo(Memory::Read_Instruction(PC + 4, true));
// Don't actually skip if it is a jump (seen in Brooktown High.)
if (delaySlot & IS_JUMP) {
PC += 4;
} else {
PC += 8;
--mips->downcount;
}
}
int MIPS_InterpretSingleStep(MIPSState *mips) {
if (!Memory::IsValid4AlignedAddress(mips->pc)) {
Core_ExecException(mips->pc, mips-> pc, ExecExceptionType::JUMP);
return 0;
}
MIPSOpcode op = Memory::Read_Opcode_JIT(mips->pc); // now unchecked
// Same reason as the run loop in MIPSInterpret_RunUntil - see ApplyHostRoundingMode.
ApplyHostRoundingMode(mips);
if (mips->inDelaySlot) {
MIPSInterpret(mips, op);
if (mips->inDelaySlot) {
mips->pc = mips->nextPC;
mips->inDelaySlot = false;
}
} else {
MIPSInterpret(mips, op);
}
RestoreHostRoundingMode();
return 1;
}
// MMIO handling, for limited LLE, just enough for VSH.
//
// Unregistered reads return a distinctive poison value (not 0) so it's obvious in a live trace
// when some later computation's input traces back to an unimplemented MMIO register, rather
// than looking like an ordinary, legitimate zero.
constexpr u32 UNKNOWN_MMIO_POISON = 0x1337BEEF;
static u8 ReadMMIO_U8(MIPSState *mips, u32 addr) {
if (!Memory::IsKernelCodeAddress(mips->pc)) {
Core_MemoryException(addr, 1, mips->pc, MemoryExceptionType::READ_WORD, "Kernel mode only");
return (u8)UNKNOWN_MMIO_POISON;
}
WARN_LOG(Log::CPU, "Unhandled MMIO Read8 at %08x", addr);
return (u8)UNKNOWN_MMIO_POISON;
}
static u16 ReadMMIO_U16(MIPSState *mips, u32 addr) {
if (!Memory::IsKernelCodeAddress(mips->pc)) {
Core_MemoryException(addr, 2, mips->pc, MemoryExceptionType::READ_WORD, "Kernel mode only");
return (u16)UNKNOWN_MMIO_POISON;
}
WARN_LOG(Log::CPU, "Unhandled MMIO Read16 at %08x", addr);
return (u16)UNKNOWN_MMIO_POISON;
}
static u32 ReadMMIO_U32(MIPSState *mips, u32 addr) {
if (!Memory::IsKernelCodeAddress(mips->pc)) {
Core_MemoryException(addr, 4, mips->pc, MemoryExceptionType::READ_WORD, "Kernel mode only");
return UNKNOWN_MMIO_POISON;
}
if (GpioMMIO::IsGpioAddress(addr)) {
return GpioMMIO::Read32(addr);
}
if (SysconSerialMMIO::IsSysconSerialAddress(addr)) {
return SysconSerialMMIO::Read32(addr);
}
WARN_LOG(Log::CPU, "Unhandled MMIO Read32 at %08x", addr);
return UNKNOWN_MMIO_POISON;
}
void WriteMMIO_U8(MIPSState *mips, u32 addr, u8 value) {
if (!Memory::IsKernelCodeAddress(mips->pc)) {
Core_MemoryException(addr, 1, mips->pc, MemoryExceptionType::WRITE_WORD, "Kernel mode only");
return;
}
WARN_LOG(Log::CPU, "Unhandled MMIO Write8 at %08x = %02x", addr, value);
}
void WriteMMIO_U16(MIPSState *mips, u32 addr, u16 value) {
if (!Memory::IsKernelCodeAddress(mips->pc)) {
Core_MemoryException(addr, 2, mips->pc, MemoryExceptionType::WRITE_WORD, "Kernel mode only");
return;
}
WARN_LOG(Log::CPU, "Unhandled MMIO Write16 at %08x = %04x", addr, value);
}
void WriteMMIO_U32(MIPSState *mips, u32 addr, u32 value) {
if (!Memory::IsKernelCodeAddress(mips->pc)) {
Core_MemoryException(addr, 4, mips->pc, MemoryExceptionType::WRITE_WORD, "Kernel mode only");
}
if (GpioMMIO::IsGpioAddress(addr)) {
GpioMMIO::Write32(addr, value);
return;
}
if (SysconSerialMMIO::IsSysconSerialAddress(addr)) {
SysconSerialMMIO::Write32(addr, value);
return;
}
WARN_LOG(Log::CPU, "Unhandled MMIO Write32 at %08x = %08x", addr, value);
}
namespace MIPSInt {
void Int_Cache(MIPSState *mips, MIPSOpcode op) {
int imm = SignExtend16ToS32(op);
int rs = _RS;
uint32_t addr = R(rs) + imm;
int func = (op >> 16) & 0x1F;
// Let's only report this once per run to be safe from impacting perf.
static bool loggedAlignment = false;
// It appears that a cache line is 0x40 (64) bytes, loops in games
// issue the cache instruction at that interval.
// These codes might be PSP-specific, they don't match regular MIPS cache codes very well
// NOTE: If you add support for more, make sure they are handled in the various Jit::Comp_Cache.
switch (func) {
// Icache
case 8:
// Invalidate the instruction cache at this address.
// We assume the CPU won't be reset during this, so no locking.
if (MIPSComp::jit) {
// Let's over invalidate to be super safe.
uint32_t alignedAddr = addr & ~0x3F;
int size = 0x40 + (addr & 0x3F);
MIPSComp::jit->InvalidateCacheAt(alignedAddr, size);
// Using a bool to avoid locking/etc. in case it's slow.
if (!loggedAlignment && (addr & 0x3F) != 0) {
// These are seen exclusively in Lego games, and are really no big deal. Reporting removed.
WARN_LOG(Log::JIT, "Unaligned icache invalidation of %08x (%08x + %d) at PC=%08x", addr, R(rs), imm, PC);
loggedAlignment = true;
}
if (alignedAddr <= PC + 4 && alignedAddr + size >= PC - 4) {
// This is probably rare so we don't use a static bool.
WARN_LOG_REPORT_ONCE(icacheInvalidatePC, Log::JIT, "Invalidating address near PC: %08x (%08x + %d) at PC=%08x", addr, R(rs), imm, PC);
}
}
break;
// Dcache
case 24:
// "Create Dirty Exclusive" - for avoiding a cacheline fill before writing to it.
// Will cause garbage on the real machine so we just ignore it, the app will overwrite the cacheline.
break;
case 25: // Hit Invalidate - zaps the line if present in cache. Should not writeback???? scary.
// No need to do anything.
break;
case 27: // D-cube. Hit Writeback Invalidate. Tony Hawk Underground 2
break;
case 30: // GTA LCS, a lot. Fill (prefetch). Tony Hawk Underground 2
break;
default:
DEBUG_LOG(Log::CPU, "cache instruction affecting %08x : function %i", addr, func);
}
PC += 4;
}
void Int_Syscall(MIPSState *mips, MIPSOpcode op) {
const u32 syscallPC = mips->pc - 4;
// Need to pre-move PC, as CallSyscall may result in a rescheduling!
// To do this neater, we'll need a little generated kernel loop that syscall can jump to and then RFI from
// but I don't see a need to bother.
if (mips->inDelaySlot) {
mips->pc = mips->nextPC;
} else {
mips->pc += 4;
}
mips->inDelaySlot = false;
// HLE code is host code - it must not run under the guest's rounding mode.
RestoreHostRoundingMode();
CallSyscallWithPC(op, syscallPC);
ApplyHostRoundingMode(mips);
}
void Int_Sync(MIPSState *mips, MIPSOpcode op) {
//DEBUG_LOG(Log::CPU, "sync");
PC += 4;
}
void Int_Break(MIPSState *mips, MIPSOpcode op) {
Reporting::ReportMessage("BREAK instruction hit");
Core_BreakException(PC);
PC += 4;
}
void Int_RelBranch(MIPSState *mips, MIPSOpcode op) {
int imm = _SIMM16_SHL2;
int rs = _RS;
int rt = _RT;
u32 addr = PC + imm + 4;
switch (op >> 26)
{
case 4: if (R(rt) == R(rs)) DelayBranchTo(mips, addr); else PC += 4; break; //beq
case 5: if (R(rt) != R(rs)) DelayBranchTo(mips, addr); else PC += 4; break; //bne
case 6: if ((s32)R(rs) <= 0) DelayBranchTo(mips, addr); else PC += 4; break; //blez
case 7: if ((s32)R(rs) > 0) DelayBranchTo(mips, addr); else PC += 4; break; //bgtz
case 20: if (R(rt) == R(rs)) DelayBranchTo(mips, addr); else SkipLikely(mips); break; //beql
case 21: if (R(rt) != R(rs)) DelayBranchTo(mips, addr); else SkipLikely(mips); break; //bnel
case 22: if ((s32)R(rs) <= 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break; //blezl
case 23: if ((s32)R(rs) > 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break; //bgtzl
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
}
void Int_RelBranchRI(MIPSState *mips, MIPSOpcode op) {
int imm = _SIMM16_SHL2;
int rs = _RS;
u32 addr = PC + imm + 4;
switch ((op>>16) & 0x1F) {
case 0: if ((s32)R(rs) < 0) DelayBranchTo(mips, addr); else PC += 4; break;//bltz
case 1: if ((s32)R(rs) >= 0) DelayBranchTo(mips, addr); else PC += 4; break;//bgez
case 2: if ((s32)R(rs) < 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bltzl
case 3: if ((s32)R(rs) >= 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bgezl
case 16: R(MIPS_REG_RA) = PC + 8; if ((s32)R(rs) < 0) DelayBranchTo(mips, addr); else PC += 4; break;//bltzal
case 17: R(MIPS_REG_RA) = PC + 8; if ((s32)R(rs) >= 0) DelayBranchTo(mips, addr); else PC += 4; break;//bgezal
case 18: R(MIPS_REG_RA) = PC + 8; if ((s32)R(rs) < 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bltzall
case 19: R(MIPS_REG_RA) = PC + 8; if ((s32)R(rs) >= 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bgezall
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
}
void Int_VBranch(MIPSState *mips, MIPSOpcode op) {
int imm = _SIMM16_SHL2;
u32 addr = PC + imm + 4;
// x, y, z, w, any, all, (invalid), (invalid)
int imm3 = (op>>18)&7;
int val = (mips->vfpuCtrl[VFPU_CTRL_CC] >> imm3) & 1;
switch ((op >> 16) & 3) {
case 0: if (!val) DelayBranchTo(mips, addr); else PC += 4; break; //bvf
case 1: if ( val) DelayBranchTo(mips, addr); else PC += 4; break; //bvt
case 2: if (!val) DelayBranchTo(mips, addr); else SkipLikely(mips); break; //bvfl
case 3: if ( val) DelayBranchTo(mips, addr); else SkipLikely(mips); break; //bvtl
}
}
void Int_FPUBranch(MIPSState *mips, MIPSOpcode op) {
int imm = _SIMM16_SHL2;
u32 addr = PC + imm + 4;
switch((op >> 16) & 0x1f)
{
case 0: if (!mips->fpcond) DelayBranchTo(mips, addr); else PC += 4; break;//bc1f
case 1: if ( mips->fpcond) DelayBranchTo(mips, addr); else PC += 4; break;//bc1t
case 2: if (!mips->fpcond) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bc1fl
case 3: if ( mips->fpcond) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bc1tl
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
}
void Int_JumpType(MIPSState *mips, MIPSOpcode op) {
if (mips->inDelaySlot) {
// There's one of these in Star Soldier at 0881808c, which seems benign.
ERROR_LOG(Log::CPU, "Jump in delay slot at %08x", mips->pc);
}
const u32 off = ((op & 0x03FFFFFF) << 2);
const u32 addr = (mips->pc & 0xF0000000) | off;
switch (op >> 26) {
case 2: //j
if (!mips->inDelaySlot)
DelayBranchTo(mips, addr);
break;
case 3: //jal
R(MIPS_REG_RA) = PC + 8;
if (!mips->inDelaySlot)
DelayBranchTo(mips, addr);
break;
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
}
void Int_JumpRegType(MIPSState *mips, MIPSOpcode op) {
if (mips->inDelaySlot) {
// There's one of these in Star Soldier at 0881808c, which seems benign.
ERROR_LOG(Log::CPU, "Jump in delay slot at %08x", mips->pc);
}
int rs = _RS;
int rd = _RD;
u32 addr = R(rs);
switch (op & 0x3f) {
case 8: //jr
if (!mips->inDelaySlot)
DelayBranchTo(mips, addr);
break;
case 9: //jalr
if (rd != 0)
R(rd) = PC + 8;
// Update rd, but otherwise do not take the branch if we're branching.
if (!mips->inDelaySlot)
DelayBranchTo(mips, addr);
break;
}
}
void Int_IType(MIPSState *mips, MIPSOpcode op) {
u32 uimm = op & 0xFFFF;
u32 suimm = SignExtend16ToU32(op);
s32 simm = SignExtend16ToS32(op);
int rt = _RT;
int rs = _RS;
if (rt == 0) { //destination register is zero register
PC += 4;
return; //nop
}
switch (op>>26)
{
case 8: R(rt) = R(rs) + simm; break; //addi
case 9: R(rt) = R(rs) + simm; break; //addiu
case 10: R(rt) = (s32)R(rs) < simm; break; //slti
case 11: R(rt) = R(rs) < suimm; break; //sltiu
case 12: R(rt) = R(rs) & uimm; break; //andi
case 13: R(rt) = R(rs) | uimm; break; //ori
case 14: R(rt) = R(rs) ^ uimm; break; //xori
case 15: R(rt) = uimm << 16; break; //lui
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_StoreSync(MIPSState *mips, MIPSOpcode op) {
int imm = (signed short)(op & 0xFFFF);
int rt = _RT;
int rs = _RS;
u32 addr = R(rs) + imm;
switch (op >> 26) {
case 48: // ll
if (rt != 0) {
if (!Memory::IsValid4AlignedAddress(addr)) {
Core_MemoryException(addr, 4, PC, MemoryExceptionType::READ_WORD, "ll");
R(rt) = 0;
} else {
R(rt) = Memory::ReadUnchecked_U32(addr);
}
}
mips->llBit = 1;
break;
case 56: // sc
if (mips->llBit) {
if (!Memory::IsValid4AlignedAddress(addr)) {
Core_MemoryException(addr, 4, PC, MemoryExceptionType::WRITE_WORD, "sc");
} else {
Memory::WriteUnchecked_U32(R(rt), addr);
}
// Report success even if the store got dropped - reporting failure just makes
// the usual retry loop spin on the same bad address forever.
if (rt != 0) {
R(rt) = 1;
}
} else if (rt != 0) {
R(rt) = 0;
}
break;
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_RType3(MIPSState *mips, MIPSOpcode op) {
int rt = _RT;
int rs = _RS;
int rd = _RD;
// Don't change $zr.
if (rd == 0) {
PC += 4;
return;
}
switch (op & 63) {
case 10: if (R(rt) == 0) R(rd) = R(rs); break; //movz
case 11: if (R(rt) != 0) R(rd) = R(rs); break; //movn
case 32: R(rd) = R(rs) + R(rt); break; //add (exception on overflow)
case 33: R(rd) = R(rs) + R(rt); break; //addu
case 34: R(rd) = R(rs) - R(rt); break; //sub (exception on overflow)
case 35: R(rd) = R(rs) - R(rt); break; //subu
case 36: R(rd) = R(rs) & R(rt); break; //and
case 37: R(rd) = R(rs) | R(rt); break; //or
case 38: R(rd) = R(rs) ^ R(rt); break; //xor
case 39: R(rd) = ~(R(rs) | R(rt)); break; //nor
case 42: R(rd) = (s32)R(rs) < (s32)R(rt); break; //slt
case 43: R(rd) = R(rs) < R(rt); break; //sltu
case 44: R(rd) = ((s32)R(rs) > (s32)R(rt)) ? R(rs) : R(rt); break; //max
case 45: R(rd) = ((s32)R(rs) < (s32)R(rt)) ? R(rs) : R(rt); break;//min
default:
_dbg_assert_msg_( 0, "Unknown MIPS instruction %08x", op.encoding);
break;
}
PC += 4;
}
// On a bad access that's set to be ignored (the default), we mirror what
// Memory::ReadOrException_*/WriteOrException_* do, which is also what the JIT's slow
// path calls: loads produce zero, stores are dropped, and PC advances either way.
// Returning without advancing PC instead would just re-execute the same instruction forever.
// When the access is set to break, Core_MemoryException has already stopped the core.
void Int_ITypeMem(MIPSState *mips, MIPSOpcode op) {
int imm = (signed short)(op&0xFFFF);
int rt = _RT;
int rs = _RS;
const u32 addr = R(rs) + imm;
if (((op >> 29) & 1) == 0 && rt == 0) {
// Don't load anything into $zr
PC += 4;
return;
}
switch (op >> 26) {
case 32:
if (!Memory::IsValidAddress(addr)) {
if (Memory::IsMMIOAccess(addr)) {
// This is a read from MMIO, so we can handle it.
R(rt) = SignExtend8ToU32(ReadMMIO_U8(mips, addr));
break;
}
Core_MemoryException(addr, 1, PC, MemoryExceptionType::READ_WORD, "lb");
R(rt) = 0;
break;
}
R(rt) = SignExtend8ToU32(Memory::ReadUnchecked_U8(addr));
break; //lb
case 33:
if (!Memory::IsValid2AlignedAddress(addr)) {
if (Memory::IsMMIOAccess(addr)) {
// This is a read from MMIO, so we can handle it.
R(rt) = SignExtend16ToU32(ReadMMIO_U16(mips, addr));
break;
}
Core_MemoryException(addr, 2, PC, MemoryExceptionType::READ_WORD, "lh");
R(rt) = 0;
break;
}
R(rt) = SignExtend16ToU32(Memory::ReadUnchecked_U16(addr));
break; //lh
case 35:
if (!Memory::IsValid4AlignedAddress(addr)) {
if (Memory::IsMMIOAccess(addr)) {
// This is a read from MMIO, so we can handle it.
R(rt) = ReadMMIO_U32(mips, addr);
break;
}
Core_MemoryException(addr, 4, PC, MemoryExceptionType::READ_WORD, "lw");
R(rt) = 0;
break;
}
R(rt) = Memory::ReadUnchecked_U32(addr);
break; //lw
case 36:
if (!Memory::IsValidAddress(addr)) {
if (Memory::IsMMIOAccess(addr)) {
// This is a read from MMIO, so we can handle it.
R(rt) = ReadMMIO_U8(mips, addr);
break;
}
Core_MemoryException(addr, 1, PC, MemoryExceptionType::READ_WORD, "lbu");
R(rt) = 0;
break;
}
R(rt) = Memory::ReadUnchecked_U8(addr);
break; //lbu
case 37:
if (!Memory::IsValid2AlignedAddress(addr)) {
if (Memory::IsMMIOAccess(addr)) {
// This is a read from MMIO, so we can handle it.
R(rt) = ReadMMIO_U16(mips, addr);
break;
}
Core_MemoryException(addr, 2, PC, MemoryExceptionType::READ_WORD, "lhu");
R(rt) = 0;
break;
}
R(rt) = Memory::ReadUnchecked_U16(addr);
break; //lhu
case 40:
if (!Memory::IsValidAddress(addr)) {
if (Memory::IsMMIOAccess(addr)) {
// This is a write to MMIO, so we can handle it.
WriteMMIO_U8(mips, addr, (u8)(R(rt)));
break;
}
Core_MemoryException(addr, 1, PC, MemoryExceptionType::WRITE_WORD, "sb");
break;
}
Memory::WriteUnchecked_U8(R(rt), addr);
break; //sb
case 41:
if (!Memory::IsValid2AlignedAddress(addr)) {
if (Memory::IsMMIOAccess(addr)) {
// This is a write to MMIO, so we can handle it.
WriteMMIO_U16(mips, addr, (u16)(R(rt)));
break;
}
Core_MemoryException(addr, 2, PC, MemoryExceptionType::WRITE_WORD, "sh");
break;
}
Memory::WriteUnchecked_U16(R(rt), addr);
break; //sh
case 43:
if (!Memory::IsValid4AlignedAddress(addr)) {
if (Memory::IsMMIOAccess(addr)) {
// This is a write to MMIO, so we can handle it.
WriteMMIO_U32(mips, addr, R(rt));
break;
}
Core_MemoryException(addr, 4, PC, MemoryExceptionType::WRITE_WORD, "sw");
break;
}
Memory::WriteUnchecked_U32(R(rt), addr);
break; //sw
// When there's an LWL and an LWR together, we should be able to peephole optimize that
// into a single non-alignment-checking LW.
case 34: //lwl
{
// Not checking for alignment here - the actual read will be aligned.
u32 mem = 0;
if (Memory::IsValidAddress(addr)) {
mem = Memory::ReadUnchecked_U32(addr & 0xfffffffc);
} else {
Core_MemoryException(addr, 4, PC, MemoryExceptionType::READ_WORD, "lwl");
}
u32 shift = (addr & 3) * 8;
u32 result = ( u32(R(rt)) & (0x00ffffff >> shift) ) | ( mem << (24 - shift) );
R(rt) = result;
}
break;
case 38: //lwr
{
// Not checking for alignment here - the actual read will be aligned.
u32 mem = 0;
if (Memory::IsValidAddress(addr)) {
mem = Memory::ReadUnchecked_U32(addr & 0xfffffffc);
} else {
Core_MemoryException(addr, 4, PC, MemoryExceptionType::READ_WORD, "lwr");
}
u32 shift = (addr & 3) * 8;
u32 regval = R(rt);
u32 result = ( regval & (0xffffff00 << (24 - shift)) ) | ( mem >> shift );
R(rt) = result;
}
break;
case 42: //swl
{
// Not checking for alignment here - the actual read/write will be aligned.
if (!Memory::IsValidAddress(addr)) {
Core_MemoryException(addr, 4, PC, MemoryExceptionType::WRITE_WORD, "swl");
break;
}
u32 shift = (addr & 3) * 8;
u32 mem = Memory::ReadUnchecked_U32(addr & 0xfffffffc);
u32 result = ( ( u32(R(rt)) >> (24 - shift) ) ) | ( mem & (0xffffff00 << shift) );
Memory::WriteUnchecked_U32(result, (addr & 0xfffffffc));
}
break;
case 46: //swr
{
// Not checking for alignment here - the actual read/write will be aligned.
if (!Memory::IsValidAddress(addr)) {
Core_MemoryException(addr, 4, PC, MemoryExceptionType::WRITE_WORD, "swr");
break;
}
u32 shift = (addr & 3) << 3;
u32 mem = Memory::ReadUnchecked_U32(addr & 0xfffffffc);
u32 result = ( ( u32(R(rt)) << shift ) | (mem & (0x00ffffff >> (24 - shift)) ) );
Memory::WriteUnchecked_U32(result, (addr & 0xfffffffc));
}
break;
default:
_dbg_assert_msg_(false,"Trying to interpret Mem instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_FPULS(MIPSState *mips, MIPSOpcode op) {
s32 offset = (s16)(op & 0xFFFF);
int ft = _FT;
int rs = _RS;
u32 addr = R(rs) + offset;
switch (op >> 26) {
case 49:
if (!Memory::IsValid4AlignedAddress(addr)) {
Core_MemoryException(addr, 4, PC, MemoryExceptionType::READ_WORD, "lwc1");
FI(ft) = 0;
break;
}
FI(ft) = Memory::ReadUnchecked_U32(addr);
break; //lwc1
case 57:
if (!Memory::IsValid4AlignedAddress(addr)) {
Core_MemoryException(addr, 4, PC, MemoryExceptionType::WRITE_WORD, "swc1");
break;
}
Memory::WriteUnchecked_U32(FI(ft), addr);
break; //swc1
default:
_dbg_assert_msg_(false,"Trying to interpret FPULS instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_mxc1(MIPSState *mips, MIPSOpcode op)
{
int fs = _FS;
int rt = _RT;
switch ((op>>21)&0x1f) {
case 0: //mfc1
if (rt != 0)
R(rt) = FI(fs);
break;
case 2: //cfc1
if (rt != 0) {
if (fs == 31) {
mips->fcr31 = (mips->fcr31 & ~(1<<23)) | ((mips->fpcond & 1)<<23);
R(rt) = mips->fcr31;
} else if (fs == 0) {
R(rt) = MIPSState::FCR0_VALUE;
} else {
WARN_LOG_REPORT(Log::CPU, "ReadFCR: Unexpected reg %d", fs);
R(rt) = 0;
}
break;
}
break;
case 4: //mtc1
FI(fs) = R(rt);
break;
case 6: //ctc1
{
u32 value = R(rt);
if (fs == 31) {
mips->fcr31 = value & 0x0181FFFF;
mips->fpcond = (value >> 23) & 1;
// Don't bother locking, assuming the CPU can't be reset now anyway.
if (MIPSComp::jit) {
// In case of DISABLE, we need to tell jit we updated FCR31.
MIPSComp::jit->UpdateFCR31();
} else {
// The interpreter emulates the rounding mode by putting the host FPU in it,
// so it has to switch right here rather than at the next block boundary.
// Restore first: the new value may be back to the default, which Apply
// deliberately doesn't write.
RestoreHostRoundingMode();
ApplyHostRoundingMode(mips);
}
} else {
WARN_LOG_REPORT(Log::CPU, "WriteFCR: Unexpected reg %d (value %08x)", fs, value);
}
DEBUG_LOG(Log::CPU, "FCR%i written to, value %08x", fs, value);
break;
}
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
PC += 4;
}
// Dummy COP0 register file, for kernel-mode code (e.g. flash0:/reboot.bin) that reads/
// writes COP0 state directly - ordinary PSP user-mode code never executes these
// instructions (kernel state is reached via HLE syscalls instead), so this doesn't need
// to model real COP0 semantics (interrupts, exceptions, TLB, ...), just be non-fatal and
// give writes-then-reads-back-same-value behavior. Not part of MIPSState (which is
// layout-sensitive for JIT register allocation) and not save-stated, in keeping with
// "dummy" - revisit if this ever needs to be more than a bail-out.
static u32 g_cop0Regs[32];
void Int_Cop0(MIPSState *mips, MIPSOpcode op) {
int rt = _RT;
int rd = _RD;
switch ((op >> 21) & 0x1f) {
case 0: //mfc0
if (rt != 0) {
// Real MIPS COP0 register 9 (Count) free-runs on its own (incrementing every
// other cycle) regardless of software writes - unlike the rest of this dummy
// shadow file, a plain "read back whatever was last written" (defaulting to a
// constant 0) would be wrong here and could plausibly starve boot-time
// calibration code of a nonzero seed/divisor. Tie it to CoreTiming instead;
// still not real COP0 semantics (no Compare-triggered interrupt), just a
// closer approximation. See docs/VSHBootInvestigation.md "Attempt 9".
if (rd == 9)
R(rt) = (u32)CoreTiming::GetTicks(mips);
else
R(rt) = g_cop0Regs[rd];
}
break;
case 4: //mtc0
g_cop0Regs[rd] = R(rt);
break;
case 10: //rdpgpr
if (rt != 0)
R(rt) = R(rd);
break;
case 11: //mfmc0 (di/ei)
if (rt != 0)
R(rt) = g_cop0Regs[12]; // Status
break;
case 14: //wrpgpr
R(rd) = R(rt);
break;
default:
_dbg_assert_msg_(false, "Trying to interpret COP0 instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_RType2(MIPSState *mips, MIPSOpcode op) {
int rs = _RS;
int rd = _RD;
// Don't change $zr.
if (rd == 0) {
PC += 4;
return;
}
switch (op & 63) {
case 22: //clz
R(rd) = clz32(R(rs));
break;
case 23: //clo
R(rd) = clz32(~R(rs));
break;
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_MulDivType(MIPSState *mips, MIPSOpcode op)
{
int rt = _RT;
int rs = _RS;
int rd = _RD;
switch (op & 63)
{
case 24: //mult
{
s64 result = (s64)(s32)R(rs) * (s64)(s32)R(rt);
u64 resultBits = (u64)(result);
LO = (u32)(resultBits);
HI = (u32)(resultBits>>32);
}
break;
case 25: //multu
{
u64 resultBits = (u64)R(rs) * (u64)R(rt);
LO = (u32)(resultBits);
HI = (u32)(resultBits>>32);
}
break;
case 28: //madd
{
u32 a=R(rs),b=R(rt),hi=HI,lo=LO;
u64 origValBits = (u64)lo | ((u64)(hi)<<32);
s64 origVal = (s64)origValBits;
s64 result = origVal + (s64)(s32)a * (s64)(s32)b;
u64 resultBits = (u64)(result);
LO = (u32)(resultBits);
HI = (u32)(resultBits>>32);
}
break;
case 29: //maddu
{
u32 a=R(rs),b=R(rt),hi=HI,lo=LO;
u64 origVal = (u64)lo | ((u64)(hi)<<32);
u64 result = origVal + (u64)a * (u64)b;
LO = (u32)(result);
HI = (u32)(result>>32);
}
break;
case 46: //msub
{
u32 a=R(rs),b=R(rt),hi=HI,lo=LO;
u64 origValBits = (u64)lo | ((u64)(hi)<<32);
s64 origVal = (s64)origValBits;
s64 result = origVal - (s64)(s32)a * (s64)(s32)b;
u64 resultBits = (u64)(result);
LO = (u32)(resultBits);
HI = (u32)(resultBits>>32);
}
break;
case 47: //msubu
{
u32 a=R(rs),b=R(rt),hi=HI,lo=LO;
u64 origVal = (u64)lo | ((u64)(hi)<<32);
u64 result = origVal - (u64)a * (u64)b;
LO = (u32)(result);
HI = (u32)(result>>32);
}
break;
case 16: if (rd != 0) R(rd) = HI; break; //mfhi
case 17: HI = R(rs); break; //mthi
case 18: if (rd != 0) R(rd) = LO; break; //mflo
case 19: LO = R(rs); break; //mtlo
case 26: //div
{
s32 a = (s32)R(rs);
s32 b = (s32)R(rt);
if (a == (s32)0x80000000 && b == -1) {
LO = 0x80000000;
HI = -1;
} else if (b != 0) {
LO = (u32)(a / b);
HI = (u32)(a % b);
} else {
LO = a < 0 ? 1 : -1;
HI = a;
}
}
break;
case 27: //divu
{
u32 a = R(rs);
u32 b = R(rt);
if (b != 0) {
LO = (a/b);
HI = (a%b);
} else {
LO = a <= 0xFFFF ? 0xFFFF : -1;
HI = a;
}
}
break;
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_ShiftType(MIPSState *mips, MIPSOpcode op)
{
int rt = _RT;
int rs = _RS;
int rd = _RD;
int sa = _FD;
// Don't change $zr.
if (rd == 0)
{
PC += 4;
return;
}
switch (op & 0x3f)
{
case 0: R(rd) = R(rt) << sa; break; //sll
case 2:
if (_RS == 0) //srl
{
R(rd) = R(rt) >> sa;
break;
}
else if (_RS == 1) //rotr
{
R(rd) = __rotr(R(rt), sa);
break;
}
else
goto wrong;
case 3: R(rd) = (u32)(((s32)R(rt)) >> sa); break; //sra
case 4: R(rd) = R(rt) << (R(rs) & 0x1F); break; //sllv
case 6:
if (_FD == 0) { //srlv
R(rd) = R(rt) >> (R(rs)&0x1F);
break;
} else if (_FD == 1) { // rotrv
R(rd) = __rotr(R(rt), R(rs));
break;
} else {
goto wrong;
}
case 7: R(rd) = (u32)(((s32)R(rt)) >> (R(rs) & 0x1F)); break; //srav
default:
wrong:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_Allegrex(MIPSState *mips, MIPSOpcode op) {
int rt = _RT;
int rd = _RD;
// Don't change $zr.
if (rd == 0) {
PC += 4;
return;
}
switch ((op >> 6) & 31) {
case 16: // seb
R(rd) = SignExtend8ToU32(R(rt));
break;
case 20: // bitrev
R(rd) = ReverseBits32(R(rt));
break;
case 24: // seh
R(rd) = SignExtend16ToU32(R(rt));
break;
default:
_dbg_assert_msg_(false,"Trying to interpret ALLEGREX instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_Allegrex2(MIPSState *mips, MIPSOpcode op) {
int rt = _RT;
int rd = _RD;
// Don't change $zr.
if (rd == 0) {
PC += 4;
return;
}
switch (op & 0x3ff) {
case 0xA0: //wsbh
R(rd) = ((R(rt) & 0xFF00FF00) >> 8) | ((R(rt) & 0x00FF00FF) << 8);
break;
case 0xE0: //wsbw
R(rd) = swap32(R(rt));
break;
default:
_dbg_assert_msg_(false,"Trying to interpret ALLEGREX instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_Special2(MIPSState *mips, MIPSOpcode op) {
static int reported = 0;
switch (op & 0x3F) {
case 36: // mfic
// move from interrupt controller, not implemented
// See related report https://report.ppsspp.org/logs/kind/316 for possible locations.
// Also see https://forums.ps2dev.org/viewtopic.php?p=32700#p32700 .
// TODO: Should we actually implement this?
if (!reported) {
WARN_LOG(Log::CPU, "MFIC Disable/Enable Interrupt CPU instruction");
reported = 1;
}
break;
case 38: // mtic
// move to interrupt controller, not implemented
if (!reported) {
WARN_LOG(Log::CPU, "MTIC Disable/Enable Interrupt CPU instruction");
reported = 1;
}
break;
}
PC += 4;
}
void Int_Special3(MIPSState *mips, MIPSOpcode op) {
int rs = _RS;
int rt = _RT;
int pos = _POS;
// Don't change $zr.
if (rt == 0) {
PC += 4;
return;
}
switch (op & 0x3f) {
case 0x0: //ext
{
int size = _SIZE + 1;
u32 sourcemask = 0xFFFFFFFFUL >> (32 - size);
R(rt) = (R(rs) >> pos) & sourcemask;
}
break;
case 0x4: //ins
{
// The size field actually holds msb (= pos + size - 1), so build the mask from
// that and shift it down, the way the JITs do. Computing the width as
// (_SIZE + 1) - pos instead would shift by 32 or more when msb < pos - undefined
// behavior, and on x86 it yields an all-ones mask that writes bits the JITs leave
// alone. Hardware calls that encoding unpredictable, so all we need is to be
// consistent and not invoke UB.
const u32 mask = 0xFFFFFFFFUL >> (31 - _SIZE);
const u32 sourcemask = mask >> pos;
const u32 destmask = sourcemask << pos;
R(rt) = (R(rt) & ~destmask) | ((R(rs) & sourcemask) << pos);
}
break;
}
PC += 4;
}
void Int_FPU2op(MIPSState *mips, MIPSOpcode op) {
int fs = _FS;
int fd = _FD;
switch (op & 0x3f)
{
case 4: F(fd) = sqrtf(F(fs)); break; //sqrt
case 5: F(fd) = fabsf(F(fs)); break; //abs
case 6: F(fd) = F(fs); break; //mov
case 7: F(fd) = -F(fs); break; //neg
case 12:
case 13:
case 14:
case 15:
if (my_isnanorinf(F(fs)))
{
FsI(fd) = my_isinf(F(fs)) && F(fs) < 0.0f ? -2147483648LL : 2147483647LL;
break;
}
switch (op & 0x3f)
{
// round.w.s is round-half-to-even, not half-away-from-zero - and its mode is fixed,
// so unlike cvt.w.s below it must not follow fcr31. round_ieee_754 is both.
case 12: FsI(fd) = (int)round_ieee_754(F(fs)); break; //round.w.s
case 13: //trunc.w.s
if (F(fs) >= 0.0f) {
FsI(fd) = (int)floorf(F(fs));
// Overflow, but it was positive.
if (FsI(fd) == -2147483648LL) {
FsI(fd) = 2147483647LL;
}
} else {
// Overflow happens to be the right value anyway.
FsI(fd) = (int)ceilf(F(fs));
}
break;
case 14: FsI(fd) = (int)ceilf (F(fs)); break; //ceil.w.s
case 15: FsI(fd) = (int)floorf(F(fs)); break; //floor.w.s
}
break;
case 32: F(fd) = (float)FsI(fs); break; //cvt.s.w
case 36:
if (my_isnanorinf(F(fs)))
{
FsI(fd) = my_isinf(F(fs)) && F(fs) < 0.0f ? -2147483648LL : 2147483647LL;
break;
}
switch (mips->fcr31 & 3)
{
case 0: FsI(fd) = (int)round_ieee_754(F(fs)); break; // RINT_0
case 1: FsI(fd) = (int)F(fs); break; // CAST_1
case 2: FsI(fd) = (int)ceilf(F(fs)); break; // CEIL_2
case 3: FsI(fd) = (int)floorf(F(fs)); break; // FLOOR_3
}
break; //cvt.w.s
default:
_dbg_assert_msg_(false,"Trying to interpret FPU2Op instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_FPUComp(MIPSState *mips, MIPSOpcode op) {
int fs = _FS;
int ft = _FT;
bool cond;
switch (op & 0xf)
{
case 0: //f
case 8: //sf
cond = false;
break;
case 1: //un
case 9: //ngle
cond = my_isnan(F(fs)) || my_isnan(F(ft));
break;
case 2: //eq
case 10: //seq
cond = !my_isnan(F(fs)) && !my_isnan(F(ft)) && (F(fs) == F(ft));
break;
case 3: //ueq
case 11: //ngl
cond = (F(fs) == F(ft)) || my_isnan(F(fs)) || my_isnan(F(ft));
break;
case 4: //olt
case 12: //lt
cond = (F(fs) < F(ft));
break;
case 5: //ult
case 13: //nge
cond = (F(fs) < F(ft)) || my_isnan(F(fs)) || my_isnan(F(ft));
break;
case 6: //ole
case 14: //le
cond = (F(fs) <= F(ft));
break;
case 7: //ule
case 15: //ngt
cond = (F(fs) <= F(ft)) || my_isnan(F(fs)) || my_isnan(F(ft));
break;
default:
_dbg_assert_msg_(false,"Trying to interpret FPUComp instruction that can't be interpreted");
cond = false;
break;
}
mips->fpcond = cond;
PC += 4;
}
void Int_FPU3op(MIPSState *mips, MIPSOpcode op) {
int ft = _FT;
int fs = _FS;
int fd = _FD;
switch (op & 0x3f)
{
case 0: F(fd) = F(fs) + F(ft); break; // add.s
case 1: F(fd) = F(fs) - F(ft); break; // sub.s
case 2: // mul.s
if ((my_isinf(F(fs)) && F(ft) == 0.0f) || (my_isinf(F(ft)) && F(fs) == 0.0f)) {
// Must be positive NAN, see #12519.
FI(fd) = 0x7fc00000;
} else {
F(fd) = F(fs) * F(ft);
}
break;
case 3: // div.s
{
// The only FPU exception we implement so far. IEEE 754 raises divide-by-zero only when
// an exact infinity comes out of finite operands, so 0/0 (invalid operation, which we
// don't detect) and inf/0 and NaN operands (no exception at all) are all excluded.
// Cause is per-instruction, so clear it here rather than leaving the last one set.
// Real hardware clears all five Cause bits in every FPU op, we only manage this one.
mips->fcr31 &= ~FCR31_CAUSE_DIV_BY_ZERO;
if (F(ft) == 0.0f && F(fs) != 0.0f && !my_isnanorinf(F(fs))) {
mips->fcr31 |= FCR31_CAUSE_DIV_BY_ZERO;
if (mips->fcr31 & FCR31_ENABLE_DIV_BY_ZERO) {
// Unmasked. Hardware takes the trap here, leaving fd alone and not setting the
// sticky flag - the handler is expected to deal with it. We can't run the
// guest's handler, and PSP threads start with this trap enabled (fcr31 =
// 0x00000e00) while games divide by zero all the time, so acting on it at all
// is a developer opt-in. The fcr31 bits above are updated either way, so what
// the game sees when it reads the register doesn't depend on the setting.
if (g_Config.bEnableFPUExceptionTraps) {
Core_FPUException(PC, FPUExceptionType::DIVIDE_BY_ZERO);
break;
}
} else {
// Masked: sticky flag, and the default result (a correctly signed infinity).
mips->fcr31 |= FCR31_FLAG_DIV_BY_ZERO;
}
}
F(fd) = F(fs) / F(ft);
break;
}
default:
_dbg_assert_msg_(false,"Trying to interpret FPU3Op instruction that can't be interpreted");
break;
}
PC += 4;
}
void Int_Interrupt(MIPSState *mips, MIPSOpcode op) {
static int reported = 0;
switch (op & 1)
{
case 0:
// unlikely to be legitimately used
if (!reported) {
Reporting::ReportMessage("INTERRUPT instruction hit (%08x) at %08x", op.encoding, mips->pc);
WARN_LOG(Log::CPU, "Disable/Enable Interrupt CPU instruction");
reported = 1;
}
break;
}
PC += 4;
}
void Int_Emuhack(MIPSState *mips, MIPSOpcode op) {
if (((op >> 24) & 3) != EMUOP_CALL_REPLACEMENT) {
_dbg_assert_msg_(false, "Trying to interpret emuhack instruction that can't be interpreted");
}
_assert_((PC & 3) == 0);
// It's a replacement func!
int index = op.encoding & 0xFFFFFF;
const ReplacementTableEntry *entry = GetReplacementFunc(index);
if (entry && entry->replaceFunc && (entry->flags & REPFLAG_DISABLED) == 0) {
// Like a syscall, a replacement function is host code - see Int_Syscall.
RestoreHostRoundingMode();
int cycles = entry->replaceFunc();
ApplyHostRoundingMode(mips);
if (entry->flags & (REPFLAG_HOOKENTER | REPFLAG_HOOKEXIT)) {
// Interpret the original instruction under the hook.
MIPSInterpret(mips, Memory::Read_Instruction(PC, true));
} else if (cycles < 0) {
// Leave PC unchanged, call the replacement again (assumes args are modified.)
mips->downcount += cycles;
} else {
PC = mips->r[MIPS_REG_RA];
mips->downcount -= cycles;
}
} else {
if (!entry || !entry->replaceFunc) {
ERROR_LOG(Log::CPU, "Bad replacement function index %i", index);
}
// Interpret the original instruction under it.
MIPSInterpret(mips, Memory::Read_Instruction(PC, true));
}
}
}