/*
DingusPPC - The Experimental PowerPC Macintosh emulator
Copyright (C) 2018-21 divingkatae and maximum
                      (theweirdo)     spatium

(Contact divingkatae#1017 or powermax#2286 on Discord for more info)

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, either version 3 of the License, or
(at your option) any later version.

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 for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

/** @file PowerPC Memory Management Unit emulation. */

#include <devices/memctrl/memctrlbase.h>
#include <memaccess.h>
#include "ppcemu.h"
#include "ppcmmu.h"

#include <array>
#include <cinttypes>
#include <functional>
#include <loguru.hpp>
#include <stdexcept>

/* pointer to exception handler to be called when a MMU exception is occured. */
void (*mmu_exception_handler)(Except_Type exception_type, uint32_t srr1_bits);

/* pointers to BAT update functions. */
std::function<void(uint32_t bat_reg)> ibat_update;
std::function<void(uint32_t bat_reg)> dbat_update;

/** PowerPC-style MMU BAT arrays (NULL initialization isn't prescribed). */
PPC_BAT_entry ibat_array[4] = {{0}};
PPC_BAT_entry dbat_array[4] = {{0}};

bool is_601_MMU = false;

//#define MMU_PROFILING // uncomment this to enable MMU profiling
//#define TLB_PROFILING // uncomment this to enable SoftTLB profiling

#ifdef MMU_PROFILING

/* global variables for lightweight MMU profiling */
uint64_t    dmem_reads_total   = 0; // counts reads from data memory
uint64_t    iomem_reads_total  = 0; // counts I/O memory reads
uint64_t    dmem_writes_total  = 0; // counts writes to data memory
uint64_t    iomem_writes_total = 0; // counts I/O memory writes
uint64_t    exec_reads_total   = 0; // counts reads from executable memory
uint64_t    bat_transl_total   = 0; // counts BAT translations
uint64_t    ptab_transl_total  = 0; // counts page table translations
uint64_t    unaligned_reads    = 0; // counts unaligned reads
uint64_t    unaligned_writes   = 0; // counts unaligned writes
uint64_t    unaligned_crossp_r = 0; // counts unaligned crosspage reads
uint64_t    unaligned_crossp_w = 0; // counts unaligned crosspage writes

#endif // MMU_PROFILING

#ifdef TLB_PROFILING

/* global variables for lightweight SoftTLB profiling */
uint64_t    num_primary_itlb_hits   = 0; // number of hits in the primary ITLB
uint64_t    num_secondary_itlb_hits = 0; // number of hits in the secondary ITLB
uint64_t    num_itlb_refills        = 0; // number of ITLB refills
uint64_t    num_primary_dtlb_hits   = 0; // number of hits in the primary DTLB
uint64_t    num_secondary_dtlb_hits = 0; // number of hits in the secondary DTLB
uint64_t    num_dtlb_refills        = 0; // number of DTLB refills
uint64_t    num_entry_replacements  = 0; // number of entry replacements

#endif // TLB_PROFILING

/** remember recently used physical memory regions for quicker translation. */
AddressMapEntry last_read_area  = {0xFFFFFFFF, 0xFFFFFFFF};
AddressMapEntry last_write_area = {0xFFFFFFFF, 0xFFFFFFFF};
AddressMapEntry last_exec_area  = {0xFFFFFFFF, 0xFFFFFFFF};
AddressMapEntry last_ptab_area  = {0xFFFFFFFF, 0xFFFFFFFF};
AddressMapEntry last_dma_area   = {0xFFFFFFFF, 0xFFFFFFFF};

void ppc_set_cur_instruction(const uint8_t* ptr) {
    ppc_cur_instruction = READ_DWORD_BE_A(ptr);
}

/** 601-style block address translation. */
static BATResult mpc601_block_address_translation(uint32_t la)
{
    uint32_t pa;    // translated physical address
    uint8_t  prot;  // protection bits for the translated address
    unsigned key;

    bool bat_hit    = false;
    unsigned msr_pr = !!(ppc_state.msr & 0x4000);

    // I/O controller interface takes precedence over BAT in 601
    // Report BAT miss if T bit is set in the corresponding SR
    if (ppc_state.sr[(la >> 28) & 0x0F] & 0x80000000) {
        return BATResult{false, 0, 0};
    }

    for (int bat_index = 0; bat_index < 4; bat_index++) {
        PPC_BAT_entry* bat_entry = &ibat_array[bat_index];

        if (bat_entry->valid && ((la & bat_entry->hi_mask) == bat_entry->bepi)) {
            bat_hit = true;

            key = (((bat_entry->access & 1) & msr_pr) |
                  (((bat_entry->access >> 1) & 1) & (msr_pr ^ 1)));

            // remapping BAT access from 601-style to PowerPC-style
            static uint8_t access_conv[8] = {2, 2, 2, 1, 0, 1, 2, 1};

            prot = access_conv[(key << 2) | bat_entry->prot];

#ifdef MMU_PROFILING
            bat_transl_total++;
#endif

            // logical to physical translation
            pa = bat_entry->phys_hi | (la & ~bat_entry->hi_mask);
            break;
        }
    }

    return BATResult{bat_hit, prot, pa};
}

/** PowerPC-style block address translation. */
template <const BATType type>
static BATResult ppc_block_address_translation(uint32_t la)
{
    uint32_t pa;    // translated physical address
    uint8_t  prot;  // protection bits for the translated address
    PPC_BAT_entry *bat_array;

    bool bat_hit    = false;
    unsigned msr_pr = !!(ppc_state.msr & 0x4000);

    bat_array = (type == BATType::IBAT) ? ibat_array : dbat_array;

    // Format: %XY
    // X - supervisor access bit, Y - problem/user access bit
    // Those bits are mutually exclusive
    unsigned access_bits = ((msr_pr ^ 1) << 1) | msr_pr;

    for (int bat_index = 0; bat_index < 4; bat_index++) {
        PPC_BAT_entry* bat_entry = &bat_array[bat_index];

        if ((bat_entry->access & access_bits) && ((la & bat_entry->hi_mask) == bat_entry->bepi)) {
            bat_hit = true;

#ifdef MMU_PROFILING
            bat_transl_total++;
#endif
            // logical to physical translation
            pa = bat_entry->phys_hi | (la & ~bat_entry->hi_mask);
            prot = bat_entry->prot;
            break;
        }
    }

    return BATResult{bat_hit, prot, pa};
}

static inline uint8_t* calc_pteg_addr(uint32_t hash)
{
    uint32_t sdr1_val, pteg_addr;

    sdr1_val = ppc_state.spr[SPR::SDR1];

    pteg_addr = sdr1_val & 0xFE000000;
    pteg_addr |= (sdr1_val & 0x01FF0000) | (((sdr1_val & 0x1FF) << 16) & ((hash & 0x7FC00) << 6));
    pteg_addr |= (hash & 0x3FF) << 6;

    if (pteg_addr >= last_ptab_area.start && pteg_addr <= last_ptab_area.end) {
        return last_ptab_area.mem_ptr + (pteg_addr - last_ptab_area.start);
    } else {
        AddressMapEntry* entry = mem_ctrl_instance->find_range(pteg_addr);
        if (entry && entry->type & (RT_ROM | RT_RAM)) {
            last_ptab_area.start   = entry->start;
            last_ptab_area.end     = entry->end;
            last_ptab_area.mem_ptr = entry->mem_ptr;
            return last_ptab_area.mem_ptr + (pteg_addr - last_ptab_area.start);
        } else {
            ABORT_F("SOS: no page table region was found at %08X!\n", pteg_addr);
        }
    }
}

static bool search_pteg(uint8_t* pteg_addr, uint8_t** ret_pte_addr, uint32_t vsid,
                        uint16_t page_index, uint8_t pteg_num)
{
    /* construct PTE matching word */
    uint32_t pte_check = 0x80000000 | (vsid << 7) | (pteg_num << 6) | (page_index >> 10);

#ifdef MMU_INTEGRITY_CHECKS
    /* PTEG integrity check that ensures that all matching PTEs have
     identical RPN, WIMG and PP bits (PPC PEM 32-bit 7.6.2, rule 5). */
    uint32_t pte_word2_check;
    bool match_found = false;

    for (int i = 0; i < 8; i++, pteg_addr += 8) {
        if (pte_check == READ_DWORD_BE_A(pteg_addr)) {
            if (match_found) {
                if ((READ_DWORD_BE_A(pteg_addr) & 0xFFFFF07B) != pte_word2_check) {
                    ABORT_F("Multiple PTEs with different RPN/WIMG/PP found!\n");
                }
            } else {
                /* isolate RPN, WIMG and PP fields */
                pte_word2_check = READ_DWORD_BE_A(pteg_addr) & 0xFFFFF07B;
                *ret_pte_addr   = pteg_addr;
            }
        }
    }
#else
    for (int i = 0; i < 8; i++, pteg_addr += 8) {
        if (pte_check == READ_DWORD_BE_A(pteg_addr)) {
            *ret_pte_addr = pteg_addr;
            return true;
        }
    }
#endif

    return false;
}

