Merge pull request #22576 from hrydgard/cpu-generic-fixes

CPU equivalence fixes, headless vertex JIT, and Mac W^X switching per thread
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-15 10:17:04 +02:00
commit 3e41ecba63
15 files changed
+208 -67

No files matched your search

+2 -2
View File
@@ -99,8 +99,8 @@ public:
// If not WX Exclusive, no need to call ProtectMemoryPages because we never change the protection from RWX.
PoisonMemory(offset);
ResetCodePtr(offset);
if (PlatformIsWXExclusive() && offset != 0) {
// Need to re-protect the part we didn't clear.
if (PlatformIsWXExclusive()) {
// Need to re-protect the part we didn't clear (with MAP_JIT, this thread's view of it all).
ProtectMemoryPages(region, offset, MEM_PROT_READ | MEM_PROT_EXEC);
}
}
+67
View File
@@ -41,6 +41,14 @@
#include <mach/vm_param.h>
#endif
#if PPSSPP_PLATFORM(MAC) && PPSSPP_ARCH(ARM64)
#include <mutex>
#include <utility>
#include <vector>
#include <pthread.h>
#define USE_MAP_JIT
#endif
#ifndef _WIN32
#include <unistd.h>
#endif
@@ -183,7 +191,43 @@ void *AllocateExecutableMemory(size_t size) {
#else // Non-Windows platforms
#ifdef USE_MAP_JIT
// With MAP_JIT, a thread switches all of its view of the JIT memory between writable and executable
// with pthread_jit_write_protect_np(), far cheaper than an mprotect() for every block compiled and
// linked. A hardened runtime without the JIT entitlement refuses the mapping: then mprotect() it is.
static std::mutex g_mapJitLock;
static int g_mapJitState; // 0 untried, 1 in use, -1 refused.
static std::vector<std::pair<uintptr_t, uintptr_t>> g_mapJitRanges;
static bool IsMapJitMemory(const void *ptr) {
std::lock_guard<std::mutex> guard(g_mapJitLock);
for (const auto &range : g_mapJitRanges) {
if ((uintptr_t)ptr >= range.first && (uintptr_t)ptr < range.second)
return true;
}
return false;
}
#endif
void *AllocateExecutableMemory(size_t size) {
#ifdef USE_MAP_JIT
{
std::lock_guard<std::mutex> guard(g_mapJitLock);
if (g_mapJitState >= 0) {
void *ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0);
if (ptr != MAP_FAILED) {
g_mapJitState = 1;
g_mapJitRanges.emplace_back((uintptr_t)ptr, (uintptr_t)ptr + size);
return ptr;
}
if (g_mapJitState == 0) {
WARN_LOG(Log::MemMap, "MAP_JIT refused (errno=%d), using mprotect", errno);
g_mapJitState = -1;
}
}
}
#endif
static char *map_hint = nullptr;
#if PPSSPP_ARCH(AMD64)
@@ -339,6 +383,19 @@ void FreeMemoryPages(void *ptr, size_t size) {
}
void FreeExecutableMemory(void *ptr, size_t size) {
#ifdef USE_MAP_JIT
if (IsMapJitMemory(ptr)) {
std::lock_guard<std::mutex> guard(g_mapJitLock);
for (size_t i = 0; i < g_mapJitRanges.size(); i++) {
if (g_mapJitRanges[i].first == (uintptr_t)ptr) {
g_mapJitRanges.erase(g_mapJitRanges.begin() + i);
break;
}
}
// Back to executable, which the protection change before freeing undid.
pthread_jit_write_protect_np(1);
}
#endif
FreeMemoryPages(ptr, size);
}
@@ -374,6 +431,16 @@ bool ProtectMemoryPages(const void* ptr, size_t size, uint32_t memProtFlags) {
_assert_msg_(false, "Bad memory protect flags %d: W^X is in effect, can't both write and exec", memProtFlags);
}
}
#ifdef USE_MAP_JIT
if (IsMapJitMemory(ptr)) {
_assert_msg_((memProtFlags & (MEM_PROT_WRITE | MEM_PROT_EXEC)) != 0, "MAP_JIT memory is writable or executable");
pthread_jit_write_protect_np((memProtFlags & MEM_PROT_WRITE) == 0);
return true;
}
#endif
if (size == 0)
return true;
// Note - VirtualProtect will affect the full pages containing the requested range.
// mprotect does not seem to, at least not on Android unless I made a mistake somewhere, so we manually round.
#ifdef _WIN32
+1
View File
@@ -208,6 +208,7 @@ static const CommandLineParam g_autoParams[] = {
{POFF(replayEnd), CmdParamType::Int, "replay-end", '\0', "When replaying a GE frame dump, only draw primitives up to this one (1-based)", CmdLineMode::Headless},
{POFF(dumpCompat), CmdParamType::Bool, "dump-compat", '\0', "Apply the game's compat.ini settings when replaying a GE frame dump (off by default)", CmdLineMode::Headless},
{POFF(hardwareTransform), CmdParamType::Bool, "hardware-transform", '\0', "Transform vertices on the GPU (default true); false transforms them on the CPU", CmdLineMode::Headless},
{POFF(vertexJit), CmdParamType::Bool, "vertex-jit", '\0', "Decode vertices with the JIT where the host can (default; =false for the C++ decoder)", CmdLineMode::Headless},
{POFF(disableGPUFeatures), CmdParamType::String, "disable-gpu-features", '\0', "Act as if the GPU lacked these, to test the fallbacks: comma-separated clip-distance, cull-distance, depth-clamp, framebuffer-fetch, dual-source-blend", CmdLineMode::Headless},
{POFF(depthFilenameSave), CmdParamType::String, "depth-save", '\0', "Save the depth buffer to FILE along with the screenshot (u32 width, u32 height, u16 depths; float depths if FILE ends in .f32)", CmdLineMode::Headless},
{POFF(screenshotRenderTarget), CmdParamType::Bool, "screenshot-render-target", '\0', "Take screenshots of the current render target instead of the displayed framebuffer", CmdLineMode::Headless},
+1
View File
@@ -195,6 +195,7 @@ struct CommandLineOptions {
// Headless: apply the game's compatibility settings when replaying a GE frame dump.
std::optional<bool> dumpCompat;
std::optional<bool> hardwareTransform;
std::optional<bool> vertexJit;
// Headless: comma-separated clip-distance, cull-distance, depth-clamp, framebuffer-fetch, dual-source-blend.
std::optional<std::string> disableGPUFeatures;
std::optional<bool> screenshotRenderTarget;
+2 -1
View File
@@ -135,7 +135,8 @@ void IRFrontend::BranchRSZeroComp(MIPSOpcode op, IRComparison cc, bool andLink,
js.downcountAmount += MIPSGetInstructionCycleEstimate(branchInfo.delaySlotOp);
MIPSGPReg lhs = rs;
if (!branchInfo.delaySlotIsNice) { // if likely, we don't need this
// The link happens after rs is read, which matters for ra (cpu/cpu_alu/branch_link_ra).
if (!branchInfo.delaySlotIsNice || (andLink && rs == MIPS_REG_RA)) {
ir.Write(IROp::Mov, IRTEMP_LHS, rs);
lhs = (MIPSGPReg)IRTEMP_LHS;
}
+78 -43
View File
@@ -688,14 +688,18 @@ namespace MIPSComp {
// d[N,M] = CONST[N,M]
int vd = _VD;
// (A 1x1 matrix has no transposed form, the bit then picks a different register.)
if (sz != M_1x1 && IsMatrixTransposed(vd)) {
// All outputs are transpositionally symmetric, so should be fine.
vd = TransposeMatrixReg(vd);
}
u8 dregs[16];
u8 dregs[16]{};
GetMatrixRegs(dregs, sz, vd);
// The constants are symmetric, so a transposed matrix comes out as Vec4s taken in transposed
// order. (Flipping the transposed bit instead names other registers for a matrix with an
// offset, which wraps around.)
if (!IsMatrixVec4(sz, dregs)) {
u8 dt[16];
for (int k = 0; k < 16; k++)
dt[k] = dregs[(k % 4) * 4 + k / 4];
if (IsMatrixVec4(sz, dt))
memcpy(dregs, dt, sizeof(dt));
}
if (!IsMatrixVec4(sz, dregs)) {
// 3x3 is decently common. It expands a lot, but let's set each.
// TODO: It might be worth using Vec4Blend for 3x3 to mask w.
@@ -729,14 +733,8 @@ namespace MIPSComp {
return;
}
// Not really about trying here, it will work if enabled.
VectorSize vsz = GetVectorSize(sz);
u8 vecs[4];
GetMatrixColumns(vd, sz, vecs);
// Each Vec4 of dregs in turn (lane i of the i-th is the diagonal).
for (int i = 0; i < 4; i++) {
u8 vec[4];
GetVectorRegs(vec, vsz, vecs[i]);
// As they are columns, they will be nicely consecutive.
Vec4Init init;
switch ((op >> 16) & 0xF) {
case 3:
@@ -752,7 +750,7 @@ namespace MIPSComp {
INVALIDOP;
return;
}
ir.Write(IROp::Vec4Init, vec[0], (int)init);
ir.Write(IROp::Vec4Init, dregs[i * 4], (int)init);
}
}
@@ -1332,16 +1330,22 @@ namespace MIPSComp {
uint8_t imm = (op >> 16) & 0x1f;
u8 sregs[4], dregs[4];
u8 sregs[4], dregs[4], outregs[4];
GetVectorRegsPrefixS(sregs, sz, _VS);
GetVectorRegsPrefixD(dregs, sz, _VD);
// 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++)
outregs[i] = direct ? dregs[i] : IRVTEMP_0 + i;
for (int i = 0; i < n; i++) {
if (imm == 0)
ir.Write(IROp::FCvtSW, dregs[i], sregs[i]);
ir.Write(IROp::FCvtSW, outregs[i], sregs[i]);
else
ir.Write(IROp::FCvtScaledSW, dregs[i], sregs[i], imm);
ir.Write(IROp::FCvtScaledSW, outregs[i], sregs[i], imm);
}
for (int i = 0; i < n && !direct; i++)
ir.Write(IROp::FMov, dregs[i], outregs[i]);
ApplyPrefixD(dregs, sz, _VD);
}
@@ -1388,9 +1392,13 @@ namespace MIPSComp {
uint8_t imm = (op >> 16) & 0x1f;
u8 sregs[4], dregs[4];
u8 sregs[4], dregs[4], outregs[4];
GetVectorRegsPrefixS(sregs, sz, _VS);
GetVectorRegsPrefixD(dregs, sz, _VD);
// 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++)
outregs[i] = direct ? dregs[i] : IRVTEMP_0 + i;
// Same values as FCR31.
uint8_t rmode = (op >> 21) & 3;
@@ -1399,24 +1407,24 @@ namespace MIPSComp {
if (imm != 0) {
for (int i = 0; i < n; i++)
ir.Write(IROp::FCvtScaledWS, dregs[i], sregs[i], imm | (rmode << 6));
ir.Write(IROp::FCvtScaledWS, outregs[i], sregs[i], imm | (rmode << 6));
} else {
for (int i = 0; i < n; i++) {
switch (IRRoundMode(rmode)) {
case IRRoundMode::RINT_0: // vf2in
ir.Write(IROp::FRound, dregs[i], sregs[i]);
ir.Write(IROp::FRound, outregs[i], sregs[i]);
break;
case IRRoundMode::CAST_1: // vf2iz
ir.Write(IROp::FTrunc, dregs[i], sregs[i]);
ir.Write(IROp::FTrunc, outregs[i], sregs[i]);
break;
case IRRoundMode::CEIL_2: // vf2iu
ir.Write(IROp::FCeil, dregs[i], sregs[i]);
ir.Write(IROp::FCeil, outregs[i], sregs[i]);
break;
case IRRoundMode::FLOOR_3: // vf2id
ir.Write(IROp::FFloor, dregs[i], sregs[i]);
ir.Write(IROp::FFloor, outregs[i], sregs[i]);
break;
default:
@@ -1424,6 +1432,8 @@ namespace MIPSComp {
}
}
}
for (int i = 0; i < n && !direct; i++)
ir.Write(IROp::FMov, dregs[i], outregs[i]);
ApplyPrefixDMask(dregs, sz, _VD);
}
@@ -1534,7 +1544,7 @@ namespace MIPSComp {
if (GetVFPUCtrlMask(imm, &mask)) {
u32 setBits = GetVFPUCtrlSetBits(imm);
if (mask != 0xFFFFFFFF || setBits != 0) {
ir.Write(IROp::FMovToGPR, IRTEMP_0, vfpuBase + voffset[imm]);
ir.Write(IROp::FMovToGPR, IRTEMP_0, vfpuBase + voffset[vs]);
ir.Write(IROp::AndConst, IRTEMP_0, IRTEMP_0, 0, mask);
if (setBits != 0)
ir.Write(IROp::OrConst, IRTEMP_0, IRTEMP_0, 0, setBits);
@@ -1637,18 +1647,32 @@ namespace MIPSComp {
MatrixSize sz = GetMtxSize(op);
int n = GetMatrixSide(sz);
// The entire matrix is scaled equally, so transpose doesn't matter. Let's normalize.
// (A 1x1 matrix has no transposed form, the bit then picks a different register.)
if (n > 1 && IsMatrixTransposed(vs) && IsMatrixTransposed(vd)) {
vs = TransposeMatrixReg(vs);
vd = TransposeMatrixReg(vd);
}
u8 sregs[16], dregs[16], tregs[1];
u8 sregs[16]{}, dregs[16]{}, tregs[1];
GetMatrixRegs(sregs, sz, vs);
GetMatrixRegs(dregs, sz, vd);
GetVectorRegs(tregs, V_Single, vt);
// Every element is scaled the same, so the elements can go in any order, as long as each
// source keeps its destination. Transposed matrices come out as Vec4s when both are taken
// in transposed order. (Flipping the transposed bit of the registers instead would name other
// registers for a matrix with an offset, which wraps around.)
if (!IsMatrixVec4(sz, sregs) || !IsMatrixVec4(sz, dregs)) {
u8 st[16], dt[16];
for (int k = 0; k < 16; k++) {
st[k] = sregs[(k % 4) * 4 + k / 4];
dt[k] = dregs[(k % 4) * 4 + k / 4];
}
if (IsMatrixVec4(sz, st) && IsMatrixVec4(sz, dt)) {
memcpy(sregs, st, sizeof(st));
memcpy(dregs, dt, sizeof(dt));
}
}
// Either way, a source that partly overlaps the destination would be read after it's written.
if (vs != vd && GetMatrixOverlap(vs, vd, sz) != OVERLAP_NONE) {
DISABLE;
}
if (GetMtx(vt) != GetMtx(vd) && IsMatrixVec4(sz, sregs) && IsMatrixVec4(sz, dregs)) {
for (int i = 0; i < n; ++i) {
ir.Write(IROp::Vec4Scale, dregs[i * 4], sregs[i * 4], tregs[0]);
@@ -1656,11 +1680,7 @@ namespace MIPSComp {
return;
}
// Element by element otherwise, which works for any size and transposition. A source that
// partly overlaps the destination would be read after it's written, though.
if (vs != vd && GetMatrixOverlap(vs, vd, sz) != OVERLAP_NONE) {
DISABLE;
}
// Element by element otherwise, which works for any size and transposition.
// The scale can be part of the destination, so keep a copy.
ir.Write(IROp::FMov, IRVTEMP_0, tregs[0]);
for (int a = 0; a < n; a++) {
@@ -2183,9 +2203,15 @@ namespace MIPSComp {
VectorSize sz = GetVecSize(op);
VectorSize outsize;
// vc2i and vuc2i read one lane, the first of the source at the size encoded, which decides
// which register that is.
int sourceReg = -1;
if (bits == 8) {
outsize = V_Quad;
sz = V_Single; // For some reason, sz is set to Quad in this case though the outsize is Single.
u8 full[4];
GetVectorRegs(full, sz, _VS);
sourceReg = full[0];
sz = V_Single;
} else {
switch (sz) {
case V_Single:
@@ -2201,6 +2227,8 @@ namespace MIPSComp {
u8 sregs[2], dregs[4], tempregs[4], srcregs[2];
GetVectorRegsPrefixS(sregs, sz, _VS);
if (sourceReg >= 0)
sregs[0] = sourceReg;
GetVectorRegsPrefixD(dregs, outsize, _VD);
memcpy(tempregs, dregs, sizeof(dregs));
memcpy(srcregs, sregs, sizeof(sregs));
@@ -2455,6 +2483,14 @@ namespace MIPSComp {
DISABLE;
}
}
// A D prefix that saturates puts the result in temps first. A lane that isn't moved keeps
// d, so they start as d.
u8 realregs[4];
GetVectorRegs(realregs, sz, _VD);
for (int i = 0; i < n; ++i) {
if (dregs[i] != realregs[i])
ir.Write(IROp::FMov, dregs[i], realregs[i]);
}
// 7 tests a CC bit that is always clear (cpu/vfpu/undefined).
if (imm3 != 6) {
// Test one bit of CC. This bit decides whether none or all subregisters are copied.
@@ -2493,13 +2529,12 @@ namespace MIPSComp {
// Vector half-float immediate
// d[0] = float(imm)
FP16 half;
half.u = op & 0xFFFF;
FP32 fval = half_to_float_fast5(half);
// Converted like vh2f: denormal halves are zero (cpu/vfpu/vfim).
const float value = vfpu_h2f_float((u16)(op & 0xFFFF));
u8 dreg;
GetVectorRegsPrefixD(&dreg, V_Single, _VT);
ir.WriteSetConstantFloat(dreg, fval.f);
ir.WriteSetConstantFloat(dreg, value);
ApplyPrefixD(&dreg, V_Single, _VT);
}
+13 -2
View File
@@ -1157,6 +1157,15 @@ bool PurgeTemps(const IRWriter &in, IRWriter &out, const IROptions &opts) {
checkMismatch(inst.src3, inst.m.types[0]);
bool cannotReplace = !readsDirectly || lenMismatch;
// Nor where a multi-lane result would cover the source it reads instead, which the
// backends don't expect (the copy was what kept them apart).
IRReg destRegs[4];
const int numDest = IRDestFPRs(inst, destRegs);
if (numDest > 1 && check.srcReg >= destRegs[0] + 32 && check.srcReg < destRegs[0] + 32 + numDest)
cannotReplace = true;
// Nor where it's read as src3 that's also the destination, which renaming would move.
if ((inst.m.flags & IRFLAG_SRC3DST) != 0 && inst.m.types[0] == 'F' && inst.src3 + 32 == check.reg)
cannotReplace = true;
if (!cannotReplace && check.srcReg >= 32 && lastWrittenTo[check.srcReg] < check.index) {
// This is probably not worth doing unless we can get rid of a temp.
if (!check.readByExit) {
@@ -1167,7 +1176,7 @@ bool PurgeTemps(const IRWriter &in, IRWriter &out, const IROptions &opts) {
inst.src1 = srcReg;
if (inst.m.types[2] == 'F' && inst.src2 == reg)
inst.src2 = srcReg;
if ((inst.m.flags & (IRFLAG_SRC3 | IRFLAG_SRC3DST)) != 0 && inst.m.types[0] == 'F' && inst.src3 == reg)
if ((inst.m.flags & IRFLAG_SRC3) != 0 && inst.m.types[0] == 'F' && inst.src3 == reg)
inst.src3 = srcReg;
// If this also writes the reg, the check ends here. A full overwrite leaves the
@@ -2322,7 +2331,9 @@ bool OptimizeLoadsAfterStores(const IRWriter &in, IRWriter &out, const IROptions
known.erase(std::remove_if(known.begin(), known.end(), [&](const Known &k) {
if (k.base != inst.src1 || !plainStore)
return true;
return !(k.offset + k.size <= inst.constant || inst.constant + info.size <= k.offset);
// Signed, as the offsets are: -16 + 16 mustn't wrap to below -12.
const s64 known = (s32)k.offset, store = (s32)inst.constant;
return !(known + k.size <= store || store + info.size <= known);
}), known.end());
}
+8 -2
View File
@@ -576,11 +576,17 @@ void IRNativeRegCacheBase::DiscardNativeReg(IRNativeReg nreg) {
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.
if (nr[nreg].mipsReg == IRREG_INVALID) {
// Nothing to do, reg not mapped.
_dbg_assert_(!nr[nreg].isDirty);
return;
}
if (nr[nreg].mipsReg == MIPS_REG_ZERO) {
// Nothing to store, but whoever flushes is about to clobber it.
_dbg_assert_(!nr[nreg].isDirty);
DiscardNativeReg(nreg);
return;
}
_dbg_assert_(!mr[nr[nreg].mipsReg].isStatic);
if (mr[nr[nreg].mipsReg].isStatic) {
ERROR_LOG(Log::JIT, "Cannot FlushNativeReg a statically mapped register");
+10 -8
View File
@@ -288,16 +288,18 @@ namespace MIPSInt {
int imm = _SIMM16_SHL2;
int rs = _RS;
u32 addr = PC + imm + 4;
// The linking ones test rs from before they link, ra included (cpu/cpu_alu/branch_link_ra).
const s32 value = (s32)R(rs);
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
case 0: if (value < 0) DelayBranchTo(mips, addr); else PC += 4; break;//bltz
case 1: if (value >= 0) DelayBranchTo(mips, addr); else PC += 4; break;//bgez
case 2: if (value < 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bltzl
case 3: if (value >= 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bgezl
case 16: R(MIPS_REG_RA) = PC + 8; if (value < 0) DelayBranchTo(mips, addr); else PC += 4; break;//bltzal
case 17: R(MIPS_REG_RA) = PC + 8; if (value >= 0) DelayBranchTo(mips, addr); else PC += 4; break;//bgezal
case 18: R(MIPS_REG_RA) = PC + 8; if (value < 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bltzall
case 19: R(MIPS_REG_RA) = PC + 8; if (value >= 0) DelayBranchTo(mips, addr); else SkipLikely(mips); break;//bgezall
default:
_dbg_assert_msg_(false,"Trying to interpret instruction that can't be interpreted");
break;
+14 -5
View File
@@ -236,6 +236,15 @@ namespace MIPSInt
PC += 4;
}
// The loads write every lane, whatever mask a pending D prefix has, and leave the prefix pending
// (cpu/vfpu/prefix_load).
static void WriteLoadedVector(MIPSState *mips, const float *d, int vt) {
const u32 dprefix = mips->vfpuCtrl[VFPU_CTRL_DPREFIX];
mips->vfpuCtrl[VFPU_CTRL_DPREFIX] = dprefix & ~0xF00;
WriteVector(mips, d, V_Quad, vt);
mips->vfpuCtrl[VFPU_CTRL_DPREFIX] = dprefix;
}
void Int_SVQ(MIPSState *mips, MIPSOpcode op) {
int imm = SignExtend16ToS32(op & 0xFFFC);
int rs = _RS;
@@ -269,7 +278,7 @@ namespace MIPSInt
d[i] = valid ? Memory::ReadUnchecked_Float(addr + 4 * i) : 0.0f;
}
}
WriteVector(mips, d, V_Quad, vt);
WriteLoadedVector(mips, d, vt);
}
break;
@@ -279,13 +288,13 @@ namespace MIPSInt
if ((addr & 0xF) || !Memory::IsValid4AlignedAddress(addr)) {
Core_MemoryException(addr, 16, PC, MemoryExceptionType::READ_WORD, "lv.q");
const float zero[4]{};
WriteVector(mips, zero, V_Quad, vt);
WriteLoadedVector(mips, zero, vt);
break;
}
#ifndef COMMON_BIG_ENDIAN
cf = reinterpret_cast<const float *>(Memory::GetPointerUnchecked(addr));
WriteVector(mips, cf, V_Quad, vt);
WriteLoadedVector(mips, cf, vt);
#else
float lvqd[4];
@@ -294,7 +303,7 @@ namespace MIPSInt
lvqd[2] = Memory::ReadUnchecked_Float(addr + 8);
lvqd[3] = Memory::ReadUnchecked_Float(addr + 12);
WriteVector(mips, lvqd, V_Quad, vt);
WriteLoadedVector(mips, lvqd, vt);
#endif
break;
@@ -465,7 +474,7 @@ namespace MIPSInt
if (type == 6) {
f[0] = (float)imm; // viim
} else if (type == 7) {
f[0] = Float16ToFloat32((u16)uimm16); // vfim
f[0] = vfpu_h2f_float((u16)uimm16); // vfim, converted like vh2f (cpu/vfpu/vfim)
} else {
_dbg_assert_msg_( 0, "Invalid Viim opcode type %d", type);
f[0] = 0;
+4
View File
@@ -680,6 +680,10 @@ u32 vfpu_h2f(u16 h) {
return sign | ((exp + 112) << 23) | (mant << 13);
}
float vfpu_h2f_float(u16 h) {
return vfpu_float_from_bits(vfpu_h2f(h));
}
float vfpu_h2f_lower(float word) {
return vfpu_float_from_bits(vfpu_h2f((u16)(vfpu_bits_from_float(word) & 0xFFFF)));
}
+2
View File
@@ -311,6 +311,8 @@ float Float16ToFloat32(unsigned short l);
// flushes below 2^-14 to zero and keeps the low ten mantissa bits of a NaN; vh2f flushes
// subnormal halves and keeps inf/NaN mantissa bits unshifted.
u32 vfpu_h2f(u16 h);
// The same, as a float (vfim).
float vfpu_h2f_float(u16 h);
// vh2f of the lower or upper half of a word, with the word and result as float bits. For the JITs.
float vfpu_h2f_lower(float word);
float vfpu_h2f_upper(float word);
+2 -3
View File
@@ -1117,9 +1117,8 @@ int main(int argc, const char* argv[]) {
// but not now.
CoreParameter coreParameter;
coreParameter.cpuCore = (CPUCore)cpuCore;
// The pspautotests expectations and frametest references were recorded with the C++ vertex
// decoder, and the JIT decoders don't match it everywhere yet.
coreParameter.bUseVertexDecoderJit = false;
// Like the app, decode vertices with the JIT where the host can run one.
coreParameter.bUseVertexDecoderJit = cmdLineOptions.vertexJit.value_or(System_GetPropertyBool(SYSPROP_CAN_JIT));
coreParameter.gpuCore = (GPUCore)gpuCore;
coreParameter.graphicsContext = graphicsContext;
coreParameter.enableSound = false;
+3
View File
@@ -87,6 +87,7 @@ class Command(object):
tests_good = [
"cpu/cpu_alu/cpu_alu",
"cpu/cpu_alu/cpu_branch",
"cpu/cpu_alu/branch_link_ra",
"cpu/cpu_alu/cpu_branch2",
"cpu/cpu_alu/cpu_div",
"cpu/cpu_alu/shift_encodings",
@@ -101,6 +102,8 @@ tests_good = [
"cpu/vfpu/prefix_branch",
"cpu/vfpu/prefix_ctrl",
"cpu/vfpu/prefix_lanes",
"cpu/vfpu/prefix_load",
"cpu/vfpu/vfim",
"cpu/vfpu/vbranch",
"cpu/vfpu/vrnd",
"cpu/vfpu/overlap",