mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 23:10:33 +02:00
The crosshair turns red, and lock-on works, when stencil bit 3 is set at the centre of the frame, which the game tests by reading the pixel from VRAM with the CPU (#17936). Hook that and read back the stencil of that row into the alpha bytes in RAM. The alpha the hardware backends keep doesn't follow stencil everywhere, so reading color isn't enough. Adds PerformReadbackStencilToMemory for this (8888 only for now). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2132 lines
79 KiB
C++
2132 lines
79 KiB
C++
// Copyright (c) 2013- PPSSPP Project.
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
// Official git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <unordered_map>
|
|
|
|
#include "Common/CommonTypes.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/Swap.h"
|
|
#include "Core/System.h"
|
|
#include "Core/Debugger/Breakpoints.h"
|
|
#include "Core/Debugger/MemBlockInfo.h"
|
|
#include "Core/Debugger/SymbolMap.h"
|
|
#include "Core/MemMap.h"
|
|
#include "Core/MIPS/JitCommon/JitCommon.h"
|
|
#include "Core/MIPS/MIPS.h"
|
|
#include "Core/MIPS/MIPSCodeUtils.h"
|
|
#include "Core/MIPS/MIPSAnalyst.h"
|
|
#include "Core/MIPS/MIPSTables.h"
|
|
#include "Core/HLE/ReplaceTables.h"
|
|
#include "Core/HLE/FunctionWrappers.h"
|
|
#include "Core/HLE/sceDisplay.h"
|
|
|
|
#include "GPU/Math3D.h"
|
|
#include "GPU/GPU.h"
|
|
#include "GPU/GPUCommon.h"
|
|
#include "Common/Math/SIMDHeaders.h"
|
|
#include "ext/xxhash.h"
|
|
|
|
enum class GPUReplacementSkip {
|
|
MEMSET = 1,
|
|
MEMCPY = 2,
|
|
MEMMOVE = 4,
|
|
};
|
|
|
|
static int skipGPUReplacements = 0;
|
|
|
|
// I think these have to be pretty accurate as these are libc replacements,
|
|
// but we can probably get away with approximating the VFPU vsin/vcos and vrot
|
|
// pretty roughly.
|
|
static int Replace_sinf() {
|
|
float f = PARAMF(0);
|
|
RETURNF(sinf(f));
|
|
return 80; // guess number of cycles
|
|
}
|
|
|
|
static int Replace_cosf() {
|
|
float f = PARAMF(0);
|
|
RETURNF(cosf(f));
|
|
return 80; // guess number of cycles
|
|
}
|
|
|
|
static int Replace_tanf() {
|
|
float f = PARAMF(0);
|
|
RETURNF(tanf(f));
|
|
return 80; // guess number of cycles
|
|
}
|
|
|
|
static int Replace_acosf() {
|
|
float f = PARAMF(0);
|
|
RETURNF(acosf(f));
|
|
return 80; // guess number of cycles
|
|
}
|
|
|
|
static int Replace_asinf() {
|
|
float f = PARAMF(0);
|
|
RETURNF(asinf(f));
|
|
return 80; // guess number of cycles
|
|
}
|
|
|
|
static int Replace_atanf() {
|
|
float f = PARAMF(0);
|
|
RETURNF(atanf(f));
|
|
return 80; // guess number of cycles
|
|
}
|
|
|
|
static int Replace_sqrtf() {
|
|
float f = PARAMF(0);
|
|
RETURNF(sqrtf(f));
|
|
return 80; // guess number of cycles
|
|
}
|
|
|
|
static int Replace_atan2f() {
|
|
float f1 = PARAMF(0);
|
|
float f2 = PARAMF(1);
|
|
RETURNF(atan2f(f1, f2));
|
|
return 120; // guess number of cycles
|
|
}
|
|
|
|
static int Replace_floorf() {
|
|
float f1 = PARAMF(0);
|
|
RETURNF(floorf(f1));
|
|
return 30; // guess number of cycles
|
|
}
|
|
|
|
static int Replace_ceilf() {
|
|
float f1 = PARAMF(0);
|
|
RETURNF(ceilf(f1));
|
|
return 30; // guess number of cycles
|
|
}
|
|
|
|
// Should probably do JIT versions of this, possibly ones that only delegate
|
|
// large copies to a C function.
|
|
static int Replace_memcpy() {
|
|
u32 destPtr = PARAM(0);
|
|
u32 srcPtr = PARAM(1);
|
|
u32 bytes = PARAM(2);
|
|
bool skip = false;
|
|
if (!bytes) {
|
|
RETURN(destPtr);
|
|
return 10;
|
|
}
|
|
|
|
if ((skipGPUReplacements & (int)GPUReplacementSkip::MEMCPY) == 0) {
|
|
if (Memory::IsVRAMAddress(destPtr) || Memory::IsVRAMAddress(srcPtr)) {
|
|
skip = gpu->PerformMemoryCopy(destPtr, srcPtr, bytes);
|
|
}
|
|
}
|
|
gpu->NotifyVideoCopy(destPtr, srcPtr, bytes);
|
|
if (!skip && bytes != 0) {
|
|
u8 *dst = Memory::GetPointerWriteRangeOrException(destPtr, bytes);
|
|
const u8 *src = Memory::GetPointerRangeOrException(srcPtr, bytes);
|
|
|
|
if (!dst || !src) {
|
|
// Already logged.
|
|
} else if (std::min(destPtr, srcPtr) + bytes > std::max(destPtr, srcPtr)) {
|
|
// Overlap. Star Ocean breaks if it's not handled in 16 bytes blocks.
|
|
const u32 blocks = bytes & ~0x0f;
|
|
for (u32 offset = 0; offset < blocks; offset += 0x10) {
|
|
memcpy(dst + offset, src + offset, 0x10);
|
|
}
|
|
for (u32 offset = blocks; offset < bytes; ++offset) {
|
|
dst[offset] = src[offset];
|
|
}
|
|
} else {
|
|
memmove(dst, src, bytes);
|
|
}
|
|
}
|
|
RETURN(destPtr);
|
|
|
|
if (MemBlockInfoDetailed(bytes)) {
|
|
// It's pretty common that games will copy video data.
|
|
// Detect that by manually reading the tag when the size looks right.
|
|
if (bytes == 512 * 272 * 4) {
|
|
char tagData[128];
|
|
size_t tagSize = FormatMemWriteTagAt(tagData, sizeof(tagData), "ReplaceMemcpy/", srcPtr, bytes);
|
|
NotifyMemInfo(MemBlockFlags::READ, srcPtr, bytes, tagData, tagSize);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, destPtr, bytes, tagData, tagSize);
|
|
|
|
if (!strcmp(tagData, "ReplaceMemcpy/VideoDecode") || !strcmp(tagData, "ReplaceMemcpy/VideoDecodeRange")) {
|
|
gpu->PerformWriteFormattedFromMemory(destPtr, bytes, 512, GE_FORMAT_8888);
|
|
}
|
|
} else {
|
|
NotifyMemInfoCopy(destPtr, srcPtr, bytes, "ReplaceMemcpy/");
|
|
}
|
|
}
|
|
|
|
return 10 + bytes / 4; // approximation
|
|
}
|
|
|
|
static int Replace_memcpy_jak() {
|
|
u32 destPtr = PARAM(0);
|
|
u32 srcPtr = PARAM(1);
|
|
u32 bytes = PARAM(2);
|
|
|
|
if (bytes == 0) {
|
|
RETURN(destPtr);
|
|
return 5;
|
|
}
|
|
|
|
bool skip = false;
|
|
bool sliced = false;
|
|
static constexpr uint32_t SLICE_SIZE = 32768;
|
|
|
|
if ((skipGPUReplacements & (int)GPUReplacementSkip::MEMCPY) == 0) {
|
|
if (Memory::IsVRAMAddress(destPtr) || Memory::IsVRAMAddress(srcPtr)) {
|
|
skip = gpu->PerformMemoryCopy(destPtr, srcPtr, bytes);
|
|
}
|
|
}
|
|
gpu->NotifyVideoCopy(destPtr, srcPtr, bytes);
|
|
if (!skip && bytes > SLICE_SIZE && bytes != 512 * 272 * 4 && !PSP_CoreParameter().compat.flags().DisableMemcpySlicing) {
|
|
// This is a very slow func. To avoid thread blocking, do a slice at a time.
|
|
// Avoiding exactly 512 * 272 * 4 to detect videos, though.
|
|
bytes = SLICE_SIZE;
|
|
sliced = true;
|
|
}
|
|
if (!skip && bytes != 0) {
|
|
u8 *dst = Memory::GetPointerWriteRangeOrException(destPtr, bytes);
|
|
const u8 *src = Memory::GetPointerRangeOrException(srcPtr, bytes);
|
|
|
|
if (dst && src) {
|
|
// Jak style overlap.
|
|
for (u32 i = 0; i < bytes; i++) {
|
|
dst[i] = src[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sliced) {
|
|
currentMIPS->r[MIPS_REG_A0] += SLICE_SIZE;
|
|
currentMIPS->r[MIPS_REG_A1] += SLICE_SIZE;
|
|
currentMIPS->r[MIPS_REG_A2] -= SLICE_SIZE;
|
|
} else {
|
|
// Jak relies on more registers coming out right than the ABI specifies.
|
|
// See the disassembly of the function for the explanations for these...
|
|
currentMIPS->r[MIPS_REG_T0] = 0;
|
|
currentMIPS->r[MIPS_REG_A0] = -1;
|
|
currentMIPS->r[MIPS_REG_A2] = 0;
|
|
// Even after slicing, this ends up correct.
|
|
currentMIPS->r[MIPS_REG_A3] = destPtr + bytes;
|
|
RETURN(destPtr);
|
|
}
|
|
|
|
if (MemBlockInfoDetailed(bytes)) {
|
|
// It's pretty common that games will copy video data.
|
|
// Detect that by manually reading the tag when the size looks right.
|
|
if (bytes == 512 * 272 * 4) {
|
|
char tagData[128];
|
|
size_t tagSize = FormatMemWriteTagAt(tagData, sizeof(tagData), "ReplaceMemcpy/", srcPtr, bytes);
|
|
NotifyMemInfo(MemBlockFlags::READ, srcPtr, bytes, tagData, tagSize);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, destPtr, bytes, tagData, tagSize);
|
|
|
|
if (!strcmp(tagData, "ReplaceMemcpy/VideoDecode") || !strcmp(tagData, "ReplaceMemcpy/VideoDecodeRange")) {
|
|
gpu->PerformWriteFormattedFromMemory(destPtr, bytes, 512, GE_FORMAT_8888);
|
|
}
|
|
} else {
|
|
NotifyMemInfoCopy(destPtr, srcPtr, bytes, "ReplaceMemcpy/");
|
|
}
|
|
}
|
|
|
|
if (sliced) {
|
|
// Negative causes the function to be run again for the next slice.
|
|
return 5 + bytes * -8 + 2;
|
|
}
|
|
return 5 + bytes * 8 + 2; // approximation. This is a slow memcpy - a byte copy loop..
|
|
}
|
|
|
|
static int Replace_memcpy16() {
|
|
u32 destPtr = PARAM(0);
|
|
u32 srcPtr = PARAM(1);
|
|
u32 bytes = PARAM(2) * 16;
|
|
bool skip = false;
|
|
|
|
if ((skipGPUReplacements & (int)GPUReplacementSkip::MEMCPY) == 0 && bytes != 0) {
|
|
if (Memory::IsVRAMAddress(destPtr) || Memory::IsVRAMAddress(srcPtr)) {
|
|
skip = gpu->PerformMemoryCopy(destPtr, srcPtr, bytes);
|
|
}
|
|
}
|
|
gpu->NotifyVideoCopy(destPtr, srcPtr, bytes);
|
|
if (!skip && bytes != 0) {
|
|
u8 *dst = Memory::GetPointerWriteRangeOrException(destPtr, bytes);
|
|
const u8 *src = Memory::GetPointerRangeOrException(srcPtr, bytes);
|
|
if (dst && src) {
|
|
memmove(dst, src, bytes);
|
|
}
|
|
}
|
|
RETURN(destPtr);
|
|
|
|
if (MemBlockInfoDetailed(bytes)) {
|
|
NotifyMemInfoCopy(destPtr, srcPtr, bytes, "ReplaceMemcpy16/");
|
|
}
|
|
|
|
return 10 + bytes / 4; // approximation
|
|
}
|
|
|
|
static int Replace_memcpy_swizzled() {
|
|
u32 destPtr = PARAM(0);
|
|
u32 srcPtr = PARAM(1);
|
|
u32 pitch = PARAM(2);
|
|
u32 h = PARAM(4);
|
|
if ((skipGPUReplacements & (int)GPUReplacementSkip::MEMCPY) == 0) {
|
|
if (Memory::IsVRAMAddress(srcPtr)) {
|
|
gpu->PerformReadbackToMemory(srcPtr, pitch * h);
|
|
}
|
|
}
|
|
u8 *dstp = Memory::GetPointerWriteRangeOrException(destPtr, pitch * h);
|
|
const u8 *srcp = Memory::GetPointerRangeOrException(srcPtr, pitch * h);
|
|
|
|
if (dstp && srcp) {
|
|
const u8 *ysrcp = srcp;
|
|
for (u32 y = 0; y < h; y += 8) {
|
|
const u8 *xsrcp = ysrcp;
|
|
for (u32 x = 0; x < pitch; x += 16) {
|
|
const u8 *src = xsrcp;
|
|
for (int n = 0; n < 8; ++n) {
|
|
memcpy(dstp, src, 16);
|
|
src += pitch;
|
|
dstp += 16;
|
|
}
|
|
xsrcp += 16;
|
|
}
|
|
ysrcp += 8 * pitch;
|
|
}
|
|
}
|
|
|
|
RETURN(0);
|
|
|
|
if (MemBlockInfoDetailed(pitch * h)) {
|
|
NotifyMemInfoCopy(destPtr, srcPtr, pitch * h, "ReplaceMemcpySwizzle/");
|
|
}
|
|
|
|
return 10 + (pitch * h) / 4; // approximation
|
|
}
|
|
|
|
static int Replace_memmove() {
|
|
u32 destPtr = PARAM(0);
|
|
u32 srcPtr = PARAM(1);
|
|
u32 bytes = PARAM(2);
|
|
bool skip = false;
|
|
|
|
if ((skipGPUReplacements & (int)GPUReplacementSkip::MEMMOVE) == 0 && bytes != 0) {
|
|
if (Memory::IsVRAMAddress(destPtr) || Memory::IsVRAMAddress(srcPtr)) {
|
|
skip = gpu->PerformMemoryCopy(destPtr, srcPtr, bytes);
|
|
}
|
|
}
|
|
gpu->NotifyVideoCopy(destPtr, srcPtr, bytes);
|
|
if (!skip && bytes != 0) {
|
|
u8 *dst = Memory::GetPointerWriteRangeOrException(destPtr, bytes);
|
|
const u8 *src = Memory::GetPointerRangeOrException(srcPtr, bytes);
|
|
if (dst && src) {
|
|
memmove(dst, src, bytes);
|
|
}
|
|
}
|
|
RETURN(destPtr);
|
|
|
|
if (MemBlockInfoDetailed(bytes)) {
|
|
NotifyMemInfoCopy(destPtr, srcPtr, bytes, "ReplaceMemmove/");
|
|
}
|
|
|
|
return 10 + bytes / 4; // approximation
|
|
}
|
|
|
|
static int Replace_memset() {
|
|
u32 destPtr = PARAM(0);
|
|
u8 value = PARAM(1);
|
|
u32 bytes = PARAM(2);
|
|
bool skip = false;
|
|
if (Memory::IsVRAMAddress(destPtr) && (skipGPUReplacements & (int)GPUReplacementSkip::MEMSET) == 0) {
|
|
skip = gpu->PerformMemorySet(destPtr, value, bytes);
|
|
}
|
|
if (!skip && bytes != 0) {
|
|
u8 *dst = Memory::GetPointerWriteRangeOrException(destPtr, bytes);
|
|
if (dst) {
|
|
memset(dst, value, bytes);
|
|
}
|
|
}
|
|
RETURN(destPtr);
|
|
|
|
NotifyMemInfo(MemBlockFlags::WRITE, destPtr, bytes, "ReplaceMemset");
|
|
|
|
return 10 + bytes / 4; // approximation
|
|
}
|
|
|
|
static int Replace_memset_jak() {
|
|
u32 destPtr = PARAM(0);
|
|
u8 value = PARAM(1);
|
|
u32 bytes = PARAM(2);
|
|
|
|
if (bytes == 0) {
|
|
RETURN(destPtr);
|
|
return 5;
|
|
}
|
|
|
|
bool skip = false;
|
|
bool sliced = false;
|
|
static constexpr uint32_t SLICE_SIZE = 32768;
|
|
if (Memory::IsVRAMAddress(destPtr) && (skipGPUReplacements & (int)GPUReplacementSkip::MEMSET) == 0) {
|
|
skip = gpu->PerformMemorySet(destPtr, value, bytes);
|
|
}
|
|
if (!skip && bytes > SLICE_SIZE && !PSP_CoreParameter().compat.flags().DisableMemcpySlicing) {
|
|
// This is a very slow func. To avoid thread blocking, do a slice at a time.
|
|
bytes = SLICE_SIZE;
|
|
sliced = true;
|
|
}
|
|
if (!skip && bytes != 0) {
|
|
u8 *dst = Memory::GetPointerWriteRangeOrException(destPtr, bytes);
|
|
if (dst) {
|
|
memset(dst, value, bytes);
|
|
}
|
|
}
|
|
|
|
NotifyMemInfo(MemBlockFlags::WRITE, destPtr, bytes, "ReplaceMemset");
|
|
|
|
if (sliced) {
|
|
currentMIPS->r[MIPS_REG_A0] += SLICE_SIZE;
|
|
currentMIPS->r[MIPS_REG_A2] -= SLICE_SIZE;
|
|
|
|
// This is approximate, and must be a negative value.
|
|
// Negative causes the function to be run again for the next slice.
|
|
return 5 + (int)SLICE_SIZE * -6 + 2;
|
|
}
|
|
|
|
// Even after slicing, this ends up correct.
|
|
currentMIPS->r[MIPS_REG_T0] = destPtr + bytes;
|
|
currentMIPS->r[MIPS_REG_A2] = -1;
|
|
currentMIPS->r[MIPS_REG_A3] = -1;
|
|
RETURN(destPtr);
|
|
|
|
return 5 + bytes * 6 + 2; // approximation
|
|
}
|
|
|
|
static uint32_t SafeStringLen(const uint32_t ptr, uint32_t maxLen = 0x07FFFFFF) {
|
|
maxLen = Memory::ClampValidSizeAt(ptr, 0x07FFFFFF);
|
|
const uint8_t *p = Memory::GetPointerRangeOrException(ptr, maxLen);
|
|
if (!p)
|
|
return 0;
|
|
const uint8_t *end = (const uint8_t *)memchr(p, '\0', maxLen);
|
|
if (!end)
|
|
return 0;
|
|
return (uint32_t)(end - p);
|
|
}
|
|
|
|
static int Replace_strlen() {
|
|
u32 srcPtr = PARAM(0);
|
|
u32 len = SafeStringLen(srcPtr);
|
|
RETURN(len);
|
|
return 7 + len * 4; // approximation
|
|
}
|
|
|
|
static int Replace_strcpy() {
|
|
u32 destPtr = PARAM(0);
|
|
u32 srcPtr = PARAM(1);
|
|
u32 len = SafeStringLen(srcPtr);
|
|
char *dst = (char *)Memory::GetPointerWriteRangeOrException(destPtr, len);
|
|
const char *src = (const char *)Memory::GetPointerRangeOrException(srcPtr, len);
|
|
if (dst && src && len != 0) {
|
|
strcpy(dst, src);
|
|
}
|
|
RETURN(destPtr);
|
|
return 10; // approximation
|
|
}
|
|
|
|
static int Replace_strncpy() {
|
|
u32 destPtr = PARAM(0);
|
|
u32 srcPtr = PARAM(1);
|
|
u32 bytes = PARAM(2);
|
|
char *dst = (char *)Memory::GetPointerRangeOrException(destPtr, bytes);
|
|
u32 srcLen = SafeStringLen(srcPtr, bytes);
|
|
const char *src = (const char *)Memory::GetPointerRangeOrException(srcPtr, srcLen == 0 ? bytes : srcLen);
|
|
if (dst && src && bytes != 0) {
|
|
strncpy(dst, src, bytes);
|
|
}
|
|
RETURN(destPtr);
|
|
return 10; // approximation
|
|
}
|
|
|
|
static int Replace_strcmp() {
|
|
u32 aLen = SafeStringLen(PARAM(0));
|
|
const char *a = (const char *)Memory::GetPointerRangeOrException(PARAM(0), aLen);
|
|
u32 bLen = SafeStringLen(PARAM(1));
|
|
const char *b = (const char *)Memory::GetPointerRangeOrException(PARAM(1), bLen);
|
|
if (a && b && aLen != 0 && bLen != 0) {
|
|
RETURN(strcmp(a, b));
|
|
} else {
|
|
RETURN(0);
|
|
}
|
|
return 10; // approximation
|
|
}
|
|
|
|
static int Replace_strncmp() {
|
|
u32 bytes = PARAM(2);
|
|
u32 aLen = SafeStringLen(PARAM(0), bytes);
|
|
const char *a = (const char *)Memory::GetPointerRangeOrException(PARAM(0), aLen == 0 ? bytes : aLen);
|
|
u32 bLen = SafeStringLen(PARAM(1), bytes);
|
|
const char *b = (const char *)Memory::GetPointerRangeOrException(PARAM(1), bLen == 0 ? bytes : bLen);
|
|
if (a && b && bytes != 0) {
|
|
RETURN(strncmp(a, b, bytes));
|
|
} else {
|
|
RETURN(0);
|
|
}
|
|
return 10 + bytes / 4; // approximation
|
|
}
|
|
|
|
static int Replace_fabsf() {
|
|
RETURNF(fabsf(PARAMF(0)));
|
|
return 4;
|
|
}
|
|
|
|
static int Replace_vmmul_q_transp() {
|
|
float_le *out = (float_le *)Memory::GetPointerRangeOrException(PARAM(0), 16 * 4);
|
|
const float_le *a = (const float_le *)Memory::GetPointerRangeOrException(PARAM(1), 16 * 4);
|
|
const float_le *b = (const float_le *)Memory::GetPointerRangeOrException(PARAM(2), 16 * 4);
|
|
|
|
// TODO: Actually use an optimized matrix multiply here...
|
|
if (out && b && a) {
|
|
#ifdef COMMON_BIG_ENDIAN
|
|
float outn[16], an[16], bn[16];
|
|
for (int i = 0; i < 16; ++i) {
|
|
an[i] = a[i];
|
|
bn[i] = b[i];
|
|
}
|
|
Matrix4ByMatrix4(outn, bn, an);
|
|
for (int i = 0; i < 16; ++i) {
|
|
out[i] = outn[i];
|
|
}
|
|
#else
|
|
Matrix4ByMatrix4(out, b, a);
|
|
#endif
|
|
}
|
|
return 16;
|
|
}
|
|
|
|
// a0 = pointer to destination address
|
|
// a1 = matrix
|
|
// a2 = source address
|
|
static int Replace_gta_dl_write_matrix() {
|
|
u32_le *ptr = (u32_le *)Memory::GetPointerWriteRangeOrException(PARAM(0), 4);
|
|
const u32_le *src = (const u32_le *)Memory::GetPointerRangeOrException(PARAM(2), 16);
|
|
u32 matrix = PARAM(1) << 24;
|
|
|
|
if (!ptr || !src) {
|
|
RETURN(0);
|
|
return 38;
|
|
}
|
|
|
|
u32_le *dest = (u32_le *)Memory::GetPointerWriteRangeOrException(ptr[0], 12 * 4);
|
|
if (!dest) {
|
|
RETURN(0);
|
|
return 38;
|
|
}
|
|
|
|
#if PPSSPP_ARCH(X86) || PPSSPP_ARCH(AMD64)
|
|
__m128i topBytes = _mm_set1_epi32(matrix);
|
|
__m128i m0 = _mm_loadu_si128((const __m128i *)src);
|
|
__m128i m1 = _mm_loadu_si128((const __m128i *)(src + 4));
|
|
__m128i m2 = _mm_loadu_si128((const __m128i *)(src + 8));
|
|
__m128i m3 = _mm_loadu_si128((const __m128i *)(src + 12));
|
|
m0 = _mm_or_si128(_mm_srli_epi32(m0, 8), topBytes);
|
|
m1 = _mm_or_si128(_mm_srli_epi32(m1, 8), topBytes);
|
|
m2 = _mm_or_si128(_mm_srli_epi32(m2, 8), topBytes);
|
|
m3 = _mm_or_si128(_mm_srli_epi32(m3, 8), topBytes);
|
|
// These three stores overlap by a word, due to the offsets.
|
|
_mm_storeu_si128((__m128i *)dest, m0);
|
|
_mm_storeu_si128((__m128i *)(dest + 3), m1);
|
|
_mm_storeu_si128((__m128i *)(dest + 6), m2);
|
|
// Store the last one in parts to not overwrite forwards (probably mostly risk free though)
|
|
_mm_storel_epi64((__m128i *)(dest + 9), m3);
|
|
m3 = _mm_srli_si128(m3, 8);
|
|
_mm_store_ss((float *)(dest + 11), _mm_castsi128_ps(m3));
|
|
#else
|
|
// Bit tricky to SIMD (note the offsets) but should be doable if not perfect
|
|
dest[0] = matrix | (src[0] >> 8);
|
|
dest[1] = matrix | (src[1] >> 8);
|
|
dest[2] = matrix | (src[2] >> 8);
|
|
dest[3] = matrix | (src[4] >> 8);
|
|
dest[4] = matrix | (src[5] >> 8);
|
|
dest[5] = matrix | (src[6] >> 8);
|
|
dest[6] = matrix | (src[8] >> 8);
|
|
dest[7] = matrix | (src[9] >> 8);
|
|
dest[8] = matrix | (src[10] >> 8);
|
|
dest[9] = matrix | (src[12] >> 8);
|
|
dest[10] = matrix | (src[13] >> 8);
|
|
dest[11] = matrix | (src[14] >> 8);
|
|
#endif
|
|
|
|
(*ptr) += 0x30;
|
|
|
|
RETURN(0);
|
|
return 38;
|
|
}
|
|
|
|
|
|
// TODO: Inline into a few NEON or SSE instructions - especially if a1 is a known immediate!
|
|
// Anyway, not sure if worth it. There's not that many matrices written per frame normally.
|
|
static int Replace_dl_write_matrix() {
|
|
u32_le *dlStruct = (u32_le *)Memory::GetPointerWriteRangeOrException(PARAM(0), 3 * 4);
|
|
const u32_le *src = (const u32_le *)Memory::GetPointerRangeOrException(PARAM(2), 16 * 4);
|
|
|
|
if (!dlStruct || !src) {
|
|
RETURN(0);
|
|
return 60;
|
|
}
|
|
|
|
u32 matrix = 0;
|
|
int count = 12;
|
|
switch (PARAM(1)) {
|
|
case 3:
|
|
matrix = 0x40000000; // tex mtx
|
|
break;
|
|
case 2:
|
|
matrix = 0x3A000000;
|
|
break;
|
|
case 1:
|
|
matrix = 0x3C000000;
|
|
break;
|
|
case 0:
|
|
matrix = 0x3E000000;
|
|
count = 16;
|
|
break;
|
|
}
|
|
|
|
u32_le *dest = (u32_le *)Memory::GetPointerWriteRangeOrException(dlStruct[2], 4 + count * 4);
|
|
if (!dest) {
|
|
RETURN(0);
|
|
return 60;
|
|
}
|
|
|
|
*dest++ = matrix;
|
|
matrix += 0x01000000;
|
|
|
|
if (count == 16) {
|
|
// Ultra SIMD friendly! These intrinsics generate pretty much perfect code,
|
|
// no point in hand rolling.
|
|
#if PPSSPP_ARCH(X86) || PPSSPP_ARCH(AMD64)
|
|
__m128i topBytes = _mm_set1_epi32(matrix);
|
|
__m128i m0 = _mm_loadu_si128((const __m128i *)src);
|
|
__m128i m1 = _mm_loadu_si128((const __m128i *)(src + 4));
|
|
__m128i m2 = _mm_loadu_si128((const __m128i *)(src + 8));
|
|
__m128i m3 = _mm_loadu_si128((const __m128i *)(src + 12));
|
|
m0 = _mm_or_si128(_mm_srli_epi32(m0, 8), topBytes);
|
|
m1 = _mm_or_si128(_mm_srli_epi32(m1, 8), topBytes);
|
|
m2 = _mm_or_si128(_mm_srli_epi32(m2, 8), topBytes);
|
|
m3 = _mm_or_si128(_mm_srli_epi32(m3, 8), topBytes);
|
|
_mm_storeu_si128((__m128i *)dest, m0);
|
|
_mm_storeu_si128((__m128i *)(dest + 4), m1);
|
|
_mm_storeu_si128((__m128i *)(dest + 8), m2);
|
|
_mm_storeu_si128((__m128i *)(dest + 12), m3);
|
|
#else
|
|
#if 0
|
|
//TODO: Finish NEON, make conditional somehow
|
|
uint32x4_t topBytes = vdupq_n_u32(matrix);
|
|
uint32x4_t m0 = vld1q_u32(dataPtr);
|
|
uint32x4_t m1 = vld1q_u32(dataPtr + 4);
|
|
uint32x4_t m2 = vld1q_u32(dataPtr + 8);
|
|
uint32x4_t m3 = vld1q_u32(dataPtr + 12);
|
|
m0 = vorr_u32(vsri_n_u32(m0, 8), topBytes); // TODO: look into VSRI
|
|
m1 = vorr_u32(vshr_n_u32(m1, 8), topBytes);
|
|
m2 = vorr_u32(vshr_n_u32(m2, 8), topBytes);
|
|
m3 = vorr_u32(vshr_n_u32(m3, 8), topBytes);
|
|
vst1q_u32(dlPtr, m0);
|
|
vst1q_u32(dlPtr + 4, m1);
|
|
vst1q_u32(dlPtr + 8, m2);
|
|
vst1q_u32(dlPtr + 12, m3);
|
|
#endif
|
|
for (int i = 0; i < count; i++) {
|
|
dest[i] = matrix | (src[i] >> 8);
|
|
}
|
|
#endif
|
|
} else {
|
|
#if PPSSPP_ARCH(X86) || PPSSPP_ARCH(AMD64)
|
|
__m128i topBytes = _mm_set1_epi32(matrix);
|
|
__m128i m0 = _mm_loadu_si128((const __m128i *)src);
|
|
__m128i m1 = _mm_loadu_si128((const __m128i *)(src + 4));
|
|
__m128i m2 = _mm_loadu_si128((const __m128i *)(src + 8));
|
|
__m128i m3 = _mm_loadu_si128((const __m128i *)(src + 12));
|
|
m0 = _mm_or_si128(_mm_srli_epi32(m0, 8), topBytes);
|
|
m1 = _mm_or_si128(_mm_srli_epi32(m1, 8), topBytes);
|
|
m2 = _mm_or_si128(_mm_srli_epi32(m2, 8), topBytes);
|
|
m3 = _mm_or_si128(_mm_srli_epi32(m3, 8), topBytes);
|
|
// These three stores overlap by a word, due to the offsets.
|
|
_mm_storeu_si128((__m128i *)dest, m0);
|
|
_mm_storeu_si128((__m128i *)(dest + 3), m1);
|
|
_mm_storeu_si128((__m128i *)(dest + 6), m2);
|
|
// Store the last one in parts to not overwrite forwards (probably mostly risk free though)
|
|
_mm_storel_epi64((__m128i *)(dest + 9), m3);
|
|
m3 = _mm_srli_si128(m3, 8);
|
|
_mm_store_ss((float *)(dest + 11), _mm_castsi128_ps(m3));
|
|
#else
|
|
// Bit tricky to SIMD (note the offsets) but should be doable if not perfect
|
|
dest[0] = matrix | (src[0] >> 8);
|
|
dest[1] = matrix | (src[1] >> 8);
|
|
dest[2] = matrix | (src[2] >> 8);
|
|
dest[3] = matrix | (src[4] >> 8);
|
|
dest[4] = matrix | (src[5] >> 8);
|
|
dest[5] = matrix | (src[6] >> 8);
|
|
dest[6] = matrix | (src[8] >> 8);
|
|
dest[7] = matrix | (src[9] >> 8);
|
|
dest[8] = matrix | (src[10] >> 8);
|
|
dest[9] = matrix | (src[12] >> 8);
|
|
dest[10] = matrix | (src[13] >> 8);
|
|
dest[11] = matrix | (src[14] >> 8);
|
|
#endif
|
|
}
|
|
|
|
NotifyMemInfo(MemBlockFlags::READ, PARAM(2), 16 * sizeof(float), "ReplaceDLWriteMatrix");
|
|
NotifyMemInfo(MemBlockFlags::WRITE, PARAM(0) + 2 * sizeof(u32), sizeof(u32), "ReplaceDLWriteMatrix");
|
|
NotifyMemInfo(MemBlockFlags::WRITE, dlStruct[2], (count + 1) * sizeof(u32), "ReplaceDLWriteMatrix");
|
|
|
|
dlStruct[2] += (1 + count) * 4;
|
|
RETURN(dlStruct[2]);
|
|
return 60;
|
|
}
|
|
|
|
static bool GetMIPSStaticAddress(u32 &addr, s32 lui_offset, s32 lw_offset) {
|
|
const MIPSOpcode upper = Memory::Read_Instruction(currentMIPS->pc + lui_offset);
|
|
if (upper != MIPS_MAKE_LUI(MIPS_GET_RT(upper), upper & 0xffff)) {
|
|
return false;
|
|
}
|
|
const MIPSOpcode lower = Memory::Read_Instruction(currentMIPS->pc + lw_offset);
|
|
if (lower != MIPS_MAKE_LW(MIPS_GET_RT(lower), MIPS_GET_RS(lower), lower & 0xffff)) {
|
|
if (lower != MIPS_MAKE_ORI(MIPS_GET_RT(lower), MIPS_GET_RS(lower), lower & 0xffff)) {
|
|
return false;
|
|
}
|
|
}
|
|
addr = ((upper & 0xffff) << 16) + (s16)(lower & 0xffff);
|
|
return true;
|
|
}
|
|
|
|
static bool GetMIPSGPAddress(u32 &addr, s32 offset) {
|
|
const MIPSOpcode loadOp = Memory::Read_Instruction(currentMIPS->pc + offset);
|
|
if (MIPS_GET_RS(loadOp) == MIPS_REG_GP) {
|
|
s16 gpoff = (s16)(u16)(loadOp & 0x0000FFFF);
|
|
addr = currentMIPS->r[MIPS_REG_GP] + gpoff;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static int Hook_godseaterburst_blit_texture() {
|
|
u32 texaddr;
|
|
// Only if there's no texture.
|
|
if (!GetMIPSStaticAddress(texaddr, 0x000c, 0x0030) || !Memory::IsValid4AlignedAddress(texaddr)) {
|
|
return 0;
|
|
}
|
|
|
|
u32 fb_infoaddr;
|
|
if (Memory::ReadUnchecked_U32(texaddr) != 0 || !GetMIPSStaticAddress(fb_infoaddr, 0x01d0, 0x01d4) || !Memory::IsValid4AlignedAddress(fb_infoaddr)) {
|
|
return 0;
|
|
}
|
|
|
|
const u32 fb_info = Memory::ReadUnchecked_U32(fb_infoaddr);
|
|
if (!Memory::IsValid4AlignedAddress(fb_info)) {
|
|
return 0;
|
|
}
|
|
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(fb_info);
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "godseaterburst_blit_texture");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_godseaterburst_depthmask_5551() {
|
|
// This function copies the 5551 framebuffer to a temporary, generating alpha based on depth.
|
|
// Depth is optional, in which case all pixels get full alpha.
|
|
// Called when your avatar changes to screenshot for save data.
|
|
uint32_t colorBuffer = currentMIPS->r[MIPS_REG_A1];
|
|
uint32_t depthBuffer = currentMIPS->r[MIPS_REG_T2];
|
|
uint32_t byteStride = currentMIPS->r[MIPS_REG_A2];
|
|
uint32_t height = currentMIPS->r[MIPS_REG_T1];
|
|
uint32_t size = byteStride * height;
|
|
|
|
if (!Memory::IsVRAMAddress(colorBuffer) || !Memory::IsValidRange(colorBuffer, size))
|
|
return 0;
|
|
if (depthBuffer != 0) {
|
|
if (!Memory::IsVRAMAddress(colorBuffer) || !Memory::IsValidRange(depthBuffer, size))
|
|
return 0;
|
|
|
|
// This is added to read from the linearized mirror.
|
|
uint32_t depthMirror = depthBuffer + 0x00200000;
|
|
// Depth download required, or it won't work and will be transparent.
|
|
gpu->PerformMemoryCopy(depthMirror, depthMirror, size, GPUCopyFlag::FORCE_DST_MATCH_MEM | GPUCopyFlag::DEPTH_REQUESTED);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, depthMirror, size, "godseaterburst_depthmask_5551");
|
|
}
|
|
|
|
gpu->PerformReadbackToMemory(colorBuffer, size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, colorBuffer, size, "godseaterburst_depthmask_5551");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_hexyzforce_monoclome_thread() {
|
|
u32 fb_info;
|
|
if (!GetMIPSStaticAddress(fb_info, -4, 0)) {
|
|
return 0;
|
|
}
|
|
if (!Memory::IsValid4AlignedAddress(fb_info)) {
|
|
return 0;
|
|
}
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(fb_info);
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "hexyzforce_monoclome_thread");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_starocean_write_stencil() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_T7];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformWriteStencilFromMemory(fb_address, 0x00088000, WriteStencil::IGNORE_ALPHA);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_topx_create_saveicon() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_V0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformMemoryCopy(fb_address, fb_address, 0x00044000, GPUCopyFlag::FORCE_DST_MATCH_MEM | GPUCopyFlag::DISALLOW_CREATE_VFB);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "topx_create_saveicon");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_ff1_battle_effect() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A1];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "ff1_battle_effect");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_dissidia_recordframe_avi() {
|
|
// This is called once per frame, and records that frame's data to avi.
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A1];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "dissidia_recordframe_avi");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_brandish_download_frame() {
|
|
u32 fb_infoaddr;
|
|
if (!GetMIPSStaticAddress(fb_infoaddr, 0x2c, 0x30)) {
|
|
return 0;
|
|
}
|
|
if (!Memory::IsValid4AlignedRange(fb_infoaddr, 256)) { // TODO: Figure out the right range to check.
|
|
return 0;
|
|
}
|
|
const u32 fb_info = Memory::ReadUnchecked_U32(fb_infoaddr);
|
|
const MIPSOpcode fb_index_load = Memory::Read_Instruction(currentMIPS->pc + 0x38);
|
|
if (fb_index_load != MIPS_MAKE_LW(MIPS_GET_RT(fb_index_load), MIPS_GET_RS(fb_index_load), fb_index_load & 0xffff)) {
|
|
return 0;
|
|
}
|
|
const int fb_index_offset = (s16)(fb_index_load & 0xffff);
|
|
const u32 fb_index = (Memory::ReadUnchecked_U32(fb_info + fb_index_offset) + 1) & 1;
|
|
const u32 fb_address = 0x4000000 + (0x44000 * fb_index);
|
|
const u32 dest_address = currentMIPS->r[MIPS_REG_A1];
|
|
if (Memory::IsRAMAddress(dest_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "brandish_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_growlanser_create_saveicon() {
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_SP] + 4);
|
|
const u32 fmt = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_SP]);
|
|
const u32 sz = fmt == GE_FORMAT_8888 ? 0x00088000 : 0x00044000;
|
|
if (Memory::IsVRAMAddress(fb_address) && fmt <= 3) {
|
|
gpu->PerformMemoryCopy(fb_address, fb_address, sz, GPUCopyFlag::FORCE_DST_MATCH_MEM | GPUCopyFlag::DISALLOW_CREATE_VFB);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, sz, "growlanser_create_saveicon");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_sd_gundam_g_generation_download_frame() {
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_SP] + 8);
|
|
const u32 fmt = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_SP] + 4);
|
|
const u32 sz = fmt == GE_FORMAT_8888 ? 0x00088000 : 0x00044000;
|
|
if (Memory::IsVRAMAddress(fb_address) && fmt <= 3) {
|
|
gpu->PerformReadbackToMemory(fb_address, sz);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, sz, "sd_gundam_g_generation_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_narisokonai_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_V0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "narisokonai_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_kirameki_school_life_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A2];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "kirameki_school_life_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_orenoimouto_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A4];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "orenoimouto_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_sakurasou_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_V0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "sakurasou_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_suikoden1_and_2_download_frame_1() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_S4];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "suikoden1_and_2_download_frame_1");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_suikoden1_and_2_download_frame_2() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_S2];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "suikoden1_and_2_download_frame_2");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_rezel_cross_download_frame() {
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_SP] + 0x1C);
|
|
const u32 fmt = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_SP] + 0x14);
|
|
const u32 sz = fmt == GE_FORMAT_8888 ? 0x00088000 : 0x00044000;
|
|
if (Memory::IsVRAMAddress(fb_address) && fmt <= 3) {
|
|
gpu->PerformReadbackToMemory(fb_address, sz);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, sz, "rezel_cross_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_kagaku_no_ensemble_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_V0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "kagaku_no_ensemble_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_soranokiseki_fc_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A2];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "soranokiseki_fc_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_soranokiseki_sc_download_frame() {
|
|
u32 fb_infoaddr;
|
|
if (!GetMIPSStaticAddress(fb_infoaddr, 0x28, 0x2C)) {
|
|
return 0;
|
|
}
|
|
if (!Memory::IsValid4AlignedAddress(fb_infoaddr)) {
|
|
return 0;
|
|
}
|
|
|
|
const u32 fb_info = Memory::ReadUnchecked_U32(fb_infoaddr);
|
|
const MIPSOpcode fb_index_load = Memory::Read_Instruction(currentMIPS->pc + 0x34);
|
|
if (fb_index_load != MIPS_MAKE_LW(MIPS_GET_RT(fb_index_load), MIPS_GET_RS(fb_index_load), fb_index_load & 0xffff)) {
|
|
return 0;
|
|
}
|
|
const int fb_index_offset = (s16)(fb_index_load & 0xffff);
|
|
const u32 fb_index = (Memory::ReadUnchecked_U32(fb_info + fb_index_offset) + 1) & 1;
|
|
const u32 fb_address = 0x4000000 + (0x44000 * fb_index);
|
|
const u32 dest_address = currentMIPS->r[MIPS_REG_A1];
|
|
if (Memory::IsRAMAddress(dest_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "soranokiseki_sc_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_bokunonatsuyasumi4_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A3];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "bokunonatsuyasumi4_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_danganronpa2_1_download_frame() {
|
|
const u32 fb_base = currentMIPS->r[MIPS_REG_V0];
|
|
const u32 fb_offset = currentMIPS->r[MIPS_REG_V1];
|
|
const u32 fb_offset_fix = fb_offset & 0xFFFFFFFC;
|
|
const u32 fb_address = fb_base + fb_offset_fix;
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "danganronpa2_1_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_danganronpa2_2_download_frame() {
|
|
const u32 fb_base = currentMIPS->r[MIPS_REG_V0];
|
|
const u32 fb_offset = currentMIPS->r[MIPS_REG_V1];
|
|
const u32 fb_offset_fix = fb_offset & 0xFFFFFFFC;
|
|
const u32 fb_address = fb_base + fb_offset_fix;
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "danganronpa2_2_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_danganronpa1_1_download_frame() {
|
|
const u32 fb_base = currentMIPS->r[MIPS_REG_A5];
|
|
const u32 fb_offset = currentMIPS->r[MIPS_REG_V0];
|
|
const u32 fb_offset_fix = fb_offset & 0xFFFFFFFC;
|
|
const u32 fb_address = fb_base + fb_offset_fix;
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "danganronpa1_1_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_danganronpa1_2_download_frame() {
|
|
const MIPSOpcode instruction = Memory::Read_Instruction(currentMIPS->pc + 0x8);
|
|
const int reg_num = instruction >> 11 & 31;
|
|
const u32 fb_base = currentMIPS->r[reg_num];
|
|
const u32 fb_offset = currentMIPS->r[MIPS_REG_V0];
|
|
const u32 fb_offset_fix = fb_offset & 0xFFFFFFFC;
|
|
const u32 fb_address = fb_base + fb_offset_fix;
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "danganronpa1_2_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_kankabanchoutbr_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A1];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "kankabanchoutbr_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_orenoimouto_download_frame_2() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A4];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "orenoimouto_download_frame_2");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_rewrite_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "rewrite_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_kudwafter_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "kudwafter_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_kumonohatateni_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "kumonohatateni_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_otomenoheihou_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "otomenoheihou_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_grisaianokajitsu_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "grisaianokajitsu_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_kokoroconnect_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A3];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "kokoroconnect_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_toheart2_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A1];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "toheart2_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_toheart2_download_frame_2() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "toheart2_download_frame_2");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_flowers_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "flowers_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_motorstorm_download_frame() {
|
|
if (!Memory::IsValid4AlignedAddress(currentMIPS->r[MIPS_REG_A1] + 0x18)) {
|
|
return 0;
|
|
}
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_A1] + 0x18);
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "motorstorm_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_utawarerumono_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "utawarerumono_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_photokano_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A1];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "photokano_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_photokano_download_frame_2() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A1];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "photokano_download_frame_2");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_gakuenheaven_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "gakuenheaven_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_youkosohitsujimura_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_V0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "youkosohitsujimura_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_zettai_hero_update_minimap_tex() {
|
|
const MIPSOpcode storeOffset = Memory::Read_Instruction(currentMIPS->pc + 4);
|
|
const uint32_t texAddr = currentMIPS->r[MIPS_REG_A0] + SignExtend16ToS32(storeOffset);
|
|
const uint32_t texSize = 64 * 64 * 1;
|
|
const uint32_t writeAddr = currentMIPS->r[MIPS_REG_V1] + SignExtend16ToS32(storeOffset);
|
|
if (Memory::IsValidRange(texAddr, texSize) && writeAddr >= texAddr && writeAddr < texAddr + texSize) {
|
|
const uint8_t currentValue = Memory::ReadUnchecked_U8(writeAddr);
|
|
if (currentValue != currentMIPS->r[MIPS_REG_A3]) {
|
|
gpu->InvalidateCache(texAddr, texSize, GPU_INVALIDATE_FORCE);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_tonyhawkp8_upload_tutorial_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformWriteColorFromMemory(fb_address, 0x00088000);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_sdgundamggenerationportable_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A3];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "sdgundamggenerationportable_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_atvoffroadfurypro_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_S2];
|
|
const u32 fb_size = (currentMIPS->r[MIPS_REG_S4] >> 3) * currentMIPS->r[MIPS_REG_S3];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, fb_size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, fb_size, "atvoffroadfurypro_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_atvoffroadfuryblazintrails_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_S5];
|
|
const u32 fb_size = (currentMIPS->r[MIPS_REG_S3] >> 3) * currentMIPS->r[MIPS_REG_S2];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, fb_size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, fb_size, "atvoffroadfuryblazintrails_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_littlebustersce_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_A0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "littlebustersce_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_shinigamitoshoujo_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_S2];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "shinigamitoshoujo_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_atvoffroadfuryprodemo_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_S5];
|
|
const u32 fb_size = ((currentMIPS->r[MIPS_REG_A0] + currentMIPS->r[MIPS_REG_A1]) >> 3) * currentMIPS->r[MIPS_REG_S2];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, fb_size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, fb_size, "atvoffroadfuryprodemo_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_unendingbloodycall_download_frame() {
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_T3];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "unendingbloodycall_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_omertachinmokunookitethelegacy_download_frame() {
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_SP] + 4);
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00044000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00044000, "omertachinmokunookitethelegacy_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Function at 0886665C in US version (Persona 1)
|
|
// Function at 08807DC4 in EU version (Persona 2)
|
|
static int Hook_persona_download_frame() {
|
|
// Depending on a global (curframe kind of thing), this either reads from
|
|
// 0x04088000 or 0x04000000 (the two addresses are hardcoded).
|
|
// We'd have to do some gnarly stuff to get this address, so let's just download both.
|
|
for (int i = 0; i < 2; i++) {
|
|
const u32 fb_address = i == 0 ? 0x04000000 : 0x04088000;
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "persona1_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_steinsgate_download_frame() {
|
|
u32 fb_offset_addr;
|
|
if (!GetMIPSStaticAddress(fb_offset_addr, 0x1C, 0x20)) {
|
|
return 0;
|
|
}
|
|
if (!Memory::IsValid4AlignedAddress(fb_offset_addr)) {
|
|
return 0;
|
|
}
|
|
const u32 fb_address = 0x04000000 + Memory::ReadUnchecked_U32(fb_offset_addr);
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "steinsgate_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_infinity_download_frame() {
|
|
// There are a few games that share this same function.
|
|
// The hash matches, but due to relocations, the addresses it references differ.
|
|
// Because of this, the address, even though hardcoded, has to be fetched from the function.
|
|
u32 magic_value_addr;
|
|
if (!GetMIPSStaticAddress(magic_value_addr, 0x08, 0x1C)) {
|
|
return 0;
|
|
}
|
|
if (!Memory::IsValid4AlignedAddress(magic_value_addr)) {
|
|
return 0;
|
|
}
|
|
// Not sure why it was done like this, but that's what the actual function does.
|
|
const u32 fb_address = (Memory::ReadUnchecked_U32(magic_value_addr) & 1) ? 0x04000000 : 0x04088000;
|
|
|
|
gpu->PerformReadbackToMemory(fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, 0x00088000, "infinity_download_frame");
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_takuyo_download_frame() {
|
|
gpu->PerformReadbackToMemory(0x04088000, 0x00088000); // The offset is hardcoded.
|
|
NotifyMemInfo(MemBlockFlags::WRITE, 0x04088000, 0x00088000, "takuyo_download_frame");
|
|
return 0;
|
|
}
|
|
|
|
// Offsets in comments are valid for the US version of "KINGDOM HEARTS Birth by Sleep".
|
|
// Function at 0x0881EF68
|
|
static int Hook_kingdomhearts_download_frame() {
|
|
const u32 fb_base = 0x04000000; // Set in 0x0880C458, doesn't seem like it's ever overwriten.
|
|
|
|
const u32 get_fb_offset = MIPSCodeUtils::GetJumpTarget(currentMIPS->pc + 0x5C); // Jump to function at 0x08821EB0. Said function returns the framebuffer offset.
|
|
if (get_fb_offset == INVALIDTARGET) {
|
|
return 0;
|
|
}
|
|
u32 fb_offset_index_addr;
|
|
if (!GetMIPSStaticAddress(fb_offset_index_addr, get_fb_offset - currentMIPS->pc, get_fb_offset + 0x04 - currentMIPS->pc)) {
|
|
return 0;
|
|
}
|
|
if (!Memory::IsValidRange(fb_offset_index_addr, 4)) {
|
|
return 0;
|
|
}
|
|
|
|
const u32 fb_offset_index = Memory::ReadUnchecked_U32(fb_offset_index_addr); // 0x08821E90-0x08821E98
|
|
if (fb_offset_index > 2) {
|
|
return 0;
|
|
}
|
|
|
|
const MIPSOpcode fb_offset_table_lui = Memory::Read_Instruction(get_fb_offset + 0x08); // 0x08821EB8
|
|
if (fb_offset_table_lui != MIPS_MAKE_LUI(MIPS_REG_A1, fb_offset_table_lui & 0xFFFF)) {
|
|
return 0;
|
|
}
|
|
const MIPSOpcode fb_offset_table_addiu = Memory::Read_Instruction(get_fb_offset + 0x10); // 0x08821EC0
|
|
if (fb_offset_table_addiu != MIPS_MAKE_ADDIU(MIPS_REG_A1, MIPS_REG_A1, fb_offset_table_addiu & 0xFFFF)) {
|
|
return 0;
|
|
}
|
|
const u32 fb_offset_table = ((fb_offset_table_lui & 0xFFFF) << 16) + (s16)(fb_offset_table_addiu & 0xFFFF);
|
|
if (!Memory::IsValidRange(fb_offset_table, 12)) {
|
|
return 0;
|
|
}
|
|
const u32 fb_offset = Memory::ReadUnchecked_U32(fb_offset_table + fb_offset_index*4); // 0x08821E98-0x08821EB0
|
|
|
|
u32 magic_ptr_addr;
|
|
if (!GetMIPSStaticAddress(magic_ptr_addr, 0x08, 0x10)) {
|
|
return 0;
|
|
}
|
|
const u32 magic_ptr = Memory::ReadUnchecked_U32(magic_ptr_addr); // 0x0881EF70, 0x0881EF78
|
|
|
|
// Function of the variable guessed.
|
|
const u8 bytes_per_pixel = Memory::ReadUnchecked_U8(magic_ptr+0x50); // 0x0881EFE0
|
|
|
|
const u32 fb_address = fb_base + fb_offset;
|
|
const u32 fb_size = (bytes_per_pixel == 2) ? 0x044000 : 0x088000; // Branch at 0x0881EFE8, s3 set at 0x0881EFB8
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformReadbackToMemory(fb_address, fb_size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, fb_size, "kingdomhearts_download_frame");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// The Everybody's Golf games and Everybody's Tennis build player portraits the same way.
|
|
// copy_portrait(dst, x, y, w, h) memcpys a rect of the 512-stride 5551 framebuffer into a texture,
|
|
// one row at a time, which memcpy's replacement can't match to a framebuffer when it's a partial row.
|
|
// The caller then sets each texel's alpha bit from the depth buffer, read straight from the linear
|
|
// mirror at 0x04288000. So both are read back here, whole rows at a time.
|
|
// Each version is hooked after the GE sync at the start, where y and h are in registers and the
|
|
// framebuffer pointer is loaded from a global soon after.
|
|
static void EverybodysCopyPortrait(u32 y, u32 h, s32 luiOffset, s32 lwOffset) {
|
|
u32 fb_info;
|
|
if (!GetMIPSStaticAddress(fb_info, luiOffset, lwOffset) || !Memory::IsValid4AlignedAddress(fb_info)) {
|
|
return;
|
|
}
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(fb_info);
|
|
const u32 colorAddr = fb_address + y * 0x400;
|
|
const u32 depthAddr = 0x04288000 + y * 0x400;
|
|
const u32 size = h * 0x400;
|
|
if (!Memory::IsVRAMAddress(colorAddr) || !Memory::IsValidRange(colorAddr, size) || !Memory::IsValidRange(depthAddr, size)) {
|
|
return;
|
|
}
|
|
gpu->PerformReadbackToMemory(colorAddr, size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, colorAddr, size, "everybodys_copy_portrait");
|
|
gpu->PerformMemoryCopy(depthAddr, depthAddr, size, GPUCopyFlag::FORCE_DST_MATCH_MEM | GPUCopyFlag::DEPTH_REQUESTED);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, depthAddr, size, "everybodys_copy_portrait");
|
|
}
|
|
|
|
// Golf 2 (caller 0x0887C060) and Tennis (caller 0x0889B13C).
|
|
static int Hook_everybodys_copy_portrait() {
|
|
EverybodysCopyPortrait(currentMIPS->r[MIPS_REG_S4], currentMIPS->r[MIPS_REG_S1], 0x0C, 0x10);
|
|
return 0;
|
|
}
|
|
|
|
// Golf (caller 0x088A1FEC).
|
|
static int Hook_everybodysgolf_copy_portrait() {
|
|
EverybodysCopyPortrait(currentMIPS->r[MIPS_REG_S0], currentMIPS->r[MIPS_REG_S4], 0x00, 0x08);
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_everybodysgolf_spinner_background() {
|
|
// The loading screen spinner copies the displayed framebuffer under it into the transparent texels
|
|
// of each of its frames, then memcpys a frame back to the screen every so often.
|
|
// Hooked after the call to sceDisplayGetFrameBuf, where v0 is the framebuffer and s0 the spinner,
|
|
// with y at 0x14 and the frame height at 0x1C.
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_V0];
|
|
const u32 obj = currentMIPS->r[MIPS_REG_S0];
|
|
if (!Memory::IsVRAMAddress(fb_address) || !Memory::IsValidRange(obj + 0x14, 0xC)) {
|
|
return 0;
|
|
}
|
|
const s32 y = std::max((s32)Memory::ReadUnchecked_U32(obj + 0x14), 0);
|
|
const s32 yEnd = std::min((s32)Memory::ReadUnchecked_U32(obj + 0x14) + (s32)Memory::ReadUnchecked_U32(obj + 0x1C), 272);
|
|
if (yEnd <= y) {
|
|
return 0;
|
|
}
|
|
const u32 addr = fb_address + y * 0x400;
|
|
const u32 size = (yEnd - y) * 0x400;
|
|
if (Memory::IsValidRange(addr, size)) {
|
|
gpu->PerformReadbackToMemory(addr, size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, addr, size, "everybodysgolf_spinner_background");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_codedarms_stencil_check() {
|
|
// The crosshair turns red, and lock-on works, when stencil bit 3 is set at the centre of the frame
|
|
// just rendered, which the game tests by reading the pixel from VRAM (#17936). Hooked at the load,
|
|
// where v0 is the pixel's address. Read back the stencil of the row it's in (the game's stride is
|
|
// 512). Only stencil: the alpha the hardware backends keep doesn't follow it everywhere.
|
|
const u32 addr = currentMIPS->r[MIPS_REG_V0];
|
|
if (!Memory::IsVRAMAddress(addr)) {
|
|
return 0;
|
|
}
|
|
const u32 row = addr & ~0x7FF;
|
|
if (Memory::IsValidRange(row, 0x800)) {
|
|
gpu->PerformReadbackStencilToMemory(row, 0x800);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_katamari_render_check() {
|
|
if (!Memory::IsValidRange(currentMIPS->r[MIPS_REG_A0] + 0x3C, 8)) {
|
|
return 0;
|
|
}
|
|
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_A0] + 0x3C);
|
|
const u32 fbInfoPtr = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_A0] + 0x40);
|
|
if (Memory::IsVRAMAddress(fb_address) && fbInfoPtr != 0 && Memory::IsValid4AlignedRange(fbInfoPtr, 0x10)) {
|
|
const u32 sizeInfoPtr = Memory::ReadUnchecked_U32(fbInfoPtr + 0x0C);
|
|
if (Memory::IsValidRange(sizeInfoPtr, 0x10)) {
|
|
// These are the values it uses to control the loop.
|
|
// Width in memory appears to be stride / 8.
|
|
const u32 width = Memory::ReadUnchecked_U16(sizeInfoPtr + 0x08) * 8;
|
|
// Height in memory is also divided by 8 (but this one isn't hardcoded.)
|
|
const u32 heightBlocks = Memory::ReadUnchecked_U16(sizeInfoPtr + 0x0A);
|
|
// For some reason this is the number of heightBlocks less 1.
|
|
const u32 heightBlockCount = Memory::ReadUnchecked_U8(fbInfoPtr + 0x08) + 1;
|
|
|
|
const u32 totalBytes = width * heightBlocks * heightBlockCount;
|
|
gpu->PerformReadbackToMemory(fb_address, totalBytes);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, totalBytes, "katamari_render_check");
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_katamari_screenshot_to_565() {
|
|
u32 fb_address;
|
|
if (GetMIPSStaticAddress(fb_address, 0x0040, 0x0044)) {
|
|
gpu->PerformReadbackToMemory(0x04000000 | fb_address, 0x00088000);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, 0x04000000 | fb_address, 0x00088000, "katamari_screenshot_to_565");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_mytranwars_upload_frame() {
|
|
u32 fb_address = currentMIPS->r[MIPS_REG_S0];
|
|
if (Memory::IsVRAMAddress(fb_address)) {
|
|
gpu->PerformWriteColorFromMemory(fb_address, 0x00088000);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static u32 marvelalliance1_copy_src = 0;
|
|
static u32 marvelalliance1_copy_dst = 0;
|
|
static u32 marvelalliance1_copy_size = 0;
|
|
|
|
static int Hook_marvelalliance1_copy_a1_before() {
|
|
marvelalliance1_copy_src = currentMIPS->r[MIPS_REG_A1];
|
|
marvelalliance1_copy_dst = currentMIPS->r[MIPS_REG_V1];
|
|
marvelalliance1_copy_size = currentMIPS->r[MIPS_REG_V0] - currentMIPS->r[MIPS_REG_A1];
|
|
|
|
if (Memory::IsValidRange(marvelalliance1_copy_src, marvelalliance1_copy_size)) {
|
|
gpu->PerformReadbackToMemory(marvelalliance1_copy_src, marvelalliance1_copy_size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, marvelalliance1_copy_src, marvelalliance1_copy_size, "marvelalliance1_copy_a1_before");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_marvelalliance1_copy_a2_before() {
|
|
marvelalliance1_copy_src = currentMIPS->r[MIPS_REG_A2];
|
|
marvelalliance1_copy_dst = currentMIPS->r[MIPS_REG_V0];
|
|
marvelalliance1_copy_size = currentMIPS->r[MIPS_REG_A1] - currentMIPS->r[MIPS_REG_A2];
|
|
|
|
if (Memory::IsValidRange(marvelalliance1_copy_src, marvelalliance1_copy_size)) {
|
|
gpu->PerformReadbackToMemory(marvelalliance1_copy_src, marvelalliance1_copy_size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, marvelalliance1_copy_src, marvelalliance1_copy_size, "marvelalliance1_copy_a2_before");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_marvelalliance1_copy_after() {
|
|
if (Memory::IsValidRange(marvelalliance1_copy_dst, marvelalliance1_copy_size)) {
|
|
gpu->PerformWriteColorFromMemory(marvelalliance1_copy_dst, marvelalliance1_copy_size);
|
|
NotifyMemInfo(MemBlockFlags::READ, marvelalliance1_copy_dst, marvelalliance1_copy_size, "marvelalliance1_copy_after");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_starocean_clear_framebuf_before() {
|
|
skipGPUReplacements |= (int)GPUReplacementSkip::MEMSET;
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_starocean_clear_framebuf_after() {
|
|
skipGPUReplacements &= ~(int)GPUReplacementSkip::MEMSET;
|
|
|
|
// This hook runs after the copy, this is the final memcpy destination.
|
|
u32 framebuf = currentMIPS->r[MIPS_REG_V0] - 512 * 4 * 271;
|
|
u32 y_address, h_address;
|
|
|
|
if (GetMIPSGPAddress(y_address, -204) && GetMIPSGPAddress(h_address, -200)) {
|
|
if (Memory::IsValid2AlignedAddress(y_address) && Memory::IsValid2AlignedAddress(h_address)) {
|
|
int y = (s16)Memory::ReadUnchecked_U16(y_address);
|
|
int h = (s16)Memory::ReadUnchecked_U16(h_address);
|
|
DEBUG_LOG(Log::HLE, "starocean_clear_framebuf() - %08x y=%d-%d", framebuf, y, h);
|
|
// TODO: This is always clearing to 0, actually, which could be faster than an upload.
|
|
gpu->PerformWriteColorFromMemory(framebuf + 512 * y * 4, 512 * h * 4);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_motorstorm_pixel_read() {
|
|
if (!Memory::IsValidRange(currentMIPS->r[MIPS_REG_A0] + 0x18, 0x20)) {
|
|
return 0;
|
|
}
|
|
const u32 fb_address = Memory::ReadUnchecked_U32(currentMIPS->r[MIPS_REG_A0] + 0x18);
|
|
const u32 fb_height = Memory::ReadUnchecked_U16(currentMIPS->r[MIPS_REG_A0] + 0x26);
|
|
const u32 fb_stride = Memory::ReadUnchecked_U16(currentMIPS->r[MIPS_REG_A0] + 0x28);
|
|
gpu->PerformReadbackToMemory(fb_address, fb_height * fb_stride);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, fb_height * fb_stride, "motorstorm_pixel_read");
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_worms_copy_normalize_alpha() {
|
|
// At this point in the function (0x0CC), s1 is the framebuf and a2 is the size.
|
|
const u32 fb_address = currentMIPS->r[MIPS_REG_S1];
|
|
const u32 fb_size = currentMIPS->r[MIPS_REG_A2];
|
|
if (Memory::IsVRAMAddress(fb_address) && Memory::IsValidRange(fb_address, fb_size)) {
|
|
gpu->PerformReadbackToMemory(fb_address, fb_size);
|
|
NotifyMemInfo(MemBlockFlags::WRITE, fb_address, fb_size, "worms_copy_normalize_alpha");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_openseason_data_decode() {
|
|
static u32 firstWritePtr = 0;
|
|
|
|
u32 curWritePtr = currentMIPS->r[MIPS_REG_A0];
|
|
u32 endPtr = currentMIPS->r[MIPS_REG_A1];
|
|
u32 writeBytes = currentMIPS->r[MIPS_REG_V0];
|
|
u32 startPtr = curWritePtr - writeBytes;
|
|
if (Memory::IsVRAMAddress(startPtr) && (firstWritePtr == 0 || startPtr < firstWritePtr)) {
|
|
firstWritePtr = startPtr;
|
|
}
|
|
if (Memory::IsVRAMAddress(endPtr) && curWritePtr == endPtr) {
|
|
gpu->PerformWriteColorFromMemory(firstWritePtr, endPtr - firstWritePtr);
|
|
firstWritePtr = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_soltrigger_render_ucschar() {
|
|
u32 targetInfoPtrPtr = currentMIPS->r[MIPS_REG_A2];
|
|
u32 targetInfoPtr = Memory::IsValidRange(targetInfoPtrPtr, 4) ? Memory::ReadUnchecked_U32(targetInfoPtrPtr) : 0;
|
|
if (Memory::IsValidRange(targetInfoPtr, 32)) {
|
|
u32 targetPtr = Memory::ReadUnchecked_U32(targetInfoPtr + 8);
|
|
u32 targetByteStride = Memory::ReadUnchecked_U32(targetInfoPtr + 16);
|
|
|
|
// We don't know the height specifically.
|
|
gpu->InvalidateCache(targetPtr, targetByteStride * 512, GPU_INVALIDATE_HINT);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_gow_fps_hack() {
|
|
if (PSP_CoreParameter().compat.flags().GoWFramerateHack60 || PSP_CoreParameter().compat.flags().FramerateHack30) {
|
|
if (PSP_CoreParameter().compat.flags().FramerateHack30) {
|
|
__DisplayWaitForVblanks("vblank start waited", 2);
|
|
} else {
|
|
__DisplayWaitForVblanks("vblank start waited", 1);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_blitz_fps_hack() {
|
|
if (PSP_CoreParameter().compat.flags().FramerateHack30) {
|
|
__DisplayWaitForVblanks("vblank start waited", 1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_brian_lara_fps_hack() {
|
|
if (PSP_CoreParameter().compat.flags().FramerateHack30) {
|
|
__DisplayWaitForVblanks("vblank start waited", 1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_gow_vortex_hack() {
|
|
if (PSP_CoreParameter().compat.flags().GoWFramerateHack60) {
|
|
// from my tests both ==0x3F800000 and !=0x3F800000 takes around 1:40-1:50, that seems to match correct behaviour
|
|
if (currentMIPS->r[MIPS_REG_S1] == 0 && currentMIPS->r[MIPS_REG_A0] == 0xC0 && currentMIPS->r[MIPS_REG_T4] != 0x3F800000) {
|
|
currentMIPS->r[MIPS_REG_S1] = 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int Hook_ZZT3_select_hack() {
|
|
if (PSP_CoreParameter().compat.flags().ZZT3SelectHack) {
|
|
if (currentMIPS->r[MIPS_REG_V0] == 0) {
|
|
currentMIPS->r[MIPS_REG_V0] = 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#define JITFUNC(f) (&MIPSComp::MIPSFrontendInterface::f)
|
|
|
|
// Can either replace with C functions or functions emitted in Asm/ArmAsm.
|
|
static const ReplacementTableEntry entries[] = {
|
|
// TODO: I think some games can be helped quite a bit by implementing the
|
|
// double-precision soft-float routines: __adddf3, __subdf3 and so on. These
|
|
// should of course be implemented JIT style, inline.
|
|
|
|
/* These two collide (same hash) and thus can't be replaced :/
|
|
{ "asinf", &Replace_asinf, 0, REPFLAG_DISABLED },
|
|
{ "acosf", &Replace_acosf, 0, REPFLAG_DISABLED },
|
|
*/
|
|
|
|
{ "sinf", &Replace_sinf, 0, REPFLAG_DISABLED },
|
|
{ "cosf", &Replace_cosf, 0, REPFLAG_DISABLED },
|
|
{ "tanf", &Replace_tanf, 0, REPFLAG_DISABLED },
|
|
{ "atanf", &Replace_atanf, 0, REPFLAG_DISABLED },
|
|
{ "sqrtf", &Replace_sqrtf, 0, REPFLAG_DISABLED },
|
|
{ "atan2f", &Replace_atan2f, 0, REPFLAG_DISABLED },
|
|
{ "floorf", &Replace_floorf, 0, REPFLAG_DISABLED },
|
|
{ "ceilf", &Replace_ceilf, 0, REPFLAG_DISABLED },
|
|
|
|
{ "memcpy", &Replace_memcpy, 0, 0 },
|
|
{ "memcpy_jak", &Replace_memcpy_jak, 0, REPFLAG_SLICED },
|
|
{ "memcpy16", &Replace_memcpy16, 0, 0 },
|
|
{ "memcpy_swizzled", &Replace_memcpy_swizzled, 0, 0 },
|
|
{ "memmove", &Replace_memmove, 0, 0 },
|
|
{ "memset", &Replace_memset, 0, 0 },
|
|
{ "memset_jak", &Replace_memset_jak, 0, REPFLAG_SLICED },
|
|
{ "strlen", &Replace_strlen, 0, REPFLAG_DISABLED },
|
|
{ "strcpy", &Replace_strcpy, 0, REPFLAG_DISABLED },
|
|
{ "strncpy", &Replace_strncpy, 0, REPFLAG_DISABLED },
|
|
{ "strcmp", &Replace_strcmp, 0, REPFLAG_DISABLED },
|
|
{ "strncmp", &Replace_strncmp, 0, REPFLAG_DISABLED },
|
|
{ "fabsf", &Replace_fabsf, JITFUNC(Replace_fabsf), REPFLAG_DISABLED },
|
|
{ "dl_write_matrix", &Replace_dl_write_matrix, 0, REPFLAG_DISABLED }, // &MIPSComp::Jit::Replace_dl_write_matrix, REPFLAG_DISABLED },
|
|
{ "dl_write_matrix_2", &Replace_dl_write_matrix, 0, REPFLAG_DISABLED },
|
|
{ "gta_dl_write_matrix", &Replace_gta_dl_write_matrix, 0, REPFLAG_DISABLED },
|
|
// dl_write_matrix_3 doesn't take the dl as a parameter, it accesses a global instead. Need to extract the address of the global from the code when replacing...
|
|
// Haven't investigated write_matrix_4 and 5 but I think they are similar to 1 and 2.
|
|
|
|
// { "vmmul_q_transp", &Replace_vmmul_q_transp, 0, REPFLAG_DISABLED },
|
|
|
|
{ "godseaterburst_blit_texture", &Hook_godseaterburst_blit_texture, 0, REPFLAG_HOOKENTER },
|
|
{ "godseaterburst_depthmask_5551", &Hook_godseaterburst_depthmask_5551, 0, REPFLAG_HOOKENTER },
|
|
{ "hexyzforce_monoclome_thread", &Hook_hexyzforce_monoclome_thread, 0, REPFLAG_HOOKENTER, 0x58 },
|
|
{ "starocean_write_stencil", &Hook_starocean_write_stencil, 0, REPFLAG_HOOKENTER, 0x260 },
|
|
{ "topx_create_saveicon", &Hook_topx_create_saveicon, 0, REPFLAG_HOOKENTER, 0x34 },
|
|
{ "ff1_battle_effect", &Hook_ff1_battle_effect, 0, REPFLAG_HOOKENTER },
|
|
// This is actually used in other games, not just Dissidia.
|
|
{ "dissidia_recordframe_avi", &Hook_dissidia_recordframe_avi, 0, REPFLAG_HOOKENTER },
|
|
{ "brandish_download_frame", &Hook_brandish_download_frame, 0, REPFLAG_HOOKENTER },
|
|
{ "growlanser_create_saveicon", &Hook_growlanser_create_saveicon, 0, REPFLAG_HOOKENTER, 0x7C },
|
|
{ "sd_gundam_g_generation_download_frame", &Hook_sd_gundam_g_generation_download_frame, 0, REPFLAG_HOOKENTER, 0x48},
|
|
{ "narisokonai_download_frame", &Hook_narisokonai_download_frame, 0, REPFLAG_HOOKENTER, 0x14 },
|
|
{ "kirameki_school_life_download_frame", &Hook_kirameki_school_life_download_frame, 0, REPFLAG_HOOKENTER },
|
|
{ "orenoimouto_download_frame", &Hook_orenoimouto_download_frame, 0, REPFLAG_HOOKENTER },
|
|
{ "sakurasou_download_frame", &Hook_sakurasou_download_frame, 0, REPFLAG_HOOKENTER, 0xF8 },
|
|
{ "suikoden1_and_2_download_frame_1", &Hook_suikoden1_and_2_download_frame_1, 0, REPFLAG_HOOKENTER, 0x9C },
|
|
{ "suikoden1_and_2_download_frame_2", &Hook_suikoden1_and_2_download_frame_2, 0, REPFLAG_HOOKENTER, 0x48 },
|
|
{ "rezel_cross_download_frame", &Hook_rezel_cross_download_frame, 0, REPFLAG_HOOKENTER, 0x54 },
|
|
{ "kagaku_no_ensemble_download_frame", &Hook_kagaku_no_ensemble_download_frame, 0, REPFLAG_HOOKENTER, 0x38 },
|
|
{ "soranokiseki_fc_download_frame", &Hook_soranokiseki_fc_download_frame, 0, REPFLAG_HOOKENTER, 0x180 },
|
|
{ "soranokiseki_sc_download_frame", &Hook_soranokiseki_sc_download_frame, 0, REPFLAG_HOOKENTER, },
|
|
{ "bokunonatsuyasumi4_download_frame", &Hook_bokunonatsuyasumi4_download_frame, 0, REPFLAG_HOOKENTER, 0x8C },
|
|
{ "danganronpa2_1_download_frame", &Hook_danganronpa2_1_download_frame, 0, REPFLAG_HOOKENTER, 0x68 },
|
|
{ "danganronpa2_2_download_frame", &Hook_danganronpa2_2_download_frame, 0, REPFLAG_HOOKENTER, 0x94 },
|
|
{ "danganronpa1_1_download_frame", &Hook_danganronpa1_1_download_frame, 0, REPFLAG_HOOKENTER, 0x78 },
|
|
{ "danganronpa1_2_download_frame", &Hook_danganronpa1_2_download_frame, 0, REPFLAG_HOOKENTER, 0xA8 },
|
|
{ "kankabanchoutbr_download_frame", &Hook_kankabanchoutbr_download_frame, 0, REPFLAG_HOOKENTER, },
|
|
{ "orenoimouto_download_frame_2", &Hook_orenoimouto_download_frame_2, 0, REPFLAG_HOOKENTER, },
|
|
{ "rewrite_download_frame", &Hook_rewrite_download_frame, 0, REPFLAG_HOOKENTER, 0x5C },
|
|
{ "kudwafter_download_frame", &Hook_kudwafter_download_frame, 0, REPFLAG_HOOKENTER, 0x58 },
|
|
{ "kumonohatateni_download_frame", &Hook_kumonohatateni_download_frame, 0, REPFLAG_HOOKENTER, },
|
|
{ "otomenoheihou_download_frame", &Hook_otomenoheihou_download_frame, 0, REPFLAG_HOOKENTER, 0x14 },
|
|
{ "grisaianokajitsu_download_frame", &Hook_grisaianokajitsu_download_frame, 0, REPFLAG_HOOKENTER, 0x14 },
|
|
{ "kokoroconnect_download_frame", &Hook_kokoroconnect_download_frame, 0, REPFLAG_HOOKENTER, 0x60 },
|
|
{ "toheart2_download_frame", &Hook_toheart2_download_frame, 0, REPFLAG_HOOKENTER, },
|
|
{ "toheart2_download_frame_2", &Hook_toheart2_download_frame_2, 0, REPFLAG_HOOKENTER, 0x18 },
|
|
{ "flowers_download_frame", &Hook_flowers_download_frame, 0, REPFLAG_HOOKENTER, 0x44 },
|
|
{ "motorstorm_download_frame", &Hook_motorstorm_download_frame, 0, REPFLAG_HOOKENTER, },
|
|
{ "utawarerumono_download_frame", &Hook_utawarerumono_download_frame, 0, REPFLAG_HOOKENTER, },
|
|
{ "photokano_download_frame", &Hook_photokano_download_frame, 0, REPFLAG_HOOKENTER, 0x2C },
|
|
{ "photokano_download_frame_2", &Hook_photokano_download_frame_2, 0, REPFLAG_HOOKENTER, },
|
|
{ "gakuenheaven_download_frame", &Hook_gakuenheaven_download_frame, 0, REPFLAG_HOOKENTER, },
|
|
{ "youkosohitsujimura_download_frame", &Hook_youkosohitsujimura_download_frame, 0, REPFLAG_HOOKENTER, 0x94 },
|
|
{ "zettai_hero_update_minimap_tex", &Hook_zettai_hero_update_minimap_tex, 0, REPFLAG_HOOKEXIT, },
|
|
{ "tonyhawkp8_upload_tutorial_frame", &Hook_tonyhawkp8_upload_tutorial_frame, 0, REPFLAG_HOOKENTER, },
|
|
{ "sdgundamggenerationportable_download_frame", &Hook_sdgundamggenerationportable_download_frame, 0, REPFLAG_HOOKENTER, 0x34 },
|
|
{ "atvoffroadfurypro_download_frame", &Hook_atvoffroadfurypro_download_frame, 0, REPFLAG_HOOKENTER, 0xA0 },
|
|
{ "atvoffroadfuryblazintrails_download_frame", &Hook_atvoffroadfuryblazintrails_download_frame, 0, REPFLAG_HOOKENTER, 0x80 },
|
|
{ "littlebustersce_download_frame", &Hook_littlebustersce_download_frame, 0, REPFLAG_HOOKENTER, },
|
|
{ "shinigamitoshoujo_download_frame", &Hook_shinigamitoshoujo_download_frame, 0, REPFLAG_HOOKENTER, 0xBC },
|
|
{ "atvoffroadfuryprodemo_download_frame", &Hook_atvoffroadfuryprodemo_download_frame, 0, REPFLAG_HOOKENTER, 0x80 },
|
|
{ "unendingbloodycall_download_frame", &Hook_unendingbloodycall_download_frame, 0, REPFLAG_HOOKENTER, 0x54 },
|
|
{ "omertachinmokunookitethelegacy_download_frame", &Hook_omertachinmokunookitethelegacy_download_frame, 0, REPFLAG_HOOKENTER, 0x88 },
|
|
{ "katamari_render_check", &Hook_katamari_render_check, 0, REPFLAG_HOOKENTER, 0, },
|
|
{ "katamari_screenshot_to_565", &Hook_katamari_screenshot_to_565, 0, REPFLAG_HOOKENTER, 0 },
|
|
{ "mytranwars_upload_frame", &Hook_mytranwars_upload_frame, 0, REPFLAG_HOOKENTER, 0x128 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_a1_before, 0, REPFLAG_HOOKENTER, 0x284 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_after, 0, REPFLAG_HOOKENTER, 0x2bc },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_a1_before, 0, REPFLAG_HOOKENTER, 0x2e8 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_after, 0, REPFLAG_HOOKENTER, 0x320 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_a2_before, 0, REPFLAG_HOOKENTER, 0x3b0 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_after, 0, REPFLAG_HOOKENTER, 0x3e8 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_a2_before, 0, REPFLAG_HOOKENTER, 0x410 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_after, 0, REPFLAG_HOOKENTER, 0x448 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_a1_before, 0, REPFLAG_HOOKENTER, 0x600 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_after, 0, REPFLAG_HOOKENTER, 0x638 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_a1_before, 0, REPFLAG_HOOKENTER, 0x664 },
|
|
{ "marvelalliance1_copy", &Hook_marvelalliance1_copy_after, 0, REPFLAG_HOOKENTER, 0x69c },
|
|
{ "starocean_clear_framebuf", &Hook_starocean_clear_framebuf_before, 0, REPFLAG_HOOKENTER, 0 },
|
|
{ "starocean_clear_framebuf", &Hook_starocean_clear_framebuf_after, 0, REPFLAG_HOOKEXIT, 0 },
|
|
{ "motorstorm_pixel_read", &Hook_motorstorm_pixel_read, 0, REPFLAG_HOOKENTER, 0 },
|
|
{ "worms_copy_normalize_alpha", &Hook_worms_copy_normalize_alpha, 0, REPFLAG_HOOKENTER, 0x0CC },
|
|
{ "openseason_data_decode", &Hook_openseason_data_decode, 0, REPFLAG_HOOKENTER, 0x2F0 },
|
|
{ "soltrigger_render_ucschar", &Hook_soltrigger_render_ucschar, 0, REPFLAG_HOOKENTER, 0 },
|
|
{ "gow_fps_hack", &Hook_gow_fps_hack, 0, REPFLAG_HOOKEXIT , 0 },
|
|
{ "gow_vortex_hack", &Hook_gow_vortex_hack, 0, REPFLAG_HOOKENTER, 0x60 },
|
|
{ "ZZT3_select_hack", &Hook_ZZT3_select_hack, 0, REPFLAG_HOOKENTER, 0xC4 },
|
|
{ "blitz_fps_hack", &Hook_blitz_fps_hack, 0, REPFLAG_HOOKEXIT , 0 },
|
|
{ "brian_lara_fps_hack", &Hook_brian_lara_fps_hack, 0, REPFLAG_HOOKEXIT , 0 },
|
|
{ "persona1_download_frame", &Hook_persona_download_frame, 0, REPFLAG_HOOKENTER, 0 },
|
|
{ "persona2_download_frame", &Hook_persona_download_frame, 0, REPFLAG_HOOKENTER, 0 },
|
|
{ "steinsgate_download_frame", &Hook_steinsgate_download_frame, 0, REPFLAG_HOOKENTER, 0 },
|
|
{ "infinity_download_frame", &Hook_infinity_download_frame, 0, REPFLAG_HOOKENTER, 0 },
|
|
{ "takuyo_1_download_frame", &Hook_takuyo_download_frame, 0, REPFLAG_HOOKENTER, 0},
|
|
{ "takuyo_2_download_frame", &Hook_takuyo_download_frame, 0, REPFLAG_HOOKENTER, 0},
|
|
{ "takuyo_3_download_frame", &Hook_takuyo_download_frame, 0, REPFLAG_HOOKENTER, 0},
|
|
{ "kingdomhearts_download_frame", &Hook_kingdomhearts_download_frame, 0, REPFLAG_HOOKENTER, 0},
|
|
{ "everybodysgolf2_copy_portrait", &Hook_everybodys_copy_portrait, 0, REPFLAG_HOOKENTER, 0x50 },
|
|
{ "everybodystennis_copy_portrait", &Hook_everybodys_copy_portrait, 0, REPFLAG_HOOKENTER, 0x54 },
|
|
{ "everybodysgolf_copy_portrait", &Hook_everybodysgolf_copy_portrait, 0, REPFLAG_HOOKENTER, 0x50 },
|
|
{ "everybodysgolf_spinner_background", &Hook_everybodysgolf_spinner_background, 0, REPFLAG_HOOKENTER, 0x138 },
|
|
{ "codedarms_stencil_check", &Hook_codedarms_stencil_check, 0, REPFLAG_HOOKENTER, 0x8C },
|
|
{}
|
|
};
|
|
|
|
|
|
struct InstalledReplacement {
|
|
int index;
|
|
// Unmasked, for invalidation. The native block cache doesn't fold mirrors when filtering ranges.
|
|
u32 address;
|
|
// What was in memory when it was installed. If the game overwrites it, the hook is dropped.
|
|
u32 origOp;
|
|
// The function the hook was found in. A HOOKEXIT hook sits on a jr ra, which new code loaded
|
|
// over the function is likely to have too, so we also drop the hook when the function's
|
|
// hash changes - checked whenever the game invalidates the icache over it.
|
|
u32 funcStart;
|
|
u32 funcSize;
|
|
u64 funcHash;
|
|
};
|
|
|
|
// Keyed by address & BLOCK_SHADOW_MASK, so mirrors share a hook the way they share memory.
|
|
// Only the CPU thread changes it, holding the lock, so that thread can read it without.
|
|
static std::map<u32, InstalledReplacement> installedReplacements;
|
|
static std::mutex installedReplacementsLock;
|
|
static std::unordered_map<std::string, std::vector<int>> replacementNameLookup;
|
|
|
|
// What the block shadow holds at a hooked instruction without a compiled block. The JIT's value
|
|
// sends the dispatcher on to the compiler, which handles the hook.
|
|
static u32 shadowHookValue = Memory::BLOCK_SHADOW_HOOK_NO_JIT;
|
|
|
|
// An entry holding a block is left alone: invalidating the block destroys it, which writes the
|
|
// hook value then (see Memory::ClearBlockShadow).
|
|
static void MarkHook(u32 address) {
|
|
if (Memory::ReadBlockShadow(address) == 0) {
|
|
Memory::WriteBlockShadow(address, shadowHookValue);
|
|
}
|
|
}
|
|
|
|
// Call after erasing the hook from the map, so destroying a block there clears its entry to 0.
|
|
static void UnmarkHook(u32 address) {
|
|
Memory::ClearBlockShadow(address, shadowHookValue);
|
|
}
|
|
|
|
// Deferred, since hooks change in HLE calls (module loading) that can run inside the block.
|
|
static void InvalidateReplacedAddress(u32 address) {
|
|
currentMIPS->InvalidateICacheRangeDeferred(address, 4);
|
|
}
|
|
|
|
// Immediates are masked out like in MIPSAnalyst::HashFunctions, which matched the function, so a
|
|
// relocation doesn't drop the hook.
|
|
static u64 HashReplacedFunc(u32 start, u32 size) {
|
|
if (!Memory::IsValidRange(start, size)) {
|
|
return 0;
|
|
}
|
|
std::vector<u32> buffer;
|
|
buffer.reserve(size / 4);
|
|
for (u32 addr = start; addr + 4 <= start + size; addr += 4) {
|
|
const MIPSOpcode op = Memory::ReadUnchecked_Instruction(addr);
|
|
const MIPSInfo flags = MIPSGetInfo(op);
|
|
u32 validbits = 0xFFFFFFFF;
|
|
if (flags & IN_IMM16) {
|
|
validbits &= ~0xFFFF;
|
|
}
|
|
if (flags & IN_IMM26) {
|
|
validbits &= ~0x03FFFFFF;
|
|
}
|
|
buffer.push_back(op.encoding & validbits);
|
|
}
|
|
return XXH3_64bits(buffer.data(), buffer.size() * sizeof(u32));
|
|
}
|
|
|
|
void Replacement_Init() {
|
|
for (int i = 0; i < (int)ARRAY_SIZE(entries); i++) {
|
|
const auto entry = &entries[i];
|
|
if (!entry->name || (entry->flags & REPFLAG_DISABLED) != 0)
|
|
continue;
|
|
replacementNameLookup[entry->name].push_back(i);
|
|
}
|
|
|
|
skipGPUReplacements = 0;
|
|
}
|
|
|
|
void Replacement_Shutdown() {
|
|
RestoreReplacedInstructions(0, 0xFFFFFFFF);
|
|
replacementNameLookup.clear();
|
|
}
|
|
|
|
void Replacement_SetBlockShadowHook(u32 value) {
|
|
const u32 oldValue = shadowHookValue;
|
|
shadowHookValue = value;
|
|
for (const auto &[key, repl] : installedReplacements) {
|
|
const u32 entry = Memory::ReadBlockShadow(key);
|
|
if (entry == 0 || entry == oldValue) {
|
|
Memory::WriteBlockShadow(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
u32 Replacement_GetBlockShadowHook() {
|
|
return shadowHookValue;
|
|
}
|
|
|
|
bool Replacement_IsHooked(u32 address) {
|
|
return installedReplacements.count(address & Memory::BLOCK_SHADOW_MASK) != 0;
|
|
}
|
|
|
|
int GetNumReplacementFuncs() {
|
|
return ARRAY_SIZE(entries);
|
|
}
|
|
|
|
std::vector<int> GetReplacementFuncIndexes(u64 hash, int funcSize) {
|
|
const char *name = MIPSAnalyst::LookupHash(hash, funcSize);
|
|
std::vector<int> emptyResult;
|
|
if (!name) {
|
|
return emptyResult;
|
|
}
|
|
|
|
auto index = replacementNameLookup.find(name);
|
|
if (index != replacementNameLookup.end()) {
|
|
return index->second;
|
|
}
|
|
return emptyResult;
|
|
}
|
|
|
|
const ReplacementTableEntry *GetReplacementFunc(size_t i) {
|
|
if (i >= ARRAY_SIZE(entries)) {
|
|
return nullptr;
|
|
}
|
|
return &entries[i];
|
|
}
|
|
|
|
static bool WriteReplaceInstruction(u32 address, int index, u32 funcStart, u32 funcSize, u64 funcHash) {
|
|
if (!Memory::IsValid4AlignedAddress(address)) {
|
|
return false;
|
|
}
|
|
const u32 key = address & Memory::BLOCK_SHADOW_MASK;
|
|
const u32 memOp = Memory::ReadUnchecked_U32(address);
|
|
auto iter = installedReplacements.find(key);
|
|
if (iter != installedReplacements.end() && iter->second.origOp == memOp) {
|
|
if (iter->second.index == index) {
|
|
return false;
|
|
}
|
|
WARN_LOG(Log::HLE, "Replacement func changed at %08x (%d -> %d)", address, iter->second.index, index);
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> guard(installedReplacementsLock);
|
|
installedReplacements[key] = { index, address, memOp, funcStart, funcSize, funcHash };
|
|
}
|
|
InvalidateReplacedAddress(address);
|
|
MarkHook(address);
|
|
return true;
|
|
}
|
|
|
|
void WriteReplaceInstructions(u32 address, u64 hash, int size) {
|
|
std::vector<int> indexes = GetReplacementFuncIndexes(hash, size);
|
|
const u64 funcHash = indexes.empty() ? 0 : HashReplacedFunc(address, size);
|
|
for (int index : indexes) {
|
|
bool didReplace = false;
|
|
const ReplacementTableEntry *entry = GetReplacementFunc(index);
|
|
if (entry->flags & REPFLAG_HOOKEXIT) {
|
|
// When hooking func exit, we search for jr ra, and replace those.
|
|
for (u32 offset = 0; offset < (u32)size; offset += 4) {
|
|
const u32 op = Memory::Read_Instruction(address + offset).encoding;
|
|
if (op == MIPS_MAKE_JR_RA()) {
|
|
if (WriteReplaceInstruction(address + offset, index, address, size, funcHash)) {
|
|
didReplace = true;
|
|
}
|
|
}
|
|
}
|
|
} else if (entry->flags & REPFLAG_HOOKENTER) {
|
|
if (WriteReplaceInstruction(address + entry->hookOffset, index, address, size, funcHash)) {
|
|
didReplace = true;
|
|
}
|
|
} else {
|
|
if (WriteReplaceInstruction(address, index, address, size, funcHash)) {
|
|
didReplace = true;
|
|
}
|
|
}
|
|
|
|
if (didReplace) {
|
|
INFO_LOG(Log::HLE, "Replaced %s at %08x with hash %016llx", entries[index].name, address, hash);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RestoreReplacedInstruction(u32 address) {
|
|
const u32 key = address & Memory::BLOCK_SHADOW_MASK;
|
|
auto iter = installedReplacements.find(key);
|
|
if (iter != installedReplacements.end()) {
|
|
const u32 hookAddress = iter->second.address;
|
|
{
|
|
std::lock_guard<std::mutex> guard(installedReplacementsLock);
|
|
installedReplacements.erase(iter);
|
|
}
|
|
InvalidateReplacedAddress(hookAddress);
|
|
UnmarkHook(hookAddress);
|
|
NOTICE_LOG(Log::HLE, "Restored replaced func at %08x", address);
|
|
}
|
|
}
|
|
|
|
std::vector<std::pair<u32, u32>> Replacement_GetHookedOps() {
|
|
std::lock_guard<std::mutex> guard(installedReplacementsLock);
|
|
std::vector<std::pair<u32, u32>> ops;
|
|
ops.reserve(installedReplacements.size());
|
|
for (const auto &[key, repl] : installedReplacements) {
|
|
ops.emplace_back(repl.address, repl.origOp);
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
void Replacement_RepairStaleHookOps(const std::vector<std::pair<u32, u32>> &hookedOps) {
|
|
int repaired = 0;
|
|
for (const auto &[address, origOp] : hookedOps) {
|
|
if (!Memory::IsValid4AlignedAddress(address)) {
|
|
continue;
|
|
}
|
|
// The Allegrex has no opcode 26, so this can only be a hook op, never the game's code.
|
|
const u32 memOp = Memory::ReadUnchecked_U32(address);
|
|
if ((memOp >> 26) == (MIPS_CALL_REPLACEMENT_OP >> 26) && memOp != origOp) {
|
|
Memory::WriteUnchecked_U32(origOp, address);
|
|
repaired++;
|
|
}
|
|
}
|
|
if (repaired) {
|
|
WARN_LOG(Log::SaveState, "Put back %d hooked instructions saved as hook ops", repaired);
|
|
}
|
|
}
|
|
|
|
void RestoreReplacedInstructions(u32 startAddr, u32 endAddr) {
|
|
if (endAddr == startAddr)
|
|
return;
|
|
// Need to be in order, or we'll hang.
|
|
if (endAddr < startAddr)
|
|
std::swap(endAddr, startAddr);
|
|
const u32 maskedStart = startAddr & Memory::BLOCK_SHADOW_MASK;
|
|
u32 maskedEnd = endAddr & Memory::BLOCK_SHADOW_MASK;
|
|
if (maskedEnd < maskedStart || endAddr - startAddr >= Memory::BLOCK_SHADOW_MASK) {
|
|
// Wraps under the mask.
|
|
maskedEnd = Memory::BLOCK_SHADOW_MASK;
|
|
}
|
|
const auto start = installedReplacements.lower_bound(maskedStart);
|
|
const auto end = installedReplacements.upper_bound(maskedEnd);
|
|
std::vector<u32> restored;
|
|
for (auto it = start; it != end; ++it) {
|
|
restored.push_back(it->second.address);
|
|
}
|
|
// Erase first, so destroying blocks there doesn't put the hook back.
|
|
{
|
|
std::lock_guard<std::mutex> guard(installedReplacementsLock);
|
|
installedReplacements.erase(start, end);
|
|
}
|
|
for (u32 hookAddress : restored) {
|
|
InvalidateReplacedAddress(hookAddress);
|
|
UnmarkHook(hookAddress);
|
|
}
|
|
INFO_LOG(Log::HLE, "Restored %d replaced funcs between %08x-%08x", (int)restored.size(), startAddr, endAddr);
|
|
}
|
|
|
|
void Replacement_CheckRange(u32 address, u32 length) {
|
|
const u32 start = address & Memory::BLOCK_SHADOW_MASK;
|
|
const u32 end = length >= Memory::BLOCK_SHADOW_MASK ? Memory::BLOCK_SHADOW_MASK : start + length;
|
|
std::vector<u32> dropped;
|
|
for (auto it = installedReplacements.begin(); it != installedReplacements.end(); ) {
|
|
const InstalledReplacement &repl = it->second;
|
|
const u32 funcStart = repl.funcStart & Memory::BLOCK_SHADOW_MASK;
|
|
if (funcStart < end && funcStart + repl.funcSize > start && HashReplacedFunc(repl.funcStart, repl.funcSize) != repl.funcHash) {
|
|
NOTICE_LOG(Log::HLE, "Replaced func changed at %08x, dropping its hook at %08x", repl.funcStart, repl.address);
|
|
dropped.push_back(repl.address);
|
|
std::lock_guard<std::mutex> guard(installedReplacementsLock);
|
|
it = installedReplacements.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
// The hook can be outside the range (a HOOKEXIT on a later jr ra), so its blocks need invalidating too.
|
|
for (u32 hookAddress : dropped) {
|
|
InvalidateReplacedAddress(hookAddress);
|
|
UnmarkHook(hookAddress);
|
|
}
|
|
}
|
|
|
|
MIPSOpcode GetReplacementOpAt(u32 address) {
|
|
const u32 key = address & Memory::BLOCK_SHADOW_MASK;
|
|
const u32 memOp = Memory::ReadUnchecked_U32(address);
|
|
auto iter = installedReplacements.find(key);
|
|
if (iter == installedReplacements.end()) {
|
|
return MIPSOpcode(memOp);
|
|
}
|
|
if (iter->second.origOp != memOp) {
|
|
// The game wrote over the hooked instruction, so the hook goes with it.
|
|
NOTICE_LOG(Log::HLE, "Replaced func changed at %08x", address);
|
|
{
|
|
std::lock_guard<std::mutex> guard(installedReplacementsLock);
|
|
installedReplacements.erase(iter);
|
|
}
|
|
UnmarkHook(address);
|
|
return MIPSOpcode(memOp);
|
|
}
|
|
return MIPSOpcode(MIPS_CALL_REPLACEMENT_OP | iter->second.index);
|
|
}
|
|
|
|
MIPSOpcode Replacement_GetDisplayOp(u32 address) {
|
|
const u32 memOp = Memory::ReadUnchecked_U32(address);
|
|
std::lock_guard<std::mutex> guard(installedReplacementsLock);
|
|
auto iter = installedReplacements.find(address & Memory::BLOCK_SHADOW_MASK);
|
|
if (iter == installedReplacements.end() || iter->second.origOp != memOp) {
|
|
return MIPSOpcode(memOp);
|
|
}
|
|
return MIPSOpcode(MIPS_CALL_REPLACEMENT_OP | iter->second.index);
|
|
}
|