static PATResult page_address_translation(uint32_t la, bool is_instr_fetch,
                                          unsigned msr_pr, int is_write)
{
    uint32_t sr_val, page_index, pteg_hash1, vsid, pte_word2;
    unsigned key, pp;
    uint8_t* pte_addr;

    sr_val = ppc_state.sr[(la >> 28) & 0x0F];
    if (sr_val & 0x80000000) {
        // check for 601-specific memory-forced I/O segments
        if (((sr_val >> 20) & 0x1FF) == 0x7F) {
            return PATResult{
                (la & 0x0FFFFFFF) | (sr_val << 28),
                0, // prot = read/write
                1  // no C bit updates
            };
        } else {
            ABORT_F("Direct-store segments not supported, LA=0x%X\n", la);
        }
    }

    /* instruction fetch from a no-execute segment will cause ISI exception */
    if ((sr_val & 0x10000000) && is_instr_fetch) {
        mmu_exception_handler(Except_Type::EXC_ISI, 0x10000000);
    }

    page_index = (la >> 12) & 0xFFFF;
    pteg_hash1 = (sr_val & 0x7FFFF) ^ page_index;
    vsid       = sr_val & 0x0FFFFFF;

    if (!search_pteg(calc_pteg_addr(pteg_hash1), &pte_addr, vsid, page_index, 0)) {
        if (!search_pteg(calc_pteg_addr(~pteg_hash1), &pte_addr, vsid, page_index, 1)) {
            if (is_instr_fetch) {
                mmu_exception_handler(Except_Type::EXC_ISI, 0x40000000);
            } else {
                ppc_state.spr[SPR::DSISR] = 0x40000000 | (is_write << 25);
                ppc_state.spr[SPR::DAR]   = la;
                mmu_exception_handler(Except_Type::EXC_DSI, 0);
            }
        }
    }

    pte_word2 = READ_DWORD_BE_A(pte_addr + 4);

    key = (((sr_val >> 29) & 1) & msr_pr) | (((sr_val >> 30) & 1) & (msr_pr ^ 1));

    /* check page access */
    pp = pte_word2 & 3;

    // the following scenarios cause DSI/ISI exception:
    // any access with key = 1 and PP = %00
    // write access with key = 1 and PP = %01
    // write access with PP = %11
    if ((key && (!pp || (pp == 1 && is_write))) || (pp == 3 && is_write)) {
        if (is_instr_fetch) {
            mmu_exception_handler(Except_Type::EXC_ISI, 0x08000000);
        } else {
            ppc_state.spr[SPR::DSISR] = 0x08000000 | (is_write << 25);
            ppc_state.spr[SPR::DAR]   = la;
            mmu_exception_handler(Except_Type::EXC_DSI, 0);
        }
    }

    /* update R and C bits */
    /* For simplicity, R is set on each access, C is set only for writes */
    pte_addr[6] |= 0x01;
    if (is_write) {
        pte_addr[7] |= 0x80;
    }

    /* return physical address, access protection and C status */
    return PATResult{
        ((pte_word2 & 0xFFFFF000) | (la & 0x00000FFF)),
        static_cast<uint8_t>((key << 2) | pp),
        static_cast<uint8_t>(pte_word2 & 0x80)
    };
}

uint8_t* mmu_get_dma_mem(uint32_t addr, uint32_t size)
{
    if (addr >= last_dma_area.start && (addr + size) <= last_dma_area.end) {
        return last_dma_area.mem_ptr + (addr - last_dma_area.start);
    } else {
        AddressMapEntry* entry = mem_ctrl_instance->find_range(addr);
        if (entry && entry->type & (RT_ROM | RT_RAM)) {
            last_dma_area.start   = entry->start;
            last_dma_area.end     = entry->end;
            last_dma_area.mem_ptr = entry->mem_ptr;
            return last_dma_area.mem_ptr + (addr - last_dma_area.start);
        } else {
            ABORT_F("SOS: DMA access to unmapped memory %08X!\n", addr);
        }
    }
}

// primary ITLB for all MMU modes
static std::array<TLBEntry, TLB_SIZE> itlb1_mode1;
static std::array<TLBEntry, TLB_SIZE> itlb1_mode2;
static std::array<TLBEntry, TLB_SIZE> itlb1_mode3;

// secondary ITLB for all MMU modes
static std::array<TLBEntry, TLB_SIZE*TLB2_WAYS> itlb2_mode1;
static std::array<TLBEntry, TLB_SIZE*TLB2_WAYS> itlb2_mode2;
static std::array<TLBEntry, TLB_SIZE*TLB2_WAYS> itlb2_mode3;

// primary DTLB for all MMU modes
static std::array<TLBEntry, TLB_SIZE> dtlb1_mode1;
static std::array<TLBEntry, TLB_SIZE> dtlb1_mode2;
static std::array<TLBEntry, TLB_SIZE> dtlb1_mode3;

// secondary DTLB for all MMU modes
static std::array<TLBEntry, TLB_SIZE*TLB2_WAYS> dtlb2_mode1;
static std::array<TLBEntry, TLB_SIZE*TLB2_WAYS> dtlb2_mode2;
static std::array<TLBEntry, TLB_SIZE*TLB2_WAYS> dtlb2_mode3;

TLBEntry *pCurITLB1; // current primary ITLB
TLBEntry *pCurITLB2; // current secondary ITLB
TLBEntry *pCurDTLB1; // current primary DTLB
TLBEntry *pCurDTLB2; // current secondary DTLB

uint32_t tlb_size_mask = TLB_SIZE - 1;

// fake TLB entry for handling of unmapped memory accesses
uint64_t    UnmappedVal = -1ULL;
TLBEntry    UnmappedMem = {TLB_INVALID_TAG, TLBFlags::PAGE_NOPHYS, 0, 0};

// Dummy page for catching writes to physical read-only pages
static std::array<uint8_t, 4096> dummy_page;

uint8_t     CurITLBMode = {0xFF}; // current ITLB mode
uint8_t     CurDTLBMode = {0xFF}; // current DTLB mode

void mmu_change_mode()
{
    uint8_t mmu_mode;

    // switch ITLB tables first
    mmu_mode = ((ppc_state.msr >> 4) & 0x2) | ((ppc_state.msr >> 14) & 1);

    if (CurITLBMode != mmu_mode) {
        switch(mmu_mode) {
            case 0: // real address mode
                pCurITLB1 = &dtlb1_mode1[0];
                pCurITLB2 = &dtlb2_mode1[0];
                break;
            case 2: // supervisor mode with instruction translation enabled
                pCurITLB1 = &dtlb1_mode2[0];
                pCurITLB2 = &dtlb2_mode2[0];
                break;
            case 3: // user mode with instruction translation enabled
                pCurITLB1 = &dtlb1_mode3[0];
                pCurITLB2 = &dtlb2_mode3[0];
                break;
        }
        CurITLBMode = mmu_mode;
    }

    // then switch DTLB tables
    mmu_mode = ((ppc_state.msr >> 3) & 0x2) | ((ppc_state.msr >> 14) & 1);

    if (CurDTLBMode != mmu_mode) {
        switch(mmu_mode) {
            case 0: // real address mode
                pCurDTLB1 = &dtlb1_mode1[0];
                pCurDTLB2 = &dtlb2_mode1[0];
                break;
            case 2: // supervisor mode with data translation enabled
                pCurDTLB1 = &dtlb1_mode2[0];
                pCurDTLB2 = &dtlb2_mode2[0];
                break;
            case 3: // user mode with data translation enabled
                pCurDTLB1 = &dtlb1_mode3[0];
                pCurDTLB2 = &dtlb2_mode3[0];
                break;
        }
        CurDTLBMode = mmu_mode;
    }
}

template <const TLBType tlb_type>
static TLBEntry* tlb2_target_entry(uint32_t gp_va)
{
    TLBEntry *tlb_entry;

    if (tlb_type == TLBType::ITLB) {
        tlb_entry = &pCurITLB2[((gp_va >> PAGE_SIZE_BITS) & tlb_size_mask) * TLB2_WAYS];
    } else {
        tlb_entry = &pCurDTLB2[((gp_va >> PAGE_SIZE_BITS) & tlb_size_mask) * TLB2_WAYS];
    }

    // select the target from invalid blocks first
    if (tlb_entry[0].tag == TLB_INVALID_TAG) {
        // update LRU bits
        tlb_entry[0].lru_bits  = 0x3;
        tlb_entry[1].lru_bits  = 0x2;
        tlb_entry[2].lru_bits &= 0x1;
        tlb_entry[3].lru_bits &= 0x1;
        return tlb_entry;
    } else if (tlb_entry[1].tag == TLB_INVALID_TAG) {
        // update LRU bits
        tlb_entry[0].lru_bits  = 0x2;
        tlb_entry[1].lru_bits  = 0x3;
        tlb_entry[2].lru_bits &= 0x1;
        tlb_entry[3].lru_bits &= 0x1;
        return &tlb_entry[1];
    } else if (tlb_entry[2].tag == TLB_INVALID_TAG) {
        // update LRU bits
        tlb_entry[0].lru_bits &= 0x1;
        tlb_entry[1].lru_bits &= 0x1;
        tlb_entry[2].lru_bits  = 0x3;
        tlb_entry[3].lru_bits  = 0x2;
        return &tlb_entry[2];
    } else if (tlb_entry[3].tag == TLB_INVALID_TAG) {
        // update LRU bits
        tlb_entry[0].lru_bits &= 0x1;
        tlb_entry[1].lru_bits &= 0x1;
        tlb_entry[2].lru_bits  = 0x2;
        tlb_entry[3].lru_bits  = 0x3;
        return &tlb_entry[3];
    } else { // no free entries, replace an existing one according with the hLRU policy
#ifdef TLB_PROFILING
        num_entry_replacements++;
#endif
        if (tlb_entry[0].lru_bits == 0) {
            // update LRU bits
            tlb_entry[0].lru_bits  = 0x3;
            tlb_entry[1].lru_bits  = 0x2;
            tlb_entry[2].lru_bits &= 0x1;
            tlb_entry[3].lru_bits &= 0x1;
            return tlb_entry;
        } else if (tlb_entry[1].lru_bits == 0) {
            // update LRU bits
            tlb_entry[0].lru_bits  = 0x2;
            tlb_entry[1].lru_bits  = 0x3;
            tlb_entry[2].lru_bits &= 0x1;
            tlb_entry[3].lru_bits &= 0x1;
            return &tlb_entry[1];
        } else if (tlb_entry[2].lru_bits == 0) {
            // update LRU bits
            tlb_entry[0].lru_bits &= 0x1;
            tlb_entry[1].lru_bits &= 0x1;
            tlb_entry[2].lru_bits  = 0x3;
            tlb_entry[3].lru_bits  = 0x2;
            return &tlb_entry[2];
        } else {
            // update LRU bits
            tlb_entry[0].lru_bits &= 0x1;
            tlb_entry[1].lru_bits &= 0x1;
            tlb_entry[2].lru_bits  = 0x2;
            tlb_entry[3].lru_bits  = 0x3;
            return &tlb_entry[3];
        }
    }
}

