// CPU controls 
#include "pch.h"

using namespace Debug;

namespace Gekko
{
	// The main driving force behind the entire emulator. All other threads are based on changing the TBR Gekko register.
	void GekkoCore::GekkoThreadProc(void* Parameter)
	{
		GekkoCore* core = (GekkoCore*)Parameter;

		if (core->suspended)
		{
			Thread::Sleep(50);
			return;
		}

		if (core->EnableTestBreakpoints)
		{
			if (core->TestBreakpoints()) {
				return;
			}
		}

		core->interp->ExecuteOpcode();
	}

	GekkoCore::GekkoCore()
	{
		cache = new Cache(this);
		icache = new Cache(this);
		
		// DEBUG
		//cache->SetLogLevel(CacheLogLevel::MemOps);
		//icache->SetLogLevel(CacheLogLevel::MemOps);

		gatherBuffer = new GatherBuffer(this);

		interp = new Interpreter(this);

		gekkoThread = EMUCreateThread(GekkoThreadProc, false, this, "GekkoCore");

		Reset();
	}

	GekkoCore::~GekkoCore()
	{
		StopOpcodeStatsThread();
		EMUJoinThread(gekkoThread);
		delete interp;
		delete gatherBuffer;
	}

	// Reset processor
	void GekkoCore::Reset()
	{
		one_second = CPU_TIMER_CLOCK;
		intFlag = false;
		exception = false;
		decreq = false;
		RESERVE = false;
		ops = 0;

		// BAT registers are scattered across the SPR address space. This is not very convenient, we will make it convenient.

		dbatu[0] = &regs.spr[SPR::DBAT0U];
		dbatu[1] = &regs.spr[SPR::DBAT1U];
		dbatu[2] = &regs.spr[SPR::DBAT2U];
		dbatu[3] = &regs.spr[SPR::DBAT3U];

		dbatl[0] = &regs.spr[SPR::DBAT0L];
		dbatl[1] = &regs.spr[SPR::DBAT1L];
		dbatl[2] = &regs.spr[SPR::DBAT2L];
		dbatl[3] = &regs.spr[SPR::DBAT3L];

		ibatu[0] = &regs.spr[SPR::IBAT0U];
		ibatu[1] = &regs.spr[SPR::IBAT1U];
		ibatu[2] = &regs.spr[SPR::IBAT2U];
		ibatu[3] = &regs.spr[SPR::IBAT3U];

		ibatl[0] = &regs.spr[SPR::IBAT0L];
		ibatl[1] = &regs.spr[SPR::IBAT1L];
		ibatl[2] = &regs.spr[SPR::IBAT2L];
		ibatl[3] = &regs.spr[SPR::IBAT3L];

		// Registers

		memset(&regs, 0, sizeof(regs));

		// Disable translation for now
		regs.msr &= ~(MSR_DR | MSR_IR);

		regs.tb.uval = 0;
		regs.spr[SPR::HID1] = 0x8000'0000;
		regs.spr[SPR::DEC] = 0;
		regs.spr[SPR::CTR] = 0;

		gatherBuffer->Reset();

		dtlb.InvalidateAll();
		itlb.InvalidateAll();
		cache->Reset();
		cache->Enable(false);
		icache->Reset();
		icache->Enable(false);
	}

	// Modify CPU counters
	void GekkoCore::Tick()
	{
		regs.tb.uval += CounterStep;         // timer

		uint32_t old = regs.spr[SPR::DEC];
		regs.spr[SPR::DEC] -= DecrementerStep;          // decrementer
		if ((old ^ regs.spr[SPR::DEC]) & 0x80000000)
		{
			if (regs.msr & MSR_EE)
			{
				decreq = 1;
				Report(Channel::CPU, "decrementer exception (OS alarm), pc:%08X\n", regs.pc);
			}
		}
	}

	int64_t GekkoCore::GetTicks()
	{
		return regs.tb.sval;
	}

	// 1 second of emulated CPU time.
	int64_t GekkoCore::OneSecond()
	{
		return one_second;
	}

	// Swap longs (no need in assembly, used by tools)
	void GekkoCore::SwapArea(uint32_t* addr, int count)
	{
		uint32_t* until = addr + count / sizeof(uint32_t);

		while (addr != until)
		{
			*addr = _BYTESWAP_UINT32(*addr);
			addr++;
		}
	}

	// Swap shorts (no need in assembly, used by tools)
	void GekkoCore::SwapAreaHalf(uint16_t* addr, int count)
	{
		uint16_t* until = addr + count / sizeof(uint16_t);

		while (addr != until)
		{
			*addr = _BYTESWAP_UINT16(*addr);
			addr++;
		}
	}

	void GekkoCore::Step()
	{
		interp->ExecuteOpcode();
	}

	void GekkoCore::AssertInterrupt()
	{
		intFlag = true;
	}

	void GekkoCore::ClearInterrupt()
	{
		intFlag = false;
	}

	void GekkoCore::Exception(Gekko::Exception code)
	{
		if (break_on_exception)
		{
			Halt("Gekko Exception: #%04X\n", (uint16_t)code);
		}

		if (trace_exceptions)
		{
			Report(Channel::CPU, "Gekko Exception: #%04X\n", (uint16_t)code);
		}

		if (exception)
		{
			Halt("CPU Double Fault!\n");
		}

		// save regs

		regs.spr[Gekko::SPR::SRR0] = regs.pc;
		regs.spr[Gekko::SPR::SRR1] = regs.msr;

		// Special processing for MMU
		if (code == Exception::EXCEPTION_ISI)
		{
			regs.spr[Gekko::SPR::SRR1] &= 0x0fff'ffff;

			switch (MmuLastResult)
			{
				case MmuResult::PageFault:
					regs.spr[Gekko::SPR::SRR1] |= 0x4000'0000;
					break;

				case MmuResult::ProtectedFetch:
					regs.spr[Gekko::SPR::SRR1] |= 0x0800'0000;
					break;

				case MmuResult::NoExecute:
					regs.spr[Gekko::SPR::SRR1] |= 0x1000'0000;
					break;

				case MmuResult::DirectStore:
					regs.spr[Gekko::SPR::SRR1] |= 0x1000'0000;
					break;

				default:
					break;
			}

			if (break_on_ISI) {
				Halt("Gekko ISI Exception at %08X\n", regs.pc);
			}
		}
		else if (code == Exception::EXCEPTION_DSI)
		{
			regs.spr[Gekko::SPR::DSISR] = 0;

			switch (MmuLastResult)
			{
				case MmuResult::PageFault:
					regs.spr[Gekko::SPR::DSISR] |= 0x4000'0000;
					break;

				case MmuResult::ProtectedRead:
					regs.spr[Gekko::SPR::DSISR] |= 0x0800'0000;
					break;

				case MmuResult::ProtectedWrite:
					regs.spr[Gekko::SPR::DSISR] |= 0x0A00'0000;
					break;

				case MmuResult::DirectStore:
					regs.spr[Gekko::SPR::DSISR] |= 0x0400'0000;
					break;

				default:
					break;
			}

			if (break_on_DSI) {
				Halt("Gekko DSI Exception at %08X.%s\n", regs.pc, regs.spr[Gekko::SPR::SDR1] != 0 ? 
					"\nThe sneaky game uses the vm library, to emulate virtual memory, which is not yet well implemented." : "");
			}
		}

		// Special processing for Program
		else if (code == Exception::EXCEPTION_PROGRAM)
		{
			regs.spr[Gekko::SPR::SRR1] &= 0x0000'ffff;

			switch (PrCause)
			{
				case PrivilegedCause::FpuEnabled:
					regs.spr[Gekko::SPR::SRR1] |= 0x0010'0000;
					break;
				case PrivilegedCause::IllegalInstruction:
					regs.spr[Gekko::SPR::SRR1] |= 0x0008'0000;
					break;
				case PrivilegedCause::Privileged:
					regs.spr[Gekko::SPR::SRR1] |= 0x0004'0000;
					break;
				case PrivilegedCause::Trap:
					regs.spr[Gekko::SPR::SRR1] |= 0x0002'0000;
					break;
				default:
					break;
			}
		}

		// disable address translation
		regs.msr &= ~(MSR_IR | MSR_DR);

		regs.msr &= ~MSR_RI;

		regs.msr &= ~MSR_EE;

		// change PC and set exception flag
		regs.pc = (uint32_t)code;
		exception = true;
	}

