/*
 * Copyright (c) 2021-2025 Rupert Carmichael
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

// Sega Master System II

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#include "cega.h"
#include "cega_mixer.h"
#include "cega_serial.h"
#include "cega_sms.h"
#include "cega_sms_io.h"
#include "cega_z80.h"

#include "eep93c46.h"
#include "smsvdp.h"
#include "sn76496.h"
#include "ym2413.h"

#define Z80ID 0

#define SIZE_STATE_SMS 60592
#define SIZE_STATE_GG 57603
#define SIZE_STATE_SMS_PRE061 60581
#define SIZE_STATE_GG_PRE061 57593
#define STATE_VERSION 0xCE6A0002

static uint8_t state[SIZE_STATE_SMS];

static uint8_t *biosdata = NULL; // BIOS Image
static size_t biossize = 0; // Size of the BIOS in bytes
static uint8_t biospages = 0; // Number of 16K BIOS pages

static uint8_t *romdata = NULL; // Game ROM
static size_t romsize = 0; // Size of the ROM in bytes
static unsigned rompages = 0; // Number of 16K ROM pages
static unsigned rompages8k = 0; // Number of 8K ROM pages

static int bios = 0; // BIOS Enable/Disable
static int fmaudio = 0; // FM Audio Enable/Disable
static int ggsmsmode = 0; // Game Gear in SMS Mode
static int mapper = 0; // Mapper currently in use
static int mset = 0; // Mapper Set or Unset

static unsigned frameready = 0;
static unsigned mcycs = 0;

static sms_sys_t smssys; // SMS System Context
static sn76496_t psg; // Sega PSG Context
static eep93c46_t eeprom; // 93C46 EEPROM Context (Game Gear baseball games)

static inline void cega_sms_set_fmctrl(void) {
    // The Japanese SMS allows the PSG to be disabled
    if (smssys.region == REGION_JP) {
        switch (smssys.fmctrl) {
            case 0: cega_mixer_chips_enable(1, 0); break;
            case 1: cega_mixer_chips_enable(0, 1); break;
            case 2: cega_mixer_chips_enable(0, 0); break;
            case 3: cega_mixer_chips_enable(1, 1); break;
        }
    }
    else { // PSG always on
        cega_mixer_chips_enable(1, smssys.fmctrl & 0x01);
    }
}

// Set the proper Media Slot based on what is in the Memory Control register
static inline void cega_sms_set_memctrl(void) {
/* Port 0x3e - Memory Control
   ===========================================================================
   |  Bit 7  |  Bit 6  |  Bit 5  |  Bit 4  |  Bit 3  |  Bit 2  |   Bit 1/0   |
   ===========================================================================
   |   Exp   |   Cart  |   Card  | Exp RAM |   BIOS  |   I/O   |   Unknown   |
   ---------------------------------------------------------------------------
    All bits are active low. No known uses for Expansion RAM exist, and Bits
    1 and 0 do not presently have a known use. I/O is not implemented in this
    function.
*/
    // Complemented for easier understanding
    uint8_t data = ~smssys.port3e;

    /* If the Cartridge Slot and BIOS Slot are both enabled, the Cartridge Slot
       takes precedence.
    */
    if (data & 0x40) // Cartridge Enabled
        cega_sms_set_mapper(mapper);
    else if (data & 0x08) // BIOS Enabled
        cega_sms_set_mapper(SMS_MAPPER_BIOS);
    else if (data & 0x20) // Card Enabled
        cega_sms_set_mapper(SMS_MAPPER_EMPTY);
    else if (data & 0x80) // Expansion Enabled
        cega_sms_set_mapper(SMS_MAPPER_EMPTY);
}

// Read a Z80 IO Port
static uint8_t cega_sms_port_rd(uint8_t port) {
    if (port < 0x40) { // 0x00 - 0x3f
        // SMS1 returns the last byte of the instruction that read the port
        // SMS2 returns 0xff - this is significantly simpler, go with this
        return 0xff;
    }
    else if (port < 0x80) { // 0x40 - 0x80:
        if (port & 0x01) // Odd returns HCount
            return smsvdp_rd_hcount();
        else // Even returns VCount
            return smsvdp_rd_vcount();
    }
    else if (port < 0xc0) {
        if (port & 0x01) // Odd returns VDP status
            return smsvdp_rd_stat();
        else // Even returns VDP data
            return smsvdp_rd_data();
    }
    else if (port == 0xf2 && fmaudio) { // 0xf2 - FM Detection Port
        return smssys.fmctrl;
    }
    else { // 0xc0 - 0xff: IO ports (Controllers)
        // Even returns IO A/B register, Odd returns IO B/misc register
        port &= 0x01;
        smssys.ctrl[port] = cega_sms_io_rd(port);

        if (port) {
            /* Region Detection
               Region detection is done by using port 0x3f. A game will set one
               or more of the output lines (Port A or B, TH or TR) with a known
               value. The value is then read back via IO port and compared. This
               is done multiple times to avoid false positives. Japanese systems
               do not support this, so if the comparisons are successful, the
               game will determine that the system is Export (Non-Japanese)
               versus Domestic (Japanese).
            */
            if (smssys.region != REGION_JP) {
                // Put output level values from port3f into the return value
                if (!(smssys.port3f & 0x02)) { // TH Port A
                    smssys.ctrl[1] &= 0xbf;
                    smssys.ctrl[1] |= ((smssys.port3f & 0x20) << 1);
                }

                if (!(smssys.port3f & 0x08)) { // TH Port B
                    smssys.ctrl[1] &= 0x7f;
                    smssys.ctrl[1] |= (smssys.port3f & 0x80);
                }
            }

            // Put output level value from port3f into the return value for TR
            if (!(smssys.port3f & 0x04)) { // TR Port B
                smssys.ctrl[1] &= 0xf7;
                smssys.ctrl[1] |= ((smssys.port3f & 0x40) >> 3);
            }
        }
        else {
            // Put output level value from port3f into the return value for TR
            if (!(smssys.port3f & 0x01)) { // TR Port A
                smssys.ctrl[0] &= 0xdf;
                smssys.ctrl[0] |= ((smssys.port3f & 0x10) << 1);
            }
        }

        return smssys.ctrl[port];
    }

    return 0xff; // Silence any potential compiler warnings
}

// Write to a Z80 IO Port
static void cega_sms_port_wr(uint8_t port, uint8_t data) {
    if (port < 0x40) { // 0x00 - 0x3f: Memory and IO Control
        /* Port 0x3f - IO Port Control
           ==========================================================
           | Output Levels (Bits 7-4)  || Pin Directions (Bits 3-0) |
           ==========================================================
           | TH B | TR B | TH A | TR A || TH B | TR B | TH A | TR A |
           ----------------------------------------------------------
           Output Levels: 1 = High, 0 = Low
           Pin Directions: 1 = Input, 0 = Output
        */
        if (port & 0x01) { // Odd goes to IO Control
            /* If the TH direction is set to input and the level for either port
               A or B will go from 0 to 1 as a result of this write, the VDP's
               HCount value must be latched. The TH direction corresponds to
               bits 1 and 3 for ports A and B respectively, while the TH levels
               correspond to bit 5 for port A, and bit 7 for port B.
            */
            uint8_t thchg_01_a =
                (data & 0x02) && (data & 0x20) && !(smssys.port3f & 0x20);
            uint8_t thchg_01_b =
                (data & 0x08) && (data & 0x80) && !(smssys.port3f & 0x80);

            if (thchg_01_a || thchg_01_b)
                smsvdp_wr_hcount();

            // Write the data to the register
            smssys.port3f = data;
        }
        else { // Even goes to Memory Control
            smssys.port3e = data;

            if (bios)
                cega_sms_set_memctrl();
        }
    }
    else if (port < 0x80) { // 0x40 - 0x7f: PSG Writes
        sn76496_wr(&psg, data); // All writes go to the PSG
    }
    else if (port < 0xc0) { // 0x80 - 0xbf: VDP Data and Control ports
        if (port & 0x01) // Odd goes to control port
            smsvdp_wr_ctrl(data);
        else // Even goes to data port
            smsvdp_wr_data(data);
    }
    else if (port == 0xf0) { // YM2413 Data port (0, select register latch)
        ym2413_wr(0, data);
    }
    else if (port == 0xf1) { // YM2413 Data port (1, select register data)
        ym2413_wr(1, data);
    }
    else if (port == 0xf2 && fmaudio) { // PSG/FM Control port
        smssys.fmctrl = data & 0x03;
        cega_sms_set_fmctrl();
    }
}

/* Sega Mapper
 * Fixed:  0x0000 - 0x03ff
 * Slot 0: 0x0400 - 0x3fff, Selectable by writes to 0xfffd (Last 15K)
 * Slot 1: 0x4000 - 0x7fff, Selectable by writes to 0xfffe
 * Slot 2: 0x8000 - 0xbfff, Selectable by writes to 0xffff
 * Note: The first 0x400 bytes are never overwritten by slot changes, in order
 *       to preserve interrupt vectors. When a bank is swapped into slot 0,
 *       this portion remains the same and the remaining 15K will be filled by
 *       the last 15K of the bank that was swapped in.
 *
 * Mapper Register - Writeable at 0xfffc
 * =======================================
 * | Bit | Description                   |
 * =======================================
 * |  7  | ROM Write Enable              |
 * |  6  | Unused                        |
 * |  5  | Unused                        |
 * |  4  | RAM Enable (0xc000 - 0xffff)  |
 * |  3  | RAM Enable (0x8000 - 0xbfff)  |
 * |  2  | RAM Bank Select               |
 * | 1-0 | ROM Bank Shift                |
 * |-------------------------------------|
 * Note: No known software uses either the Bank Shift feature, the ability to
 *       swap RAM into the System RAM slot at 0xc000 - 0xffff, or ROM Write.
 *
 */