static TLBEntry* itlb2_refill(uint32_t guest_va)
{
    BATResult bat_res;
    uint32_t phys_addr;
    TLBEntry *tlb_entry;
    uint16_t flags = 0;

    /* instruction address translation if enabled */
    if (ppc_state.msr & 0x20) {
        // attempt block address translation first
        if (is_601_MMU) {
            bat_res = mpc601_block_address_translation(guest_va);
        } else {
            bat_res = ppc_block_address_translation<BATType::IBAT>(guest_va);
        }
        if (bat_res.hit) {
            // check block protection
            // only PP = 0 (no access) causes ISI exception
            if (!bat_res.prot) {
                mmu_exception_handler(Except_Type::EXC_ISI, 0x08000000);
            }
            phys_addr = bat_res.phys;
            flags |= TLBFlags::TLBE_FROM_BAT; // tell the world we come from
        } else {
            // page address translation
            PATResult pat_res = page_address_translation(guest_va, true,
                                                         !!(ppc_state.msr & 0x4000), 0);
            phys_addr = pat_res.phys;
            flags = TLBFlags::TLBE_FROM_PAT; // tell the world we come from
        }
    } else { // instruction translation disabled
        phys_addr = guest_va;
    }

    // look up host virtual address
    AddressMapEntry* reg_desc = mem_ctrl_instance->find_range(phys_addr);
    if (reg_desc) {
        if (reg_desc->type & RT_MMIO) {
            ABORT_F("Instruction fetch from MMIO region at 0x%08X!\n", phys_addr);
        }
        // refill the secondary TLB
        const uint32_t tag = guest_va & ~0xFFFUL;
        tlb_entry = tlb2_target_entry<TLBType::ITLB>(tag);
        tlb_entry->tag = tag;
        tlb_entry->flags = flags | TLBFlags::PAGE_MEM;
        tlb_entry->host_va_offs_r = (int64_t)reg_desc->mem_ptr - guest_va +
                                    (phys_addr - reg_desc->start);
    } else {
        ABORT_F("Instruction fetch from unmapped memory at 0x%08X!\n", phys_addr);
    }

    return tlb_entry;
}

static TLBEntry* dtlb2_refill(uint32_t guest_va, int is_write)
{
    BATResult bat_res;
    uint32_t phys_addr;
    uint16_t flags = 0;
    TLBEntry *tlb_entry;

    const uint32_t tag = guest_va & ~0xFFFUL;

    /* data address translation if enabled */
    if (ppc_state.msr & 0x10) {
        // attempt block address translation first
        if (is_601_MMU) {
            bat_res = mpc601_block_address_translation(guest_va);
        } else {
            bat_res = ppc_block_address_translation<BATType::DBAT>(guest_va);
        }
        if (bat_res.hit) {
            // check block protection
            if (!bat_res.prot || ((bat_res.prot & 1) && is_write)) {
                LOG_F(9, "BAT DSI exception in TLB2 refill!");
                LOG_F(9, "Attempt to write to read-only region, LA=0x%08X, PC=0x%08X!", guest_va, ppc_state.pc);
                ppc_state.spr[SPR::DSISR] = 0x08000000 | (is_write << 25);
                ppc_state.spr[SPR::DAR]   = guest_va;
                mmu_exception_handler(Except_Type::EXC_DSI, 0);
            }
            phys_addr = bat_res.phys;
            flags = TLBFlags::PTE_SET_C; // prevent PTE.C updates for BAT
            flags |= TLBFlags::TLBE_FROM_BAT; // tell the world we come from
            if (bat_res.prot == 2) {
                flags |= TLBFlags::PAGE_WRITABLE;
            }
        } else {
            // page address translation
            PATResult pat_res = page_address_translation(guest_va, false,
                                                         !!(ppc_state.msr & 0x4000), is_write);
            phys_addr = pat_res.phys;
            flags = TLBFlags::TLBE_FROM_PAT; // tell the world we come from
            if (pat_res.prot <= 2 || pat_res.prot == 6) {
                flags |= TLBFlags::PAGE_WRITABLE;
            }
            if (is_write || pat_res.pte_c_status) {
                // C-bit of the PTE is already set so the TLB logic
                // doesn't need to update it anymore
                flags |= TLBFlags::PTE_SET_C;
            }
        }
    } else { // data translation disabled
        phys_addr = guest_va;
        flags = TLBFlags::PTE_SET_C; // no PTE.C updates in real addressing mode
        flags |= TLBFlags::PAGE_WRITABLE; // assume physical pages are writable
    }

    // look up host virtual address
    AddressMapEntry* reg_desc = mem_ctrl_instance->find_range(phys_addr);
    if (reg_desc) {
        // refill the secondary TLB
        tlb_entry = tlb2_target_entry<TLBType::DTLB>(tag);
        tlb_entry->tag = tag;
        if (reg_desc->type & RT_MMIO) { // MMIO region
            tlb_entry->flags = flags | TLBFlags::PAGE_IO;
            tlb_entry->reg_desc = reg_desc;
        } else { // memory region backed by host memory
            tlb_entry->flags = flags | TLBFlags::PAGE_MEM;
            tlb_entry->host_va_offs_r = (int64_t)reg_desc->mem_ptr - guest_va +
                                        (phys_addr - reg_desc->start);
            if (reg_desc->type == RT_ROM) {
                // redirect writes to the dummy page for ROM regions
                tlb_entry->host_va_offs_w = (int64_t)&dummy_page - guest_va;
            } else {
                tlb_entry->host_va_offs_w = tlb_entry->host_va_offs_r;
            }
        }
        return tlb_entry;
    } else {
        LOG_F(WARNING, "Access to unmapped physical memory, phys_addr=0x%08X\n", phys_addr);
        return &UnmappedMem;
    }
}

template <const TLBType tlb_type>
static inline TLBEntry* lookup_secondary_tlb(uint32_t guest_va, uint32_t tag) {
    TLBEntry *tlb_entry;

    if (tlb_type == TLBType::ITLB) {
        tlb_entry = &pCurITLB2[((guest_va >> PAGE_SIZE_BITS) & tlb_size_mask) * TLB2_WAYS];
    } else {
        tlb_entry = &pCurDTLB2[((guest_va >> PAGE_SIZE_BITS) & tlb_size_mask) * TLB2_WAYS];
    }

    if (tlb_entry->tag == tag) {
        // update LRU bits
        tlb_entry[0].lru_bits  = 0x3;
        tlb_entry[1].lru_bits  = 0x2;
        tlb_entry[2].lru_bits &= 0x1;
        tlb_entry[3].lru_bits &= 0x1;
    } else if (tlb_entry[1].tag == tag) {
        tlb_entry = &tlb_entry[1];
        // update LRU bits
        tlb_entry[0].lru_bits  = 0x2;
        tlb_entry[1].lru_bits  = 0x3;
        tlb_entry[2].lru_bits &= 0x1;
        tlb_entry[3].lru_bits &= 0x1;
    } else if (tlb_entry[2].tag == tag) {
        tlb_entry = &tlb_entry[2];
        // update LRU bits
        tlb_entry[0].lru_bits &= 0x1;
        tlb_entry[1].lru_bits &= 0x1;
        tlb_entry[2].lru_bits  = 0x3;
        tlb_entry[3].lru_bits  = 0x2;
    } else if (tlb_entry[3].tag == tag) {
        tlb_entry = &tlb_entry[3];
        // update LRU bits
        tlb_entry[0].lru_bits &= 0x1;
        tlb_entry[1].lru_bits &= 0x1;
        tlb_entry[2].lru_bits  = 0x2;
        tlb_entry[3].lru_bits  = 0x3;
    } else {
        return nullptr;
    }
    return tlb_entry;
}