	uint32_t GekkoCore::EffectiveToPhysical(uint32_t ea, MmuAccess type, int& WIMG)
	{
		return EffectiveToPhysicalMmu(ea, type, WIMG);
	}
}


// Gekko Memory interface

namespace Gekko
{

	// Centralized hub which attracts all memory access requests from the interpreter or recompiler 
	// (as well as those who they pretend, for example HLE or Debugger).

	void GekkoCore::ReadByte(uint32_t addr, uint32_t* reg)
	{
		int WIMG;
		if (EnableTestReadBreakpoints)
		{
			TestReadBreakpoints(addr);
		}

		uint32_t pa = EffectiveToPhysical(addr, MmuAccess::Read, WIMG);
		if (pa == BadAddress)
		{
			regs.spr[(int)SPR::DAR] = addr;
			Exception(Exception::EXCEPTION_DSI);
			return;
		}

		if (cache->IsEnabled() && (WIMG & WIMG_I) == 0)
		{
			cache->ReadByte(pa, reg);
			return;
		}

		PIReadByte(pa, reg);
	}

	void GekkoCore::WriteByte(uint32_t addr, uint32_t data)
	{
		int WIMG;
		if (EnableTestWriteBreakpoints)
		{
			TestWriteBreakpoints(addr);
		}

		uint32_t pa = EffectiveToPhysical(addr, MmuAccess::Write, WIMG);
		if (pa == BadAddress)
		{
			regs.spr[(int)SPR::DAR] = addr;
			Exception(Exception::EXCEPTION_DSI);
			return;
		}

		if (RESERVE && pa == RESERVE_ADDR)
		{
			RESERVE = false;
		}

		if (regs.spr[Gekko::SPR::HID2] & HID2_WPE)
		{
			if ((pa & ~0x1f) == (regs.spr[Gekko::SPR::WPAR] & ~0x1f))
			{
				gatherBuffer->Write8((uint8_t)data);
				return;
			}
		}

		if (cache->IsEnabled() && (WIMG & WIMG_I) == 0)
		{
			cache->WriteByte(pa, data);
			if ((WIMG & WIMG_W) == 0)
				return;
		}

		PIWriteByte(pa, data);
	}

	void GekkoCore::ReadHalf(uint32_t addr, uint32_t* reg)
	{
		int WIMG;
		if (EnableTestReadBreakpoints)
		{
			TestReadBreakpoints(addr);
		}

		uint32_t pa = EffectiveToPhysical(addr, MmuAccess::Read, WIMG);
		if (pa == BadAddress)
		{
			regs.spr[(int)SPR::DAR] = addr;
			Exception(Exception::EXCEPTION_DSI);
			return;
		}

		if (cache->IsEnabled() && (WIMG & WIMG_I) == 0)
		{
			cache->ReadHalf(pa, reg);
			return;
		}

		PIReadHalf(pa, reg);
	}

	void GekkoCore::WriteHalf(uint32_t addr, uint32_t data)
	{
		int WIMG;
		if (EnableTestWriteBreakpoints)
		{
			TestWriteBreakpoints(addr);
		}

		uint32_t pa = EffectiveToPhysical(addr, MmuAccess::Write, WIMG);
		if (pa == BadAddress)
		{
			regs.spr[(int)SPR::DAR] = addr;
			Exception(Exception::EXCEPTION_DSI);
			return;
		}

		if (RESERVE && pa == RESERVE_ADDR)
		{
			RESERVE = false;
		}

		if (regs.spr[Gekko::SPR::HID2] & HID2_WPE)
		{
			if ((pa & ~0x1f) == (regs.spr[Gekko::SPR::WPAR] & ~0x1f))
			{
				gatherBuffer->Write16((uint16_t)data);
				return;
			}
		}

		if (cache->IsEnabled() && (WIMG & WIMG_I) == 0)
		{
			cache->WriteHalf(pa, data);
			if ((WIMG & WIMG_W) == 0)
				return;
		}

		PIWriteHalf(pa, data);
	}

	void GekkoCore::ReadWord(uint32_t addr, uint32_t* reg)
	{
		int WIMG;
		if (EnableTestReadBreakpoints)
		{
			TestReadBreakpoints(addr);
		}

		uint32_t pa = EffectiveToPhysical(addr, MmuAccess::Read, WIMG);
		if (pa == BadAddress)
		{
			regs.spr[(int)SPR::DAR] = addr;
			Exception(Exception::EXCEPTION_DSI);
			return;
		}

		if (cache->IsEnabled() && (WIMG & WIMG_I) == 0)
		{
			cache->ReadWord(pa, reg);
			return;
		}

		PIReadWord(pa, reg);
	}

	void GekkoCore::WriteWord(uint32_t addr, uint32_t data)
	{
		int WIMG;
		if (EnableTestWriteBreakpoints)
		{
			TestWriteBreakpoints(addr);
		}

		uint32_t pa = EffectiveToPhysical(addr, MmuAccess::Write, WIMG);
		if (pa == BadAddress)
		{
			regs.spr[(int)SPR::DAR] = addr;
			Exception(Exception::EXCEPTION_DSI);
			return;
		}

		if (RESERVE && pa == RESERVE_ADDR)
		{
			RESERVE = false;
		}

		if (regs.spr[Gekko::SPR::HID2] & HID2_WPE)
		{
			if ((pa & ~0x1f) == (regs.spr[Gekko::SPR::WPAR] & ~0x1f))
			{
				gatherBuffer->Write32(data);
				return;
			}
		}

		if (cache->IsEnabled() && (WIMG & WIMG_I) == 0)
		{
			cache->WriteWord(pa, data);
			if ((WIMG & WIMG_W) == 0)
				return;
		}

		PIWriteWord(pa, data);
	}

