// 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 <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/MIPSCodeUtils.h"
#include "Core/MIPS/MIPSAnalyst.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"

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;
	}

	// Some games use memcpy on executable code.  We need to flush emuhack ops.
	currentMIPS->InvalidateICache(srcPtr, bytes);
	if ((skipGPUReplacements & (int)GPUReplacementSkip::MEMCPY) == 0) {
		if (Memory::IsVRAMAddress(destPtr) || Memory::IsVRAMAddress(srcPtr)) {
			skip = gpu->PerformMemoryCopy(destPtr, srcPtr, bytes);
		}
	}
	if (!skip && bytes != 0) {
		u8 *dst = Memory::GetPointerWriteRange(destPtr, bytes);
		const u8 *src = Memory::GetPointerRange(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;

	currentMIPS->InvalidateICache(srcPtr, bytes);
	if ((skipGPUReplacements & (int)GPUReplacementSkip::MEMCPY) == 0) {
		if (Memory::IsVRAMAddress(destPtr) || Memory::IsVRAMAddress(srcPtr)) {
			skip = gpu->PerformMemoryCopy(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::GetPointerWriteRange(destPtr, bytes);
		const u8 *src = Memory::GetPointerRange(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;

	// Some games use memcpy on executable code.  We need to flush emuhack ops.
	if (bytes != 0)
		currentMIPS->InvalidateICache(srcPtr, bytes);
	if ((skipGPUReplacements & (int)GPUReplacementSkip::MEMCPY) == 0 && bytes != 0) {
		if (Memory::IsVRAMAddress(destPtr) || Memory::IsVRAMAddress(srcPtr)) {
			skip = gpu->PerformMemoryCopy(destPtr, srcPtr, bytes);
		}
	}
	if (!skip && bytes != 0) {
		u8 *dst = Memory::GetPointerWriteRange(destPtr, bytes);
		const u8 *src = Memory::GetPointerRange(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::GetPointerWriteRange(destPtr, pitch * h);
	const u8 *srcp = Memory::GetPointerRange(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;

	// Some games use memcpy on executable code.  We need to flush emuhack ops.
	if ((skipGPUReplacements & (int)GPUReplacementSkip::MEMMOVE) == 0 && bytes != 0) {
		currentMIPS->InvalidateICache(srcPtr, bytes);
		if (Memory::IsVRAMAddress(destPtr) || Memory::IsVRAMAddress(srcPtr)) {
			skip = gpu->PerformMemoryCopy(destPtr, srcPtr, bytes);
		}
	}
	if (!skip && bytes != 0) {
		u8 *dst = Memory::GetPointerWriteRange(destPtr, bytes);
		const u8 *src = Memory::GetPointerRange(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::GetPointerWriteRange(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::GetPointerWriteRange(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::ValidSize(ptr, 0x07FFFFFF);
	const uint8_t *p = Memory::GetPointerRange(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::GetPointerWriteRange(destPtr, len);
	const char *src = (const char *)Memory::GetPointerRange(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::GetPointerRange(destPtr, bytes);
	u32 srcLen = SafeStringLen(srcPtr, bytes);
	const char *src = (const char *)Memory::GetPointerRange(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::GetPointerRange(PARAM(0), aLen);
	u32 bLen = SafeStringLen(PARAM(1));
	const char *b = (const char *)Memory::GetPointerRange(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::GetPointerRange(PARAM(0), aLen == 0 ? bytes : aLen);
	u32 bLen = SafeStringLen(PARAM(1), bytes);
	const char *b = (const char *)Memory::GetPointerRange(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::GetPointerRange(PARAM(0), 16 * 4);
	const float_le *a = (const float_le *)Memory::GetPointerRange(PARAM(1), 16 * 4);
	const float_le *b = (const float_le *)Memory::GetPointerRange(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::GetPointerWriteRange(PARAM(0), 4);
	const u32_le *src = (const u32_le *)Memory::GetPointerRange(PARAM(2), 16);
	u32 matrix = PARAM(1) << 24;

	if (!ptr || !src) {
		RETURN(0);
		return 38;
	}

	u32_le *dest = (u32_le *)Memory::GetPointerWriteRange(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::GetPointerWriteRange(PARAM(0), 3 * 4);
	const u32_le *src = (const u32_le *)Memory::GetPointerRange(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::GetPointerWriteRange(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, true);
	if (upper != MIPS_MAKE_LUI(MIPS_GET_RT(upper), upper & 0xffff)) {
		return false;
	}
	const MIPSOpcode lower = Memory::Read_Instruction(currentMIPS->pc + lw_offset, true);
	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, true);
	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)) {
		return 0;
	}
	u32 fb_infoaddr;
	if (Memory::Read_U32(texaddr) != 0 || !GetMIPSStaticAddress(fb_infoaddr, 0x01d0, 0x01d4)) {
		return 0;
	}

	const u32 fb_info = Memory::Read_U32(fb_infoaddr);
	const u32 fb_address = Memory::Read_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;
	}

	const u32 fb_address = Memory::Read_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;
	}
	const u32 fb_info = Memory::Read_U32(fb_infoaddr);
	const MIPSOpcode fb_index_load = Memory::Read_Instruction(currentMIPS->pc + 0x38, true);
	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::Read_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::Read_U32(currentMIPS->r[MIPS_REG_SP] + 4);
	const u32 fmt = Memory::Read_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::Read_U32(currentMIPS->r[MIPS_REG_SP] + 8);
	const u32 fmt = Memory::Read_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::Read_U32(currentMIPS->r[MIPS_REG_SP] + 0x1C);
	const u32 fmt = Memory::Read_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;
	}
	const u32 fb_info = Memory::Read_U32(fb_infoaddr);
	const MIPSOpcode fb_index_load = Memory::Read_Instruction(currentMIPS->pc + 0x34, true);
	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::Read_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, true);
	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() {
	const u32 fb_address = Memory::Read_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, true);
	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::Read_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::Read_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_katamari_render_check() {
	const u32 fb_address = Memory::Read_U32(currentMIPS->r[MIPS_REG_A0] + 0x3C);
	const u32 fbInfoPtr = Memory::Read_U32(currentMIPS->r[MIPS_REG_A0] + 0x40);
	if (Memory::IsVRAMAddress(fb_address) && fbInfoPtr != 0) {
		const u32 sizeInfoPtr = Memory::Read_U32(fbInfoPtr + 0x0C);
		// These are the values it uses to control the loop.
		// Width in memory appears to be stride / 8.
		const u32 width = Memory::Read_U16(sizeInfoPtr + 0x08) * 8;
		// Height in memory is also divided by 8 (but this one isn't hardcoded.)
		const u32 heightBlocks = Memory::Read_U16(sizeInfoPtr + 0x0A);
		// For some reason this is the number of heightBlocks less 1.
		const u32 heightBlockCount = Memory::Read_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)) {
		int y = (s16)Memory::Read_U16(y_address);
		int h = (s16)Memory::Read_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() {
	u32 fb_address = Memory::Read_U32(currentMIPS->r[MIPS_REG_A0] + 0x18);
	u32 fb_height = Memory::Read_U16(currentMIPS->r[MIPS_REG_A0] + 0x26);
	u32 fb_stride = Memory::Read_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.
	u32 fb_address = currentMIPS->r[MIPS_REG_S1];
	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::Read_U32(targetInfoPtr + 8);
		u32 targetByteStride = Memory::Read_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_ALLOWINLINE | 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 },
	{}
};


static std::map<u32, u32> replacedInstructions;
static std::unordered_map<std::string, std::vector<int> > replacementNameLookup;

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() {
	replacedInstructions.clear();
	replacementNameLookup.clear();
}

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 prevInstr = Memory::Read_Instruction(address, false).encoding;
	if (MIPS_IS_REPLACEMENT(prevInstr)) {
		int prevIndex = prevInstr & MIPS_EMUHACK_VALUE_MASK;
		if (prevIndex == index) {
			return false;
		}
		WARN_LOG(Log::HLE, "Replacement func changed at %08x (%d -> %d)", address, prevIndex, index);
		// Make sure we don't save the old replacement.
		prevInstr = replacedInstructions[address];
	}

	if (MIPS_IS_RUNBLOCK(Memory::Read_U32(address))) {
		WARN_LOG(Log::HLE, "Replacing jitted func address %08x", address);
	}
	replacedInstructions[address] = prevInstr;
	Memory::Write_U32(MIPS_EMUHACK_CALL_REPLACEMENT | index, address);
	return true;
}

void WriteReplaceInstructions(u32 address, u64 hash, int size) {
	std::vector<int> indexes = GetReplacementFuncIndexes(hash, 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, false).encoding;
				if (op == MIPS_MAKE_JR_RA()) {
					if (WriteReplaceInstruction(address + offset, index)) {
						didReplace = true;
					}
				}
			}
		} else if (entry->flags & REPFLAG_HOOKENTER) {
			if (WriteReplaceInstruction(address + entry->hookOffset, index)) {
				didReplace = true;
			}
		} else {
			if (WriteReplaceInstruction(address, index)) {
				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 curInstr = Memory::Read_U32(address);
	if (MIPS_IS_REPLACEMENT(curInstr)) {
		Memory::Write_U32(replacedInstructions[address], address);
		NOTICE_LOG(Log::HLE, "Restored replaced func at %08x", address);
	} else {
		NOTICE_LOG(Log::HLE, "Replaced func changed at %08x", address);
	}
	replacedInstructions.erase(address);
}

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 auto start = replacedInstructions.lower_bound(startAddr);
	const auto end = replacedInstructions.upper_bound(endAddr);
	int restored = 0;
	for (auto it = start; it != end; ++it) {
		const u32 addr = it->first;
		const u32 curInstr = Memory::Read_U32(addr);
		if (MIPS_IS_REPLACEMENT(curInstr)) {
			Memory::Write_U32(it->second, addr);
			++restored;
		}
	}
	INFO_LOG(Log::HLE, "Restored %d replaced funcs between %08x-%08x", restored, startAddr, endAddr);
	replacedInstructions.erase(start, end);
}

std::map<u32, u32> SaveAndClearReplacements() {
	std::map<u32, u32> saved;
	for (const auto &[addr, instr] : replacedInstructions) {
		// This will not retain jit blocks.
		const u32 curInstr = Memory::Read_Opcode_JIT(addr).encoding;
		if (MIPS_IS_REPLACEMENT(curInstr)) {
			saved[addr] = curInstr;
			Memory::Write_U32(instr, addr);
		}
	}
	return saved;
}

void RestoreSavedReplacements(const std::map<u32, u32> &saved) {
	for (const auto &[addr, instr] : saved) {
		// Just put the replacements back.
		Memory::Write_U32(instr, addr);
	}
}

bool GetReplacedOpAt(u32 address, u32 *op) {
	u32 instr = Memory::Read_Opcode_JIT(address).encoding;
	if (MIPS_IS_REPLACEMENT(instr)) {
		auto iter = replacedInstructions.find(address);
		if (iter != replacedInstructions.end()) {
			*op = iter->second;
			return true;
		} else {
			return false;
		}
	}
	return false;
}

bool CanReplaceJalTo(u32 dest, const ReplacementTableEntry **entry, u32 *funcSize) {
	MIPSOpcode op(Memory::Read_Opcode_JIT(dest));
	if (!MIPS_IS_REPLACEMENT(op.encoding))
		return false;

	// Make sure we don't replace if there are any breakpoints inside.
	*funcSize = g_symbolMap->GetFunctionSize(dest);
	if (*funcSize == SymbolMap::INVALID_ADDRESS) {
		if (g_breakpoints.IsAddressBreakPoint(dest)) {
			return false;
		}
		*funcSize = (u32)sizeof(u32);
	} else {
		if (g_breakpoints.RangeContainsBreakPoint(dest, *funcSize)) {
			return false;
		}
	}

	int index = op.encoding & MIPS_EMUHACK_VALUE_MASK;
	*entry = GetReplacementFunc(index);
	if (!*entry) {
		ERROR_LOG(Log::HLE, "ReplaceJalTo: Invalid replacement op %08x at %08x", op.encoding, dest);
		return false;
	}

	if ((*entry)->flags & (REPFLAG_HOOKENTER | REPFLAG_HOOKEXIT | REPFLAG_DISABLED | REPFLAG_SLICED)) {
		// If it's a hook, we can't replace the jal, we have to go inside the func.
		return false;
	}
	return true;
}