uint8_t *mmu_translate_imem(uint32_t vaddr)
{
    TLBEntry *tlb1_entry, *tlb2_entry;
    uint8_t *host_va;

#ifdef MMU_PROFILING
    exec_reads_total++;
#endif

    const uint32_t tag = vaddr & ~0xFFFUL;

    // look up guest virtual address in the primary ITLB
    tlb1_entry = &pCurITLB1[(vaddr >> PAGE_SIZE_BITS) & tlb_size_mask];
    if (tlb1_entry->tag == tag) { // primary ITLB hit -> fast path
#ifdef TLB_PROFILING
        num_primary_itlb_hits++;
#endif
        host_va = (uint8_t *)(tlb1_entry->host_va_offs_r + vaddr);
    } else {
        // primary ITLB miss -> look up address in the secondary ITLB
        tlb2_entry = lookup_secondary_tlb<TLBType::ITLB>(vaddr, tag);
        if (tlb2_entry == nullptr) {
#ifdef TLB_PROFILING
            num_itlb_refills++;
#endif
            // secondary ITLB miss ->
            // perform full address translation and refill the secondary ITLB
            tlb2_entry = itlb2_refill(vaddr);
        }
#ifdef TLB_PROFILING
        else {
            num_secondary_itlb_hits++;
        }
#endif
        // refill the primary ITLB
        tlb1_entry->tag = tag;
        tlb1_entry->flags = tlb2_entry->flags;
        tlb1_entry->host_va_offs_r = tlb2_entry->host_va_offs_r;
        host_va = (uint8_t *)(tlb1_entry->host_va_offs_r + vaddr);
    }

    ppc_set_cur_instruction(host_va);

    return host_va;
}

void tlb_flush_entry(uint32_t ea)
{
    TLBEntry *tlb_entry, *tlb1, *tlb2;

    const uint32_t tag = ea & ~0xFFFUL;

    for (int m = 0; m < 6; m++) {
        switch (m) {
        case 0:
            tlb1 = &itlb1_mode1[0];
            tlb2 = &itlb2_mode1[0];
            break;
        case 1:
            tlb1 = &itlb1_mode2[0];
            tlb2 = &itlb2_mode2[0];
            break;
        case 2:
            tlb1 = &itlb1_mode3[0];
            tlb2 = &itlb2_mode3[0];
            break;
        case 3:
            tlb1 = &dtlb1_mode1[0];
            tlb2 = &dtlb2_mode1[0];
            break;
        case 4:
            tlb1 = &dtlb1_mode2[0];
            tlb2 = &dtlb2_mode2[0];
            break;
        case 5:
            tlb1 = &dtlb1_mode3[0];
            tlb2 = &dtlb2_mode3[0];
            break;
        }

        // flush primary TLB
        tlb_entry = &tlb1[(ea >> PAGE_SIZE_BITS) & tlb_size_mask];
        if (tlb_entry->tag == tag) {
            tlb_entry->tag = TLB_INVALID_TAG;
            //LOG_F(INFO, "Invalidated primary TLB entry at 0x%X", ea);
        }

        // flush secondary TLB
        tlb_entry = &tlb2[((ea >> PAGE_SIZE_BITS) & tlb_size_mask) * TLB2_WAYS];
        for (int i = 0; i < TLB2_WAYS; i++) {
            if (tlb_entry[i].tag == tag) {
                tlb_entry[i].tag = TLB_INVALID_TAG;
                //LOG_F(INFO, "Invalidated secondary TLB entry at 0x%X", ea);
            }
        }
    }
}

template <const TLBType tlb_type>
void tlb_flush_entries(TLBFlags type)
{
    TLBEntry *m1_tlb, *m2_tlb, *m3_tlb;
    int i;

    if (tlb_type == TLBType::ITLB) {
        m1_tlb = &itlb1_mode1[0];
        m2_tlb = &itlb1_mode2[0];
        m3_tlb = &itlb1_mode3[0];
    } else {
        m1_tlb = &dtlb1_mode1[0];
        m2_tlb = &dtlb1_mode2[0];
        m3_tlb = &dtlb1_mode3[0];
    }

    // Flush entries from the primary TLBs
    for (i = 0; i < TLB_SIZE; i++) {
        if (m1_tlb[i].flags & type) {
            m1_tlb[i].tag = TLB_INVALID_TAG;
        }

        if (m2_tlb[i].flags & type) {
            m2_tlb[i].tag = TLB_INVALID_TAG;
        }

        if (m3_tlb[i].flags & type) {
            m3_tlb[i].tag = TLB_INVALID_TAG;
        }
    }

    if (tlb_type == TLBType::ITLB) {
        m1_tlb = &itlb2_mode1[0];
        m2_tlb = &itlb2_mode2[0];
        m3_tlb = &itlb2_mode3[0];
    } else {
        m1_tlb = &dtlb2_mode1[0];
        m2_tlb = &dtlb2_mode2[0];
        m3_tlb = &dtlb2_mode3[0];
    }

    // Flush entries from the secondary TLBs
    for (i = 0; i < TLB_SIZE * TLB2_WAYS; i++) {
        if (dtlb2_mode1[i].flags & type) {
            dtlb2_mode1[i].tag = TLB_INVALID_TAG;
        }

        if (dtlb2_mode2[i].flags & type) {
            dtlb2_mode2[i].tag = TLB_INVALID_TAG;
        }

        if (dtlb2_mode3[i].flags & type) {
            dtlb2_mode3[i].tag = TLB_INVALID_TAG;
        }
    }
}

bool gTLBFlushBatEntries = false;
bool gTLBFlushPatEntries = false;

template <const TLBType tlb_type>
void tlb_flush_bat_entries()
{
    if (!gTLBFlushBatEntries)
        return;

    tlb_flush_entries<tlb_type>(TLBE_FROM_BAT);

    gTLBFlushBatEntries = false;
}

template <const TLBType tlb_type>
void tlb_flush_pat_entries()
{
    if (!gTLBFlushPatEntries)
        return;

    tlb_flush_entries<tlb_type>(TLBE_FROM_PAT);

    gTLBFlushPatEntries = false;
}

static void mpc601_bat_update(uint32_t bat_reg)
{
    PPC_BAT_entry *ibat_entry, *dbat_entry;
    uint32_t bsm, hi_mask;
    int upper_reg_num;

    upper_reg_num = bat_reg & 0xFFFFFFFE;

    ibat_entry = &ibat_array[(bat_reg - 528) >> 1];
    dbat_entry = &dbat_array[(bat_reg - 528) >> 1];

    if (ppc_state.spr[bat_reg | 1] & 0x40) {
        bsm     = ppc_state.spr[upper_reg_num + 1] & 0x3F;
        hi_mask = ~((bsm << 17) | 0x1FFFF);

        ibat_entry->valid   = true;
        ibat_entry->access  = (ppc_state.spr[upper_reg_num] >> 2) & 3;
        ibat_entry->prot    = ppc_state.spr[upper_reg_num] & 3;
        ibat_entry->hi_mask = hi_mask;
        ibat_entry->phys_hi = ppc_state.spr[upper_reg_num + 1] & hi_mask;
        ibat_entry->bepi    = ppc_state.spr[upper_reg_num] & hi_mask;

        // copy IBAT entry to DBAT entry
        *dbat_entry = *ibat_entry;
    } else {
        // disable the corresponding BAT paars
        ibat_entry->valid = false;
        dbat_entry->valid = false;
    }

    // MPC601 has unified BATs so we're going to fush both ITLB and DTLB
    if (!gTLBFlushBatEntries) {
        gTLBFlushBatEntries = true;
        add_ctx_sync_action(&tlb_flush_bat_entries<TLBType::ITLB>);
        add_ctx_sync_action(&tlb_flush_bat_entries<TLBType::DTLB>);
    }
}

static void ppc_ibat_update(uint32_t bat_reg)
{
    int upper_reg_num;
    uint32_t bl, hi_mask;
    PPC_BAT_entry* bat_entry;

    upper_reg_num = bat_reg & 0xFFFFFFFE;

    if (ppc_state.spr[upper_reg_num] & 3) {    // is that BAT pair valid?
        bat_entry = &ibat_array[(bat_reg - 528) >> 1];
        bl        = (ppc_state.spr[upper_reg_num] >> 2) & 0x7FF;
        hi_mask   = ~((bl << 17) | 0x1FFFF);

        bat_entry->access  = ppc_state.spr[upper_reg_num] & 3;
        bat_entry->prot    = ppc_state.spr[upper_reg_num + 1] & 3;
        bat_entry->hi_mask = hi_mask;
        bat_entry->phys_hi = ppc_state.spr[upper_reg_num + 1] & hi_mask;
        bat_entry->bepi    = ppc_state.spr[upper_reg_num] & hi_mask;

        if (!gTLBFlushBatEntries) {
            gTLBFlushBatEntries = true;
            add_ctx_sync_action(&tlb_flush_bat_entries<TLBType::ITLB>);
        }
    }
}