	void GekkoCore::ReadDouble(uint32_t addr, uint64_t* reg)
	{
		int WIMG;
		if (EnableTestReadBreakpoints)
		{
			TestReadBreakpoints(addr);
		}

		uint32_t pa = EffectiveToPhysical(addr, MmuAccess::Read, WIMG);
		if (pa == BadAddress)
		{
			regs.spr[(int)SPR::DAR] = addr;
			Exception(Exception::EXCEPTION_DSI);
			return;
		}

		if (cache->IsEnabled() && (WIMG & WIMG_I) == 0)
		{
			cache->ReadDouble(pa, reg);
			return;
		}

		// It is suspected that this type of single-beat transaction is not supported by Flipper PI.

		PIReadDouble(pa, reg);
	}

	void GekkoCore::WriteDouble(uint32_t addr, uint64_t* data)
	{
		int WIMG;
		if (EnableTestWriteBreakpoints)
		{
			TestWriteBreakpoints(addr);
		}

		uint32_t pa = EffectiveToPhysical(addr, MmuAccess::Write, WIMG);
		if (pa == BadAddress)
		{
			regs.spr[(int)SPR::DAR] = addr;
			Exception(Exception::EXCEPTION_DSI);
			return;
		}

		if (RESERVE && pa == RESERVE_ADDR)
		{
			RESERVE = false;
		}

		if (regs.spr[Gekko::SPR::HID2] & HID2_WPE)
		{
			if ((pa & ~0x1f) == (regs.spr[Gekko::SPR::WPAR] & ~0x1f))
			{
				gatherBuffer->Write64(*data);
				return;
			}
		}

		if (cache->IsEnabled() && (WIMG & WIMG_I) == 0)
		{
			cache->WriteDouble(pa, data);
			if ((WIMG & WIMG_W) == 0)
				return;
		}

		// It is suspected that this type of single-beat transaction is not supported by Flipper PI.

		PIWriteDouble(pa, data);
	}

	void GekkoCore::Fetch(uint32_t addr, uint32_t* reg)
	{
		int WIMG;

		uint32_t pa = EffectiveToPhysical(addr, MmuAccess::Execute, WIMG);
		if (pa == BadAddress)
		{
			Exception(Exception::EXCEPTION_ISI);
			return;
		}

		// You don't need to use the ICache in BS1, even if it is enabled
		if (pa >= BOOTROM_START_ADDRESS) {
			PIReadWord(pa, reg);
			return;
		}

		if (icache->IsEnabled() && (WIMG & WIMG_I) == 0)
		{
			icache->ReadWord(pa, reg);
			return;
		}

		PIReadWord(pa, reg);
	}

	uint8_t* GekkoCore::GetDataCachePointer(uint32_t phys_addr)
	{
		return cache->GetCachePointer(phys_addr);
	}

}


namespace Gekko
{
	bool TLB::Exists(uint32_t ea, uint32_t& pa, int& WIMG)
	{
		auto it = tlb.find(ea >> 12);
		if (it != tlb.end())
		{
			TLBEntry* entry = it->second;
			pa = (entry->addressTag << 12) | (ea & 0xfff);
			WIMG = entry->wimg;
			return true;
		}
		return false;
	}

	void TLB::Map(uint32_t ea, uint32_t pa, uint32_t pc, int WIMG)
	{
		TLBEntry* entry = new TLBEntry;
		entry->addressTag = pa >> 12;
		entry->wimg = WIMG;
		entry->pc = pc;
		tlb[ea >> 12] = entry;
	}

	void TLB::Invalidate(uint32_t ea)
	{
		auto it = tlb.find(ea >> 12);
		if (it != tlb.end())
		{
			delete it->second;
			tlb.erase(it);
		}
	}

	void TLB::InvalidateAll()
	{
		for (auto it = tlb.begin(); it != tlb.end(); ++it)
		{
			delete it->second;
		}
		tlb.clear();
	}

	void TLB::Dump()
	{
		for (auto it = tlb.begin(); it != tlb.end(); ++it)
		{
			uint32_t ea = it->first << 12;
			TLBEntry* entry = it->second;
			Report(Channel::CPU, "EA 0x%08X -> PA 0x%08X (wimg: %d, pc: 0x%08X)\n", ea, entry->addressTag << 12, entry->wimg, entry->pc);
		}
	}
}



// Support for breakpoints.

// After switching the Gekko emulation as a separate thread, the implementation of breakpoints is trivial. 
// When a breakpoint occurs, we just do Suspend of the Gekko thread. 
// And since all the other subsystems are tied to the Gekko thread (except for the UI and Debugger, it has an independent thread)
// the whole system stops with the processor.

namespace Gekko
{
	void GekkoCore::AddBreakpoint(uint32_t addr)
	{
		breakPointsLock.Lock();
		bool exists = false;
		for (auto it = breakPointsExecute.begin(); it != breakPointsExecute.end(); ++it)
		{
			if (*it == addr)
			{
				exists = true;
				break;
			}
		}
		if (!exists)
		{
			Report(Channel::CPU, "Breakpoint added: 0x%08X\n", addr);
			breakPointsExecute.push_back(addr);
			EnableTestBreakpoints = true;
		}
		breakPointsLock.Unlock();
	}

	void GekkoCore::RemoveBreakpoint(uint32_t addr)
	{
		breakPointsLock.Lock();
		bool exists = false;
		for (auto it = breakPointsExecute.begin(); it != breakPointsExecute.end(); ++it)
		{
			if (*it == addr)
			{
				exists = true;
				break;
			}
		}
		if (exists)
		{
			Report(Channel::CPU, "Breakpoint removed: 0x%08X\n", addr);
			breakPointsExecute.remove(addr);
		}
		if (breakPointsExecute.size() == 0)
		{
			EnableTestBreakpoints = false;
		}
		breakPointsLock.Unlock();
	}

	void GekkoCore::AddReadBreak(uint32_t addr)
	{
		breakPointsLock.Lock();
		breakPointsRead.push_back(addr);
		breakPointsLock.Unlock();
		EnableTestReadBreakpoints = true;
	}

	void GekkoCore::AddWriteBreak(uint32_t addr)
	{
		breakPointsLock.Lock();
		breakPointsWrite.push_back(addr);
		breakPointsLock.Unlock();
		EnableTestWriteBreakpoints = true;
	}

	void GekkoCore::ClearBreakpoints()
	{
		breakPointsLock.Lock();
		breakPointsExecute.clear();
		breakPointsRead.clear();
		breakPointsWrite.clear();
		breakPointsLock.Unlock();
		EnableTestBreakpoints = false;
		EnableTestReadBreakpoints = false;
		EnableTestWriteBreakpoints = false;
	}

	bool GekkoCore::TestBreakpoints()
	{
		if (oneShotBreakpoint != BadAddress && regs.pc == oneShotBreakpoint)
		{
			Halt("One shot breakpoint at addr: 0x%08X\n", oneShotBreakpoint);
			oneShotBreakpoint = BadAddress;
			return true;
		}

		uint32_t addr = BadAddress;

		breakPointsLock.Lock();
		for (auto it = breakPointsExecute.begin(); it != breakPointsExecute.end(); ++it)
		{
			if (*it == regs.pc)
			{
				addr = *it;
				break;
			}
		}
		breakPointsLock.Unlock();

		if (addr != BadAddress)
		{
			Halt("Gekko suspended at addr: 0x%08X\n", addr);
			return true;
		}

		return false;
	}