static uint8_t cega_sms_mem_rd(uint16_t addr) {
    if (addr < 0x0400) { // Unpaged to preserve interrupt vectors
        return romdata[addr];
    }
    else if (addr < 0x4000) { // ROM Slot 0
        return romdata[(smssys.romslot[0] * SIZE_16K) + (addr & 0x3fff)];
    }
    else if (addr < 0x8000) { // ROM Slot 1
        return romdata[(smssys.romslot[1] * SIZE_16K) + (addr & 0x3fff)];
    }
    else if (addr < 0xc000) { // ROM Slot 2
        // Check if RAM is enabled
        if (smssys.mreg[0] & 0x08) {
            // Check which RAM bank to use
            if (smssys.mreg[0] & 0x04)
                return smssys.cartram[0x4000 + (addr & 0x3fff)];
            else
                return smssys.cartram[addr & 0x3fff];
        }

        // If RAM is not enabled, return ROM data from the bank in Slot 2
        return romdata[(smssys.romslot[2] * SIZE_16K) + (addr & 0x3fff)];
    }
    else if (addr >= 0xc000) { // System RAM
        return smssys.ram[addr & 0x1fff];
    }

    return 0xff;
}

static void cega_sms_mem_wr(uint16_t addr, uint8_t data) {
    // Registers/Slot Control
    if (addr == 0xfffc)
        smssys.mreg[0] = data;
    else if (addr == 0xfffd)
        smssys.romslot[0] = data % rompages;
    else if (addr == 0xfffe)
        smssys.romslot[1] = data % rompages;
    else if (addr == 0xffff)
        smssys.romslot[2] = data % rompages;

    // Handle any Cartridge RAM that has been enabled
    if (smssys.mreg[0] & 0x08) {
        smssys.cartram_enabled = 1; // Enable Cartridge RAM (For SRAM saving)

        // Write to the correct bank of Cartridge RAM
        if (addr > 0x7fff && addr < 0xc000) {
            if (smssys.mreg[0] & 0x04)
                smssys.cartram[0x4000 + (addr & 0x3fff)] = data;
            else
                smssys.cartram[addr & 0x3fff] = data;
        }
    }

    // Write to System RAM
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* BIOS Mapper
 * Fixed:  0x0000 - 0x03ff
 * Slot 0: 0x0400 - 0x3fff, Selectable by writes to 0xfffd (Last 15K)
 * Slot 1: 0x4000 - 0x7fff, Selectable by writes to 0xfffe
 * Slot 2: 0x8000 - 0xbfff, Selectable by writes to 0xffff
 * Note: This is the same as the Sega Mapper, but without SRAM support.
 */
static uint8_t cega_sms_mem_rd_bios(uint16_t addr) {
    if (addr < 0x0400) // Unpaged to preserve interrupt vectors
        return biosdata[addr];
    else if (addr < 0x4000) // BIOS Slot 0
        return biosdata[(smssys.romslot[0] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0x8000) // BIOS Slot 1
        return biosdata[(smssys.romslot[1] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0xc000) // BIOS Slot 2
        return biosdata[(smssys.romslot[2] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_bios(uint16_t addr, uint8_t data) {
    // Registers/Slot Control
    if (addr == 0xfffd)
        smssys.romslot[0] = data % biospages;
    else if (addr == 0xfffe)
        smssys.romslot[1] = data % biospages;
    else if (addr == 0xffff)
        smssys.romslot[2] = data % biospages;

    // Write to System RAM
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* Game Gear BIOS Mapper
 * Fixed: 0x0000 - 0x03ff - BIOS
 * Fixed: 0x0400 - 0xbfff - ROM
 */
static uint8_t cega_sms_mem_rd_bios_gg(uint16_t addr) {
    if (addr < 0x0400) // Unpaged to preserve interrupt vectors
        return biosdata[addr];
    else if (addr < 0xc000)
        return romdata[addr];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

/* Empty Slot
 * Read and Write RAM contents only
 */
static uint8_t cega_sms_mem_rd_empty(uint16_t addr) {
    if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_empty(uint16_t addr, uint8_t data) {
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* Codemasters Mapper
 * Slot 0: 0x0000 - 0x3fff, Selectable by writes to 0x0000
 * Slot 1: 0x4000 - 0x7fff, Selectable by writes to 0x4000
 * Slot 2: 0x8000 - 0xbfff, Selectable by writes to 0x8000
 * Note: Ernie Els Golf (Game Gear) uses on-cartridge RAM, mapped from
 *  0xa000 - 0xc000 if data written to 0x4000 has bit 7 set (0x80)
 */
static uint8_t cega_sms_mem_rd_cm(uint16_t addr) {
    if (addr < 0x4000) // ROM Slot 0
        return romdata[(smssys.romslot[0] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0x8000) // ROM Slot 1
        return romdata[(smssys.romslot[1] * SIZE_16K) + (addr & 0x3fff)];
    else if (smssys.mreg[0] && ((addr > 0x9fff) && (addr < 0xc000)))
        return smssys.cartram[addr & 0x1fff];
    else if (addr < 0xc000) // ROM Slot 2
        return romdata[(smssys.romslot[2] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_cm(uint16_t addr, uint8_t data) {
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
    else if (smssys.mreg[0] && ((addr > 0x9fff) && (addr < 0xc000)))
        smssys.cartram[addr & 0x1fff] = data;
    else if (addr == 0x0000)
        smssys.romslot[0] = data % rompages;
    else if (addr == 0x4000) {
        smssys.romslot[1] = data % rompages;
        smssys.mreg[0] = data & 0x80;
    }
    else if (addr == 0x8000)
        smssys.romslot[2] = data % rompages;
}

/* Janggun Mapper
 * Fixed:  0x0000 - 0x3fff
 * Slot 0: 0x4000 - 0x5fff, Selectable by writes to 0x4000
 * Slot 1: 0x6000 - 0x7fff, Selectable by writes to 0x6000
 * Slot 2: 0x8000 - 0x9fff, Selectable by writes to 0x8000
 * Slot 3: 0xa000 - 0xbfff, Selectable by writes to 0xa000
 * Note:
 *  Writes pass a 6-bit value (mask: 0x3f) representing the bank to be selected.
 *  Slots 0 and 1, or slots 2 and 3 can be selected as 16K banks by writes to
 *  0xfffe and 0xffff respectively. When writes to these addresses occur, flags
 *  are set based on the value of the 6th bit (0x40) of the data being written.
 *  If the 6th bit is set to 1, the data read from these banks will be reversed.
 *  Additionally, writes to these addresses will be written to system RAM.
 */
static uint8_t cega_sms_mem_rd_janggun(uint16_t addr) {
    // Return value may need to be reversed, so cannot be returned immediately
    uint8_t temp;

    if (addr < 0x4000) // Fixed 16K Bank
        temp = romdata[addr & 0x3fff];
    else if (addr < 0x6000) // First Selectable 8K or 16K bank
        temp = romdata[(smssys.romslot[0] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x8000) // Second Selectable 8K bank, segment 2 of 16K bank
        temp = romdata[(smssys.romslot[1] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xa000) // Third Selectable 8K bank or Second 16K bank
        temp = romdata[(smssys.romslot[2] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xc000) // Fourth Selectable 8K bank, segment 2 of 16K bank
        temp = romdata[(smssys.romslot[3] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr >= 0xc000) // System RAM
        temp = smssys.ram[addr & 0x1fff];

    uint8_t page = addr >> 14; // 16K Page, value includes fixed 16K bank

    // Reverse the bits in the return value if the bit reverse flag is set
    if (smssys.mreg[0] & (1 << page)) {
        temp = (temp & 0xf0) >> 4 | (temp & 0x0f) << 4;
        temp = (temp & 0xcc) >> 2 | (temp & 0x33) << 2;
        temp = (temp & 0xaa) >> 1 | (temp & 0x55) << 1;
    }

    return temp;
}

static void cega_sms_mem_wr_janggun(uint16_t addr, uint8_t data) {
    if (addr == 0x4000) { // 0x4000 - 0x5fff
        smssys.romslot[0] = data & 0x3f;
        return;
    }
    else if (addr == 0x6000) { // 0x6000 - 0x7fff
        smssys.romslot[1] = data & 0x3f; // 3
        return;
    }
    else if (addr == 0x8000) { // 0x8000 - 0x9fff
        smssys.romslot[2] = data & 0x3f; // 0
        return;
    }
    else if (addr == 0xa000) { // 0xa000 - 0xbfff
        smssys.romslot[3] = data & 0x3f; // 1
        return;
    }
    else if (addr == 0xfffe) { // 0x4000 - 0x7fff (16K page)
        smssys.romslot[0] = (data & 0x3f) << 1;
        smssys.romslot[1] = ((data & 0x3f) + 1) << 1;

        // Set reverse flags for banks 2 and 3 (0th and 1st selectable banks)
        if (data & 0x40)
            smssys.mreg[0] |= 0x02;
        else
            smssys.mreg[0] &= ~0x02;
    }
    else if (addr == 0xffff) { // 0x8000 - 0xbfff (16K page)
        smssys.romslot[2] = (data & 0x3f) << 1;
        smssys.romslot[3] = ((data & 0x3f) + 1) << 1;

        // Set reverse flags for banks 4 and 5 (2nd and 3rd selectable banks)
        if (data & 0x40)
            smssys.mreg[0] |= 0x04;
        else
            smssys.mreg[0] &= ~0x04;
    }

    // System RAM
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}


/* Korea Mapper
 * Slot 0: 0x0000 - 0x3fff, Fixed
 * Slot 1: 0x4000 - 0x7fff, Fixed
 * Slot 2: 0x8000 - 0xbfff, Selectable by writes to 0xa000
*/
static uint8_t cega_sms_mem_rd_korea(uint16_t addr) {
    if (addr < 0x8000) // ROM Slots 0 and 1, Fixed
        return romdata[addr & 0x7fff];
    else if (addr < 0xc000) // ROM Slot 2
        return romdata[(smssys.romslot[2] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_korea(uint16_t addr, uint8_t data) {
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
    else if (addr == 0xa000)
        smssys.romslot[2] = data % rompages;
}

/* MSX Mapper
 * Fixed:  0x0000 - 0x3fff
 * Slot 0: 0x4000 - 0x5fff, Selectable by writes to 0x0002
 * Slot 1: 0x6000 - 0x7fff, Selectable by writes to 0x0003
 * Slot 2: 0x8000 - 0x9fff, Selectable by writes to 0x0000
 * Slot 3: 0xa000 - 0xbfff, Selectable by writes to 0x0001
 * Note: MSX Mapper uses 8K banks
*/
static uint8_t cega_sms_mem_rd_msx(uint16_t addr) {
    if (addr <= 0x3fff)
        return romdata[addr];
    else if (addr < 0x6000)
        return romdata[(smssys.romslot[0] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x8000)
        return romdata[(smssys.romslot[1] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xa000)
        return romdata[(smssys.romslot[2] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xc000)
        return romdata[(smssys.romslot[3] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_msx(uint16_t addr, uint8_t data) {
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
    else if (addr == 0x0000)
        smssys.romslot[2] = data % rompages8k;
    else if (addr == 0x0001)
        smssys.romslot[3] = data % rompages8k;
    else if (addr == 0x0002)
        smssys.romslot[0] = data % rompages8k;
    else if (addr == 0x0003)
        smssys.romslot[1] = data % rompages8k;
}

/* Nemesis Mapper
 * Fixed: 0x0000-0x1fff, Fixed to the last 8K bank of ROM
 * Fixed: 0x2000-0x3fff, Fixed to the second 8K bank of ROM
 * Slot 0: 0x4000 - 0x5fff, Selectable by writes to 0x0002
 * Slot 1: 0x6000 - 0x7fff, Selectable by writes to 0x0003
 * Slot 2: 0x8000 - 0x9fff, Selectable by writes to 0x0000
 * Slot 3: 0xa000 - 0xbfff, Selectable by writes to 0x0001
 * Note: Strange variant of the MSX Mapper, also uses 8K banks
*/
static uint8_t cega_sms_mem_rd_nemesis(uint16_t addr) {
    if (addr < 0x2000)
        return romdata[((rompages8k - 1) * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x4000)
        return romdata[addr];
    else if (addr < 0x6000)
        return romdata[(smssys.romslot[0] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x8000)
        return romdata[(smssys.romslot[1] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xa000)
        return romdata[(smssys.romslot[2] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xc000)
        return romdata[(smssys.romslot[3] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}


/* No Mapper
 * Read ROM data at the address specified
 */
static uint8_t cega_sms_mem_rd_none(uint16_t addr) {
    if (addr < romsize)
        return romdata[addr];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_none(uint16_t addr, uint8_t data) {
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* 4 PAK All Action Mapper
 * Slot 0: 0x0000 - 0x3fff, Selectable by writes to 0x3ffe
 * Slot 1: 0x4000 - 0x7fff, Selectable by writes to 0x7fff
 * Slot 2: 0x8000 - 0xbfff, Selectable by writes to 0xbfff
 * Note: For Slot 2, the bank will be selected based on the data written added
 *  to bits 4 and 5 (0x30) of the slot selected in Slot 0.
 */
static uint8_t cega_sms_mem_rd_4paa(uint16_t addr) {
    if (addr < 0x4000) // ROM Slot 0
        return romdata[(smssys.romslot[0] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0x8000) // ROM Slot 1
        return romdata[(smssys.romslot[1] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0xc000) // ROM Slot 2
        return romdata[(smssys.romslot[2] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_4paa(uint16_t addr, uint8_t data) {
    if (addr == 0x3ffe)
        smssys.romslot[0] = data;
    else if (addr == 0x7fff)
        smssys.romslot[1] = data;
    else if (addr == 0xbfff)
        smssys.romslot[2] = (data + (smssys.romslot[0] & 0x30));

    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* X in 1 Mapper
 * Slot 0: 0x0000 - 0x7fff, Selectable by writes to 0xffff
 */
static uint8_t cega_sms_mem_rd_xin1(uint16_t addr) {
    if (addr < 0x8000) // ROM Slot 1
        return romdata[(smssys.romslot[0] * SIZE_32K) + (addr & 0x7fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_xin1(uint16_t addr, uint8_t data) {
    if (addr == 0xffff)
        smssys.romslot[0] = data % (rompages >> 1); // 32K Pages

    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* 93C46 EEPROM Mapper
 * Fixed:  0x0000 - 0x03ff
 * Slot 0: 0x0400 - 0x3fff, Selectable by writes to 0xfffd (Last 15K)
 * Slot 1: 0x4000 - 0x7fff, Selectable by writes to 0xfffe
 * Slot 2: 0x8000 - 0xbfff, Selectable by writes to 0xffff
 * Note: Writing 1 to Mapper Register Bit 7 (0x80) reinitialize the EEPROM,
 *       while writes to Mapper Register Bit 3 (0x08) determine whether reads
 *       and writes at 0x8000 are used to access the EEPROM. Reads return the
 *       serial data on pin 0 (DO), while pins 1 and 2 (CLK and CS) return
 *       clock and chip select status. Writes are similar, but pin 0 is DI in
 *       this case, and bits are written serially. Other than the EEPROM
 *       details, this is essentially the Sega Mapper.
 */
static uint8_t cega_sms_mem_rd_93c46(uint16_t addr) {
    if (addr < 0x0400) { // Unpaged to preserve interrupt vectors
        return romdata[addr];
    }
    else if (addr < 0x4000) { // ROM Slot 0
        return romdata[(smssys.romslot[0] * SIZE_16K) + (addr & 0x3fff)];
    }
    else if (addr < 0x8000) { // ROM Slot 1
        return romdata[(smssys.romslot[1] * SIZE_16K) + (addr & 0x3fff)];
    }
    else if (addr < 0xc000) { // ROM Slot 2
        if (smssys.mreg[0] & 0x08 && addr == 0x8000) // EEPROM Reads
            return eep93c46_rd(&eeprom);

        // If RAM is not enabled, return ROM data from the bank in Slot 2
        return romdata[(smssys.romslot[2] * SIZE_16K) + (addr & 0x3fff)];
    }
    else if (addr >= 0xc000) { // System RAM
        return smssys.ram[addr & 0x1fff];
    }

    return 0xff;
}

static void cega_sms_mem_wr_93c46(uint16_t addr, uint8_t data) {
    // Registers/Slot Control
    if (addr == 0xfffc) {
        smssys.mreg[0] = data;

        if (smssys.mreg[0] & 0x80) // Reset EEPROM state
            eep93c46_init(&eeprom);
    }
    else if (addr == 0xfffd) {
        smssys.romslot[0] = data % rompages;
    }
    else if (addr == 0xfffe) {
        smssys.romslot[1] = data % rompages;
    }
    else if (addr == 0xffff) {
        smssys.romslot[2] = data % rompages;
    }

    if (smssys.mreg[0] & 0x08 && addr == 0x8000) { // EEPROM Writes
        smssys.cartram_enabled = 1; // Use Cartridge RAM for EEPROM saves
        eep93c46_wr(&eeprom, smssys.cartram, data);
    }

    // Write to System RAM
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* Korean 2000 XOR 1F Mapper
 * Fixed:  0x0000 - 0x3fff
 * Slot 0: 0x4000 - 0x5fff - Selectable 8K Bank
 * Slot 1: 0x6000 - 0x7fff - Selectable 8K Bank
 * Slot 2: 0x8000 - 0x9fff - Selectable 8K Bank
 * Slot 3: 0xa000 - 0xbfff - Selectable 8K Bank
 * Note: This mapper code is experimental at the time this note was written.
 *       There is currently only one known dump which uses this mapper, and
 *       more details may arise at a later date. Information from MEKA.
 *       There are 4 selectable 8K banks, selected by writes where the 13th
 *       address bit is set, and the 14th is not. The data written is then
 *       XOR'ed against 0x1f...0x1c to select the 4 8K banks. In cases where
 *       bank change writes are done, nothing is written to System RAM.
 */
static uint8_t cega_sms_mem_rd_kr20001f(uint16_t addr) {
    if (addr < 0x4000) // Fixed
        return romdata[addr];
    else if (addr < 0x6000) // ROM Slot 0
        return romdata[(smssys.romslot[0] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x8000) // ROM Slot 1
        return romdata[(smssys.romslot[1] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xa000) // ROM Slot 2
        return romdata[(smssys.romslot[2] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xc000) // ROM Slot 3
        return romdata[(smssys.romslot[3] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_kr20001f(uint16_t addr, uint8_t data) {
    if ((addr & 0x6000) == 0x2000) {
        smssys.romslot[0] = data ^ 0x1f;
        smssys.romslot[1] = data ^ 0x1e;
        smssys.romslot[2] = data ^ 0x1d;
        smssys.romslot[3] = data ^ 0x1c;
        return;
    }

    // Write to System RAM
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* Korean MSX 8KB 0300 Mapper
 * Fixed:  0x0000 - 0x1fff
 * Slot 0: 0x2000 - 0x3fff - Selectable 8K Bank
 * Slot 1: 0x4000 - 0x5fff - Selectable 8K Bank
 * Slot 2: 0x6000 - 0x7fff - Selectable 8K Bank
 * Slot 3: 0x8000 - 0x9fff - Selectable 8K Bank
 * Mirror: 0xa000 - 0xbfff - Mirror of Slot 0
 * Note: This mapper has a fixed 8K bank with 4 selectable 8K banks followed by
 *       a mirror of the first bank (Slot 0). This information comes from MEKA.
 *       Slot 0 Control: Writes from 0x0000 to 0x00ff
 *       Slot 1 Control: Writes from 0x0100 to 0x01ff
 *       Slot 2 Control: Writes from 0x0200 to 0x02ff
 *       Slot 3 Control: Writes from 0x0300 to 0x03ff
 */
static uint8_t cega_sms_mem_rd_krmsx8k300(uint16_t addr) {
    if (addr < 0x2000) // Fixed
        return romdata[addr];
    else if (addr < 0x4000) // ROM Slot 0
        return romdata[(smssys.romslot[0] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x6000) // ROM Slot 1
        return romdata[(smssys.romslot[1] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x8000) // ROM Slot 2
        return romdata[(smssys.romslot[2] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xa000) // ROM Slot 3
        return romdata[(smssys.romslot[3] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xc000) // ROM Slot 0 (Mirror)
        return romdata[(smssys.romslot[0] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_krmsx8k300(uint16_t addr, uint8_t data) {
    switch (addr & 0xff00) {
        case 0x0000: {
            smssys.romslot[3] = data % rompages8k;
            return;
        }
        case 0x0100: {
            smssys.romslot[1] = data % rompages8k;
            return;
        }
        case 0x0200: {
            smssys.romslot[0] = data % rompages8k;
            return;
        }
        case 0x0300: {
            smssys.romslot[2] = data % rompages8k;
            return;
        }
    }

    // Write to System RAM
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* Korean BFFC and FFFE Mappers
 * Slot 0: 0x0000 - 0x3fff Selectable 16K Bank
 * Slot 1: 0x4000 - 0x7fff Selectable 16K Bank
 * Slot 2: 0x8000 - 0x9fff Selectable 8K Bank
 * Slot 3: 0xa000 - 0xbfff Selectable 8K Bank
 * Note: This mapper has 2 16K selectable banks followed by 2 8K selectable
 *       banks. For the BFFC Mapper, writes to 0xbffc change the slot addresses
 *       and in most cases the 8K slots receive an Open Bus value (not
 *       emulated). Only the Namco games actually use the 8K slots. For the
 *       FFFE mapper, writes to 0xfffe change the slot addresses. All
 *       information from MEKA.
 */
static uint8_t cega_sms_mem_rd_krbffc_fffe(uint16_t addr) {
    if (addr < 0x4000) // ROM Slot 0
        return romdata[(smssys.romslot[0] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0x8000) // ROM Slot 1
        return romdata[(smssys.romslot[1] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0xa000) // ROM Slot 2
        return romdata[(smssys.romslot[2] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xc000) // ROM Slot 3
        return romdata[(smssys.romslot[3] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_krbffc(uint16_t addr, uint8_t data) {
    if (addr == 0xbffc) {
        uint8_t upper = 0x00;
        uint8_t lower = 0x00;

        switch (data & 0xc0) {
            case 0x00: { // 32K SMS/SG Game
                lower = data & 0x3e;
                upper = (data & 0x3e) | 1;
                smssys.romslot[0] = lower % rompages;
                smssys.romslot[1] = upper % rompages;
                smssys.romslot[2] = smssys.romslot[3] = // Open Bus
                    ((0x3f % rompages) << 1) + 1;
                break;
            }
            case 0x40: { // 16K SMS/SG Game
                lower = upper = data & 0x3f;
                smssys.romslot[0] = lower % rompages;
                smssys.romslot[1] = upper % rompages;
                smssys.romslot[2] = smssys.romslot[3] = // Open Bus
                    ((0x3f % rompages) << 1) + 1;
                break;
            }
            case 0x80: { // MSX Regular (16K BIOS + 16K ROM)
                lower = 0x20;
                upper = data & 0x3f;
                smssys.romslot[0] = lower % rompages;
                smssys.romslot[1] = upper % rompages;
                smssys.romslot[2] = smssys.romslot[3] = // Open Bus
                    ((0x3f % rompages) << 1) + 1;
                break;
            }
            case 0xc0: { // Namco (16K BIOS, 2x8K ROM mirrored - ABBA order)
                lower = 0x20;
                upper = data & 0x3f;
                smssys.romslot[0] = lower % rompages;
                smssys.romslot[1] = upper % rompages;
                smssys.romslot[2] = ((upper % rompages) << 1) + 1;
                smssys.romslot[3] = (upper % rompages) << 1;
                break;
            }
        }

        return;
    }

    // Write to System RAM
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

static void cega_sms_mem_wr_krfffe(uint16_t addr, uint8_t data) {
    if (addr == 0xfffe) {
        if ((data & 0x40) == 0x40) {
            smssys.romslot[0] = (data & 0x1e) % rompages;
            smssys.romslot[1] = ((data & 0x1e) % rompages) + 1;
        }
        else {
            smssys.romslot[0] = 0;
            smssys.romslot[1] = (data & 0x1f) % rompages;
        }

        if ((data & 0x60) == 0x20) {
            smssys.romslot[2] = (((data & 0x1f) % rompages) << 1) + 1;
            smssys.romslot[3] = ((data & 0x1f) % rompages) << 1;
        }
        else {
            smssys.romslot[2] = 0x3f;
            smssys.romslot[3] = 0x3f;
        }

        return;
    }

    // Write to System RAM
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* Korean FFF5 Mapper
 * Slot 0: 0x0000 - 0x3fff Selectable 16K Bank
 * Slot 1: 0x4000 - 0x7fff Selectable 16K Bank
 * Slot 2: 0x8000 - 0xbfff Selectable 16K Bank
 * Note: This is a simple mapper with 3 selectable 16K banks. Writes to 0xfff5
 *       set all 3 slots, with slot 2 being a mirror of slot 1, as well as
 *       setting the value of a mapper register. Writes to 0xfffe set slot 1
 *       using the value of the mapper register plus the newly written data,
 *       with a mask applied which depends on the value of the mapper register.
 *       Writes to 0xffff have the same behaviour as 0xfffe, but slot 2 is set
 *       rather than slot 1. All addresses are ORed against 0x4010 in this case
 *       because some games assume these bits to be set when accessing the
 *       registers being written to. Data being written to the mapper register
 *       corresponds to 32K banks, but since we are mapping 16K banks, it must
 *       be multiplied by 2. The page mask behaviour is not fully implemented,
 *       but functions correctly for all known dumps at the time of writing.
 *       All information from MEKA.
 */
static uint8_t cega_sms_mem_rd_krfff5(uint16_t addr) {
    if (addr < 0x4000) // ROM Slot 0
        return romdata[(smssys.romslot[0] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0x8000) // ROM Slot 1
        return romdata[(smssys.romslot[1] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0xc000) // ROM Slot 2
        return romdata[(smssys.romslot[2] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_krfff5(uint16_t addr, uint8_t data) {
    // 16K Page Mask
    uint8_t pmask = smssys.mreg[0] >= 0x10 ? 0x1f : 0x0f;

    switch (addr | 0x4010) {
        case 0xfff5: {
            smssys.mreg[0] = data;
            smssys.romslot[0] = (data << 1) % rompages;
            smssys.romslot[1] = ((data << 1) % rompages) + 1;
            smssys.romslot[2] = ((data << 1) % rompages) + 1;
            break;
        }
        case 0xfffe: {
            smssys.romslot[1] =
                ((smssys.mreg[0] << 1) % rompages) + (data & pmask);
            break;
        }
        case 0xffff: {
            smssys.romslot[2] =
                ((smssys.mreg[0] << 1) % rompages) + (data & pmask);
            break;
        }
    }

    // Write to System RAM
    if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* Korean MSX 32KB 2000 Mapper
 * Fixed:  0x0000 - 0x3fff
 * Slot 0: 0x4000 - 0xbfff - Selectable 32K Bank
 * Note: This mapper has a fixed 16K bank and a selectable 32K bank. Writes to
 *       0x2000 select the bank. Information from MEKA.
 */
static uint8_t cega_sms_mem_rd_krmsx32k2000(uint16_t addr) {
    if (addr < 0x4000) // Fixed
        return romdata[addr];
    else if (addr < 0xc000) // ROM Slot 0 (32K)
        return romdata[(smssys.romslot[0] * SIZE_32K) + addr];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];
    return 0xff;
}

static void cega_sms_mem_wr_krmsx32k2000(uint16_t addr, uint8_t data) {
    if (addr == 0x2000)
        smssys.romslot[0] = data;
    else if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

/* Korean 0000 XOR FF Mapper
 * Fixed:  0x0000 - 0x3fff
 * Slot 0: 0x4000 - 0x7fff - Selectable 16K Bank
 * Slot 1: 0x8000 - 0xbfff - Selectable 16K Bank
 * Note: Selectable banks are chosen via writes to 0x0000. If the data written
 *       has all 4 top bits set, slots 0 and 1 are mapped to the same 16K bank,
 *       which follows immediately after the Fixed bank. Otherwise, the top 4
 *       bits are inverted and shifted right 3 positions. Only the original top
 *       4 bits are used, meaning a mask of 0x1e is applied to determine how
 *       slots 0 and 1 will be mapped -- these are mapped in a contiguous 32K
 *       block. Information from MEKA.
 */
static uint8_t cega_sms_mem_rd_kr0000ff(uint16_t addr) {
    if (addr < 0x4000) // Fixed
        return romdata[addr];
    else if (addr < 0x8000) // ROM Slot 0 (16K)
        return romdata[(smssys.romslot[0] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0xc000) // ROM Slot 1 (16K)
        return romdata[(smssys.romslot[1] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];
    return 0xff;
}

static void cega_sms_mem_wr_kr0000ff(uint16_t addr, uint8_t data) {
    if (addr == 0x0000) {
        if ((data & 0xf0) == 0xf0) {
            smssys.romslot[0] = smssys.romslot[1] = 1;
        }
        else {
            unsigned mapoffs = ((data >> 3) ^ 0x1e) & 0x1e;
            smssys.romslot[0] = mapoffs;
            smssys.romslot[1] = mapoffs + 1;
        }
    }
    else if (addr >= 0xc000) {
        smssys.ram[addr & 0x1fff] = data;
    }
}

/* Korean MSX/SMS 8000 Mapper
 * Slot 0: 0x0000 - 0x1fff - Selectable 8K Bank
 * Slot 1: 0x2000 - 0x3fff - Selectable 8K Bank
 * Slot 2: 0x4000 - 0x7fff - Selectable 16K Bank
 * Slot 3: 0x8000 - 0xbfff - Selectable 16K Bank
 * Note: This mapper is quite an oddity, and must have the first two slots set
 *       to 0x3c at boot or reset. The first two slots are 8K, while the second
 *       two are 16K. Technically they should all be 8K, but since the values
 *       are always sequential for this address space, it is safe to map them
 *       as 16K blocks. Information from MEKA.
*/
static uint8_t cega_sms_mem_rd_krmsxsms8000(uint16_t addr) {
    if (addr < 0x2000) // ROM Slot 0 (8K)
        return romdata[(smssys.romslot[0] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x4000) // ROM Slot 1 (8K)
        return romdata[(smssys.romslot[1] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x8000) // ROM Slot 2 (16K)
        return romdata[(smssys.romslot[2] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr < 0xc000) // ROM Slot 2 (16K)
        return romdata[(smssys.romslot[3] * SIZE_16K) + (addr & 0x3fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];
    return 0xff;
}

static void cega_sms_mem_wr_krmsxsms8000(uint16_t addr, uint8_t data) {
    if (addr == 0x8000) {
        // Special case for Zemina Best 25
        if (smssys.mreg[0] == 0xff)
            data ^= 0x22;
        smssys.mreg[0] = data;

        if (data & 0x80) {
            smssys.romslot[0] = (data ^ 3) % rompages8k;
            smssys.romslot[1] = (data ^ 2) % rompages8k;
        }
        else {
            smssys.romslot[0] = 0x3c;
            smssys.romslot[1] = 0x3c;
        }

        smssys.romslot[2] = ((data ^ 1) >> 1) % rompages;
        smssys.romslot[3] = ((data ^ 3) >> 1) % rompages;
    }
    else if (addr >= 0xc000) {
        smssys.ram[addr & 0x1fff] = data;
    }
}

/* Korean FFF3/FFFC Mapper
 * Slot 0: 0x0000 - 0x1fff - Selectable 8K Bank
 * Slot 1: 0x2000 - 0x3fff - Selectable 8K Bank
 * Slot 2: 0x4000 - 0x5fff - Selectable 8K Bank
 * Slot 3: 0x6000 - 0x7fff - Selectable 8K Bank
 * Slot 4: 0x8000 - 0x9fff - Selectable 8K Bank
 * Slot 5: 0xa000 - 0xbfff - Selectable 8K Bank
 * Note: Information about this mapper comes from MEKA. There are 6 selectable
 *       8K ROM banks, which are typically actually in contiguous blocks of 16K
 *       with the exception of the MSX 8K Permuted configuration.
*/

static uint8_t cega_sms_mem_rd_krfff3fffc(uint16_t addr) {
    if (addr < 0x2000) // ROM Slot 0
        return romdata[(smssys.romslot[0] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x4000) // ROM Slot 1
        return romdata[(smssys.romslot[1] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x6000) // ROM Slot 2
        return romdata[(smssys.romslot[2] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0x8000) // ROM Slot 3
        return romdata[(smssys.romslot[3] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xa000) // ROM Slot 4
        return romdata[(smssys.romslot[4] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr < 0xc000) // ROM Slot 5
        return romdata[(smssys.romslot[5] * SIZE_8K) + (addr & 0x1fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];

    return 0xff;
}

static void cega_sms_mem_wr_krfff3fffc(uint16_t addr, uint8_t data) {
    uint16_t addr_assumed = addr | 0x4000;
    if (addr_assumed == 0xfff3 || addr_assumed == 0xfffc) {
        if (addr_assumed == 0xfff3) { // Mapper
            smssys.mreg[0] = data;
        }
        else if (addr_assumed == 0xfffc) { // Mode
            smssys.mreg[1] = data;
        }

        int odd = smssys.mreg[0] & 0x01;
        int page0 = (smssys.mreg[1] & 0x10) * 8 + (smssys.mreg[0] & 0x3e) * 2;
        int page1 = page0 + 1;
        int page2 = page0 + 2;
        int page3 = page0 + 3;

        switch (smssys.mreg[1] & 0xe0) {
            case 0x00: { // 16K SMS
                smssys.romslot[0] = (odd ? page2 : page0) % rompages8k;
                smssys.romslot[1] = (odd ? page3 : page1) % rompages8k;
                smssys.romslot[2] = (odd ? page2 : page0) % rompages8k;
                smssys.romslot[3] = (odd ? page3 : page1) % rompages8k;
                smssys.romslot[4] = 0xff % rompages8k;
                smssys.romslot[5] = 0xff % rompages8k;
                break;
            }
            case 0x20: { // 32K SMS
                smssys.romslot[0] = page0 % rompages8k;
                smssys.romslot[1] = page1 % rompages8k;
                smssys.romslot[2] = page2 % rompages8k;
                smssys.romslot[3] = page3 % rompages8k;
                smssys.romslot[4] = 0xff % rompages8k;
                smssys.romslot[5] = 0xff % rompages8k;
                break;
            }
            case 0x40: { // MSX mapped at 0x4000
                smssys.romslot[0] = 0x80 % rompages8k;
                smssys.romslot[1] = 0x81 % rompages8k;
                smssys.romslot[2] = (odd ? page2 : page0) % rompages8k;
                smssys.romslot[3] = (odd ? page3 : page1) % rompages8k;
                smssys.romslot[4] = 0xff % rompages8k;
                smssys.romslot[5] = 0xff % rompages8k;
                break;
            }
            case 0x60: { // MSX mapped at 0x8000
                smssys.romslot[0] = 0x80 % rompages8k;
                smssys.romslot[1] = 0x81 % rompages8k;
                smssys.romslot[2] = 0xfe % rompages8k;
                smssys.romslot[3] = 0xff % rompages8k;
                smssys.romslot[4] = (odd ? page2 : page0) % rompages8k;
                smssys.romslot[5] = (odd ? page3 : page1) % rompages8k;
                break;
            }
            case 0x80: { // MSX 8K Permuted
                smssys.romslot[0] = 0x80 % rompages8k;
                smssys.romslot[1] = 0x81 % rompages8k;
                smssys.romslot[2] = (odd ? page2 : page0) % rompages8k;
                smssys.romslot[3] = (odd ? page3 : page1) % rompages8k;
                smssys.romslot[4] = (odd ? page3 : page1) % rompages8k;
                smssys.romslot[5] = (odd ? page2 : page0) % rompages8k;
                break;
            }
            case 0xa0: { // MSX 16K Permuted
                smssys.romslot[0] = 0x80 % rompages8k;
                smssys.romslot[1] = 0x81 % rompages8k;
                smssys.romslot[2] = (odd ? page2 : page0) % rompages8k;
                smssys.romslot[3] = (odd ? page3 : page1) % rompages8k;
                smssys.romslot[4] = (odd ? page0 : page2) % rompages8k;
                smssys.romslot[5] = (odd ? page1 : page3) % rompages8k;
                break;
            }
            default: {
                for (unsigned i = 0; i < 6; ++i)
                    smssys.romslot[i] = 0xff % rompages8k;
                break;
            }
        }
    }

    if (addr >= 0xc000) {
        smssys.ram[addr & 0x1fff] = data;
    }
}

/* Korean SMS 32KB 2000 Mapper
 * Slot 0: 0x0000 - 0x7fff - Selectable 32K Bank
 * Note: This mapper has a selectable 32K bank, selected by writes to 0x2000.
 *       Reads between 0x8000 and 0xbfff are mirrored to the first 16K of the
 *       selectable 32K bank. Information from MEKA.
 */
static uint8_t cega_sms_mem_rd_krsms32k2000(uint16_t addr) {
    if (addr < 0xc000) // ROM Slot 0 (32K)
        return romdata[(smssys.romslot[0] * SIZE_32K) + (addr & 0x7fff)];
    else if (addr >= 0xc000) // System RAM
        return smssys.ram[addr & 0x1fff];
    return 0xff;
}

static void cega_sms_mem_wr_krsms32k2000(uint16_t addr, uint8_t data) {
    if (addr == 0x2000)
        smssys.romslot[0] = data;
    else if (addr >= 0xc000)
        smssys.ram[addr & 0x1fff] = data;
}

// Load a BIOS into memory
static int cega_sms_bios_load(void *data, size_t size) {
    biosdata = (uint8_t*)data; // Assign internal ROM pointer
    biossize = size; // Record the true size of the ROM data in bytes
    biospages = (biossize / SIZE_16K) + (biossize % SIZE_16K ? 1 : 0);
    cega_sms_set_mapper(SMS_MAPPER_BIOS);
    return 1;
}

int cega_sms_bios_load_file(const char *biospath) {
    FILE *file;

    if (!(file = fopen(biospath, "rb")))
        return 0;

    // Find out the file's size
    fseek(file, 0, SEEK_END);
    biossize = ftell(file);
    fseek(file, 0, SEEK_SET);

    // Allocate memory for the BIOS
    biosdata = (uint8_t*)calloc(biossize, sizeof(uint8_t));

    if (!fread(biosdata, biossize, 1, file)) {
        fclose(file);
        return 0;
    }

    fclose(file);

    biospages = (biossize / SIZE_16K) + (biossize % SIZE_16K ? 1 : 0);
    cega_sms_set_mapper(SMS_MAPPER_BIOS);
    return 1;
}

// Load a ROM into memory
static int cega_sms_rom_load(void *data, size_t size) {
    romdata = (uint8_t*)data; // Assign internal ROM pointer
    romsize = size; // Record the true size of the ROM data in bytes

    // Check for padded ROM
    size_t padding = romsize % SIZE_8K;
    if (padding == 0x200) { // 512 byte padding
        romdata += padding;
        romsize -= padding;
    }

    // Find out how many pages of ROM data there are
    // Use modulus to discover if there is a page that is not quite full sized
    rompages = (romsize / SIZE_16K) + (romsize % SIZE_16K ? 1 : 0);
    rompages8k = (romsize / SIZE_8K) + (romsize % SIZE_8K ? 1 : 0);

    return 1;
}

// Initialize the SMS
void cega_sms_init(void) {
    /* The initial RAM state for the Sega Master System is difficult to truly
     * discern, but 0xf0 seems to give the best result. Below is a list of
     * games that are known to be sensitive to the RAM's startup value:
     * Alf (USA)
     * Desert Strike (Europe) (En,Fr,De,Es)
     * Populous (Europe, Brazil)
     * Sagak-ui Bimil (Korea) (Unl)
     * Super Boy II (Korea) (Unl)
     * Street Master (Korea) (Unl)
    */
    for (int i = 0; i < SIZE_SMSRAM; ++i)
        smssys.ram[i] = 0xf0;

    // Initialize SRAM/EEPROM to all 1s
    for (int i = 0; i < SIZE_32K; ++i)
        smssys.cartram[i] = 0xff;

    /* If a BIOS is enabled, Port 0x3e will immediately be written with all 0s.
       At the end of the boot sequence, the value will be 0xab.
    */
    smssys.port3e = 0xab;

    // Default value for Port 0x3f is all 1s
    smssys.port3f = 0xff;

    // Set Cart Registers
    for (unsigned i = 0; i < 4; ++i)
        smssys.mreg[0] = 0x00;

    // Set ROM Slots
    for (unsigned i = 0; i < 8; ++i)
        smssys.romslot[0] = i;

    // Initialize the Z80
    cega_z80_init(0);

    // Initialize the VDP
    smsvdp_init();

    // Initialize controller port handlers
    cega_sms_io_init();

    // Set default controller port states
    smssys.ctrl[0] = smssys.ctrl[1] = 0xff;

    // Set function pointer for frame execution
    cega_exec = &cega_sms_exec;

    // Set function pointers for I/O Reads/Writes
    cega_z80_port_rd[Z80ID] = &cega_sms_port_rd;
    cega_z80_port_wr[Z80ID] = &cega_sms_port_wr;

    // Default to the Sega Mapper for Memory Reads/Writes
    cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd;
    cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr;
    mapper = SMS_MAPPER_SEGA;

    // Set function pointers for ROM loading
    cega_bios_load = &cega_sms_bios_load;
    cega_rom_load = &cega_sms_rom_load;

    // Set function pointers for states
    cega_state_size = &cega_sms_state_size;
    cega_state_load = &cega_sms_state_load;
    cega_state_load_raw = &cega_sms_state_load_raw;
    cega_state_save = &cega_sms_state_save;
    cega_state_save_raw = &cega_sms_state_save_raw;

    // Set function pointers for SRAM
    cega_sram_load = &cega_sms_sram_load;
    cega_sram_save = &cega_sms_sram_save;

    // Initialize PSG
    sn76496_init(&psg, 0, NTAP, NSHIFT);
    cega_mixer_set_psg(&psg);
}

// Deinitialize any allocated memory
void cega_sms_deinit(void) {
    if (biosdata && (bios & 0x80))
        free(biosdata);
}

void cega_sms_reset(void) {
    /* If a BIOS is present, register writes to ports 0x3e and 0x3f are done.
       These values are set here manually to allow emulation without a BIOS.
    */
    smssys.port3e = 0xab;
    smssys.port3f = 0xff;

    // Set Cart Registers
    for (unsigned i = 0; i < 4; ++i)
        smssys.mreg[0] = 0x00;

    // Set ROM Slots
    for (unsigned i = 0; i < 8; ++i)
        smssys.romslot[i] = mapper == SMS_MAPPER_KR_FFF3_FFFC ? 0 : i;

    if (mapper == SMS_MAPPER_KR_MSX_SMS_8000)
        smssys.romslot[0] = smssys.romslot[1] = 0x3c;

    // Reset the Z80
    cega_z80_reset(0);

    // Reset the VDP
    smsvdp_init();
    smsvdp_set_region(smssys.region > REGION_US);

    if (cega_get_system() == SYSTEM_GG) {
        // Set defaults for Game Gear Registers
        smssys.ggreg[0] = 0xc0;
        smssys.ggreg[1] = 0x7f;
        smssys.ggreg[2] = 0xff;
        smssys.ggreg[3] = 0x00;
        smssys.ggreg[4] = 0xff;
        smssys.ggreg[5] = 0x00;
        smssys.port3e = 0xa8;
        sn76496_init(&psg, 1, NTAP, NSHIFT);
    }
    else {
        // Reinitialize PSG
        sn76496_init(&psg, 0, NTAP, NSHIFT);
    }
}

// Set BIOS Enable/Disable flag
void cega_sms_set_bios(int enable) {
    // Bits set: 0 = Enable, 1 = BIOS Only, 7 = BIOS loaded internally
    bios = enable;

    if (enable & 0x02) {
        mset = 1;
        mapper = SMS_MAPPER_EMPTY;
        biosdata = romdata;
        biossize = romsize;
        biospages = rompages;
    }
}

// Set FM Audio Enable/Disable flag
void cega_sms_set_fmaudio(int enable) {
    fmaudio = enable;
}

// Set the Region for the SMS
void cega_sms_set_region(int region) {
    smssys.region = region;
}

// Set the Memory Mapper
void cega_sms_set_mapper(int m) {
    switch (m) {
        case SMS_MAPPER_BIOS: { // BIOS Mapper
            cega_z80_mem_rd[Z80ID] =
                (ggsmsmode || cega_get_system() == SYSTEM_GG) ?
                &cega_sms_mem_rd_bios_gg : &cega_sms_mem_rd_bios;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_bios;
            break;
        }
        case SMS_MAPPER_SEGA: { // Sega Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr;
            break;
        }
        case SMS_MAPPER_CM: { // Codemasters Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_cm;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_cm;
            smssys.romslot[2] = 0x00;
            break;
        }
        case SMS_MAPPER_JANGGUN: { // Janggun Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_janggun;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_janggun;
            break;
        }
        case SMS_MAPPER_KOREA: { // Korea Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_korea;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_korea;
            break;
        }
        case SMS_MAPPER_MSX: { // MSX Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_msx;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_msx;
            break;
        }
        case SMS_MAPPER_NEMESIS: { // Nemesis Mapper (MSX Mapper Variant)
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_nemesis;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_msx;
            break;
        }
        case SMS_MAPPER_NONE: {
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_none;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_none;
            break;
        }
        case SMS_MAPPER_4PAA: { // 4 PAK All Action Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_4paa;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_4paa;
            break;
        }
        case SMS_MAPPER_XIN1: { // X in 1 Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_xin1;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_xin1;
            break;
        }
        case SMS_MAPPER_93C46: { // 93C46 EEPROM Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_93c46;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_93c46;
            break;
        }
        case SMS_MAPPER_KR_2000_XOR_1F: { // Korean 2000 XOR 1F Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_kr20001f;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_kr20001f;
            break;
        }
        case SMS_MAPPER_KR_MSX_8KB_0300: { // Korean MSX 8KB 0300 Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_krmsx8k300;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_krmsx8k300;
            break;
        }
        case SMS_MAPPER_KR_BFFC: { // Korean BFFC Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_krbffc_fffe;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_krbffc;
            break;
        }
        case SMS_MAPPER_KR_FFFE: { // Korean FFFE Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_krbffc_fffe;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_krfffe;
            break;
        }
        case SMS_MAPPER_KR_FFF5: { // Korean FFF5 Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_krfff5;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_krfff5;
            break;
        }
        case SMS_MAPPER_KR_MSX_32KB_2000: { // Korean MSX 32KB 2000 Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_krmsx32k2000;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_krmsx32k2000;
            break;
        }
        case SMS_MAPPER_KR_0000_XOR_FF: {
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_kr0000ff;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_kr0000ff;
            break;
        }
        case SMS_MAPPER_KR_MSX_SMS_8000: {
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_krmsxsms8000;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_krmsxsms8000;
            break;
        }
        case SMS_MAPPER_KR_FFF3_FFFC: {
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_krfff3fffc;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_krfff3fffc;
            break;
        }
        case SMS_MAPPER_KR_SMS_32KB_2000: { // Korean SMS 32KB 2000 Mapper
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_krsms32k2000;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_krsms32k2000;
            break;
        }
        default: { // Empty Slot
            cega_z80_mem_rd[Z80ID] = &cega_sms_mem_rd_empty;
            cega_z80_mem_wr[Z80ID] = &cega_sms_mem_wr_empty;
            break;
        }
    }

    if (!mset) {
        mapper = m;
        mset = 1;
    }
}

// Return the size of a state
size_t cega_sms_state_size(void) {
    return cega_get_system() == SYSTEM_GG ? SIZE_STATE_GG : SIZE_STATE_SMS;
}

// Load raw state data into the running system
int cega_sms_state_load_raw(const void *sstate, size_t sz) {
    uint8_t *st = (uint8_t*)sstate;
    cega_serial_begin();

    unsigned stver = 0; // State Version
    if (cega_get_system() == SYSTEM_GG) {
        if (sz == SIZE_STATE_GG_PRE061)
            stver = 0;
        else if (sz < SIZE_STATE_GG)
            stver = 1;
        else if ((cega_serial_peek32(st) & 0xffff0000) == 0xCE6A0000)
            stver = cega_serial_pop32(st) & 0xffff;
        else
            return 0;
    }
    else {
        if (sz == SIZE_STATE_SMS_PRE061)
            stver = 0;
        else if (sz < SIZE_STATE_SMS)
            stver = 1;
        else if ((cega_serial_peek32(st) & 0xffff0000) == 0xCE6A0000)
            stver = cega_serial_pop32(st) & 0xffff;
        else
            return 0;
    }

    cega_serial_popblk(smssys.ram, st, SIZE_SMSRAM);
    cega_serial_popblk(smssys.cartram, st, SIZE_32K);
    smssys.ctrl[0] = cega_serial_pop8(st);
    smssys.ctrl[1] = cega_serial_pop8(st);
    smssys.port3e = cega_serial_pop8(st);
    smssys.port3f = cega_serial_pop8(st);
    smssys.region = cega_serial_pop8(st);
    for (unsigned i = 0; i < (stver > 0 ? 4 : 1); ++i)
        smssys.mreg[i] = cega_serial_pop8(st);
    for (unsigned i = 0; i < (stver > 0 ? 8 : 4); ++i)
        smssys.romslot[i] = cega_serial_pop8(st);
    smssys.cartram_enabled = cega_serial_pop8(st);
    cega_mixer_set_cycs(cega_serial_pop8(st), cega_serial_pop8(st));
    cega_z80_state_load(0, st);

    psg.clatch = cega_serial_pop8(st);
    for (size_t i = 0; i < 4; ++i)
        psg.attenuator[i] = cega_serial_pop8(st);
    for (size_t i = 0; i < 3; ++i)
        psg.frequency[i] = cega_serial_pop16(st);
    psg.noise = cega_serial_pop8(st);
    psg.lfsr = cega_serial_pop16(st);
    for (size_t i = 0; i < 4; ++i)
        psg.counter[i] = cega_serial_pop16(st);
    for (size_t i = 0; i < 4; ++i)
        psg.output[i] = cega_serial_pop16(st);
    psg.freqff = cega_serial_pop8(st);
    psg.stctrl = cega_serial_pop8(st);

    smsvdp_t *vdp = smsvdp_ctx();
    cega_serial_popblk(vdp->vram, st, SIZE_VRAM);
    cega_serial_popblk(vdp->cram, st, SIZE_CRAM);
    vdp->tbl_col = cega_serial_pop16(st);
    vdp->tbl_pgen = cega_serial_pop16(st);
    vdp->tbl_pname = cega_serial_pop16(st);
    vdp->tbl_sattr = cega_serial_pop16(st);
    vdp->tbl_spgen = cega_serial_pop16(st);
    vdp->line = cega_serial_pop16(st);
    vdp->cyc = cega_serial_pop16(st);
    vdp->dot = cega_serial_pop16(st);
    vdp->addr = cega_serial_pop16(st);
    vdp->code = cega_serial_pop8(st);
    for (unsigned i = 0; i < 16; ++i)
        vdp->ctrl[i] = cega_serial_pop8(st);
    vdp->stat = cega_serial_pop8(st);
    vdp->rdbuf = cega_serial_pop8(st);
    vdp->alatch = cega_serial_pop8(st);
    vdp->clatch = cega_serial_pop8(st);
    vdp->wlatch = cega_serial_pop8(st);
    vdp->hscroll = cega_serial_pop8(st);
    vdp->hcount = cega_serial_pop8(st);
    vdp->lcount = cega_serial_pop8(st);
    vdp->int_fr_pending = cega_serial_pop8(st);
    vdp->int_ln_pending = cega_serial_pop8(st);
    vdp->paused = cega_serial_pop8(st);
    smsvdp_rehash();

    if (cega_get_system() == SYSTEM_SMS) {
        smssys.fmctrl = cega_serial_pop8(st);
        ym2413_state_load(st);
        cega_sms_set_fmctrl();
    }
    else if (cega_get_system() == SYSTEM_GG) {
        for (unsigned i = 0; i < 6; ++i)
            smssys.ggreg[i] = cega_serial_pop8(st);
        eeprom.clk = cega_serial_pop8(st);
        eeprom.cs = cega_serial_pop8(st);
        eeprom.cyc = cega_serial_pop8(st);
        eeprom.dataout = cega_serial_pop8(st);
        eeprom.opcode = cega_serial_pop8(st);
        eeprom.addr = cega_serial_pop8(st);
        eeprom.mode = cega_serial_pop8(st);
        eeprom.wrenable = cega_serial_pop8(st);
        eeprom.datain = cega_serial_pop16(st);
    }
    return 1;
}

// Load a state from a file
int cega_sms_state_load(const char *filename) {
    FILE *file;
    size_t filesize, result;
    void *sstatefile;

    // Open the file for reading
    file = fopen(filename, "rb");
    if (!file)
        return 0;

    // Find out the file's size
    fseek(file, 0, SEEK_END);
    filesize = ftell(file);
    fseek(file, 0, SEEK_SET);

    // Allocate memory to read the file into
    sstatefile = (void*)calloc(filesize, sizeof(uint8_t));
    if (sstatefile == NULL) {
        fclose(file);
        return 0;
    }

    // Read the file into memory and then close it
    result = fread(sstatefile, sizeof(uint8_t), filesize, file);
    if (result != filesize) {
        free(sstatefile);
        fclose(file);
        return 0;
    }
    fclose(file);

    // File has been read, now copy it into the emulator
    int ret = cega_sms_state_load_raw((const void*)sstatefile, filesize);

    // Free the allocated memory
    free(sstatefile);

    return ret;
}

// Snapshot the running state and return the address of the raw data
const void* cega_sms_state_save_raw(void) {
    cega_serial_begin();
    cega_serial_push32(state, STATE_VERSION);
    cega_serial_pushblk(state, smssys.ram, SIZE_SMSRAM);
    cega_serial_pushblk(state, smssys.cartram, SIZE_32K);
    cega_serial_push8(state, smssys.ctrl[0]);
    cega_serial_push8(state, smssys.ctrl[1]);
    cega_serial_push8(state, smssys.port3e);
    cega_serial_push8(state, smssys.port3f);
    cega_serial_push8(state, smssys.region);
    for (unsigned i = 0; i < 4; ++i)
        cega_serial_push8(state, smssys.mreg[i]);
    for (unsigned i = 0; i < 8; ++i)
        cega_serial_push8(state, smssys.romslot[i]);
    cega_serial_push8(state, smssys.cartram_enabled);
    uint8_t psgcycs, ymcycs;
    cega_mixer_get_cycs(&psgcycs, &ymcycs);
    cega_serial_push8(state, psgcycs);
    cega_serial_push8(state, ymcycs);
    cega_z80_state_save(0, state);

    cega_serial_push8(state, psg.clatch);
    for (size_t i = 0; i < 4; ++i)
        cega_serial_push8(state, psg.attenuator[i]);
    for (size_t i = 0; i < 3; ++i)
        cega_serial_push16(state, psg.frequency[i]);
    cega_serial_push8(state, psg.noise);
    cega_serial_push16(state, psg.lfsr);
    for (size_t i = 0; i < 4; ++i)
        cega_serial_push16(state, psg.counter[i]);
    for (size_t i = 0; i < 4; ++i)
        cega_serial_push16(state, psg.output[i]);
    cega_serial_push8(state, psg.freqff);
    cega_serial_push8(state, psg.stctrl);

    smsvdp_t *vdp = smsvdp_ctx();
    cega_serial_pushblk(state, vdp->vram, SIZE_VRAM);
    cega_serial_pushblk(state, vdp->cram, SIZE_CRAM);
    cega_serial_push16(state, vdp->tbl_col);
    cega_serial_push16(state, vdp->tbl_pgen);
    cega_serial_push16(state, vdp->tbl_pname);
    cega_serial_push16(state, vdp->tbl_sattr);
    cega_serial_push16(state, vdp->tbl_spgen);
    cega_serial_push16(state, vdp->line);
    cega_serial_push16(state, vdp->cyc);
    cega_serial_push16(state, vdp->dot);
    cega_serial_push16(state, vdp->addr);
    cega_serial_push8(state, vdp->code);
    for (unsigned i = 0; i < 16; ++i)
        cega_serial_push8(state, vdp->ctrl[i]);
    cega_serial_push8(state, vdp->stat);
    cega_serial_push8(state, vdp->rdbuf);
    cega_serial_push8(state, vdp->alatch);
    cega_serial_push8(state, vdp->clatch);
    cega_serial_push8(state, vdp->wlatch);
    cega_serial_push8(state, vdp->hscroll);
    cega_serial_push8(state, vdp->hcount);
    cega_serial_push8(state, vdp->lcount);
    cega_serial_push8(state, vdp->int_fr_pending);
    cega_serial_push8(state, vdp->int_ln_pending);
    cega_serial_push8(state, vdp->paused);

    if (cega_get_system() == SYSTEM_SMS) {
        cega_serial_push8(state, smssys.fmctrl);
        ym2413_state_save(state);
    }
    else if (cega_get_system() == SYSTEM_GG) {
        for (unsigned i = 0; i < 6; ++i)
            cega_serial_push8(state, smssys.ggreg[i]);
        cega_serial_push8(state, eeprom.clk);
        cega_serial_push8(state, eeprom.cs);
        cega_serial_push8(state, eeprom.cyc);
        cega_serial_push8(state, eeprom.dataout);
        cega_serial_push8(state, eeprom.opcode);
        cega_serial_push8(state, eeprom.addr);
        cega_serial_push8(state, eeprom.mode);
        cega_serial_push8(state, eeprom.wrenable);
        cega_serial_push16(state, eeprom.datain);
    }

    return (const void*)state;
}

// Save a state to a file
int cega_sms_state_save(const char *filename) {
    // Open the file for writing
    FILE *file;
    file = fopen(filename, "wb");
    if (!file)
        return 0;

    // Snapshot the running state and get the memory address
    uint8_t *sstate = (uint8_t*)cega_sms_state_save_raw();

    // Write and close the file
    fwrite(sstate, cega_sms_state_size(), sizeof(uint8_t), file);
    fclose(file);

    return 1; // Success!
}

// Load SRAM
int cega_sms_sram_load(const char *filename) {
    FILE *file;
    size_t filesize, result;

    // Open the file for reading
    file = fopen(filename, "rb");
    if (!file)
        return 2;

    // Find out the file's size
    fseek(file, 0, SEEK_END);
    filesize = ftell(file);
    fseek(file, 0, SEEK_SET);

    if (filesize > SIZE_32K) {
        fclose(file);
        return 0;
    }

    // Read the file into the system's Cartridge RAM slot and then close it
    result = fread(smssys.cartram, sizeof(uint8_t), filesize, file);
    if (result != filesize) {
        fclose(file);
        return 0;
    }

    fclose(file);

    return 1; // Success!
}

// Save SRAM
int cega_sms_sram_save(const char *filename) {
    if (!smssys.cartram_enabled)
        return 2;

    FILE *file;
    file = fopen(filename, "wb");
    if (!file)
        return 0;

    // Write and close the file
    fwrite(smssys.cartram, SIZE_32K, sizeof(uint8_t), file);
    fclose(file);

    return 1; // Success!
}

/*
 * Game Gear
 */
static uint8_t cega_gg_port_rd(uint8_t port) {
    if (port < 0x07) {
        if (port == 0x00) {
            // Bit 7 for Start, Bit 6 for Region (0 = Domestic, 1 = Export)
            smssys.ggreg[0] = ~cega_input_cb[0](2) &
                (smssys.region == REGION_JP ? 0x80 : 0xc0);
            return smssys.ggreg[0];
        }
        else if (port == 0x06) { // Stereo Control for PSG
            return 0xff; // This should never actually be read
        }
        else {
            return smssys.ggreg[port];
        }
    }
    else if (port < 0x40) { // 0x07 - 0x3f: Always return 0xff
        return 0xff;
    }
    else if (port < 0x80) { // 0x40 - 0x80:
        if (port & 0x01) // Odd returns HCount
            return smsvdp_rd_hcount();
        else // Even returns VCount
            return smsvdp_rd_vcount();
    }
    else if (port < 0xc0) {
        if (port & 0x01) // Odd returns VDP status
            return smsvdp_rd_stat();
        else // Even returns VDP data
            return smsvdp_rd_data();
    }
    else if ((port == 0xc0) || (port == 0xdc)) { // IO A/B register
        smssys.ctrl[0] = ~cega_input_cb[0](0); // Read frontend input state
        return smssys.ctrl[0];
    }
    else if ((port == 0xc1) || (port == 0xdd)) { // IO B/misc register
        smssys.ctrl[1] = ~cega_input_cb[0](1); // Read frontend input state
        return smssys.ctrl[1];
    }

    return 0xff; // Silence any potential compiler warnings
}

static void cega_gg_port_wr(uint8_t port, uint8_t data) {
    if (port < 0x07) {
        if (port == 0x00) // Read-only
            return;
        else if (port == 0x06)
            sn76496_wr_stereo(&psg, data);
        else
            smssys.ggreg[port] = data;
    }
    else if (port < 0x40) { // 0x07 - 0x3f: Memory/IO Control
        if (port & 0x01) { // Odd goes to IO Control
            // Details are the same as for SMS
            uint8_t thchg_01_a =
                (data & 0x02) && (data & 0x20) && !(smssys.port3f & 0x20);
            uint8_t thchg_01_b =
                (data & 0x08) && (data & 0x80) && !(smssys.port3f & 0x80);

            if (thchg_01_a || thchg_01_b)
                smsvdp_wr_hcount();

            // Write the data to the register
            smssys.port3f = data;
        }
        else { // Even goes to Memory Control
            smssys.port3e = data;

            if (bios)
                cega_sms_set_memctrl();
        }
    }
    else if (port < 0x80) { // 0x40 - 0x7f: PSG Writes
        sn76496_wr(&psg, data); // All writes go to the PSG
    }
    else if (port < 0xc0) { // 0x80 - 0xbf: VDP Data and Control ports
        if (port & 0x01) // Odd goes to control port
            smsvdp_wr_ctrl(data);
        else // Even goes to data port
            smsvdp_wr_data(data);
    }
}

// Do Game Gear specific initialization tasks
void cega_gg_init(void) {
    for (int i = 0; i < SIZE_SMSRAM; ++i)
        smssys.ram[i] = 0x00;

    // Set function pointers for I/O Reads/Writes
    cega_z80_port_rd[Z80ID] = &cega_gg_port_rd;
    cega_z80_port_wr[Z80ID] = &cega_gg_port_wr;

    // Set defaults for Game Gear Registers
    smssys.ggreg[0] = 0xc0;
    smssys.ggreg[1] = 0x7f;
    smssys.ggreg[2] = 0xff;
    smssys.ggreg[3] = 0x00;
    smssys.ggreg[4] = 0xff;
    smssys.ggreg[5] = 0x00;

    smssys.port3e = 0xa8;

    // Set the VDP to Game Gear mode unless this is an SMS mode game
    smsvdp_set_ggmode(!ggsmsmode);

    // Initialize PSG
    sn76496_init(&psg, 1, NTAP, NSHIFT);
    cega_mixer_set_psg(&psg);

    // Initialize EEPROM
    eep93c46_init(&eeprom);
}

// Set Game Gear SMS Mode
void cega_gg_set_smsmode(int enable) {
    ggsmsmode = enable;
    smsvdp_set_ggmode(!enable);
}

// Run emulation for one frame
void cega_sms_exec(void) {
    while (!frameready) {
        // Clock the CPU and PSG/FM chips every third master cycle
        if (mcycs % 3 == 0) {
            cega_z80_exec(Z80ID);
            cega_mixer_chips_exec();
        }

        // Clock the VDP every second master cycle
        if (mcycs % 2 == 0) {
            if (smsvdp_exec())
                frameready = 1; // Frame is ready
        }

        ++mcycs;
    }

    frameready = 0;

    // Resample audio and push to the frontend
    cega_mixer_resamp();
}