static void ppc_dbat_update(uint32_t bat_reg)
{
    int upper_reg_num;
    uint32_t bl, hi_mask;
    PPC_BAT_entry* bat_entry;

    upper_reg_num = bat_reg & 0xFFFFFFFE;

    if (ppc_state.spr[upper_reg_num] & 3) {    // is that BAT pair valid?
        bat_entry = &dbat_array[(bat_reg - 536) >> 1];
        bl        = (ppc_state.spr[upper_reg_num] >> 2) & 0x7FF;
        hi_mask   = ~((bl << 17) | 0x1FFFF);

        bat_entry->access  = ppc_state.spr[upper_reg_num] & 3;
        bat_entry->prot    = ppc_state.spr[upper_reg_num + 1] & 3;
        bat_entry->hi_mask = hi_mask;
        bat_entry->phys_hi = ppc_state.spr[upper_reg_num + 1] & hi_mask;
        bat_entry->bepi    = ppc_state.spr[upper_reg_num] & hi_mask;

        if (!gTLBFlushBatEntries) {
            gTLBFlushBatEntries = true;
            add_ctx_sync_action(&tlb_flush_bat_entries<TLBType::DTLB>);
        }
    }
}

void mmu_pat_ctx_changed()
{
    // Page address translation context changed so we need to flush
    // all PAT entries from both ITLB and DTLB
    if (!gTLBFlushPatEntries) {
        gTLBFlushPatEntries = true;
        add_ctx_sync_action(&tlb_flush_pat_entries<TLBType::ITLB>);
        add_ctx_sync_action(&tlb_flush_pat_entries<TLBType::DTLB>);
    }
}

void mmu_print_regs()
{
    LOG_SCOPE_FUNCTION(INFO);
    LOG_F(INFO, "MSR = 0x%X\n", ppc_state.msr);

    LOG_F(INFO, "BAT registers:");

    for (int i = 0; i < 4; i++) {
        LOG_F(INFO, "IBAT%dU = 0x%X, IBAT%dL = 0x%X",
              i, ppc_state.spr[528+i*2],
              i, ppc_state.spr[529+i*2]);
    }

    if (!is_601_MMU) {
        for (int i = 0; i < 4; i++) {
            LOG_F(INFO, "DBAT%dU = 0x%X, DBAT%dL = 0x%X",
                  i, ppc_state.spr[536+i*2],
                  i, ppc_state.spr[537+i*2]);
        }
    }

    LOG_F(INFO, "");
    LOG_F(INFO, "SDR1 = 0x%X\n", ppc_state.spr[SPR::SDR1]);
    LOG_F(INFO, "Segment registers:");

    for (int i = 0; i < 16; i++) {
        LOG_F(INFO, "SR%d = 0x%X", i, ppc_state.sr[i]);
    }
}

// Forward declarations.
static uint32_t read_unaligned(uint32_t guest_va, uint8_t *host_va, uint32_t size);
static void write_unaligned(uint32_t guest_va, uint8_t *host_va, uint32_t value,
                            uint32_t size);

template <class T>
inline T mmu_read_vmem(uint32_t guest_va)
{
    TLBEntry *tlb1_entry, *tlb2_entry;
    uint8_t *host_va;

    const uint32_t tag = guest_va & ~0xFFFUL;

    // look up guest virtual address in the primary TLB
    tlb1_entry = &pCurDTLB1[(guest_va >> PAGE_SIZE_BITS) & tlb_size_mask];
    if (tlb1_entry->tag == tag) { // primary TLB hit -> fast path
#ifdef TLB_PROFILING
        num_primary_dtlb_hits++;
#endif
        host_va = (uint8_t *)(tlb1_entry->host_va_offs_r + guest_va);
    } else {
        // primary TLB miss -> look up address in the secondary TLB
        tlb2_entry = lookup_secondary_tlb<TLBType::DTLB>(guest_va, tag);
        if (tlb2_entry == nullptr) {
#ifdef TLB_PROFILING
            num_dtlb_refills++;
#endif
            // secondary TLB miss ->
            // perform full address translation and refill the secondary TLB
            tlb2_entry = dtlb2_refill(guest_va, 0);
            if (tlb2_entry->flags & PAGE_NOPHYS) {
                return UnmappedVal;
            }
        }
#ifdef TLB_PROFILING
        else {
            num_secondary_dtlb_hits++;
        }
#endif

        if (tlb2_entry->flags & TLBFlags::PAGE_MEM) { // is it a real memory region?
            // refill the primary TLB
            *tlb1_entry = *tlb2_entry;
            host_va = (uint8_t *)(tlb1_entry->host_va_offs_r + guest_va);
        } else { // otherwise, it's an access to a memory-mapped device
#ifdef MMU_PROFILING
            iomem_reads_total++;
#endif
            return (
                tlb2_entry->reg_desc->devobj->read(tlb2_entry->reg_desc->start,
                                                   guest_va - tlb2_entry->reg_desc->start,
                                                   sizeof(T))
            );
        }
    }

#ifdef MMU_PROFILING
    dmem_reads_total++;
#endif

    // handle unaligned memory accesses
    if (sizeof(T) > 1 && (guest_va & (sizeof(T) - 1))) {
        return read_unaligned(guest_va, host_va, sizeof(T));
    }

    // handle aligned memory accesses
    switch(sizeof(T)) {
        case 1:
            return *host_va;
        case 2:
            return READ_WORD_BE_A(host_va);
        case 4:
            return READ_DWORD_BE_A(host_va);
        case 8:
            return READ_QWORD_BE_A(host_va);
    }
}

// explicitely instantiate all required mmu_read_vmem variants
template uint8_t  mmu_read_vmem<uint8_t>(uint32_t guest_va);
template uint16_t mmu_read_vmem<uint16_t>(uint32_t guest_va);
template uint32_t mmu_read_vmem<uint32_t>(uint32_t guest_va);
template uint64_t mmu_read_vmem<uint64_t>(uint32_t guest_va);

template <class T>
inline void mmu_write_vmem(uint32_t guest_va, T value)
{
    TLBEntry *tlb1_entry, *tlb2_entry;
    uint8_t *host_va;

    const uint32_t tag = guest_va & ~0xFFFUL;

    // look up guest virtual address in the primary TLB
    tlb1_entry = &pCurDTLB1[(guest_va >> PAGE_SIZE_BITS) & tlb_size_mask];
    if (tlb1_entry->tag == tag) { // primary TLB hit -> fast path
#ifdef TLB_PROFILING
        num_primary_dtlb_hits++;
#endif
        if (!(tlb1_entry->flags & TLBFlags::PAGE_WRITABLE)) {
            ppc_state.spr[SPR::DSISR] = 0x08000000 | (1 << 25);
            ppc_state.spr[SPR::DAR]   = guest_va;
            mmu_exception_handler(Except_Type::EXC_DSI, 0);
        }
        if (!(tlb1_entry->flags & TLBFlags::PTE_SET_C)) {
            // perform full page address translation to update PTE.C bit
            PATResult pat_res = page_address_translation(guest_va, false,
                                                         !!(ppc_state.msr & 0x4000), true);
            tlb1_entry->flags |= TLBFlags::PTE_SET_C;

            // don't forget to update the secondary TLB as well
            tlb2_entry = lookup_secondary_tlb<TLBType::DTLB>(guest_va, tag);
            if (tlb2_entry != nullptr) {
                tlb2_entry->flags |= TLBFlags::PTE_SET_C;
            }
        }
        host_va = (uint8_t *)(tlb1_entry->host_va_offs_w + guest_va);
    } else {
        // primary TLB miss -> look up address in the secondary TLB
        tlb2_entry = lookup_secondary_tlb<TLBType::DTLB>(guest_va, tag);
        if (tlb2_entry == nullptr) {
#ifdef TLB_PROFILING
            num_dtlb_refills++;
#endif
            // secondary TLB miss ->
            // perform full address translation and refill the secondary TLB
            tlb2_entry = dtlb2_refill(guest_va, 1);
            if (tlb2_entry->flags & PAGE_NOPHYS) {
                return;
            }
        }
#ifdef TLB_PROFILING
        else {
            num_secondary_dtlb_hits++;
        }
#endif

        if (!(tlb2_entry->flags & TLBFlags::PAGE_WRITABLE)) {
            ppc_state.spr[SPR::DSISR] = 0x08000000 | (1 << 25);
            ppc_state.spr[SPR::DAR]   = guest_va;
            mmu_exception_handler(Except_Type::EXC_DSI, 0);
        }

        if (!(tlb2_entry->flags & TLBFlags::PTE_SET_C)) {
            // perform full page address translation to update PTE.C bit
            PATResult pat_res = page_address_translation(guest_va, false,
                                                         !!(ppc_state.msr & 0x4000), true);
            tlb2_entry->flags |= TLBFlags::PTE_SET_C;
        }

        if (tlb2_entry->flags & TLBFlags::PAGE_MEM) { // is it a real memory region?
            // refill the primary TLB
            *tlb1_entry = *tlb2_entry;
            host_va = (uint8_t *)(tlb1_entry->host_va_offs_w + guest_va);
        } else { // otherwise, it's an access to a memory-mapped device
#ifdef MMU_PROFILING
            iomem_writes_total++;
#endif
            tlb2_entry->reg_desc->devobj->write(tlb2_entry->reg_desc->start,
                                                guest_va - tlb2_entry->reg_desc->start,
                                                value, sizeof(T));
            return;
        }
    }

#ifdef MMU_PROFILING
    dmem_writes_total++;
#endif

    // handle unaligned memory accesses
    if (sizeof(T) > 1 && (guest_va & (sizeof(T) - 1))) {
        write_unaligned(guest_va, host_va, value, sizeof(T));
        return;
    }

    // handle aligned memory accesses
    switch(sizeof(T)) {
        case 1:
            *host_va = value;
            break;
        case 2:
            WRITE_WORD_BE_A(host_va, value);
            break;
        case 4:
            WRITE_DWORD_BE_A(host_va, value);
            break;
        case 8:
            WRITE_QWORD_BE_A(host_va, value);
            break;
    }
}