	void GekkoCore::TestReadBreakpoints(uint32_t accessAddress)
	{
		uint32_t addr = BadAddress;

		breakPointsLock.Lock();
		for (auto it = breakPointsRead.begin(); it != breakPointsRead.end(); ++it)
		{
			if (*it == accessAddress)
			{
				addr = *it;
				break;
			}
		}
		breakPointsLock.Unlock();

		if (addr != BadAddress)
		{
			Halt("Gekko suspended trying to read: 0x%08X\n", addr);
		}
	}

	void GekkoCore::TestWriteBreakpoints(uint32_t accessAddress)
	{
		uint32_t addr = BadAddress;

		breakPointsLock.Lock();
		for (auto it = breakPointsWrite.begin(); it != breakPointsWrite.end(); ++it)
		{
			if (*it == accessAddress)
			{
				addr = *it;
				break;
			}
		}
		breakPointsLock.Unlock();

		if (addr != BadAddress)
		{
			Halt("Gekko suspended trying to write: 0x%08X\n", addr);
		}
	}

	void GekkoCore::AddOneShotBreakpoint(uint32_t addr)
	{
		oneShotBreakpoint = addr;
		EnableTestBreakpoints = true;
	}

	void GekkoCore::ToggleBreakpoint(uint32_t addr)
	{
		if (IsBreakpoint(addr))
			RemoveBreakpoint(addr);
		else
			AddBreakpoint(addr);
	}

	bool GekkoCore::IsBreakpoint(uint32_t addr)
	{
		bool exists = false;
		breakPointsLock.Lock();
		for (auto it = breakPointsExecute.begin(); it != breakPointsExecute.end(); ++it)
		{
			if (*it == addr)
			{
				exists = true;
				break;
			}
		}
		breakPointsLock.Unlock();
		return exists;
	}
}


// Gekko caches support (including Data locked cache)

// As defined by the PowerPC architecture, they are physically indexed.

// Emulation of access to the cache is simple - a copy is created for the main memory, same size as the main RAM.
// If cached access is performed, all reads and writes are made from this buffer, otherwise from RAM.
// Invalidation causes new data to be loaded from RAM into the cache buffer.

// We do not support scattering for a locked cache and assume that it is locked as a fixed chunk.


namespace Gekko
{
	Cache::Cache(GekkoCore* core)
	{
		this->core = core;

		cacheData = new uint8_t[cacheSize];

		modifiedBlocks = new bool[cacheSize >> 5];

		invalidBlocks = new bool[cacheSize >> 5];

		LockedCache = new uint8_t[16 * 1024];

		Reset();
	}

	Cache::~Cache()
	{
		delete[] cacheData;
		delete[] modifiedBlocks;
		delete[] invalidBlocks;
		delete[] LockedCache;
	}

	void Cache::Reset()
	{
		Report(Channel::CPU, "Cache::Reset\n");
		FlashInvalidate();
	}

	void Cache::Enable(bool enable)
	{
		enabled = enable;

		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::Enable %i\n", enable ? 1 : 0);
		}
	}

	void Cache::Freeze(bool freeze)
	{
		frozen = freeze;

		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::Freeze %i\n", freeze ? 1 : 0);
		}
	}

	void Cache::LockedEnable(bool enable)
	{
		lcenabled = enable;

		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::LockedEnable %i\n", enable ? 1 : 0);
		}
	}

	bool Cache::IsDirty(uint32_t pa)
	{
		size_t blockNum = pa >> 5;
		return modifiedBlocks[blockNum];
	}

	void Cache::SetDirty(uint32_t pa, bool dirty)
	{
		size_t blockNum = pa >> 5;

		if (dirty == modifiedBlocks[blockNum])
			return;

		modifiedBlocks[blockNum] = dirty;

		if (log >= CacheLogLevel::MemOps && dirty)
		{
			Report(Channel::CPU, "Cache::SetDirty. pa: 0x%08X\n", pa & ~0x1f);
		}
	}

	bool Cache::IsInvalid(uint32_t pa)
	{
		size_t blockNum = pa >> 5;
		return invalidBlocks[blockNum];
	}

	void Cache::SetInvalid(uint32_t pa, bool invalid)
	{
		size_t blockNum = pa >> 5;

		if (invalid == invalidBlocks[blockNum])
			return;

		invalidBlocks[blockNum] = invalid;

		if (log >= CacheLogLevel::MemOps && invalid)
		{
			Report(Channel::CPU, "Cache::SetInvalid. pa: 0x%08X\n", pa & ~0x1f);
		}
	}

	void Cache::Flush(uint32_t pa)
	{
		if (pa >= cacheSize)
			return;

		if (IsDirty(pa) && !IsInvalid(pa))
		{
			CastOut(pa);
			SetDirty(pa, false);
		}
		SetInvalid(pa, true);

		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::Flush 0x%08X, pc: 0x%08X\n", pa, core->regs.pc);
		}
	}

	void Cache::Invalidate(uint32_t pa)
	{
		if (pa >= cacheSize)
			return;

		SetInvalid(pa, true);

		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::Invalidate 0x%08X, pc: 0x%08X\n", pa, core->regs.pc);
		}
	}

	void Cache::FlashInvalidate()
	{
		size_t blocks_num = cacheSize >> 5;
		for (size_t n = 0; n < blocks_num; n++) {
			modifiedBlocks[n] = false;
			invalidBlocks[n] = true;
		}
	}

	void Cache::Store(uint32_t pa)
	{
		if (pa >= cacheSize)
			return;

		if (IsDirty(pa) && !IsInvalid(pa))
		{
			CastOut(pa);
			SetDirty(pa, false);
		}

		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::Store 0x%08X\n", pa);
		}
	}

	void Cache::Touch(uint32_t pa)
	{
		if (pa >= cacheSize)
			return;

		if (!IsDirty(pa))
		{
			CastIn(pa);
			SetInvalid(pa, false);
			SetDirty(pa, false);			// Valid & Not Dirty
		}

		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::Touch 0x%08X\n", pa);
		}
	}

	void Cache::TouchForStore(uint32_t pa)
	{
		if (pa >= cacheSize)
			return;

		if (!IsDirty(pa))
		{
			CastIn(pa);
			SetInvalid(pa, false);
			SetDirty(pa, true);				// Valid & Dirty
		}

		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::TouchForStore 0x%08X\n", pa);
		}
	}

	void Cache::Zero(uint32_t pa)
	{
		if (pa >= cacheSize)
			return;

		memset(&cacheData[pa & ~0x1f], 0, 32);
		SetDirty(pa, true);
		SetInvalid(pa, false);

		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::Zero 0x%08X\n", pa);
		}
	}

	void Cache::ZeroLocked(uint32_t pa)
	{
		if (log >= CacheLogLevel::Commands)
		{
			Report(Channel::CPU, "Cache::ZeroLocked 0x%08X\n", pa);
		}

		LockedCacheAddr = pa & ~0x3FFF;
	}

	// The documentation says that the cache is casted by single-beat transactions, but for speed we will do casting by burts.

	void Cache::CastIn(uint32_t pa)
	{
		assert(pa < cacheSize);

		if (frozen)
			return;

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::CastIn: 0x%08X\n", pa & ~0x1f);
		}

		PIReadBurst(pa & ~0x1f, &cacheData[pa & ~0x1f]);
	}

	void Cache::CastOut(uint32_t pa)
	{
		assert(pa < cacheSize);

		if (frozen)
			return;

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::CastOut: 0x%08X\n", pa & ~0x1f);
		}

		PIWriteBurst(pa & ~0x1f, &cacheData[pa & ~0x1f]);
	}

	void Cache::ReadByte(uint32_t addr, uint32_t* reg)
	{
		// Locked cache
		if (IsLockedEnable())
		{
			if ((addr & ~0x3fff) == LockedCacheAddr)
			{
				uint8_t* ptr = &LockedCache[addr & 0x3fff];
				*reg = (uint32_t)*ptr;
				return;
			}
		}

		if (addr >= cacheSize)
			return;

		if (IsInvalid(addr))
		{
			CastIn(addr);
			SetInvalid(addr, false);
			SetDirty(addr, false);
		}
		*reg = cacheData[addr];

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::ReadByte. addr: 0x%08X, *reg: 0x%08X\n", addr, *reg);
		}
	}

	void Cache::WriteByte(uint32_t addr, uint32_t data)
	{
		// Locked cache
		if (IsLockedEnable())
		{
			if ((addr & ~0x3fff) == LockedCacheAddr)
			{
				uint8_t* ptr = &LockedCache[addr & 0x3fff];
				*ptr = (uint8_t)data;
				return;
			}
		}

		if (addr >= cacheSize)
			return;

		if (IsInvalid(addr))
		{
			CastIn(addr);
			SetInvalid(addr, false);
		}
		cacheData[addr] = (uint8_t)data;

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::WriteByte. addr: 0x%08X, data: 0x%08X\n", addr, data);
		}

		SetDirty(addr, true);
	}

	void Cache::ReadHalf(uint32_t addr, uint32_t* reg)
	{
		// Locked cache
		if (IsLockedEnable())
		{
			if ((addr & ~0x3fff) == LockedCacheAddr)
			{
				uint8_t* ptr = &LockedCache[addr & 0x3fff];
				*reg = (uint32_t)_BYTESWAP_UINT16(*(uint16_t*)ptr);
				return;
			}
		}

		if (addr >= cacheSize)
			return;

		if (IsInvalid(addr))
		{
			CastIn(addr);
			SetInvalid(addr, false);
			SetDirty(addr, false);
		}

		if ((addr & 0x1f) > (32 - sizeof(uint16_t)))
		{
			if (complain_unaligned) {
				Report(Channel::CPU, "Cache::ReadHalf: Unaligned cache access addr:0x%08X!\n", addr);
			}

			uint32_t nextCacheLineAddr = addr + sizeof(uint16_t);

			if (IsInvalid(nextCacheLineAddr))
			{
				CastIn(nextCacheLineAddr);
				SetInvalid(nextCacheLineAddr, false);
				SetDirty(nextCacheLineAddr, false);
			}
		}

		*reg = _BYTESWAP_UINT16(*(uint16_t*)&cacheData[addr]);

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::ReadHalf. addr: 0x%08X, *reg: 0x%08X\n", addr, *reg);
		}
	}

	void Cache::WriteHalf(uint32_t addr, uint32_t data)
	{
		// Locked cache
		if (IsLockedEnable())
		{
			if ((addr & ~0x3fff) == LockedCacheAddr)
			{
				uint8_t* ptr = &LockedCache[addr & 0x3fff];
				*(uint16_t*)ptr = _BYTESWAP_UINT16((uint16_t)data);
				return;
			}
		}

		if (addr >= cacheSize)
			return;

		if (IsInvalid(addr))
		{
			CastIn(addr);
			SetInvalid(addr, false);
		}

		if ((addr & 0x1f) > (32 - sizeof(uint16_t)))
		{
			if (complain_unaligned) {
				Report(Channel::CPU, "Cache::WriteHalf: Unaligned cache access addr:0x%08X!\n", addr);
			}

			uint32_t nextCacheLineAddr = addr + sizeof(uint16_t);

			if (IsInvalid(nextCacheLineAddr))
			{
				CastIn(nextCacheLineAddr);
				SetInvalid(nextCacheLineAddr, false);
				SetDirty(nextCacheLineAddr, false);
			}

			SetDirty(nextCacheLineAddr, true);
		}

		*(uint16_t*)&cacheData[addr] = _BYTESWAP_UINT16((uint16_t)data);

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::WriteHalf. addr: 0x%08X, data: 0x%08X\n", addr, data);
		}

		SetDirty(addr, true);
	}

	void Cache::ReadWord(uint32_t addr, uint32_t* reg)
	{
		// Locked cache
		if (IsLockedEnable())
		{
			if ((addr & ~0x3fff) == LockedCacheAddr)
			{
				uint8_t* ptr = &LockedCache[addr & 0x3fff];
				*reg = _BYTESWAP_UINT32(*(uint32_t*)ptr);
				return;
			}
		}

		if (addr >= cacheSize)
			return;

		if (IsInvalid(addr))
		{
			CastIn(addr);
			SetInvalid(addr, false);
			SetDirty(addr, false);
		}

		if ((addr & 0x1f) > (32 - sizeof(uint32_t)))
		{
			if (complain_unaligned) {
				Report(Channel::CPU, "Cache::ReadWord: Unaligned cache access addr:0x%08X!\n", addr);
			}

			uint32_t nextCacheLineAddr = addr + sizeof(uint32_t);

			if (IsInvalid(nextCacheLineAddr))
			{
				CastIn(nextCacheLineAddr);
				SetInvalid(nextCacheLineAddr, false);
				SetDirty(nextCacheLineAddr, false);
			}
		}

		*reg = _BYTESWAP_UINT32(*(uint32_t*)&cacheData[addr]);

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::ReadWord. addr: 0x%08X, *reg: 0x%08X\n", addr, *reg);
		}
	}

	void Cache::WriteWord(uint32_t addr, uint32_t data)
	{
		// Locked cache
		if (IsLockedEnable())
		{
			if ((addr & ~0x3fff) == LockedCacheAddr)
			{
				uint8_t* ptr = &LockedCache[addr & 0x3fff];
				*(uint32_t*)ptr = _BYTESWAP_UINT32(data);
				return;
			}
		}

		if (addr >= cacheSize)
			return;

		if (IsInvalid(addr))
		{
			CastIn(addr);
			SetInvalid(addr, false);
		}

		if ((addr & 0x1f) > (32 - sizeof(uint32_t)))
		{
			if (complain_unaligned) {
				Report(Channel::CPU, "Cache::WriteWord: Unaligned cache access addr:0x%08X!\n", addr);
			}

			uint32_t nextCacheLineAddr = addr + sizeof(uint32_t);

			if (IsInvalid(nextCacheLineAddr))
			{
				CastIn(nextCacheLineAddr);
				SetInvalid(nextCacheLineAddr, false);
				SetDirty(nextCacheLineAddr, false);
			}

			SetDirty(nextCacheLineAddr, true);
		}

		*(uint32_t*)&cacheData[addr] = _BYTESWAP_UINT32(data);

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::WriteWord. addr: 0x%08X, data: 0x%08X\n", addr, data);
		}

		SetDirty(addr, true);
	}

	void Cache::ReadDouble(uint32_t addr, uint64_t* reg)
	{
		// Locked cache
		if (IsLockedEnable())
		{
			if ((addr & ~0x3fff) == LockedCacheAddr)
			{
				uint8_t* buf = &LockedCache[addr & 0x3fff];
				*reg = _BYTESWAP_UINT64(*(uint64_t*)buf);
				return;
			}
		}

		if (addr >= cacheSize)
			return;

		if (IsInvalid(addr))
		{
			CastIn(addr);
			SetInvalid(addr, false);
			SetDirty(addr, false);
		}

		if ((addr & 0x1f) > (32 - sizeof(uint64_t)))
		{
			if (complain_unaligned) {
				Report(Channel::CPU, "Cache::ReadDouble: Unaligned cache access addr:0x%08X!\n", addr);
			}

			uint32_t nextCacheLineAddr = addr + sizeof(uint64_t);

			if (IsInvalid(nextCacheLineAddr))
			{
				CastIn(nextCacheLineAddr);
				SetInvalid(nextCacheLineAddr, false);
				SetDirty(nextCacheLineAddr, false);
			}
		}

		*reg = _BYTESWAP_UINT64(*(uint64_t*)&cacheData[addr]);

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::ReadDouble. addr: 0x%08X, *reg: 0x%llX\n", addr, *reg);
		}
	}

	void Cache::WriteDouble(uint32_t addr, uint64_t* data)
	{
		// Locked cache
		if (IsLockedEnable())
		{
			if ((addr & ~0x3fff) == LockedCacheAddr)
			{
				uint8_t* buf = &LockedCache[addr & 0x3fff];
				*(uint64_t*)buf = _BYTESWAP_UINT64(*data);
				return;
			}
		}

		if (addr >= cacheSize)
			return;

		if (IsInvalid(addr))
		{
			CastIn(addr);
			SetInvalid(addr, false);
		}

		if ((addr & 0x1f) > (32 - sizeof(uint64_t)))
		{
			if (complain_unaligned) {
				Report(Channel::CPU, "Cache::WriteDouble: Unaligned cache access addr:0x%08X!\n", addr);
			}

			uint32_t nextCacheLineAddr = addr + sizeof(uint64_t);

			if (IsInvalid(nextCacheLineAddr))
			{
				CastIn(nextCacheLineAddr);
				SetInvalid(nextCacheLineAddr, false);
				SetDirty(nextCacheLineAddr, false);
			}

			SetDirty(nextCacheLineAddr, true);
		}

		*(uint64_t*)&cacheData[addr] = _BYTESWAP_UINT64(*data);

		if (log >= CacheLogLevel::MemOps)
		{
			Report(Channel::CPU, "Cache::WriteDouble. addr: 0x%08X, data: 0x%llX\n", addr, data);
		}

		SetDirty(addr, true);
	}

	void Cache::LockedCacheDma(bool MemToCache, uint32_t memaddr, uint32_t lcaddr, size_t bursts)
	{
		if (MemToCache)
		{   // 1 load - transfer from external memory to locked cache

			if (log >= CacheLogLevel::MemOps)
			{
				Report(Channel::CPU, "Load Locked Cache: memadr: 0x%08X, lcaddr: 0x%08X, bursts: %i\n", memaddr, lcaddr, bursts);
			}

			for (size_t i = 0; i < bursts; i++)
			{
				PIReadBurst(memaddr, &LockedCache[lcaddr & 0x3fff]);
				memaddr += 32;
				lcaddr += 32;
			}
		}
		else
		{   // 0 store -  transfer from locked cache to external memory 

			if (log >= CacheLogLevel::MemOps)
			{
				Report(Channel::CPU, "Store Locked Cache: memadr: 0x%08X, lcaddr: 0x%08X, bursts: %i\n", memaddr, lcaddr, bursts);
			}

			for (size_t i = 0; i < bursts; i++)
			{
				PIWriteBurst(memaddr, &LockedCache[lcaddr & 0x3fff]);
				memaddr += 32;
				lcaddr += 32;
			}
		}
	}
}