// explicitely instantiate all required mmu_write_vmem variants
template void mmu_write_vmem<uint8_t>(uint32_t guest_va,   uint8_t value);
template void mmu_write_vmem<uint16_t>(uint32_t guest_va, uint16_t value);
template void mmu_write_vmem<uint32_t>(uint32_t guest_va, uint32_t value);
template void mmu_write_vmem<uint64_t>(uint32_t guest_va, uint64_t value);

static uint32_t read_unaligned(uint32_t guest_va, uint8_t *host_va, uint32_t size)
{
    uint32_t result = 0;

    // is it a misaligned cross-page read?
    if (((guest_va & 0xFFF) + size) > 0x1000) {
#ifdef MMU_PROFILING
        unaligned_crossp_r++;
#endif
        // Break such a memory access into multiple, bytewise accesses.
        // Because such accesses suffer a performance penalty, they will be
        // presumably very rare so don't waste time optimizing the code below.
        for (int i = 0; i < size; guest_va++, i++) {
            result = (result << 8) | mmu_read_vmem<uint8_t>(guest_va);
        }
    } else {
#ifdef MMU_PROFILING
        unaligned_reads++;
#endif
        switch(size) {
            case 2:
                return READ_WORD_BE_U(host_va);
            case 4:
                return READ_DWORD_BE_U(host_va);
            case 8: // FIXME: should we raise alignment exception here?
                return READ_QWORD_BE_U(host_va);
        }
    }
    return result;
}

static void write_unaligned(uint32_t guest_va, uint8_t *host_va, uint32_t value,
                            uint32_t size)
{
    // is it a misaligned cross-page write?
    if (((guest_va & 0xFFF) + size) > 0x1000) {
#ifdef MMU_PROFILING
        unaligned_crossp_w++;
#endif
        // Break such a memory access into multiple, bytewise accesses.
        // Because such accesses suffer a performance penalty, they will be
        // presumably very rare so don't waste time optimizing the code below.

        uint32_t shift = (size - 1) * 8;

        for (int i = 0; i < size; shift -= 8, guest_va++, i++) {
            mmu_write_vmem<uint8_t>(guest_va, (value >> shift) & 0xFF);
        }
    } else {
#ifdef MMU_PROFILING
        unaligned_writes++;
#endif
        switch(size) {
            case 2:
                WRITE_WORD_BE_U(host_va, value);
                break;
            case 4:
                WRITE_DWORD_BE_U(host_va, value);
                break;
            case 8: // FIXME: should we raise alignment exception here?
                WRITE_QWORD_BE_U(host_va, value);
                break;
        }
    }
}

/* MMU profiling. */
#ifdef MMU_PROFILING

#include "utils/profiler.h"
#include <memory>

class MMUProfile : public BaseProfile {
public:
    MMUProfile() : BaseProfile("PPC_MMU") {};

    void populate_variables(std::vector<ProfileVar>& vars) {
        vars.clear();

        vars.push_back({.name = "Data Memory Reads Total",
                        .format = ProfileVarFmt::DEC,
                        .value = dmem_reads_total});

        vars.push_back({.name = "I/O Memory Reads Total",
                        .format = ProfileVarFmt::DEC,
                        .value = iomem_reads_total});

        vars.push_back({.name = "Data Memory Writes Total",
                        .format = ProfileVarFmt::DEC,
                        .value = dmem_writes_total});

        vars.push_back({.name = "I/O Memory Writes Total",
                        .format = ProfileVarFmt::DEC,
                        .value = iomem_writes_total});

        vars.push_back({.name = "Reads from Executable Memory",
                        .format = ProfileVarFmt::DEC,
                        .value = exec_reads_total});

        vars.push_back({.name = "BAT Translations Total",
                        .format = ProfileVarFmt::DEC,
                        .value = bat_transl_total});

        vars.push_back({.name = "Page Table Translations Total",
                        .format = ProfileVarFmt::DEC,
                        .value = ptab_transl_total});

        vars.push_back({.name = "Unaligned Reads Total",
                        .format = ProfileVarFmt::DEC,
                        .value = unaligned_reads});

        vars.push_back({.name = "Unaligned Writes Total",
                        .format = ProfileVarFmt::DEC,
                        .value = unaligned_writes});

        vars.push_back({.name = "Unaligned Crosspage Reads Total",
                        .format = ProfileVarFmt::DEC,
                        .value = unaligned_crossp_r});

        vars.push_back({.name = "Unaligned Crosspage Writes Total",
                        .format = ProfileVarFmt::DEC,
                        .value = unaligned_crossp_w});
    };

    void reset() {
        dmem_reads_total   = 0;
        iomem_reads_total  = 0;
        dmem_writes_total  = 0;
        iomem_writes_total = 0;
        exec_reads_total   = 0;
        bat_transl_total   = 0;
        ptab_transl_total  = 0;
        unaligned_reads    = 0;
        unaligned_writes   = 0;
        unaligned_crossp_r = 0;
        unaligned_crossp_w = 0;
    };
};
#endif

/* SoftTLB profiling. */
#ifdef TLB_PROFILING

#include "utils/profiler.h"
#include <memory>

class TLBProfile : public BaseProfile {
public:
    TLBProfile() : BaseProfile("PPC:MMU:TLB") {};

    void populate_variables(std::vector<ProfileVar>& vars) {
        vars.clear();

        vars.push_back({.name = "Number of hits in the primary ITLB",
            .format = ProfileVarFmt::DEC,
            .value = num_primary_itlb_hits});

        vars.push_back({.name = "Number of hits in the secondary ITLB",
            .format = ProfileVarFmt::DEC,
            .value = num_secondary_itlb_hits});

        vars.push_back({.name = "Number of ITLB refills",
            .format = ProfileVarFmt::DEC,
            .value = num_itlb_refills});

        vars.push_back({.name = "Number of hits in the primary DTLB",
            .format = ProfileVarFmt::DEC,
            .value = num_primary_dtlb_hits});

        vars.push_back({.name = "Number of hits in the secondary DTLB",
            .format = ProfileVarFmt::DEC,
            .value = num_secondary_dtlb_hits});

        vars.push_back({.name = "Number of DTLB refills",
            .format = ProfileVarFmt::DEC,
            .value = num_dtlb_refills});

        vars.push_back({.name = "Number of replaced TLB entries",
            .format = ProfileVarFmt::DEC,
            .value = num_entry_replacements});
    };

    void reset() {
        num_primary_dtlb_hits   = 0;
        num_secondary_dtlb_hits = 0;
        num_dtlb_refills        = 0;
        num_entry_replacements = 0;
    };
};
#endif

//=================== Old and slow code. Kept for reference =================
#if 0
template <class T, const bool is_aligned>
static inline T read_phys_mem(AddressMapEntry *mru_rgn, uint32_t addr)
{
    if (addr < mru_rgn->start || (addr + sizeof(T)) > mru_rgn->end) {
        AddressMapEntry* entry = mem_ctrl_instance->find_range(addr);
        if (entry) {
            *mru_rgn = *entry;
        } else {
            LOG_F(ERROR, "Read from unmapped memory at 0x%08X!\n", addr);
            return (-1ULL ? sizeof(T) == 8 : -1UL);
        }
    }

    if (mru_rgn->type & (RT_ROM | RT_RAM)) {
#ifdef MMU_PROFILING
        dmem_reads_total++;
#endif

        switch(sizeof(T)) {
        case 1:
            return *(mru_rgn->mem_ptr + (addr - mru_rgn->start));
        case 2:
            if (is_aligned) {
                return READ_WORD_BE_A(mru_rgn->mem_ptr + (addr - mru_rgn->start));
            } else {
                return READ_WORD_BE_U(mru_rgn->mem_ptr + (addr - mru_rgn->start));
            }
        case 4:
            if (is_aligned) {
                return READ_DWORD_BE_A(mru_rgn->mem_ptr + (addr - mru_rgn->start));
            } else {
                return READ_DWORD_BE_U(mru_rgn->mem_ptr + (addr - mru_rgn->start));
            }
        case 8:
            if (is_aligned) {
                return READ_QWORD_BE_A(mru_rgn->mem_ptr + (addr - mru_rgn->start));
            }
        default:
            LOG_F(ERROR, "READ_PHYS: invalid size %lu passed\n", sizeof(T));
            return (-1ULL ? sizeof(T) == 8 : -1UL);
        }
    } else if (mru_rgn->type & RT_MMIO) {
#ifdef MMU_PROFILING
        iomem_reads_total++;
#endif

        return (mru_rgn->devobj->read(mru_rgn->start,
                addr - mru_rgn->start, sizeof(T)));
    } else {
        LOG_F(ERROR, "READ_PHYS: invalid region type!\n");
        return (-1ULL ? sizeof(T) == 8 : -1UL);
    }
}