namespace Gekko
{
	void GatherBuffer::Reset()
	{
		memset(fifo, 0, sizeof(fifo));
		readPtr = 0;
		writePtr = 0;
		retireTimeout = 0;

		if (log)
		{
			Report(Channel::CPU, "GatherBuffer::Reset");
		}
	}

	size_t GatherBuffer::GatherSize()
	{
		if (writePtr >= readPtr)
		{
			return writePtr - readPtr;
		}
		else
		{
			return (sizeof(fifo) - readPtr) + writePtr;
		}
	}

	void GatherBuffer::WriteBytes(uint8_t* data, size_t size)
	{
		if (log)
		{
			char byteText[10];
			std::string text;

			for (int i = 0; i < size; i++)
			{
				sprintf(byteText, "%02X ", data[i]);
				text += byteText;
			}

			Report(Channel::CPU, "GatherBuffer::WriteBytes: %s", text.c_str());
		}

		if (size < 4)
		{
			for (int i = 0; i < size; i++)
			{
				fifo[writePtr] = data[i];
				writePtr++;
				if (writePtr >= sizeof(fifo))
				{
					writePtr = 0;
				}
			}
		}
		else
		{
			if ((writePtr + size) < sizeof(fifo))
			{
				memcpy(&fifo[writePtr], data, size);
				writePtr += size;
			}
			else
			{
				size_t part1Size = sizeof(fifo) - writePtr;
				memcpy(&fifo[writePtr], data, part1Size);
				writePtr = size - part1Size;
				memcpy(fifo, data + part1Size, writePtr);
			}
		}

		if (GatherSize() >= 32)
		{
			uint8_t burstData[32];

			for (int i = 0; i < sizeof(burstData); i++)
			{
				burstData[i] = fifo[readPtr];
				readPtr++;
				if (readPtr >= sizeof(fifo))
				{
					readPtr = 0;
				}
			}

			if (log)
			{
				Report(Channel::CPU, "Burst gather buffer. Bytes left: %zi\n", GatherSize());
			}

			PIWriteBurst(core->regs.spr[(int)SPR::WPAR] & ~0x1f, burstData);
		}
	}

	void GatherBuffer::Write8(uint8_t value)
	{
		WriteBytes(&value, 1);
	}

	void GatherBuffer::Write16(uint16_t value)
	{
		uint8_t data[2];
		*(uint16_t*)data = _BYTESWAP_UINT16(value);
		WriteBytes(data, 2);
	}

	void GatherBuffer::Write32(uint32_t value)
	{
		uint8_t data[4];
		*(uint32_t*)data = _BYTESWAP_UINT32(value);
		WriteBytes(data, 4);
	}

	void GatherBuffer::Write64(uint64_t value)
	{
		uint8_t data[8];
		*(uint64_t*)data = _BYTESWAP_UINT64(value);
		WriteBytes(data, 8);
	}