template <class T, const bool is_aligned>
static inline void write_phys_mem(AddressMapEntry *mru_rgn, uint32_t addr, T value)
{
    if (addr < mru_rgn->start || (addr + sizeof(T)) > mru_rgn->end) {
        AddressMapEntry* entry = mem_ctrl_instance->find_range(addr);
        if (entry) {
            *mru_rgn = *entry;
        } else {
            LOG_F(ERROR, "Write to unmapped memory at 0x%08X!\n", addr);
            return;
        }
    }

    if (mru_rgn->type & RT_RAM) {
#ifdef MMU_PROFILING
        dmem_writes_total++;
#endif

        switch(sizeof(T)) {
        case 1:
            *(mru_rgn->mem_ptr + (addr - mru_rgn->start)) = value;
            break;
        case 2:
            if (is_aligned) {
                WRITE_WORD_BE_A(mru_rgn->mem_ptr + (addr - mru_rgn->start), value);
            } else {
                WRITE_WORD_BE_U(mru_rgn->mem_ptr + (addr - mru_rgn->start), value);
            }
            break;
        case 4:
            if (is_aligned) {
                WRITE_DWORD_BE_A(mru_rgn->mem_ptr + (addr - mru_rgn->start), value);
            } else {
                WRITE_DWORD_BE_U(mru_rgn->mem_ptr + (addr - mru_rgn->start), value);
            }
            break;
        case 8:
            if (is_aligned) {
                WRITE_QWORD_BE_A(mru_rgn->mem_ptr + (addr - mru_rgn->start), value);
            }
            break;
        default:
            LOG_F(ERROR, "WRITE_PHYS: invalid size %lu passed\n", sizeof(T));
            return;
        }
    } else if (mru_rgn->type & RT_MMIO) {
#ifdef MMU_PROFILING
        iomem_writes_total++;
#endif

        mru_rgn->devobj->write(mru_rgn->start, addr - mru_rgn->start, value,
                               sizeof(T));
    } else {
        LOG_F(ERROR, "WRITE_PHYS: invalid region type!\n");
    }
}

/** PowerPC-style MMU data address translation. */
static uint32_t ppc_mmu_addr_translate(uint32_t la, int is_write)
{
    uint32_t pa; /* translated physical address */

    bool bat_hit    = false;
    unsigned msr_pr = !!(ppc_state.msr & 0x4000);

    // Format: %XY
    // X - supervisor access bit, Y - problem/user access bit
    // Those bits are mutually exclusive
    unsigned access_bits = ((msr_pr ^ 1) << 1) | msr_pr;

    for (int bat_index = 0; bat_index < 4; bat_index++) {
        PPC_BAT_entry* bat_entry = &dbat_array[bat_index];

        if ((bat_entry->access & access_bits) && ((la & bat_entry->hi_mask) == bat_entry->bepi)) {
            bat_hit = true;

#ifdef MMU_PROFILING
            bat_transl_total++;
#endif

            if (!bat_entry->prot || ((bat_entry->prot & 1) && is_write)) {
                ppc_state.spr[SPR::DSISR] = 0x08000000 | (is_write << 25);
                ppc_state.spr[SPR::DAR]   = la;
                mmu_exception_handler(Except_Type::EXC_DSI, 0);
            }

            // logical to physical translation
            pa = bat_entry->phys_hi | (la & ~bat_entry->hi_mask);
            break;
        }
    }

    /* page address translation */
    if (!bat_hit) {
        PATResult pat_res = page_address_translation(la, false, msr_pr, is_write);
        pa = pat_res.phys;

#ifdef MMU_PROFILING
        ptab_transl_total++;
#endif
    }

    return pa;
}

static void mem_write_unaligned(uint32_t addr, uint32_t value, uint32_t size) {
#ifdef MMU_DEBUG
    LOG_F(WARNING, "Attempt to write unaligned %d bytes to 0x%08X\n", size, addr);
#endif

    if (((addr & 0xFFF) + size) > 0x1000) {
        // Special case: unaligned cross-page writes
#ifdef MMU_PROFILING
        unaligned_crossp_w++;
#endif

        uint32_t phys_addr;
        uint32_t shift = (size - 1) * 8;

        // Break misaligned memory accesses into multiple, bytewise accesses
        // and retranslate on page boundary.
        // Because such accesses suffer a performance penalty, they will be
        // presumably very rare so don't care much about performance.
        for (int i = 0; i < size; shift -= 8, addr++, phys_addr++, i++) {
            if ((ppc_state.msr & 0x10) && (!i || !(addr & 0xFFF))) {
                phys_addr = ppc_mmu_addr_translate(addr, 1);
            }

            write_phys_mem<uint8_t, false>(&last_write_area, phys_addr,
                                          (value >> shift) & 0xFF);
        }
    } else {
        // data address translation if enabled
        if (ppc_state.msr & 0x10) {
            addr = ppc_mmu_addr_translate(addr, 1);
        }

        if (size == 2) {
            write_phys_mem<uint16_t, false>(&last_write_area, addr, value);
        } else {
            write_phys_mem<uint32_t, false>(&last_write_area, addr, value);
        }

#ifdef MMU_PROFILING
        unaligned_writes++;
#endif
    }
}

static inline uint64_t tlb_translate_addr(uint32_t guest_va)
{
    TLBEntry *tlb1_entry, *tlb2_entry;

    const uint32_t tag = guest_va & ~0xFFFUL;

    // look up address in the primary TLB
    tlb1_entry = &pCurDTLB1[(guest_va >> PAGE_SIZE_BITS) & tlb_size_mask];
    if (tlb1_entry->tag == tag) { // primary TLB hit -> fast path
        return tlb1_entry->host_va_offs_r + guest_va;
    } else { // primary TLB miss -> look up address in the secondary TLB
        tlb2_entry = &pCurDTLB2[((guest_va >> PAGE_SIZE_BITS) & tlb_size_mask) * TLB2_WAYS];
        if (tlb2_entry->tag == tag) {
            // update LRU bits
            tlb2_entry[0].lru_bits  = 0x3;
            tlb2_entry[1].lru_bits  = 0x2;
            tlb2_entry[2].lru_bits &= 0x1;
            tlb2_entry[3].lru_bits &= 0x1;
        } else if (tlb2_entry[1].tag == tag) {
            tlb2_entry = &tlb2_entry[1];
            // update LRU bits
            tlb2_entry[0].lru_bits  = 0x2;
            tlb2_entry[1].lru_bits  = 0x3;
            tlb2_entry[2].lru_bits &= 0x1;
            tlb2_entry[3].lru_bits &= 0x1;
        } else if (tlb2_entry[2].tag == tag) {
            tlb2_entry = &tlb2_entry[2];
            // update LRU bits
            tlb2_entry[0].lru_bits &= 0x1;
            tlb2_entry[1].lru_bits &= 0x1;
            tlb2_entry[2].lru_bits  = 0x3;
            tlb2_entry[3].lru_bits  = 0x2;
        } else if (tlb2_entry[3].tag == tag) {
            tlb2_entry = &tlb2_entry[3];
            // update LRU bits
            tlb2_entry[0].lru_bits &= 0x1;
            tlb2_entry[1].lru_bits &= 0x1;
            tlb2_entry[2].lru_bits  = 0x2;
            tlb2_entry[3].lru_bits  = 0x3;
        } else { // secondary TLB miss ->
            // perform full address translation and refill the secondary TLB
            tlb2_entry = dtlb2_refill(guest_va, 0);
        }

        if (tlb2_entry->flags & TLBFlags::PAGE_MEM) { // is it a real memory region?
            // refill the primary TLB
            tlb1_entry->tag = tag;
            tlb1_entry->flags = tlb2_entry->flags;
            tlb1_entry->host_va_offs_r = tlb2_entry->host_va_offs_r;
            return tlb1_entry->host_va_offs_r + guest_va;
        } else { // an attempt to access a memory-mapped device
            return guest_va - tlb2_entry->reg_desc->start;
        }
    }
}

static uint32_t mem_grab_unaligned(uint32_t addr, uint32_t size) {
    uint32_t ret = 0;

#ifdef MMU_DEBUG
    LOG_F(WARNING, "Attempt to read unaligned %d bytes from 0x%08X\n", size, addr);
#endif

    if (((addr & 0xFFF) + size) > 0x1000) {
        // Special case: misaligned cross-page reads
#ifdef MMU_PROFILING
        unaligned_crossp_r++;
#endif

        uint32_t phys_addr;
        uint32_t res = 0;

        // Break misaligned memory accesses into multiple, bytewise accesses
        // and retranslate on page boundary.
        // Because such accesses suffer a performance penalty, they will be
        // presumably very rare so don't care much about performance.
        for (int i = 0; i < size; addr++, phys_addr++, i++) {
            tlb_translate_addr(addr);
            if ((ppc_state.msr & 0x10) && (!i || !(addr & 0xFFF))) {
                phys_addr = ppc_mmu_addr_translate(addr, 0);
            }

            res = (res << 8) |
                read_phys_mem<uint8_t, false>(&last_read_area, phys_addr);
        }
        return res;

    } else {
        /* data address translation if enabled */
        if (ppc_state.msr & 0x10) {
            addr = ppc_mmu_addr_translate(addr, 0);
        }

        if (size == 2) {
            return read_phys_mem<uint16_t, false>(&last_read_area, addr);
        } else {
            return read_phys_mem<uint32_t, false>(&last_read_area, addr);
        }

#ifdef MMU_PROFILING
        unaligned_reads++;
#endif
    }

    return ret;
}