	bool GatherBuffer::NotEmpty()
	{
		// The GatherBuffer has an undocumented feature - after a certain number of cycles the data in it is destroyed and it becomes free

		retireTimeout++;
		if (retireTimeout >= GEKKOCORE_GATHER_BUFFER_RETIRE_TICKS)
		{
			Reset();
		}

		return readPtr != writePtr;
	}
}


// MMU never throws Gekko exceptions. If something went wrong, BadAddress is returned. Then the consumer decides what to do.

// The result of the MMU is placed in MmuLastResult and translated address as output.

// We also use BadAddress as a signal that the translation did not pass (a special address that is usually not used by anyone).

namespace Gekko
{
	// Native address translation defined by PowerPC architecture. There are some alien moments (Hash for Page Tables), but overall its fine.

	uint32_t GekkoCore::EffectiveToPhysicalMmu(uint32_t ea, MmuAccess type, int& WIMG)
	{
		uint32_t pa;

		WIMG = 0;

		// Try TLB

#if GEKKOCORE_USE_TLB
		TLB* tlb = (type == MmuAccess::Execute) ? &itlb : &dtlb;

		if (tlb->Exists(ea, pa, WIMG))
		{
			return pa;
		}
#endif

		// First, try the block translation, if it doesn’t work, try the Page Table.

		if (!BlockAddressTranslation(ea, pa, type, WIMG))
		{
			pa = SegmentTranslation(ea, type, WIMG);
		}

		return pa;
	}

	bool GekkoCore::BlockAddressTranslation(uint32_t ea, uint32_t& pa, MmuAccess type, int& WIMG)
	{
		// Ignore BAT access rights for now (not used in embodiment system)

		if (type == MmuAccess::Execute)
		{
			if ((regs.msr & MSR_IR) == 0)
			{
				WIMG = WIMG_G;
				pa = ea;
				return true;
			}

			for (int n = 0; n < 4; n++)
			{
				bool valid = (*ibatu[n] & 3) != 0;
				if (!valid)
					continue;

				uint32_t bepi = BATBEPI(*ibatu[n]);
				uint32_t bl = BATBL(*ibatu[n]);
				uint32_t tst = (ea >> 17) & (0x7800 | ~bl);
				if (bepi == tst)
				{
					pa = BATBRPN(*ibatl[n]) | ((ea >> 17) & bl);
					pa = (pa << 17) | (ea & 0x1ffff);
					WIMG = (*ibatl[n] >> 3) & 0xf;
					MmuLastResult = MmuResult::Ok;
					// Put in TLB
#if GEKKOCORE_USE_TLB
					itlb.Map(ea, pa, Core->regs.pc, WIMG);
#endif
					return true;
				}
			}
		}
		else
		{
			if ((regs.msr & MSR_DR) == 0)
			{
				WIMG = WIMG_M | WIMG_G;
				pa = ea;
				return true;
			}

			for (int n = 0; n < 4; n++)
			{
				bool valid = (*dbatu[n] & 3) != 0;
				if (!valid)
					continue;

				uint32_t bepi = BATBEPI(*dbatu[n]);
				uint32_t bl = BATBL(*dbatu[n]);
				uint32_t tst = (ea >> 17) & (0x7800 | ~bl);
				if (bepi == tst)
				{
					pa = BATBRPN(*dbatl[n]) | ((ea >> 17) & bl);
					pa = (pa << 17) | (ea & 0x1ffff);
					WIMG = (*dbatl[n] >> 3) & 0xf;
					MmuLastResult = MmuResult::Ok;
					// Put in TLB
#if GEKKOCORE_USE_TLB
					dtlb.Map(ea, pa, Core->regs.pc, WIMG);
#endif
					return true;
				}
			}
		}

		// No BAT match, continue page table translation

		return false;
	}

	uint32_t GekkoCore::SegmentTranslation(uint32_t ea, MmuAccess type, int& WIMG)
	{
		int ptegUpper;

		uint32_t sr = regs.sr[ea >> 28];

		// Direct Store (T=1) is used by default by Dolphin OS to throw a DSI/ISI exception
		if (sr & 0x8000'0000)
		{
			MmuLastResult = MmuResult::DirectStore;
			return BadAddress;
		}

		if (sr & 0x1000'0000 && type == MmuAccess::Execute)
		{
			MmuLastResult = MmuResult::NoExecute;
			return BadAddress;
		}

		uint32_t key;
		if (regs.msr & MSR_PR)
		{
			key = sr & 0x2000'0000 ? 4 : 0;     // Kp
		}
		else
		{
			key = sr & 0x4000'0000 ? 4 : 0;     // Ks
		}

		// Calculate PTEG physical addresses

		uint64_t vpn = (((uint64_t)sr & 0x00ffffff) << 16) | ((ea >> 12) & 0xffff);
		uint32_t hash = ((uint32_t)(vpn >> 16) & 0x7ffff) ^ ((uint32_t)vpn & 0xffff);

		uint32_t sdr = regs.spr[(int)SPR::SDR1];

		uint32_t primaryPteAddr = 0;
		ptegUpper = (sdr >> 16) & 0x1ff;
		ptegUpper |= (sdr & 0x1ff) & ((hash >> 10) & 0x1ff);
		primaryPteAddr |= sdr & 0xfe00'0000;
		primaryPteAddr |= ptegUpper << 16;
		primaryPteAddr |= (hash & 0x3ff) << 6;

		hash = ~hash;

		uint32_t secondaryPteAddr = 0;
		ptegUpper = (sdr >> 16) & 0x1ff;
		ptegUpper |= (sdr & 0x1ff) & ((hash >> 10) & 0x1ff);
		secondaryPteAddr |= sdr & 0xfe00'0000;
		secondaryPteAddr |= ptegUpper << 16;
		secondaryPteAddr |= (hash & 0x3ff) << 6;

		// TODO: unify these loops

		// Try Primary PTEGs

		for (int i = 0; i < 8; i++)
		{
			// Load PTE

			uint32_t pte[2];

			PIReadWord(primaryPteAddr, &pte[0]);
			PIReadWord(primaryPteAddr + 4, &pte[1]);

			// Check Hash Bit

			if ((pte[0] & 0x40) != 0)
			{
				primaryPteAddr += 8;
				continue;
			}

			// Valid and suitable? (PTE [VSID, API, V] = Seg Desc [VSID], EA[API], 1)

			if (pte[0] & 0x8000'0000 &&
				((pte[0] >> 7) & 0xffffff) == ((vpn >> 16) & 0xffffff) &&
				(pte[0] & 0x3f) == ((vpn >> 10) & 0x3f))
			{
				// Referenced
				pte[1] |= 0x100;

				// Check Protection
				uint32_t pp = key | (pte[1] & 3);
				bool protectViolation = false;
				if (type == MmuAccess::Read || type == MmuAccess::Execute)
				{
					if (pp == 0b100)
					{
						protectViolation = true;
					}
				}
				else if (type == MmuAccess::Write)
				{
					if (pp == 0b011 || pp == 0b100 || pp == 0b101 || pp == 0b111)
					{
						protectViolation = true;
					}
				}

				if (type == MmuAccess::Write && !protectViolation)
				{
					pte[1] |= 0x80;     // Changed
				}
				PIWriteWord(primaryPteAddr + 4, pte[1]);

				if (protectViolation)
				{
					switch (type)
					{
						case MmuAccess::Read:
							MmuLastResult = MmuResult::ProtectedRead;
							break;
						case MmuAccess::Write:
							MmuLastResult = MmuResult::ProtectedWrite;
							break;
						case MmuAccess::Execute:
							MmuLastResult = MmuResult::ProtectedFetch;
							break;
					}

					return BadAddress;
				}

				uint32_t pa = (pte[1] & ~0xfff) | (ea & 0xfff);
				if (type != MmuAccess::Execute)
				{
					WIMG = (pte[1] >> 3) & 0xf;
				}
				// Put in TLB
#if GEKKOCORE_USE_TLB
				if (type == MmuAccess::Execute) {
					itlb.Map(ea, pa, Core->regs.pc, WIMG);
				}
				else {
					dtlb.Map(ea, pa, Core->regs.pc, WIMG);
				}
#endif
				MmuLastResult = MmuResult::Ok;
				return pa;
			}
			else
			{
				// Referenced
				pte[1] |= 0x100;
				PIWriteWord(primaryPteAddr + 4, pte[1]);
			}

			primaryPteAddr += 8;
		}

		// Try Secondary PTEGs

		for (int i = 0; i < 8; i++)
		{
			// Load PTE

			uint32_t pte[2];

			PIReadWord(secondaryPteAddr, &pte[0]);
			PIReadWord(secondaryPteAddr + 4, &pte[1]);

			// Check Hash Bit

			if ((pte[0] & 0x40) == 0)
			{
				secondaryPteAddr += 8;
				continue;
			}

			// Valid and suitable? (PTE [VSID, API, V] = Seg Desc [VSID], EA[API], 1)

			if (pte[0] & 0x8000'0000 &&
				((pte[0] >> 7) & 0xffffff) == ((vpn >> 16) & 0xffffff) &&
				(pte[0] & 0x3f) == ((vpn >> 10) & 0x3f))
			{
				// Referenced
				pte[1] |= 0x100;

				// Check Protection
				uint32_t pp = key | (pte[1] & 3);
				bool protectViolation = false;
				if (type == MmuAccess::Read || type == MmuAccess::Execute)
				{
					if (pp == 0b100)
					{
						protectViolation = true;
					}
				}
				else if (type == MmuAccess::Write)
				{
					if (pp == 0b011 || pp == 0b100 || pp == 0b101 || pp == 0b111)
					{
						protectViolation = true;
					}
				}

				// Changed
				if (type == MmuAccess::Write && !protectViolation)
				{
					pte[1] |= 0x80;
				}
				PIWriteWord(secondaryPteAddr + 4, pte[1]);

				if (protectViolation)
				{
					switch (type)
					{
						case MmuAccess::Read:
							MmuLastResult = MmuResult::ProtectedRead;
							break;
						case MmuAccess::Write:
							MmuLastResult = MmuResult::ProtectedWrite;
							break;
						case MmuAccess::Execute:
							MmuLastResult = MmuResult::ProtectedFetch;
							break;
					}

					return BadAddress;
				}

				uint32_t pa = (pte[1] & ~0xfff) | (ea & 0xfff);
				if (type != MmuAccess::Execute)
				{
					WIMG = (pte[1] >> 3) & 0xf;
				}
				// Put in TLB
#if GEKKOCORE_USE_TLB
				if (type == MmuAccess::Execute) {
					itlb.Map(ea, pa, Core->regs.pc, WIMG);
				}
				else {
					dtlb.Map(ea, pa, Core->regs.pc, WIMG);
				}
#endif
				MmuLastResult = MmuResult::Ok;
				return pa;
			}
			else
			{
				// Referenced
				pte[1] |= 0x100;
				PIWriteWord(secondaryPteAddr + 4, pte[1]);
			}

			secondaryPteAddr += 8;
		}

		MmuLastResult = MmuResult::PageFault;
		return BadAddress;
	}

	void GekkoCore::DumpDTLB()
	{
		dtlb.Dump();
	}

	void GekkoCore::DumpITLB()
	{
		itlb.Dump();
	}

	void GekkoCore::InvalidateTLBAll()
	{
		dtlb.InvalidateAll();
		itlb.InvalidateAll();
	}

}


// This module deals with the maintenance of statistics on the use of opcodes.

namespace Gekko
{
	struct OpcodeSortedEntry
	{
		Instruction instr;
		int count;
	};

	bool GekkoCore::IsOpcodeStatsEnabled()
	{
		return opcodeStatsEnabled;
	}

	void GekkoCore::EnableOpcodeStats(bool enable)
	{
		opcodeStatsEnabled = enable;
	}

	static int OpcodeStatsCompare(const void* a, const void* b)
	{
		OpcodeSortedEntry* as = (OpcodeSortedEntry*)a;
		OpcodeSortedEntry* bs = (OpcodeSortedEntry*)b;
		return bs->count - as->count;
	}

	void GekkoCore::PrintOpcodeStats(size_t maxCount)
	{
		OpcodeSortedEntry unsorted[(size_t)Instruction::Max];

		// Sort statistics

		for (size_t i = 0; i < (size_t)Instruction::Max; i++)
		{
			unsorted[i].instr = (Instruction)i;
			unsorted[i].count = opcodeStats[i];
		}

		qsort(unsorted, (size_t)Instruction::Max, sizeof(OpcodeSortedEntry), OpcodeStatsCompare);

		// Print out

		if (maxCount > (size_t)Instruction::Max)
		{
			maxCount = (size_t)Instruction::Max;
		}

		for (size_t i = 0; i < maxCount; i++)
		{
			DecoderInfo info;

			info.instr = unsorted[i].instr;
			Report(Channel::CPU, "%s: %i\n", GekkoDisasm::InstrToString(&info).c_str(), unsorted[i].count);
		}

		Report(Channel::CPU, "  \n");
	}

	void GekkoCore::ResetOpcodeStats()
	{
		memset(opcodeStats, 0, sizeof(opcodeStats));
	}

	// Thread that displays statistics on the use of opcodes once per second.

	void GekkoCore::OpcodeStatsThreadProc(void* Parameter)
	{
		GekkoCore* core = (GekkoCore*)Parameter;

		if (core->opcodeStatsEnabled && core->IsRunning())
		{
			core->PrintOpcodeStats(10);
			core->ResetOpcodeStats();
		}

		Thread::Sleep(1000);
	}

	void GekkoCore::RunOpcodeStatsThread()
	{
		if (opcodeStatsThread == nullptr)
		{
			opcodeStatsThread = EMUCreateThread(OpcodeStatsThreadProc, false, this, "OpcodeStats");
			EnableOpcodeStats(true);
		}
	}

	void GekkoCore::StopOpcodeStatsThread()
	{
		if (opcodeStatsThread != nullptr)
		{
			EMUJoinThread(opcodeStatsThread);
			opcodeStatsThread = nullptr;
			EnableOpcodeStats(false);
		}
	}

}