void mem_write_byte(uint32_t addr, uint8_t value) {
    mmu_write_vmem<uint8_t>(addr, value);

    /* data address translation if enabled */
    if (ppc_state.msr & 0x10) {
        addr = ppc_mmu_addr_translate(addr, 1);
    }

    write_phys_mem<uint8_t, true>(&last_write_area, addr, value);
}

void mem_write_word(uint32_t addr, uint16_t value) {
    mmu_write_vmem<uint16_t>(addr, value);

    if (addr & 1) {
        mem_write_unaligned(addr, value, 2);
        return;
    }

    /* data address translation if enabled */
    if (ppc_state.msr & 0x10) {
        addr = ppc_mmu_addr_translate(addr, 1);
    }

    write_phys_mem<uint16_t, true>(&last_write_area, addr, value);
}

void mem_write_dword(uint32_t addr, uint32_t value) {
    mmu_write_vmem<uint32_t>(addr, value);

    if (addr & 3) {
        mem_write_unaligned(addr, value, 4);
        return;
    }

    /* data address translation if enabled */
    if (ppc_state.msr & 0x10) {
        addr = ppc_mmu_addr_translate(addr, 1);
    }

    write_phys_mem<uint32_t, true>(&last_write_area, addr, value);
}

void mem_write_qword(uint32_t addr, uint64_t value) {
    mmu_write_vmem<uint64_t>(addr, value);

    if (addr & 7) {
        ABORT_F("SOS! Attempt to write unaligned QWORD to 0x%08X\n", addr);
    }

    /* data address translation if enabled */
    if (ppc_state.msr & 0x10) {
        addr = ppc_mmu_addr_translate(addr, 1);
    }

    write_phys_mem<uint64_t, true>(&last_write_area, addr, value);
}

/** Grab a value from memory into a register */
uint8_t mem_grab_byte(uint32_t addr) {
    tlb_translate_addr(addr);

    /* data address translation if enabled */
    if (ppc_state.msr & 0x10) {
        addr = ppc_mmu_addr_translate(addr, 0);
    }

    return read_phys_mem<uint8_t, true>(&last_read_area, addr);
}

uint16_t mem_grab_word(uint32_t addr) {
    tlb_translate_addr(addr);

    if (addr & 1) {
        return mem_grab_unaligned(addr, 2);
    }

    /* data address translation if enabled */
    if (ppc_state.msr & 0x10) {
        addr = ppc_mmu_addr_translate(addr, 0);
    }

    return read_phys_mem<uint16_t, true>(&last_read_area, addr);
}

uint32_t mem_grab_dword(uint32_t addr) {
    tlb_translate_addr(addr);

    if (addr & 3) {
        return mem_grab_unaligned(addr, 4);
    }

    /* data address translation if enabled */
    if (ppc_state.msr & 0x10) {
        addr = ppc_mmu_addr_translate(addr, 0);
    }

    return read_phys_mem<uint32_t, true>(&last_read_area, addr);
}

uint64_t mem_grab_qword(uint32_t addr) {
    tlb_translate_addr(addr);

    if (addr & 7) {
        ABORT_F("SOS! Attempt to read unaligned QWORD at 0x%08X\n", addr);
    }

    /* data address translation if enabled */
    if (ppc_state.msr & 0x10) {
        addr = ppc_mmu_addr_translate(addr, 0);
    }

    return read_phys_mem<uint64_t, true>(&last_read_area, addr);
}

/** PowerPC-style MMU instruction address translation. */
static uint32_t mmu_instr_translation(uint32_t la)
{
    uint32_t pa; /* translated physical address */

    bool bat_hit    = false;
    unsigned msr_pr = !!(ppc_state.msr & 0x4000);

    // Format: %XY
    // X - supervisor access bit, Y - problem/user access bit
    // Those bits are mutually exclusive
    unsigned access_bits = ((msr_pr ^ 1) << 1) | msr_pr;

    for (int bat_index = 0; bat_index < 4; bat_index++) {
        PPC_BAT_entry* bat_entry = &ibat_array[bat_index];

        if ((bat_entry->access & access_bits) && ((la & bat_entry->hi_mask) == bat_entry->bepi)) {
            bat_hit = true;

#ifdef MMU_PROFILING
            bat_transl_total++;
#endif

            if (!bat_entry->prot) {
                mmu_exception_handler(Except_Type::EXC_ISI, 0x08000000);
            }

            // logical to physical translation
            pa = bat_entry->phys_hi | (la & ~bat_entry->hi_mask);
            break;
        }
    }

    /* page address translation */
    if (!bat_hit) {
        PATResult pat_res = page_address_translation(la, true, msr_pr, 0);
        pa = pat_res.phys;

#ifdef MMU_PROFILING
        ptab_transl_total++;
#endif
    }

    return pa;
}

uint8_t* quickinstruction_translate(uint32_t addr) {
    uint8_t* real_addr;

#ifdef MMU_PROFILING
    exec_reads_total++;
#endif

    /* perform instruction address translation if enabled */
    if (ppc_state.msr & 0x20) {
        addr = mmu_instr_translation(addr);
    }

    if (addr >= last_exec_area.start && addr <= last_exec_area.end) {
        real_addr = last_exec_area.mem_ptr + (addr - last_exec_area.start);
        ppc_set_cur_instruction(real_addr);
    } else {
        AddressMapEntry* entry = mem_ctrl_instance->find_range(addr);
        if (entry && entry->type & (RT_ROM | RT_RAM)) {
            last_exec_area.start   = entry->start;
            last_exec_area.end     = entry->end;
            last_exec_area.mem_ptr = entry->mem_ptr;
            real_addr              = last_exec_area.mem_ptr + (addr - last_exec_area.start);
            ppc_set_cur_instruction(real_addr);
        } else {
            ABORT_F("Attempt to execute code at %08X!\n", addr);
        }
    }

    return real_addr;
}
#endif

uint64_t mem_read_dbg(uint32_t virt_addr, uint32_t size) {
    uint32_t save_dsisr, save_dar;
    uint64_t ret_val;

    /* save MMU-related CPU state */
    save_dsisr            = ppc_state.spr[SPR::DSISR];
    save_dar              = ppc_state.spr[SPR::DAR];
    mmu_exception_handler = dbg_exception_handler;

    try {
        switch (size) {
        case 1:
            ret_val = mmu_read_vmem<uint8_t>(virt_addr);
            break;
        case 2:
            ret_val = mmu_read_vmem<uint16_t>(virt_addr);
            break;
        case 4:
            ret_val = mmu_read_vmem<uint32_t>(virt_addr);
            break;
        case 8:
            ret_val = mmu_read_vmem<uint64_t>(virt_addr);
            break;
        default:
            ret_val = mmu_read_vmem<uint8_t>(virt_addr);
        }
    } catch (std::invalid_argument& exc) {
        /* restore MMU-related CPU state */
        mmu_exception_handler     = ppc_exception_handler;
        ppc_state.spr[SPR::DSISR] = save_dsisr;
        ppc_state.spr[SPR::DAR]   = save_dar;

        /* rethrow MMU exception */
        throw exc;
    }

    /* restore MMU-related CPU state */
    mmu_exception_handler     = ppc_exception_handler;
    ppc_state.spr[SPR::DSISR] = save_dsisr;
    ppc_state.spr[SPR::DAR]   = save_dar;

    return ret_val;
}

void ppc_mmu_init(uint32_t cpu_version)
{
    mmu_exception_handler = ppc_exception_handler;

    if ((cpu_version >> 16) == 1) {
        // use 601-style BATs
        ibat_update = &mpc601_bat_update;
        is_601_MMU = true;
    } else {
        // use PPC-style BATs
        ibat_update = &ppc_ibat_update;
        dbat_update = &ppc_dbat_update;
        is_601_MMU = false;
    }

    // invalidate all IDTLB entries
    for (auto &tlb_el : itlb1_mode1) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
    }

    for (auto &tlb_el : itlb1_mode2) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
    }

    for (auto &tlb_el : itlb1_mode3) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
    }

    for (auto &tlb_el : itlb2_mode1) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
    }

    for (auto &tlb_el : itlb2_mode2) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
    }

    for (auto &tlb_el : itlb2_mode3) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
    }

    // invalidate all DTLB entries
    for (auto &tlb_el : dtlb1_mode1) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
        tlb_el.host_va_offs_w = 0;
    }

    for (auto &tlb_el : dtlb1_mode2) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
        tlb_el.host_va_offs_w = 0;
    }

    for (auto &tlb_el : dtlb1_mode3) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
        tlb_el.host_va_offs_w = 0;
    }

    for (auto &tlb_el : dtlb2_mode1) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
        tlb_el.host_va_offs_w = 0;
    }

    for (auto &tlb_el : dtlb2_mode2) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
        tlb_el.host_va_offs_w = 0;
    }

    for (auto &tlb_el : dtlb2_mode3) {
        tlb_el.tag = TLB_INVALID_TAG;
        tlb_el.flags = 0;
        tlb_el.lru_bits = 0;
        tlb_el.host_va_offs_r = 0;
        tlb_el.host_va_offs_w = 0;
    }

    mmu_change_mode();

#ifdef MMU_PROFILING
    gProfilerObj->register_profile("PPC:MMU",
        std::unique_ptr<BaseProfile>(new MMUProfile()));
#endif

#ifdef TLB_PROFILING
    gProfilerObj->register_profile("PPC:MMU:TLB",
    std::unique_ptr<BaseProfile>(new TLBProfile()));
#endif
}
