#include "core/gb/gb.h"

#include <array>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>

#include "core/base/check.h"
#include "core/base/file_util.h"
#include "core/base/message.h"
#include "core/base/sizes.h"
#include "core/base/system.h"
#include "core/gb/gbCheats.h"
#include "core/gb/gbGfx.h"
#include "core/gb/gbGlobals.h"
#include "core/gb/gbMemory.h"
#include "core/gb/gbSGB.h"
#include "core/gb/gbSound.h"
#include "core/gba/gbaSound.h"

#if !defined(NO_LINK)
#include "core/gba/gbaLink.h"
#endif  // !defined(NOLINK)

#if !defined(__LIBRETRO__)
#include "core/base/image_util.h"
#include "core/base/patch.h"
#endif  // defined(__LIBRETRO__)

#ifdef __GNUC__
#define _stricmp strcasecmp
#endif

extern uint8_t* g_pix;

namespace {

// Mapper functions.
void (*g_mapper)(uint16_t, uint8_t) = nullptr;
void (*g_mapperRAM)(uint16_t, uint8_t) = nullptr;
uint8_t (*g_mapperReadRAM)(uint16_t) = nullptr;
void (*g_mapperUpdateClock)() = nullptr;

// Set to true on battery load error.
bool g_gbBatteryError = false;

#ifndef __LIBRETRO__

// This structure is used to save and restore the battery-backed RAM. It is
// similar to the iovec structure used in Linux.
struct VBamIoVec {
    // Pointer to the raw data.
    void* data;
    // Data length.
    size_t length;
    // Optional leeway for compatibility with older versions.
    int leeway = 0;
    // Optional action to take on read failure.
    // If this is set, `g_gbBatteryError` will not be set.
    void (*action_on_failure)() = nullptr;
};
std::vector<VBamIoVec> g_vbamIoVecs;

void ResetMBC3RTC() {
    time(&gbDataMBC3.mapperLastTime);
    struct tm* lt;
    lt = localtime(&gbDataMBC3.mapperLastTime);
    gbDataMBC3.mapperSeconds = lt->tm_sec;
    gbDataMBC3.mapperMinutes = lt->tm_min;
    gbDataMBC3.mapperHours = lt->tm_hour;
    gbDataMBC3.mapperDays = lt->tm_yday & 255;
    gbDataMBC3.mapperControl =
        (gbDataMBC3.mapperControl & 0xfe) | (lt->tm_yday > 255 ? 1 : 0);
}

void ResetTama5RTC() {
    uint8_t gbDaysinMonth[12] = {31, 28, 31, 30, 31, 30,
                                 31, 31, 30, 31, 30, 31};
    time(&gbDataTAMA5.mapperLastTime);
    struct tm* lt;
    lt = localtime(&gbDataTAMA5.mapperLastTime);
    gbDataTAMA5.mapperSeconds = lt->tm_sec;
    gbDataTAMA5.mapperMinutes = lt->tm_min;
    gbDataTAMA5.mapperHours = lt->tm_hour;
    gbDataTAMA5.mapperDays = 1;
    gbDataTAMA5.mapperMonths = 1;
    gbDataTAMA5.mapperYears = 1970;
    int days = lt->tm_yday + 365 * 3;
    while (days) {
        gbDataTAMA5.mapperDays++;
        days--;
        if (gbDataTAMA5.mapperDays >
            gbDaysinMonth[gbDataTAMA5.mapperMonths - 1]) {
            gbDataTAMA5.mapperDays = 1;
            gbDataTAMA5.mapperMonths++;
            if (gbDataTAMA5.mapperMonths > 12) {
                gbDataTAMA5.mapperMonths = 1;
                gbDataTAMA5.mapperYears++;
                if ((gbDataTAMA5.mapperYears & 3) == 0)
                    gbDaysinMonth[1] = 29;
                else
                    gbDaysinMonth[1] = 28;
            }
        }
    }
    gbDataTAMA5.mapperControl =
        (gbDataTAMA5.mapperControl & 0xfe) | (lt->tm_yday > 255 ? 1 : 0);
}

void ResetHuc3RTC() {
    time(&gbRTCHuC3.mapperLastTime);
    localtime(&gbRTCHuC3.mapperLastTime);
}

bool WriteBatteryFile(const char* file_name) {
    if (g_gbBatteryError) {
        return false;
    }
    if (!g_gbCartData.has_battery()) {
        return true;
    }

    FILE* file = utilOpenFile(file_name, "wb");
    if (file == nullptr) {
        systemMessage(MSG_ERROR_CREATING_FILE, N_("Error creating file %s"),
                      file_name);
        return false;
    }

    for (const VBamIoVec& vec : g_vbamIoVecs) {
        fwrite(vec.data, 1, vec.length, file);
    }

    fclose(file);
    return true;
}

bool ReadBatteryFile(const char* file_name) {
    systemSaveUpdateCounter = SYSTEM_SAVE_NOT_UPDATED;
    if (!g_gbCartData.has_battery()) {
        return false;
    }

    gzFile gzFile = utilAutoGzOpen(file_name, "rb");
    if (gzFile == nullptr) {
        return false;
    }

    bool ret = true;
    int total_read = 0;
    for (const VBamIoVec& vec : g_vbamIoVecs) {
        const int sizeToRead = vec.length;
        const int sizeToReadWithLeeway = sizeToRead + vec.leeway;
        const int read = gzread(gzFile, vec.data, sizeToRead);
        total_read += read;
        if (read < 0 || (read != sizeToRead && read != sizeToReadWithLeeway)) {
            if (vec.action_on_failure) {
                systemMessage(
                    MSG_FAILED_TO_READ_RTC,
                    N_("Failed to read RTC from save game %s (continuing)"),
                    file_name);
                ret = false;
                vec.action_on_failure();
            } else {
                systemMessage(
                    MSG_FAILED_TO_READ_SGM,
                    N_("Battery file's size incompatible with the rom settings %s (%d).\nWarning: save of the battery file is now disabled!"),
                    file_name,
                    total_read);
                g_gbBatteryError = true;
                gzclose(gzFile);
                ret = false;
                break;
            }
        }
    }

    if (!ret) {
        // No need to perform any further check, just close the file.
        gzclose(gzFile);
        return false;
    }

    // Check if the battery file is larger than expected.
    uint8_t data = 0;
    const int read = gzread(gzFile, &data, 1);
    total_read += read;
    if (read != 0) {
        systemMessage(
            MSG_FAILED_TO_READ_SGM,
            N_("Battery file's size incompatible with the rom settings %s (%d).\nWarning: save of the battery file is now disabled!"),
            file_name, total_read);
        g_gbBatteryError = true;
        ret = false;
    }

    if (gzclose(gzFile) != Z_OK) {
        systemMessage(MSG_FAILED_TO_READ_SGM,
                      N_("Battery file is corrupted (%s).\nWarning: save of the battery file is now disabled!"),
                      file_name);
        g_gbBatteryError = true;
        ret = false;
    }

    return ret;
}

#endif // !__LIBRETRO__

#if !defined(FINAL_VERSION)
// debugging
bool memorydebug = false;
#endif  // FINAL_VERSION

// These are the default palettes when launching a GB game for GBC, GBA and
// GBA SP, respectively.
static constexpr std::array<uint16_t, 0x40> kGbGbcPalette = {
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x0600, 0xfdf3, 0x041c, 0xf5db, 0x4419, 0x57ea, 0x2808, 0x9b75,
    0x129b, 0xfce0, 0x22da, 0x4ac5, 0x2d71, 0xf0c2, 0x5137, 0x2d41,
    0x6b2d, 0x2215, 0xbe0a, 0xc053, 0xfe5f, 0xe000, 0xbe10, 0x914d,
    0x7f91, 0x02b5, 0x77ac, 0x14e5, 0xcf89, 0xa03d, 0xfd50, 0x91ff,
};

static constexpr std::array<uint16_t, 0x40> kGbGbaPalette = {
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0xbe00, 0xfdfd, 0xbd69, 0x7baf, 0xf5ff, 0x3f8f, 0xcee5, 0x5bf7,
    0xb35b, 0xef97, 0xef9f, 0x97f7, 0x82bf, 0x9f3d, 0xddde, 0xbad5,
    0x3cba, 0xdfd7, 0xedea, 0xfeda, 0xf7f9, 0xfdee, 0x6d2f, 0xf0e6,
    0xf7f0, 0xf296, 0x3bf1, 0xe211, 0x69ba, 0x3d0d, 0xdfd3, 0xa6ba,
};

static constexpr std::array<uint16_t, 0x40> kGbGbaSpPalette = {
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff, 0x7fff,
    0x9c00, 0x6340, 0x10c6, 0xdb97, 0x7622, 0x3e57, 0x2e12, 0x95c3,
    0x1095, 0x488c, 0x8241, 0xde8c, 0xfabc, 0x0e81, 0x7675, 0xfdec,
    0xddfd, 0x5995, 0x066a, 0xed1e, 0x1e84, 0x1d14, 0x11c3, 0x2749,
    0xa727, 0x6266, 0xe27b, 0xe3fc, 0x1f76, 0xf158, 0x468e, 0xa540,
};

static constexpr size_t kTama5RamSize = k256B;

int gbGetValue(int min, int max, int v) {
    return (int)(min + (float)(max - min) *
                           (2.0 * (v / 31.0) - (v / 31.0) * (v / 31.0)));
}

void gbGenFilter() {
    for (int r = 0; r < 32; r++) {
        for (int g = 0; g < 32; g++) {
            for (int b = 0; b < 32; b++) {
                int nr =
                    gbGetValue(gbGetValue(4, 14, g), gbGetValue(24, 29, g), r) -
                    4;
                int ng = gbGetValue(gbGetValue(4 + gbGetValue(0, 5, r),
                                               14 + gbGetValue(0, 3, r), b),
                                    gbGetValue(24 + gbGetValue(0, 3, r),
                                               29 + gbGetValue(0, 1, r), b),
                                    g) -
                         4;
                int nb = gbGetValue(gbGetValue(4 + gbGetValue(0, 5, r),
                                               14 + gbGetValue(0, 3, r), g),
                                    gbGetValue(24 + gbGetValue(0, 3, r),
                                               29 + gbGetValue(0, 1, r), g),
                                    b) -
                         4;
                gbColorFilter[(b << 10) | (g << 5) | r] =
                    (nb << 10) | (ng << 5) | nr;
            }
        }
    }
}

// Initializes the `g_gbCartData` variable with the data in `gbRom`, expecting a
// size of `romSize` bytes. Returns true on success.
bool gbInitializeRom(size_t romSize) {
    g_gbCartData = gbCartData(gbRom, romSize);

    if (!g_gbCartData) {
        switch (g_gbCartData.validity()) {
            case gbCartData::Validity::kValid:
            case gbCartData::Validity::kUninitialized:
                VBAM_NOTREACHED();
                break;
            case gbCartData::Validity::kSizeTooSmall:
                systemMessage(MSG_UNSUPPORTED_ROM_SIZE,
                              N_("Unsupported rom size %02x"), romSize);
                break;
            case gbCartData::Validity::kUnknownMapperType:
                systemMessage(MSG_UNKNOWN_CARTRIDGE_TYPE,
                              N_("Unknown cartridge type %02x"),
                              g_gbCartData.mapper_flag());
                break;
            case gbCartData::Validity::kUnknownRomSize:
                systemMessage(MSG_UNSUPPORTED_ROM_SIZE,
                              N_("Unsupported rom size %02x"),
                              g_gbCartData.rom_flag());
                break;
            case gbCartData::Validity::kUnknownRamSize:
                systemMessage(MSG_UNSUPPORTED_RAM_SIZE,
                              N_("Unsupported ram size %02x"),
                              g_gbCartData.ram_flag());
                break;
            case gbCartData::Validity::kNoNintendoLogo:
                systemMessage(MSG_INVALID_GAME_BOY_NINTENDO_LOGO,
                              N_("No Nintendo logo in header"));
                break;
            case gbCartData::Validity::kInvalidHeaderChecksum:
                systemMessage(
                    MSG_INVALID_HEADER_CHECKSUM,
                    N_("Invalid header checksum. Found: %02x. Expected: %02x"),
                    g_gbCartData.header_checksum(),
                    g_gbCartData.actual_header_checksum());
                break;
        }
        g_gbCartData = gbCartData();
        return false;
    }

    // We ignore romSize > romHeaderSize here, for backwards compatibility. This
    // is necessary for some ROM hacks.
    const size_t romHeaderSize = g_gbCartData.rom_size();
    if (romSize < romHeaderSize) {
        uint8_t* gbRomNew = (uint8_t*)realloc(gbRom, romHeaderSize);
        if (!gbRomNew) {
            return false;
        };
        gbRom = gbRomNew;

        // Not sure if it's 0x00, 0xff or random data.
        std::fill(gbRom + romSize, gbRom + romHeaderSize, (uint8_t)0);
    }

    // Override for compatibility.
    gbRom[0x147] = g_gbCartData.mapper_flag();

    // The initial RAM byte value.
    uint8_t gbRamFill = 0xff;

    switch (g_gbCartData.mapper_type()) {
        case gbCartData::MapperType::kNone:
        case gbCartData::MapperType::kMbc1:
            g_mapper = mapperMBC1ROM;
            g_mapperRAM = mapperMBC1RAM;
            g_mapperReadRAM = mapperMBC1ReadRAM;
            break;
        case gbCartData::MapperType::kMbc2:
            g_mapper = mapperMBC2ROM;
            g_mapperRAM = mapperMBC2RAM;
            break;
        case gbCartData::MapperType::kMmm01:
            g_mapper = mapperMMM01ROM;
            g_mapperRAM = mapperMMM01RAM;
            break;
        case gbCartData::MapperType::kMbc3:
        case gbCartData::MapperType::kPocketCamera:
            g_mapper = mapperMBC3ROM;
            g_mapperRAM = mapperMBC3RAM;
            g_mapperReadRAM = mapperMBC3ReadRAM;
            break;
        case gbCartData::MapperType::kMbc5:
            g_mapper = mapperMBC5ROM;
            g_mapperRAM = mapperMBC5RAM;
            g_mapperReadRAM = mapperMBC5ReadRAM;
            break;
        case gbCartData::MapperType::kMbc7:
            g_mapper = mapperMBC7ROM;
            g_mapperRAM = mapperMBC7RAM;
            g_mapperReadRAM = mapperMBC7ReadRAM;
            break;
        case gbCartData::MapperType::kGameGenie:
            // Clean-up Game Genie hardware registers.
            for (int i = 0; i < 0x20; i++) {
                gbRom[0x4000 + i] = 0;
            }
            g_mapper = mapperGGROM;
            break;
        case gbCartData::MapperType::kGameShark:
            g_mapper = mapperGS3ROM;
            break;
        case gbCartData::MapperType::kTama5:
            gbRamFill = 0x00;
            if (gbTAMA5ram == nullptr) {
                gbTAMA5ram = (uint8_t*)calloc(1, kTama5RamSize);
                if (gbTAMA5ram == nullptr) {
                    return false;
                }
            }

            g_mapperRAM = mapperTAMA5RAM;
            g_mapperReadRAM = mapperTAMA5ReadRAM;
            g_mapperUpdateClock = memoryUpdateTAMA5Clock;
            break;
        case gbCartData::MapperType::kHuC3:
            g_mapper = mapperHuC3ROM;
            g_mapperRAM = mapperHuC3RAM;
            g_mapperReadRAM = mapperHuC3ReadRAM;
            break;
        case gbCartData::MapperType::kHuC1:
            g_mapper = mapperHuC1ROM;
            g_mapperRAM = mapperHuC1RAM;
            break;
        default:
            systemMessage(MSG_UNKNOWN_CARTRIDGE_TYPE,
                          N_("Unknown cartridge type"));
            return false;
    }

    // We need to explicitly reset gbRam here as the patch application process
    // may have changed the RAM size.
    if (gbRam != nullptr) {
        free(gbRam);
        gbRam = nullptr;
    }

    const size_t ramSize = g_gbCartData.ram_size();
    if (g_gbCartData.HasRam()) {
        // Always allocate 4 KiB to prevent access issues down the line.
        gbRam = (uint8_t*)malloc(std::max(k4KiB, ramSize));
        if (gbRam == nullptr) {
            return false;
        }
        memset(gbRam, gbRamFill, ramSize);
    }

    gbGenFilter();
    gbSgbInit();
    setColorizerHack(false);

#if !defined(__LIBRETRO__)

    // Populate the IO vectors for battery save/load.
    g_vbamIoVecs.clear();
    if (g_gbCartData.has_battery()) {
        if (g_gbCartData.HasRam()) {
            g_vbamIoVecs.push_back({gbRam, ramSize});
        }

        switch (g_gbCartData.mapper_type()) {
            case gbCartData::MapperType::kMbc3:
                if (g_gbCartData.has_rtc()) {
                    g_vbamIoVecs.push_back({&gbDataMBC3.mapperSeconds,
                                            MBC3_RTC_DATA_SIZE, -4,
                                            &ResetMBC3RTC});
                }
                break;
            case gbCartData::MapperType::kTama5:
                g_vbamIoVecs.push_back({gbTAMA5ram, kTama5RamSize});
                g_vbamIoVecs.push_back({&gbDataTAMA5.mapperSeconds,
                                        TAMA5_RTC_DATA_SIZE, -4,
                                        &ResetTama5RTC});
                break;
            case gbCartData::MapperType::kHuC3:
                g_vbamIoVecs.push_back({&gbRTCHuC3.mapperLastTime,
                                        HUC3_RTC_DATA_SIZE, -4,
                                        &ResetHuc3RTC});
                break;
            case gbCartData::MapperType::kNone:
            case gbCartData::MapperType::kMbc1:
            case gbCartData::MapperType::kMbc2:
            case gbCartData::MapperType::kMbc5:
            case gbCartData::MapperType::kMbc6:
            case gbCartData::MapperType::kMbc7:
            case gbCartData::MapperType::kHuC1:
            case gbCartData::MapperType::kMmm01:
            case gbCartData::MapperType::kPocketCamera:
            case gbCartData::MapperType::kGameGenie:
            case gbCartData::MapperType::kGameShark:
            case gbCartData::MapperType::kUnknown:
                // Do nothing more.
                break;
        }
    }

#endif  // !defined(__LIBRETRO__)

    return true;
}

}  // namespace

bool inBios = false;

extern uint16_t gbLineMix[kGBWidth];

// registers
gbRegister PC;
gbRegister SP;
gbRegister AF;
gbRegister BC;
gbRegister DE;
gbRegister HL;
uint16_t IFF = 0;
// 0xff04
uint8_t register_DIV = 0;
// 0xff05
uint8_t register_TIMA = 0;
// 0xff06
uint8_t register_TMA = 0;
// 0xff07
uint8_t register_TAC = 0;
// 0xff0f
uint8_t register_IF = 0;
// 0xff40
uint8_t register_LCDC = 0;
// 0xff41
uint8_t register_STAT = 0;
// 0xff42
uint8_t register_SCY = 0;
// 0xff43
uint8_t register_SCX = 0;
// 0xff44
uint8_t register_LY = 0;
// 0xff45
uint8_t register_LYC = 0;
// 0xff46
uint8_t register_DMA = 0;
// 0xff4a
uint8_t register_WY = 0;
// 0xff4b
uint8_t register_WX = 0;
// 0xff4f
uint8_t register_VBK = 0;
// 0xff51
uint8_t register_HDMA1 = 0;
// 0xff52
uint8_t register_HDMA2 = 0;
// 0xff53
uint8_t register_HDMA3 = 0;
// 0xff54
uint8_t register_HDMA4 = 0;
// 0xff55
uint8_t register_HDMA5 = 0;
// 0xff70
uint8_t register_SVBK = 0;
// 0xffff
uint8_t register_IE = 0;

// ticks definition
int GBDIV_CLOCK_TICKS = 64;
int GBLCD_MODE_0_CLOCK_TICKS = 51;
int GBLCD_MODE_1_CLOCK_TICKS = 1140;
int GBLCD_MODE_2_CLOCK_TICKS = 20;
int GBLCD_MODE_3_CLOCK_TICKS = 43;
int GBLY_INCREMENT_CLOCK_TICKS = 114;
int GBTIMER_MODE_0_CLOCK_TICKS = 256;
int GBTIMER_MODE_1_CLOCK_TICKS = 4;
int GBTIMER_MODE_2_CLOCK_TICKS = 16;
int GBTIMER_MODE_3_CLOCK_TICKS = 64;
int GBSERIAL_CLOCK_TICKS = 128;
int GBSYNCHRONIZE_CLOCK_TICKS = 52920;

// state variables

// general
gbCartData g_gbCartData;
int clockTicks = 0;
bool gbSystemMessage = false;
int gbGBCColorType = 0;
int gbHardware = 0;
int gbRemainingClockTicks = 0;
int gbOldClockTicks = 0;
int gbIntBreak = 0;
int gbInterruptLaunched = 0;
// breakpoint
bool breakpoint = false;
// interrupt
int gbInt48Signal = 0;
int gbInterruptWait = 0;
// serial
int gbSerialOn = 0;
int gbSerialTicks = 0;
int gbSerialBits = 0;
// timer
int gbTimerOn = 0;
int gbTimerTicks = GBTIMER_MODE_0_CLOCK_TICKS;
int gbTimerClockTicks = GBTIMER_MODE_0_CLOCK_TICKS;
int gbTimerMode = 0;
bool gbIncreased = false;
// The internal timer is always active, and it is
// not reset by writing to register_TIMA/TMA, but by
// writing to register_DIV...
int gbInternalTimer = 0x55;
const uint8_t gbTimerMask[4] = { 0xff, 0x3, 0xf, 0x3f };
bool gbTimerModeChange = false;
bool gbTimerOnChange = false;
// lcd
bool gbScreenOn = true;
int gbLcdMode = 2;
int gbLcdModeDelayed = 2;
int gbLcdTicks = GBLCD_MODE_2_CLOCK_TICKS - 1;
int gbLcdTicksDelayed = GBLCD_MODE_2_CLOCK_TICKS;
int gbLcdLYIncrementTicks = 114;
int gbLcdLYIncrementTicksDelayed = 115;
int gbScreenTicks = 0;
uint8_t gbSCYLine[300];
uint8_t gbSCXLine[300];
uint8_t gbBgpLine[300];
uint8_t gbObp0Line[300];
uint8_t gbObp1Line[300];
uint8_t gbSpritesTicks[300];
uint8_t oldRegister_WY;
bool gbLYChangeHappened = false;
bool gbLCDChangeHappened = false;
int gbLine99Ticks = 1;
int gbRegisterLYLCDCOffOn = 0;
int inUseRegister_WY = 0;

// Used to keep track of the line that ellapse
// when screen is off
int gbWhiteScreen = 0;
bool gbBlackScreen = false;
int register_LCDCBusy = 0;

// div
int gbDivTicks = GBDIV_CLOCK_TICKS;
// cgb
int gbVramBank = 0;
int gbWramBank = 1;
//sgb
bool gbSgbResetFlag = false;
// gbHdmaDestination is 0x99d0 on startup (tested on HW)
// but I'm not sure what gbHdmaSource is...
int gbHdmaSource = 0x99d0;
int gbHdmaDestination = 0x99d0;
int gbHdmaBytes = 0x0000;
int gbHdmaOn = 0;
int gbSpeed = 0;
// frame counting
int gbFrameCount = 0;
int gbFrameSkip = 0;
int gbFrameSkipCount = 0;
// timing
uint32_t gbLastTime = 0;
int gbSynchronizeTicks = GBSYNCHRONIZE_CLOCK_TICKS;
// emulator features
int gbCaptureNumber = 0;
bool gbCapture = false;
bool gbCapturePrevious = false;
int gbJoymask[4] = { 0, 0, 0, 0 };
static bool allow_colorizer_hack;

int gbCycles[] = {
    //  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
    1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1, // 0
    1, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1, // 1
    2, 3, 2, 2, 1, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1, // 2
    2, 3, 2, 2, 3, 3, 3, 1, 2, 2, 2, 2, 1, 1, 2, 1, // 3
    1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 4
    1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 5
    1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 6
    2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, // 7
    1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 8
    1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 9
    1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // a
    1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // b
    2, 3, 3, 4, 3, 4, 2, 4, 2, 4, 3, 2, 3, 6, 2, 4, // c
    2, 3, 3, 1, 3, 4, 2, 4, 2, 4, 3, 1, 3, 1, 2, 4, // d
    3, 3, 2, 1, 1, 4, 2, 4, 4, 1, 4, 1, 1, 1, 2, 4, // e
    3, 3, 2, 1, 1, 4, 2, 4, 3, 2, 4, 1, 0, 1, 2, 4 // f
};

int gbCyclesCB[] = {
    //  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 0
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 1
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 2
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 3
    2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, // 4
    2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, // 5
    2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, // 6
    2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, // 7
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 8
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 9
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // a
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // b
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // c
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // d
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // e
    2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2 // f
};

uint16_t DAATable[] = {
    0x0080, 0x0100, 0x0200, 0x0300, 0x0400, 0x0500, 0x0600, 0x0700,
    0x0800, 0x0900, 0x1000, 0x1100, 0x1200, 0x1300, 0x1400, 0x1500,
    0x1000, 0x1100, 0x1200, 0x1300, 0x1400, 0x1500, 0x1600, 0x1700,
    0x1800, 0x1900, 0x2000, 0x2100, 0x2200, 0x2300, 0x2400, 0x2500,
    0x2000, 0x2100, 0x2200, 0x2300, 0x2400, 0x2500, 0x2600, 0x2700,
    0x2800, 0x2900, 0x3000, 0x3100, 0x3200, 0x3300, 0x3400, 0x3500,
    0x3000, 0x3100, 0x3200, 0x3300, 0x3400, 0x3500, 0x3600, 0x3700,
    0x3800, 0x3900, 0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500,
    0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500, 0x4600, 0x4700,
    0x4800, 0x4900, 0x5000, 0x5100, 0x5200, 0x5300, 0x5400, 0x5500,
    0x5000, 0x5100, 0x5200, 0x5300, 0x5400, 0x5500, 0x5600, 0x5700,
    0x5800, 0x5900, 0x6000, 0x6100, 0x6200, 0x6300, 0x6400, 0x6500,
    0x6000, 0x6100, 0x6200, 0x6300, 0x6400, 0x6500, 0x6600, 0x6700,
    0x6800, 0x6900, 0x7000, 0x7100, 0x7200, 0x7300, 0x7400, 0x7500,
    0x7000, 0x7100, 0x7200, 0x7300, 0x7400, 0x7500, 0x7600, 0x7700,
    0x7800, 0x7900, 0x8000, 0x8100, 0x8200, 0x8300, 0x8400, 0x8500,
    0x8000, 0x8100, 0x8200, 0x8300, 0x8400, 0x8500, 0x8600, 0x8700,
    0x8800, 0x8900, 0x9000, 0x9100, 0x9200, 0x9300, 0x9400, 0x9500,
    0x9000, 0x9100, 0x9200, 0x9300, 0x9400, 0x9500, 0x9600, 0x9700,
    0x9800, 0x9900, 0x0090, 0x0110, 0x0210, 0x0310, 0x0410, 0x0510,
    0x0090, 0x0110, 0x0210, 0x0310, 0x0410, 0x0510, 0x0610, 0x0710,
    0x0810, 0x0910, 0x1010, 0x1110, 0x1210, 0x1310, 0x1410, 0x1510,
    0x1010, 0x1110, 0x1210, 0x1310, 0x1410, 0x1510, 0x1610, 0x1710,
    0x1810, 0x1910, 0x2010, 0x2110, 0x2210, 0x2310, 0x2410, 0x2510,
    0x2010, 0x2110, 0x2210, 0x2310, 0x2410, 0x2510, 0x2610, 0x2710,
    0x2810, 0x2910, 0x3010, 0x3110, 0x3210, 0x3310, 0x3410, 0x3510,
    0x3010, 0x3110, 0x3210, 0x3310, 0x3410, 0x3510, 0x3610, 0x3710,
    0x3810, 0x3910, 0x4010, 0x4110, 0x4210, 0x4310, 0x4410, 0x4510,
    0x4010, 0x4110, 0x4210, 0x4310, 0x4410, 0x4510, 0x4610, 0x4710,
    0x4810, 0x4910, 0x5010, 0x5110, 0x5210, 0x5310, 0x5410, 0x5510,
    0x5010, 0x5110, 0x5210, 0x5310, 0x5410, 0x5510, 0x5610, 0x5710,
    0x5810, 0x5910, 0x6010, 0x6110, 0x6210, 0x6310, 0x6410, 0x6510,
    0x6010, 0x6110, 0x6210, 0x6310, 0x6410, 0x6510, 0x6610, 0x6710,
    0x6810, 0x6910, 0x7010, 0x7110, 0x7210, 0x7310, 0x7410, 0x7510,
    0x7010, 0x7110, 0x7210, 0x7310, 0x7410, 0x7510, 0x7610, 0x7710,
    0x7810, 0x7910, 0x8010, 0x8110, 0x8210, 0x8310, 0x8410, 0x8510,
    0x8010, 0x8110, 0x8210, 0x8310, 0x8410, 0x8510, 0x8610, 0x8710,
    0x8810, 0x8910, 0x9010, 0x9110, 0x9210, 0x9310, 0x9410, 0x9510,
    0x9010, 0x9110, 0x9210, 0x9310, 0x9410, 0x9510, 0x9610, 0x9710,
    0x9810, 0x9910, 0xA010, 0xA110, 0xA210, 0xA310, 0xA410, 0xA510,
    0xA010, 0xA110, 0xA210, 0xA310, 0xA410, 0xA510, 0xA610, 0xA710,
    0xA810, 0xA910, 0xB010, 0xB110, 0xB210, 0xB310, 0xB410, 0xB510,
    0xB010, 0xB110, 0xB210, 0xB310, 0xB410, 0xB510, 0xB610, 0xB710,
    0xB810, 0xB910, 0xC010, 0xC110, 0xC210, 0xC310, 0xC410, 0xC510,
    0xC010, 0xC110, 0xC210, 0xC310, 0xC410, 0xC510, 0xC610, 0xC710,
    0xC810, 0xC910, 0xD010, 0xD110, 0xD210, 0xD310, 0xD410, 0xD510,
    0xD010, 0xD110, 0xD210, 0xD310, 0xD410, 0xD510, 0xD610, 0xD710,
    0xD810, 0xD910, 0xE010, 0xE110, 0xE210, 0xE310, 0xE410, 0xE510,
    0xE010, 0xE110, 0xE210, 0xE310, 0xE410, 0xE510, 0xE610, 0xE710,
    0xE810, 0xE910, 0xF010, 0xF110, 0xF210, 0xF310, 0xF410, 0xF510,
    0xF010, 0xF110, 0xF210, 0xF310, 0xF410, 0xF510, 0xF610, 0xF710,
    0xF810, 0xF910, 0x0090, 0x0110, 0x0210, 0x0310, 0x0410, 0x0510,
    0x0090, 0x0110, 0x0210, 0x0310, 0x0410, 0x0510, 0x0610, 0x0710,
    0x0810, 0x0910, 0x1010, 0x1110, 0x1210, 0x1310, 0x1410, 0x1510,
    0x1010, 0x1110, 0x1210, 0x1310, 0x1410, 0x1510, 0x1610, 0x1710,
    0x1810, 0x1910, 0x2010, 0x2110, 0x2210, 0x2310, 0x2410, 0x2510,
    0x2010, 0x2110, 0x2210, 0x2310, 0x2410, 0x2510, 0x2610, 0x2710,
    0x2810, 0x2910, 0x3010, 0x3110, 0x3210, 0x3310, 0x3410, 0x3510,
    0x3010, 0x3110, 0x3210, 0x3310, 0x3410, 0x3510, 0x3610, 0x3710,
    0x3810, 0x3910, 0x4010, 0x4110, 0x4210, 0x4310, 0x4410, 0x4510,
    0x4010, 0x4110, 0x4210, 0x4310, 0x4410, 0x4510, 0x4610, 0x4710,
    0x4810, 0x4910, 0x5010, 0x5110, 0x5210, 0x5310, 0x5410, 0x5510,
    0x5010, 0x5110, 0x5210, 0x5310, 0x5410, 0x5510, 0x5610, 0x5710,
    0x5810, 0x5910, 0x6010, 0x6110, 0x6210, 0x6310, 0x6410, 0x6510,
    0x0600, 0x0700, 0x0800, 0x0900, 0x0A00, 0x0B00, 0x0C00, 0x0D00,
    0x0E00, 0x0F00, 0x1000, 0x1100, 0x1200, 0x1300, 0x1400, 0x1500,
    0x1600, 0x1700, 0x1800, 0x1900, 0x1A00, 0x1B00, 0x1C00, 0x1D00,
    0x1E00, 0x1F00, 0x2000, 0x2100, 0x2200, 0x2300, 0x2400, 0x2500,
    0x2600, 0x2700, 0x2800, 0x2900, 0x2A00, 0x2B00, 0x2C00, 0x2D00,
    0x2E00, 0x2F00, 0x3000, 0x3100, 0x3200, 0x3300, 0x3400, 0x3500,
    0x3600, 0x3700, 0x3800, 0x3900, 0x3A00, 0x3B00, 0x3C00, 0x3D00,
    0x3E00, 0x3F00, 0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500,
    0x4600, 0x4700, 0x4800, 0x4900, 0x4A00, 0x4B00, 0x4C00, 0x4D00,
    0x4E00, 0x4F00, 0x5000, 0x5100, 0x5200, 0x5300, 0x5400, 0x5500,
    0x5600, 0x5700, 0x5800, 0x5900, 0x5A00, 0x5B00, 0x5C00, 0x5D00,
    0x5E00, 0x5F00, 0x6000, 0x6100, 0x6200, 0x6300, 0x6400, 0x6500,
    0x6600, 0x6700, 0x6800, 0x6900, 0x6A00, 0x6B00, 0x6C00, 0x6D00,
    0x6E00, 0x6F00, 0x7000, 0x7100, 0x7200, 0x7300, 0x7400, 0x7500,
    0x7600, 0x7700, 0x7800, 0x7900, 0x7A00, 0x7B00, 0x7C00, 0x7D00,
    0x7E00, 0x7F00, 0x8000, 0x8100, 0x8200, 0x8300, 0x8400, 0x8500,
    0x8600, 0x8700, 0x8800, 0x8900, 0x8A00, 0x8B00, 0x8C00, 0x8D00,
    0x8E00, 0x8F00, 0x9000, 0x9100, 0x9200, 0x9300, 0x9400, 0x9500,
    0x9600, 0x9700, 0x9800, 0x9900, 0x9A00, 0x9B00, 0x9C00, 0x9D00,
    0x9E00, 0x9F00, 0x0090, 0x0110, 0x0210, 0x0310, 0x0410, 0x0510,
    0x0610, 0x0710, 0x0810, 0x0910, 0x0A10, 0x0B10, 0x0C10, 0x0D10,
    0x0E10, 0x0F10, 0x1010, 0x1110, 0x1210, 0x1310, 0x1410, 0x1510,
    0x1610, 0x1710, 0x1810, 0x1910, 0x1A10, 0x1B10, 0x1C10, 0x1D10,
    0x1E10, 0x1F10, 0x2010, 0x2110, 0x2210, 0x2310, 0x2410, 0x2510,
    0x2610, 0x2710, 0x2810, 0x2910, 0x2A10, 0x2B10, 0x2C10, 0x2D10,
    0x2E10, 0x2F10, 0x3010, 0x3110, 0x3210, 0x3310, 0x3410, 0x3510,
    0x3610, 0x3710, 0x3810, 0x3910, 0x3A10, 0x3B10, 0x3C10, 0x3D10,
    0x3E10, 0x3F10, 0x4010, 0x4110, 0x4210, 0x4310, 0x4410, 0x4510,
    0x4610, 0x4710, 0x4810, 0x4910, 0x4A10, 0x4B10, 0x4C10, 0x4D10,
    0x4E10, 0x4F10, 0x5010, 0x5110, 0x5210, 0x5310, 0x5410, 0x5510,
    0x5610, 0x5710, 0x5810, 0x5910, 0x5A10, 0x5B10, 0x5C10, 0x5D10,
    0x5E10, 0x5F10, 0x6010, 0x6110, 0x6210, 0x6310, 0x6410, 0x6510,
    0x6610, 0x6710, 0x6810, 0x6910, 0x6A10, 0x6B10, 0x6C10, 0x6D10,
    0x6E10, 0x6F10, 0x7010, 0x7110, 0x7210, 0x7310, 0x7410, 0x7510,
    0x7610, 0x7710, 0x7810, 0x7910, 0x7A10, 0x7B10, 0x7C10, 0x7D10,
    0x7E10, 0x7F10, 0x8010, 0x8110, 0x8210, 0x8310, 0x8410, 0x8510,
    0x8610, 0x8710, 0x8810, 0x8910, 0x8A10, 0x8B10, 0x8C10, 0x8D10,
    0x8E10, 0x8F10, 0x9010, 0x9110, 0x9210, 0x9310, 0x9410, 0x9510,
    0x9610, 0x9710, 0x9810, 0x9910, 0x9A10, 0x9B10, 0x9C10, 0x9D10,
    0x9E10, 0x9F10, 0xA010, 0xA110, 0xA210, 0xA310, 0xA410, 0xA510,
    0xA610, 0xA710, 0xA810, 0xA910, 0xAA10, 0xAB10, 0xAC10, 0xAD10,
    0xAE10, 0xAF10, 0xB010, 0xB110, 0xB210, 0xB310, 0xB410, 0xB510,
    0xB610, 0xB710, 0xB810, 0xB910, 0xBA10, 0xBB10, 0xBC10, 0xBD10,
    0xBE10, 0xBF10, 0xC010, 0xC110, 0xC210, 0xC310, 0xC410, 0xC510,
    0xC610, 0xC710, 0xC810, 0xC910, 0xCA10, 0xCB10, 0xCC10, 0xCD10,
    0xCE10, 0xCF10, 0xD010, 0xD110, 0xD210, 0xD310, 0xD410, 0xD510,
    0xD610, 0xD710, 0xD810, 0xD910, 0xDA10, 0xDB10, 0xDC10, 0xDD10,
    0xDE10, 0xDF10, 0xE010, 0xE110, 0xE210, 0xE310, 0xE410, 0xE510,
    0xE610, 0xE710, 0xE810, 0xE910, 0xEA10, 0xEB10, 0xEC10, 0xED10,
    0xEE10, 0xEF10, 0xF010, 0xF110, 0xF210, 0xF310, 0xF410, 0xF510,
    0xF610, 0xF710, 0xF810, 0xF910, 0xFA10, 0xFB10, 0xFC10, 0xFD10,
    0xFE10, 0xFF10, 0x0090, 0x0110, 0x0210, 0x0310, 0x0410, 0x0510,
    0x0610, 0x0710, 0x0810, 0x0910, 0x0A10, 0x0B10, 0x0C10, 0x0D10,
    0x0E10, 0x0F10, 0x1010, 0x1110, 0x1210, 0x1310, 0x1410, 0x1510,
    0x1610, 0x1710, 0x1810, 0x1910, 0x1A10, 0x1B10, 0x1C10, 0x1D10,
    0x1E10, 0x1F10, 0x2010, 0x2110, 0x2210, 0x2310, 0x2410, 0x2510,
    0x2610, 0x2710, 0x2810, 0x2910, 0x2A10, 0x2B10, 0x2C10, 0x2D10,
    0x2E10, 0x2F10, 0x3010, 0x3110, 0x3210, 0x3310, 0x3410, 0x3510,
    0x3610, 0x3710, 0x3810, 0x3910, 0x3A10, 0x3B10, 0x3C10, 0x3D10,
    0x3E10, 0x3F10, 0x4010, 0x4110, 0x4210, 0x4310, 0x4410, 0x4510,
    0x4610, 0x4710, 0x4810, 0x4910, 0x4A10, 0x4B10, 0x4C10, 0x4D10,
    0x4E10, 0x4F10, 0x5010, 0x5110, 0x5210, 0x5310, 0x5410, 0x5510,
    0x5610, 0x5710, 0x5810, 0x5910, 0x5A10, 0x5B10, 0x5C10, 0x5D10,
    0x5E10, 0x5F10, 0x6010, 0x6110, 0x6210, 0x6310, 0x6410, 0x6510,
    0x00C0, 0x0140, 0x0240, 0x0340, 0x0440, 0x0540, 0x0640, 0x0740,
    0x0840, 0x0940, 0x0A40, 0x0B40, 0x0C40, 0x0D40, 0x0E40, 0x0F40,
    0x1040, 0x1140, 0x1240, 0x1340, 0x1440, 0x1540, 0x1640, 0x1740,
    0x1840, 0x1940, 0x1A40, 0x1B40, 0x1C40, 0x1D40, 0x1E40, 0x1F40,
    0x2040, 0x2140, 0x2240, 0x2340, 0x2440, 0x2540, 0x2640, 0x2740,
    0x2840, 0x2940, 0x2A40, 0x2B40, 0x2C40, 0x2D40, 0x2E40, 0x2F40,
    0x3040, 0x3140, 0x3240, 0x3340, 0x3440, 0x3540, 0x3640, 0x3740,
    0x3840, 0x3940, 0x3A40, 0x3B40, 0x3C40, 0x3D40, 0x3E40, 0x3F40,
    0x4040, 0x4140, 0x4240, 0x4340, 0x4440, 0x4540, 0x4640, 0x4740,
    0x4840, 0x4940, 0x4A40, 0x4B40, 0x4C40, 0x4D40, 0x4E40, 0x4F40,
    0x5040, 0x5140, 0x5240, 0x5340, 0x5440, 0x5540, 0x5640, 0x5740,
    0x5840, 0x5940, 0x5A40, 0x5B40, 0x5C40, 0x5D40, 0x5E40, 0x5F40,
    0x6040, 0x6140, 0x6240, 0x6340, 0x6440, 0x6540, 0x6640, 0x6740,
    0x6840, 0x6940, 0x6A40, 0x6B40, 0x6C40, 0x6D40, 0x6E40, 0x6F40,
    0x7040, 0x7140, 0x7240, 0x7340, 0x7440, 0x7540, 0x7640, 0x7740,
    0x7840, 0x7940, 0x7A40, 0x7B40, 0x7C40, 0x7D40, 0x7E40, 0x7F40,
    0x8040, 0x8140, 0x8240, 0x8340, 0x8440, 0x8540, 0x8640, 0x8740,
    0x8840, 0x8940, 0x8A40, 0x8B40, 0x8C40, 0x8D40, 0x8E40, 0x8F40,
    0x9040, 0x9140, 0x9240, 0x9340, 0x9440, 0x9540, 0x9640, 0x9740,
    0x9840, 0x9940, 0x9A40, 0x9B40, 0x9C40, 0x9D40, 0x9E40, 0x9F40,
    0xA040, 0xA140, 0xA240, 0xA340, 0xA440, 0xA540, 0xA640, 0xA740,
    0xA840, 0xA940, 0xAA40, 0xAB40, 0xAC40, 0xAD40, 0xAE40, 0xAF40,
    0xB040, 0xB140, 0xB240, 0xB340, 0xB440, 0xB540, 0xB640, 0xB740,
    0xB840, 0xB940, 0xBA40, 0xBB40, 0xBC40, 0xBD40, 0xBE40, 0xBF40,
    0xC040, 0xC140, 0xC240, 0xC340, 0xC440, 0xC540, 0xC640, 0xC740,
    0xC840, 0xC940, 0xCA40, 0xCB40, 0xCC40, 0xCD40, 0xCE40, 0xCF40,
    0xD040, 0xD140, 0xD240, 0xD340, 0xD440, 0xD540, 0xD640, 0xD740,
    0xD840, 0xD940, 0xDA40, 0xDB40, 0xDC40, 0xDD40, 0xDE40, 0xDF40,
    0xE040, 0xE140, 0xE240, 0xE340, 0xE440, 0xE540, 0xE640, 0xE740,
    0xE840, 0xE940, 0xEA40, 0xEB40, 0xEC40, 0xED40, 0xEE40, 0xEF40,
    0xF040, 0xF140, 0xF240, 0xF340, 0xF440, 0xF540, 0xF640, 0xF740,
    0xF840, 0xF940, 0xFA40, 0xFB40, 0xFC40, 0xFD40, 0xFE40, 0xFF40,
    0xA050, 0xA150, 0xA250, 0xA350, 0xA450, 0xA550, 0xA650, 0xA750,
    0xA850, 0xA950, 0xAA50, 0xAB50, 0xAC50, 0xAD50, 0xAE50, 0xAF50,
    0xB050, 0xB150, 0xB250, 0xB350, 0xB450, 0xB550, 0xB650, 0xB750,
    0xB850, 0xB950, 0xBA50, 0xBB50, 0xBC50, 0xBD50, 0xBE50, 0xBF50,
    0xC050, 0xC150, 0xC250, 0xC350, 0xC450, 0xC550, 0xC650, 0xC750,
    0xC850, 0xC950, 0xCA50, 0xCB50, 0xCC50, 0xCD50, 0xCE50, 0xCF50,
    0xD050, 0xD150, 0xD250, 0xD350, 0xD450, 0xD550, 0xD650, 0xD750,
    0xD850, 0xD950, 0xDA50, 0xDB50, 0xDC50, 0xDD50, 0xDE50, 0xDF50,
    0xE050, 0xE150, 0xE250, 0xE350, 0xE450, 0xE550, 0xE650, 0xE750,
    0xE850, 0xE950, 0xEA50, 0xEB50, 0xEC50, 0xED50, 0xEE50, 0xEF50,
    0xF050, 0xF150, 0xF250, 0xF350, 0xF450, 0xF550, 0xF650, 0xF750,
    0xF850, 0xF950, 0xFA50, 0xFB50, 0xFC50, 0xFD50, 0xFE50, 0xFF50,
    0x00D0, 0x0150, 0x0250, 0x0350, 0x0450, 0x0550, 0x0650, 0x0750,
    0x0850, 0x0950, 0x0A50, 0x0B50, 0x0C50, 0x0D50, 0x0E50, 0x0F50,
    0x1050, 0x1150, 0x1250, 0x1350, 0x1450, 0x1550, 0x1650, 0x1750,
    0x1850, 0x1950, 0x1A50, 0x1B50, 0x1C50, 0x1D50, 0x1E50, 0x1F50,
    0x2050, 0x2150, 0x2250, 0x2350, 0x2450, 0x2550, 0x2650, 0x2750,
    0x2850, 0x2950, 0x2A50, 0x2B50, 0x2C50, 0x2D50, 0x2E50, 0x2F50,
    0x3050, 0x3150, 0x3250, 0x3350, 0x3450, 0x3550, 0x3650, 0x3750,
    0x3850, 0x3950, 0x3A50, 0x3B50, 0x3C50, 0x3D50, 0x3E50, 0x3F50,
    0x4050, 0x4150, 0x4250, 0x4350, 0x4450, 0x4550, 0x4650, 0x4750,
    0x4850, 0x4950, 0x4A50, 0x4B50, 0x4C50, 0x4D50, 0x4E50, 0x4F50,
    0x5050, 0x5150, 0x5250, 0x5350, 0x5450, 0x5550, 0x5650, 0x5750,
    0x5850, 0x5950, 0x5A50, 0x5B50, 0x5C50, 0x5D50, 0x5E50, 0x5F50,
    0x6050, 0x6150, 0x6250, 0x6350, 0x6450, 0x6550, 0x6650, 0x6750,
    0x6850, 0x6950, 0x6A50, 0x6B50, 0x6C50, 0x6D50, 0x6E50, 0x6F50,
    0x7050, 0x7150, 0x7250, 0x7350, 0x7450, 0x7550, 0x7650, 0x7750,
    0x7850, 0x7950, 0x7A50, 0x7B50, 0x7C50, 0x7D50, 0x7E50, 0x7F50,
    0x8050, 0x8150, 0x8250, 0x8350, 0x8450, 0x8550, 0x8650, 0x8750,
    0x8850, 0x8950, 0x8A50, 0x8B50, 0x8C50, 0x8D50, 0x8E50, 0x8F50,
    0x9050, 0x9150, 0x9250, 0x9350, 0x9450, 0x9550, 0x9650, 0x9750,
    0x9850, 0x9950, 0x9A50, 0x9B50, 0x9C50, 0x9D50, 0x9E50, 0x9F50,
    0xFA40, 0xFB40, 0xFC40, 0xFD40, 0xFE40, 0xFF40, 0x00C0, 0x0140,
    0x0240, 0x0340, 0x0440, 0x0540, 0x0640, 0x0740, 0x0840, 0x0940,
    0x0A40, 0x0B40, 0x0C40, 0x0D40, 0x0E40, 0x0F40, 0x1040, 0x1140,
    0x1240, 0x1340, 0x1440, 0x1540, 0x1640, 0x1740, 0x1840, 0x1940,
    0x1A40, 0x1B40, 0x1C40, 0x1D40, 0x1E40, 0x1F40, 0x2040, 0x2140,
    0x2240, 0x2340, 0x2440, 0x2540, 0x2640, 0x2740, 0x2840, 0x2940,
    0x2A40, 0x2B40, 0x2C40, 0x2D40, 0x2E40, 0x2F40, 0x3040, 0x3140,
    0x3240, 0x3340, 0x3440, 0x3540, 0x3640, 0x3740, 0x3840, 0x3940,
    0x3A40, 0x3B40, 0x3C40, 0x3D40, 0x3E40, 0x3F40, 0x4040, 0x4140,
    0x4240, 0x4340, 0x4440, 0x4540, 0x4640, 0x4740, 0x4840, 0x4940,
    0x4A40, 0x4B40, 0x4C40, 0x4D40, 0x4E40, 0x4F40, 0x5040, 0x5140,
    0x5240, 0x5340, 0x5440, 0x5540, 0x5640, 0x5740, 0x5840, 0x5940,
    0x5A40, 0x5B40, 0x5C40, 0x5D40, 0x5E40, 0x5F40, 0x6040, 0x6140,
    0x6240, 0x6340, 0x6440, 0x6540, 0x6640, 0x6740, 0x6840, 0x6940,
    0x6A40, 0x6B40, 0x6C40, 0x6D40, 0x6E40, 0x6F40, 0x7040, 0x7140,
    0x7240, 0x7340, 0x7440, 0x7540, 0x7640, 0x7740, 0x7840, 0x7940,
    0x7A40, 0x7B40, 0x7C40, 0x7D40, 0x7E40, 0x7F40, 0x8040, 0x8140,
    0x8240, 0x8340, 0x8440, 0x8540, 0x8640, 0x8740, 0x8840, 0x8940,
    0x8A40, 0x8B40, 0x8C40, 0x8D40, 0x8E40, 0x8F40, 0x9040, 0x9140,
    0x9240, 0x9340, 0x9440, 0x9540, 0x9640, 0x9740, 0x9840, 0x9940,
    0x9A40, 0x9B40, 0x9C40, 0x9D40, 0x9E40, 0x9F40, 0xA040, 0xA140,
    0xA240, 0xA340, 0xA440, 0xA540, 0xA640, 0xA740, 0xA840, 0xA940,
    0xAA40, 0xAB40, 0xAC40, 0xAD40, 0xAE40, 0xAF40, 0xB040, 0xB140,
    0xB240, 0xB340, 0xB440, 0xB540, 0xB640, 0xB740, 0xB840, 0xB940,
    0xBA40, 0xBB40, 0xBC40, 0xBD40, 0xBE40, 0xBF40, 0xC040, 0xC140,
    0xC240, 0xC340, 0xC440, 0xC540, 0xC640, 0xC740, 0xC840, 0xC940,
    0xCA40, 0xCB40, 0xCC40, 0xCD40, 0xCE40, 0xCF40, 0xD040, 0xD140,
    0xD240, 0xD340, 0xD440, 0xD540, 0xD640, 0xD740, 0xD840, 0xD940,
    0xDA40, 0xDB40, 0xDC40, 0xDD40, 0xDE40, 0xDF40, 0xE040, 0xE140,
    0xE240, 0xE340, 0xE440, 0xE540, 0xE640, 0xE740, 0xE840, 0xE940,
    0xEA40, 0xEB40, 0xEC40, 0xED40, 0xEE40, 0xEF40, 0xF040, 0xF140,
    0xF240, 0xF340, 0xF440, 0xF540, 0xF640, 0xF740, 0xF840, 0xF940,
    0x9A50, 0x9B50, 0x9C50, 0x9D50, 0x9E50, 0x9F50, 0xA050, 0xA150,
    0xA250, 0xA350, 0xA450, 0xA550, 0xA650, 0xA750, 0xA850, 0xA950,
    0xAA50, 0xAB50, 0xAC50, 0xAD50, 0xAE50, 0xAF50, 0xB050, 0xB150,
    0xB250, 0xB350, 0xB450, 0xB550, 0xB650, 0xB750, 0xB850, 0xB950,
    0xBA50, 0xBB50, 0xBC50, 0xBD50, 0xBE50, 0xBF50, 0xC050, 0xC150,
    0xC250, 0xC350, 0xC450, 0xC550, 0xC650, 0xC750, 0xC850, 0xC950,
    0xCA50, 0xCB50, 0xCC50, 0xCD50, 0xCE50, 0xCF50, 0xD050, 0xD150,
    0xD250, 0xD350, 0xD450, 0xD550, 0xD650, 0xD750, 0xD850, 0xD950,
    0xDA50, 0xDB50, 0xDC50, 0xDD50, 0xDE50, 0xDF50, 0xE050, 0xE150,
    0xE250, 0xE350, 0xE450, 0xE550, 0xE650, 0xE750, 0xE850, 0xE950,
    0xEA50, 0xEB50, 0xEC50, 0xED50, 0xEE50, 0xEF50, 0xF050, 0xF150,
    0xF250, 0xF350, 0xF450, 0xF550, 0xF650, 0xF750, 0xF850, 0xF950,
    0xFA50, 0xFB50, 0xFC50, 0xFD50, 0xFE50, 0xFF50, 0x00D0, 0x0150,
    0x0250, 0x0350, 0x0450, 0x0550, 0x0650, 0x0750, 0x0850, 0x0950,
    0x0A50, 0x0B50, 0x0C50, 0x0D50, 0x0E50, 0x0F50, 0x1050, 0x1150,
    0x1250, 0x1350, 0x1450, 0x1550, 0x1650, 0x1750, 0x1850, 0x1950,
    0x1A50, 0x1B50, 0x1C50, 0x1D50, 0x1E50, 0x1F50, 0x2050, 0x2150,
    0x2250, 0x2350, 0x2450, 0x2550, 0x2650, 0x2750, 0x2850, 0x2950,
    0x2A50, 0x2B50, 0x2C50, 0x2D50, 0x2E50, 0x2F50, 0x3050, 0x3150,
    0x3250, 0x3350, 0x3450, 0x3550, 0x3650, 0x3750, 0x3850, 0x3950,
    0x3A50, 0x3B50, 0x3C50, 0x3D50, 0x3E50, 0x3F50, 0x4050, 0x4150,
    0x4250, 0x4350, 0x4450, 0x4550, 0x4650, 0x4750, 0x4850, 0x4950,
    0x4A50, 0x4B50, 0x4C50, 0x4D50, 0x4E50, 0x4F50, 0x5050, 0x5150,
    0x5250, 0x5350, 0x5450, 0x5550, 0x5650, 0x5750, 0x5850, 0x5950,
    0x5A50, 0x5B50, 0x5C50, 0x5D50, 0x5E50, 0x5F50, 0x6050, 0x6150,
    0x6250, 0x6350, 0x6450, 0x6550, 0x6650, 0x6750, 0x6850, 0x6950,
    0x6A50, 0x6B50, 0x6C50, 0x6D50, 0x6E50, 0x6F50, 0x7050, 0x7150,
    0x7250, 0x7350, 0x7450, 0x7550, 0x7650, 0x7750, 0x7850, 0x7950,
    0x7A50, 0x7B50, 0x7C50, 0x7D50, 0x7E50, 0x7F50, 0x8050, 0x8150,
    0x8250, 0x8350, 0x8450, 0x8550, 0x8650, 0x8750, 0x8850, 0x8950,
    0x8A50, 0x8B50, 0x8C50, 0x8D50, 0x8E50, 0x8F50, 0x9050, 0x9150,
    0x9250, 0x9350, 0x9450, 0x9550, 0x9650, 0x9750, 0x9850, 0x9950,
};

uint8_t ZeroTable[256] = {
    0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0
};

// Title checksums that are treated specially by the CGB boot ROM
static const uint8_t gbColorizationChecksums[79] = {
    0x00, 0x88, 0x16, 0x36, 0xD1, 0xDB, 0xF2, 0x3C, 0x8C, 0x92, 0x3D, 0x5C,
    0x58, 0xC9, 0x3E, 0x70, 0x1D, 0x59, 0x69, 0x19, 0x35, 0xA8, 0x14, 0xAA,
    0x75, 0x95, 0x99, 0x34, 0x6F, 0x15, 0xFF, 0x97, 0x4B, 0x90, 0x17, 0x10,
    0x39, 0xF7, 0xF6, 0xA2, 0x49, 0x4E, 0x43, 0x68, 0xE0, 0x8B, 0xF0, 0xCE,
    0x0C, 0x29, 0xE8, 0xB7, 0x86, 0x9A, 0x52, 0x01, 0x9D, 0x71, 0x9C, 0xBD,
    0x5D, 0x6D, 0x67, 0x3F, 0x6B, 0xB3, 0x46, 0x28, 0xA5, 0xC6, 0xD3, 0x27,
    0x61, 0x18, 0x66, 0x6A, 0xBF, 0x0D, 0xF4
};

// For titles with colliding checksums, the fourth character of the game title
// for disambiguation.
static const uint8_t gbColorizationDisambigChars[29] = {
    'B', 'E', 'F', 'A', 'A', 'R', 'B', 'E',
    'K', 'E', 'K', ' ', 'R', '-', 'U', 'R',
    'A', 'R', ' ', 'I', 'N', 'A', 'I', 'L',
    'I', 'C', 'E', ' ', 'R'
};

// Palette ID | (Flags << 5)
static const uint8_t gbColorizationPaletteInfo[94] = {
    0x7C, 0x08, 0x12, 0xA3, 0xA2, 0x07, 0x87, 0x4B, 0x20, 0x12, 0x65, 0xA8,
    0x16, 0xA9, 0x86, 0xB1, 0x68, 0xA0, 0x87, 0x66, 0x12, 0xA1, 0x30, 0x3C,
    0x12, 0x85, 0x12, 0x64, 0x1B, 0x07, 0x06, 0x6F, 0x6E, 0x6E, 0xAE, 0xAF,
    0x6F, 0xB2, 0xAF, 0xB2, 0xA8, 0xAB, 0x6F, 0xAF, 0x86, 0xAE, 0xA2, 0xA2,
    0x12, 0xAF, 0x13, 0x12, 0xA1, 0x6E, 0xAF, 0xAF, 0xAD, 0x06, 0x4C, 0x6E,
    0xAF, 0xAF, 0x12, 0x7C, 0xAC, 0xA8, 0x6A, 0x6E, 0x13, 0xA0, 0x2D, 0xA8,
    0x2B, 0xAC, 0x64, 0xAC, 0x6D, 0x87, 0xBC, 0x60, 0xB4, 0x13, 0x72, 0x7C,
    0xB5, 0xAE, 0xAE, 0x7C, 0x7C, 0x65, 0xA2, 0x6C, 0x64, 0x85
};

// Uncompressed palette data from the CGB boot ROM
static const uint16_t gbColorizationPaletteData[32][3][4] = {
    {
        { 0x7FFF, 0x01DF, 0x0112, 0x0000 },
        { 0x7FFF, 0x7EEB, 0x001F, 0x7C00 },
        { 0x7FFF, 0x42B5, 0x3DC8, 0x0000 },
    },
    {
        { 0x231F, 0x035F, 0x00F2, 0x0009 },
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x4FFF, 0x7ED2, 0x3A4C, 0x1CE0 },
    },
    {
        { 0x7FFF, 0x7FFF, 0x7E8C, 0x7C00 },
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
        { 0x03ED, 0x7FFF, 0x255F, 0x0000 },
    },
    {
        { 0x7FFF, 0x7FFF, 0x7E8C, 0x7C00 },
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x036A, 0x021F, 0x03FF, 0x7FFF },
    },
    {
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x03EF, 0x01D6, 0x0000 },
    },
    {
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x7EEB, 0x001F, 0x7C00 },
        { 0x7FFF, 0x03EA, 0x011F, 0x0000 },
    },
    {
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x7EEB, 0x001F, 0x7C00 },
        { 0x7FFF, 0x027F, 0x001F, 0x0000 },
    },
    {
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x7FFF, 0x7EEB, 0x001F, 0x7C00 },
        { 0x7FFF, 0x03FF, 0x001F, 0x0000 },
    },
    {
        { 0x299F, 0x001A, 0x000C, 0x0000 },
        { 0x7C00, 0x7FFF, 0x3FFF, 0x7E00 },
        { 0x7E74, 0x03FF, 0x0180, 0x0000 },
    },
    {
        { 0x7FFF, 0x01DF, 0x0112, 0x0000 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x67FF, 0x77AC, 0x1A13, 0x2D6B },
    },
    {
        { 0x0000, 0x7FFF, 0x421F, 0x1CF2 },
        { 0x0000, 0x7FFF, 0x421F, 0x1CF2 },
        { 0x7ED6, 0x4BFF, 0x2175, 0x0000 },
    },
    {
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x3FFF, 0x7E00, 0x001F },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
    },
    {
        { 0x231F, 0x035F, 0x00F2, 0x0009 },
        { 0x7FFF, 0x7EEB, 0x001F, 0x7C00 },
        { 0x7FFF, 0x6E31, 0x454A, 0x0000 },
    },
    {
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
        { 0x7FFF, 0x6E31, 0x454A, 0x0000 },
    },
    {
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x7FFF, 0x1BEF, 0x0200, 0x0000 },
    },
    {
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x7FFF, 0x1BEF, 0x0200, 0x0000 },
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
    },
    {
        { 0x7FFF, 0x1BEF, 0x0200, 0x0000 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
    },
    {
        { 0x7FFF, 0x03E0, 0x0206, 0x0120 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
    },
    {
        { 0x7FFF, 0x1BEF, 0x0200, 0x0000 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
    },
    {
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x1BEF, 0x0200, 0x0000 },
        { 0x0000, 0x4200, 0x037F, 0x7FFF },
    },
    {
        { 0x03FF, 0x001F, 0x000C, 0x0000 },
        { 0x7FFF, 0x1BEF, 0x0200, 0x0000 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
    },
    {
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x7FFF, 0x42B5, 0x3DC8, 0x0000 },
    },
    {
        { 0x7FFF, 0x5294, 0x294A, 0x0000 },
        { 0x7FFF, 0x5294, 0x294A, 0x0000 },
        { 0x7FFF, 0x5294, 0x294A, 0x0000 },
    },
    {
        { 0x7FFF, 0x1BEF, 0x0200, 0x0000 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x53FF, 0x4A5F, 0x7E52, 0x0000 },
    },
    {
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x1BEF, 0x0200, 0x0000 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
    },
    {
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
        { 0x639F, 0x4279, 0x15B0, 0x04CB },
    },
    {
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x7FFF, 0x1BEF, 0x0200, 0x0000 },
        { 0x7FFF, 0x03FF, 0x012F, 0x0000 },
    },
    {
        { 0x7FFF, 0x033F, 0x0193, 0x0000 },
        { 0x7FFF, 0x033F, 0x0193, 0x0000 },
        { 0x7FFF, 0x033F, 0x0193, 0x0000 },
    },
    {
        { 0x7FFF, 0x421F, 0x1CF2, 0x0000 },
        { 0x7FFF, 0x7E8C, 0x7C00, 0x0000 },
        { 0x7FFF, 0x1BEF, 0x6180, 0x0000 },
    },
    {
        { 0x2120, 0x8022, 0x8281, 0x1110 },
        { 0xFF7F, 0xDF7F, 0x1201, 0x0001 },
        { 0xFF00, 0xFF7F, 0x1F03, 0x0000 },
    },
    {
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
    },
    {
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
        { 0x7FFF, 0x32BF, 0x00D0, 0x0000 },
    }
};

#define GBSAVE_GAME_VERSION_1 1
#define GBSAVE_GAME_VERSION_2 2
#define GBSAVE_GAME_VERSION_3 3
#define GBSAVE_GAME_VERSION_4 4
#define GBSAVE_GAME_VERSION_5 5
#define GBSAVE_GAME_VERSION_6 6
#define GBSAVE_GAME_VERSION_7 7
#define GBSAVE_GAME_VERSION_8 8
#define GBSAVE_GAME_VERSION_9 9
#define GBSAVE_GAME_VERSION_10 10
#define GBSAVE_GAME_VERSION_11 11
#define GBSAVE_GAME_VERSION_12 12
#define GBSAVE_GAME_VERSION GBSAVE_GAME_VERSION_12

void setColorizerHack(bool value)
{
    allow_colorizer_hack = value;
}

bool allowColorizerHack(void)
{
    if (gbHardware & 0xA)
        return (allow_colorizer_hack);
    return false;
}

static inline bool gbVramReadAccessValid(void)
{
    // A lot of 'ugly' checks... But only way to emulate this particular behaviour...
    if (allowColorizerHack()||
        ((gbHardware & 0xa) && ((gbLcdModeDelayed != 3) || (((register_LY == 0) && (gbScreenOn == false) && (register_LCDC & 0x80)) && (gbLcdLYIncrementTicksDelayed == (GBLY_INCREMENT_CLOCK_TICKS - GBLCD_MODE_2_CLOCK_TICKS))))) ||
        ((gbHardware & 0x5) && (gbLcdModeDelayed != 3) && ((gbLcdMode != 3) || ((register_LY == 0) && ((gbScreenOn == false) && (register_LCDC & 0x80)) && (gbLcdLYIncrementTicks == (GBLY_INCREMENT_CLOCK_TICKS - GBLCD_MODE_2_CLOCK_TICKS))))))
        return true;
    return false;
}

static inline bool gbVramWriteAccessValid(void)
{
    if (allowColorizerHack() ||
        // No access to Vram during mode 3
        // (used to emulate the gfx differences between GB & GBC-GBA/SP in Stunt Racer)
        (gbLcdModeDelayed != 3) ||
        // This part is used to emulate a small difference between hardwares
        // (check 8-in-1's arrow on GBA/GBC to verify it)
        ((register_LY == 0) && ((gbHardware & 0xa) && (gbScreenOn == false) && (register_LCDC & 0x80)) && (gbLcdLYIncrementTicksDelayed == (GBLY_INCREMENT_CLOCK_TICKS - GBLCD_MODE_2_CLOCK_TICKS))))
        return true;
    return false;
}

static inline bool gbCgbPaletteAccessValid(void)
{
    // No access to gbPalette during mode 3 (Color Panel Demo)
    if (allowColorizerHack() ||
        ((gbLcdModeDelayed != 3) && (!((gbLcdMode == 0) && (gbLcdTicks >= (GBLCD_MODE_0_CLOCK_TICKS - gbSpritesTicks[299] - 1)))) && (!gbSpeed)) ||
        (gbSpeed && ((gbLcdMode == 1) || (gbLcdMode == 2) || ((gbLcdMode == 3) && (gbLcdTicks > (GBLCD_MODE_3_CLOCK_TICKS - 2))) || ((gbLcdMode == 0) && (gbLcdTicks <= (GBLCD_MODE_0_CLOCK_TICKS - gbSpritesTicks[299] - 2))))))
        return true;
    return false;
}

bool gbIsGameboyRom(char* file)
{
    if (strlen(file) > 4) {
        char* p = strrchr(file, '.');

        if (p != NULL) {
            if (_stricmp(p, ".gb") == 0)
                return true;
            if (_stricmp(p, ".dmg") == 0)
                return true;
            if (_stricmp(p, ".gbc") == 0)
                return true;
            if (_stricmp(p, ".cgb") == 0)
                return true;
            if (_stricmp(p, ".sgb") == 0)
                return true;
        }
    }

    return false;
}

void gbCopyMemory(uint16_t d, uint16_t s, int count)
{
    while (count) {
        gbMemoryMap[d >> 12][d & 0x0fff] = gbMemoryMap[s >> 12][s & 0x0fff];
        s++;
        d++;
        count--;
    }
}

void gbDoHdma()
{

    gbCopyMemory((gbHdmaDestination & 0x1ff0) | 0x8000,
        gbHdmaSource & 0xfff0,
        0x10);

    gbHdmaDestination += 0x10;
    if (gbHdmaDestination == 0xa000)
        gbHdmaDestination = 0x8000;

    gbHdmaSource += 0x10;
    if (gbHdmaSource == 0x8000)
        gbHdmaSource = 0xa000;

    register_HDMA2 = gbHdmaSource & 0xff;
    register_HDMA1 = gbHdmaSource >> 8;

    register_HDMA4 = gbHdmaDestination & 0xff;
    register_HDMA3 = gbHdmaDestination >> 8;

    gbHdmaBytes -= 0x10;
    gbMemory[0xff55] = --register_HDMA5;
    if (register_HDMA5 == 0xff)
        gbHdmaOn = 0;

    // We need to add the dmaClockticks for HDMA !
    if (gbSpeed)
        gbDmaTicks = 17;
    else
        gbDmaTicks = 9;

    if (IFF & 0x80)
        gbDmaTicks++;
}

// fix for Harley and Lego Racers
void gbCompareLYToLYC()
{
    if (register_LCDC & 0x80) {
        if (register_LY == register_LYC) {

            // mark that we have a match
            register_STAT |= 4;

            // check if we need an interrupt
            if (register_STAT & 0x40) {
                // send LCD interrupt only if no interrupt 48h signal...
                if (!gbInt48Signal) {
                    register_IF |= 2;
                }
                gbInt48Signal |= 8;
            }
        } else // no match
        {
            register_STAT &= 0xfb;
            gbInt48Signal &= ~8;
        }
    }
}

void gbWriteMemory(uint16_t address, uint8_t value)
{

    if (address < 0x8000) {
#ifndef FINAL_VERSION
        if (memorydebug && (address > 0x3fff || address < 0x2000)) {
            log("Memory register write %04x=%02x PC=%04x\n",
                address,
                value,
                PC.W);
        }

#endif
        if (g_mapper)
            (*g_mapper)(address, value);
        return;
    }

    if (address < 0xa000) {

        if (gbVramWriteAccessValid())
            gbMemoryMap[address >> 12][address & 0x0fff] = value;
        return;
    }

    // Used for the mirroring of 0xC000 in 0xE000
    if ((address >= 0xe000) && (address < 0xfe00))
        address &= ~0x2000;

    if (address < 0xc000) {
#ifndef FINAL_VERSION
        if (memorydebug) {
            log("Memory register write %04x=%02x PC=%04x\n",
                address,
                value,
                PC.W);
        }
#endif

        // Is that a correct fix ??? (it used to be 'if (g_mapper)')...
        if (g_mapperRAM)
            (*g_mapperRAM)(address, value);
        return;
    }

    if (address < 0xfe00) {
        gbMemoryMap[address >> 12][address & 0x0fff] = value;
        return;
    }

    // OAM not accessible during mode 2 & 3.
    if (address < 0xfea0) {
        if (((gbHardware & 0xa) && ((gbLcdMode | gbLcdModeDelayed) & 2)) || ((gbHardware & 5) && (((gbLcdModeDelayed == 2) && (gbLcdTicksDelayed <= GBLCD_MODE_2_CLOCK_TICKS)) || (gbLcdModeDelayed == 3))))
            return;
        else {
            gbMemory[address] = value;
            return;
        }
    }

    if ((address > 0xfea0) && (address < 0xff00)) { // GBC allows reading/writing to that area
        gbMemory[address] = value;
        return;
    }

    switch (address & 0x00ff) {

    case 0x00: {
        gbMemory[0xff00] = ((gbMemory[0xff00] & 0xcf) | (value & 0x30) | 0xc0);
        if (gbSgbMode) {
            gbSgbDoBitTransfer(value);
        }
        return;
    }

    case 0x01: {
        gbMemory[0xff01] = value;
        return;
    }

    // serial control
    case 0x02: {
        gbSerialOn = (value & 0x80);
#ifndef NO_LINK
        //trying to detect whether the game has exited multiplay mode, pokemon blue start w/ 0x7e while pocket racing start w/ 0x7c
        if (EmuReseted || (gbMemory[0xff02] & 0x7c) || (value & 0x7c) || (!(value & 0x81))) {
            LinkFirstTime = true;
        }
        EmuReseted = false;
        gbMemory[0xff02] = value;
        if (gbSerialOn && (GetLinkMode() == LINK_GAMEBOY_IPC || GetLinkMode() == LINK_GAMEBOY_SOCKET
        || GetLinkMode() == LINK_DISCONNECTED || coreOptions.winGbPrinterEnabled)) {

            gbSerialTicks = GBSERIAL_CLOCK_TICKS;

            LinkIsWaiting = true;

            //Do data exchange, master initiate the transfer
            //may cause visual artifact if not processed immediately, is it due to IRQ stuff or invalid data being exchanged?
            if ((value & 1)) { //internal clock
                if (gbSerialFunction) {
                    gbSIO_SC = value;
                    gbMemory[0xff01] = gbSerialFunction(gbMemory[0xff01]); //gbSerialFunction/gbStartLink/gbPrinter
                } else
                    gbMemory[0xff01] = 0xff;
                gbMemory[0xff02] &= 0x7f;
                gbSerialOn = 0;
                gbMemory[0xff0f] = register_IF |= 8;
            }
#ifdef OLD_GB_LINK
            if (linkConnected) {
                if (value & 1) {
                    linkSendByte(0x100 | gbMemory[0xFF01]);
                    Sleep(5);
                }
            }
#endif
        }
#else
        gbMemory[0xff02] = value;
        if (gbSerialOn)
            gbSerialTicks = GBSERIAL_CLOCK_TICKS;
#endif
        gbSerialBits = 0;
        return;
    }

    case 0x04: {
        // DIV register resets on any write
        // (not totally perfect, but better than nothing)
        gbMemory[0xff04] = register_DIV = 0;
        gbDivTicks = GBDIV_CLOCK_TICKS;
        // Another weird timer 'bug' :
        // Writing to DIV register resets the internal timer,
        // and can also increase TIMA/trigger an interrupt
        // in some cases...
        if (gbTimerOn && !(gbInternalTimer & (gbTimerClockTicks >> 1))) {
            gbMemory[0xff05] = ++register_TIMA;
            if (register_TIMA == 0) {
                // timer overflow!

                // reload timer modulo
                gbMemory[0xff05] = register_TIMA = register_TMA;

                // flag interrupt
                gbMemory[0xff0f] = register_IF |= 4;
            }
        }
        gbInternalTimer = 0xff;
        return;
    }
    case 0x05:
        gbMemory[0xff05] = register_TIMA = value;
        return;

    case 0x06:
        gbMemory[0xff06] = register_TMA = value;
        return;

    // TIMER control
    case 0x07: {

        gbTimerModeChange = (((value & 3) != (register_TAC & 3)) && (value & register_TAC & 4)) ? true : false;
        gbTimerOnChange = (((value ^ register_TAC) & 4) == 4) ? true : false;

        gbTimerOn = (value & 4);

        if (gbTimerOnChange || gbTimerModeChange) {
            gbTimerMode = value & 3;

            switch (gbTimerMode) {
            case 0:
                gbTimerClockTicks = GBTIMER_MODE_0_CLOCK_TICKS;
                break;
            case 1:
                gbTimerClockTicks = GBTIMER_MODE_1_CLOCK_TICKS;
                break;
            case 2:
                gbTimerClockTicks = GBTIMER_MODE_2_CLOCK_TICKS;
                break;
            case 3:
                gbTimerClockTicks = GBTIMER_MODE_3_CLOCK_TICKS;
                break;
            }
        }

        // This looks weird, but this emulates a bug in which register_TIMA
        // is increased when writing to register_TAC
        // (This fixes Korodice's long-delay between menus bug).

        if (gbTimerOnChange || gbTimerModeChange) {
            bool temp = false;

            if ((gbTimerOn && !gbTimerModeChange) && (gbTimerMode & 2) && !(gbInternalTimer & 0x80) && (gbInternalTimer & (gbTimerClockTicks >> 1)) && !(gbInternalTimer & (gbTimerClockTicks >> 5)))
                temp = true;
            else if ((!gbTimerOn && !gbTimerModeChange && gbTimerOnChange) && ((gbTimerTicks - 1) < (gbTimerClockTicks >> 1)))
                temp = true;
            else if (gbTimerOn && gbTimerModeChange && !gbTimerOnChange) {
                switch (gbTimerMode & 3) {
                case 0x00:
                    temp = false;
                    break;
                case 0x01:
                    if (((gbInternalTimer & 0x82) == 2) && (gbTimerTicks > (clockTicks + 1)))
                        temp = true;
                    break;
                case 0x02:
                    if (((gbInternalTimer & 0x88) == 0x8) && (gbTimerTicks > (clockTicks + 1)))
                        temp = true;
                    break;
                case 0x03:
                    if (((gbInternalTimer & 0xA0) == 0x20) && (gbTimerTicks > (clockTicks + 1)))
                        temp = true;
                    break;
                }
            }

            if (temp) {
                gbMemory[0xff05] = ++register_TIMA;
                if ((register_TIMA & 0xff) == 0) {
                    // timer overflow!

                    // reload timer modulo
                    gbMemory[0xff05] = register_TIMA = register_TMA;

                    // flag interrupt
                    gbMemory[0xff0f] = register_IF |= 4;
                }
            }
        }
        gbMemory[0xff07] = register_TAC = value;
        return;
    }

    case 0x0f: {
        gbMemory[0xff0f] = register_IF = value;
        //gbMemory[0xff0f] = register_IE |= value;
        return;
    }

    case 0x10:
    case 0x11:
    case 0x12:
    case 0x13:
    case 0x14:
    case 0x15:
    case 0x16:
    case 0x17:
    case 0x18:
    case 0x19:
    case 0x1a:
    case 0x1b:
    case 0x1c:
    case 0x1d:
    case 0x1e:
    case 0x1f:
    case 0x20:
    case 0x21:
    case 0x22:
    case 0x23:
    case 0x24:
    case 0x25:
    case 0x26:
    case 0x27:
    case 0x28:
    case 0x29:
    case 0x2a:
    case 0x2b:
    case 0x2c:
    case 0x2d:
    case 0x2e:
    case 0x2f:
    case 0x30:
    case 0x31:
    case 0x32:
    case 0x33:
    case 0x34:
    case 0x35:
    case 0x36:
    case 0x37:
    case 0x38:
    case 0x39:
    case 0x3a:
    case 0x3b:
    case 0x3c:
    case 0x3d:
    case 0x3e:
    case 0x3f:
        // Sound registers handled by blargg
        gbSoundEvent(soundTicks, address, value);
        //gbMemory[address] = value;
        return;

    case 0x40: {
        int lcdChange = (register_LCDC & 0x80) ^ (value & 0x80);

        // don't draw the window if it was not enabled and not being drawn before
        if (!(register_LCDC & 0x20) && (value & 0x20) && gbWindowLine == -1 && register_LY > register_WY)
            gbWindowLine = 146;
        // 007 fix : don't draw the first window's 1st line if it's enable 'too late'
        // (ie. if register_LY == register_WY when enabling it)
        // and move it to the next line
        else if (!(register_LCDC & 0x20) && (value & 0x20) && (register_LY == register_WY))
            gbWindowLine = -2;

        gbMemory[0xff40] = register_LCDC = value;

        if (lcdChange) {
            if ((value & 0x80) && (!register_LCDCBusy)) {

                //  if (!gbWhiteScreen && !gbSgbMask)

                //  systemDrawScreen();

                gbRegisterLYLCDCOffOn = (register_LY + kGBHeight) % 154;

                gbLcdTicks = GBLCD_MODE_2_CLOCK_TICKS - (gbSpeed ? 2 : 1);
                gbLcdTicksDelayed = GBLCD_MODE_2_CLOCK_TICKS - (gbSpeed ? 1 : 0);
                gbLcdLYIncrementTicks = GBLY_INCREMENT_CLOCK_TICKS - (gbSpeed ? 2 : 1);
                gbLcdLYIncrementTicksDelayed = GBLY_INCREMENT_CLOCK_TICKS - (gbSpeed ? 1 : 0);
                gbLcdMode = 2;
                gbLcdModeDelayed = 2;
                gbMemory[0xff41] = register_STAT = (register_STAT & 0xfc) | 2;
                gbMemory[0xff44] = register_LY = 0x00;
                gbInt48Signal = 0;
                gbLYChangeHappened = false;
                gbLCDChangeHappened = false;
                gbWindowLine = 146;
                oldRegister_WY = 146;

                // Fix for Namco Gallery Vol.2
                // (along with updating register_LCDC at the start of 'case 0x40') :
                if (register_STAT & 0x20) {
                    // send LCD interrupt only if no interrupt 48h signal...
                    if (!gbInt48Signal) {
                        gbMemory[0xff0f] = register_IF |= 2;
                    }
                    gbInt48Signal |= 4;
                }
                gbCompareLYToLYC();

            } else {

                register_LCDCBusy = clockTicks + 6;

                //used to update the screen with white lines when it's off.
                //(it looks strange, but it's pretty accurate)

                gbWhiteScreen = 0;

                gbScreenTicks = ((150 - register_LY) * GBLY_INCREMENT_CLOCK_TICKS + (49 << (gbSpeed ? 1 : 0)));

                // disable the screen rendering
                gbScreenOn = false;
                gbLcdTicks = 0;
                gbLcdMode = 0;
                gbLcdModeDelayed = 0;
                gbMemory[0xff41] = register_STAT &= 0xfc;
                gbInt48Signal = 0;
            }
        }
        return;
    }

    // STAT
    case 0x41: {
        //register_STAT = (register_STAT & 0x87) |
        //      (value & 0x7c);
        gbMemory[0xff41] = register_STAT = (value & 0xf8) | (register_STAT & 0x07); // fix ?
        // TODO:
        // GB bug from Devrs FAQ
        // http://www.devrs.com/gb/files/faqs.html#GBBugs
        // 2018-7-26 Backported STAT register bug behavior
        // Corrected : it happens if Lcd Mode < 2, but also if LY == LYC whatever
        // Lcd Mode is, and if !gbInt48Signal in all cases. The screen being off
        // doesn't matter (the bug will still happen).
        // That fixes 'Satoru Nakajima - F-1 Hero' crash bug.
        // Games below relies on this bug, , and are incompatible with the GBC.
        // - Road Rash: crash after player screen
        // - Zerg no Densetsu: crash right after showing a small portion of intro
        // - 2019-07-18 - Speedy Gonzalez status bar relies on this as well.

        if ((gbHardware & 5)
            && (((!gbInt48Signal) && (gbLcdMode < 2) && (register_LCDC & 0x80))
            || (register_LY == register_LYC))) {

            // send LCD interrupt only if no interrupt 48h signal...
            if (!gbInt48Signal)
                gbMemory[0xff0f] = register_IF |= 2;
        }

        gbInt48Signal &= ((register_STAT >> 3) & 0xF);

        if ((register_LCDC & 0x80)) {
            if ((register_STAT & 0x08) && (gbLcdMode == 0)) {
                if (!gbInt48Signal) {
                    gbMemory[0xff0f] = register_IF |= 2;
                }
                gbInt48Signal |= 1;
            }
            if ((register_STAT & 0x10) && (gbLcdMode == 1)) {
                if (!gbInt48Signal) {
                    gbMemory[0xff0f] = register_IF |= 2;
                }
                gbInt48Signal |= 2;
            }
            if ((register_STAT & 0x20) && (gbLcdMode == 2)) {
                if (!gbInt48Signal) {
                    gbMemory[0xff0f] = register_IF |= 2;
                }
                //gbInt48Signal |= 4;
            }
            gbCompareLYToLYC();

            gbMemory[0xff0f] = register_IF;
            gbMemory[0xff41] = register_STAT;
        }
        return;
    }

    // SCY
    case 0x42: {
        int temp = -1;

        if ((gbLcdMode == 3) || (gbLcdModeDelayed == 3))
            temp = ((GBLY_INCREMENT_CLOCK_TICKS - GBLCD_MODE_2_CLOCK_TICKS) - gbLcdLYIncrementTicks);

        if (temp >= 0) {
            for (int i = temp << (gbSpeed ? 1 : 2); i < 300; i++)
                if (temp < 300)
                    gbSCYLine[i] = value;
        }

        else
            memset(gbSCYLine, value, sizeof(gbSCYLine));

        gbMemory[0xff42] = register_SCY = value;
        return;
    }

    // SCX
    case 0x43: {
        int temp = -1;

        if (gbLcdModeDelayed == 3)
            temp = ((GBLY_INCREMENT_CLOCK_TICKS - GBLCD_MODE_2_CLOCK_TICKS) - gbLcdLYIncrementTicksDelayed);

        if (temp >= 0) {
            for (int i = temp << (gbSpeed ? 1 : 2); i < 300; i++)
                if (temp < 300)
                    gbSCXLine[i] = value;
        }

        else
            memset(gbSCXLine, value, sizeof(gbSCXLine));

        gbMemory[0xff43] = register_SCX = value;
        return;
    }

    // LY
    case 0x44: {
        // read only
        return;
    }

    // LYC
    case 0x45: {
        if (register_LYC != value) {
            gbMemory[0xff45] = register_LYC = value;
            if (register_LCDC & 0x80) {
                gbCompareLYToLYC();
            }
        }
        return;
    }

    // DMA!
    case 0x46: {
        int source = value * 0x0100;

        gbCopyMemory(0xfe00,
            source,
            0xa0);
        gbMemory[0xff46] = register_DMA = value;
        return;
    }

    // BGP
    case 0x47: {

        int temp = -1;

        gbMemory[0xff47] = value;

        if (gbLcdModeDelayed == 3)
            temp = ((GBLY_INCREMENT_CLOCK_TICKS - GBLCD_MODE_2_CLOCK_TICKS) - gbLcdLYIncrementTicksDelayed);

        if (temp >= 0) {
            for (int i = temp << (gbSpeed ? 1 : 2); i < 300; i++)
                if (temp < 300)
                    gbBgpLine[i] = value;
        } else
            memset(gbBgpLine, value, sizeof(gbBgpLine));

        gbBgp[0] = value & 0x03;
        gbBgp[1] = (value & 0x0c) >> 2;
        gbBgp[2] = (value & 0x30) >> 4;
        gbBgp[3] = (value & 0xc0) >> 6;
        break;
    }

    // OBP0
    case 0x48: {
        int temp = -1;

        gbMemory[0xff48] = value;

        if (gbLcdModeDelayed == 3)
            temp = ((GBLY_INCREMENT_CLOCK_TICKS - GBLCD_MODE_2_CLOCK_TICKS) - gbLcdLYIncrementTicksDelayed);

        if (temp >= 0) {
            for (int i = temp << (gbSpeed ? 1 : 2); i < 300; i++)
                if (temp < 300)
                    gbObp0Line[i] = value;
        } else
            memset(gbObp0Line, value, sizeof(gbObp0Line));

        gbObp0[0] = value & 0x03;
        gbObp0[1] = (value & 0x0c) >> 2;
        gbObp0[2] = (value & 0x30) >> 4;
        gbObp0[3] = (value & 0xc0) >> 6;
        break;
    }

    // OBP1
    case 0x49: {
        int temp = -1;

        gbMemory[0xff49] = value;

        if (gbLcdModeDelayed == 3)
            temp = ((GBLY_INCREMENT_CLOCK_TICKS - GBLCD_MODE_2_CLOCK_TICKS) - gbLcdLYIncrementTicksDelayed);

        if (temp >= 0) {
            for (int i = temp << (gbSpeed ? 1 : 2); i < 300; i++)
                if (temp < 300)
                    gbObp1Line[i] = value;
        } else
            memset(gbObp1Line, value, sizeof(gbObp1Line));

        gbObp1[0] = value & 0x03;
        gbObp1[1] = (value & 0x0c) >> 2;
        gbObp1[2] = (value & 0x30) >> 4;
        gbObp1[3] = (value & 0xc0) >> 6;
        break;
    }

    // WY
    case 0x4a:
        gbMemory[0xff4a] = register_WY = value;
        if ((register_LY <= register_WY) && ((gbWindowLine < 0) || (gbWindowLine >= (int)kGBHeight))) {
            gbWindowLine = -1;
            oldRegister_WY = register_WY;
        }
        return;

    // WX
    case 0x4b:
        gbMemory[0xff4b] = register_WX = value;
        return;

    // KEY1
    case 0x4d: {
        if (gbCgbMode) {
            gbMemory[0xff4d] = (gbMemory[0xff4d] & 0x80) | (value & 1) | 0x7e;
            return;
        }
    } break;

    // VBK
    case 0x4f: {
        if (gbCgbMode) {
            value = value & 1;
            if (value == gbVramBank)
                return;

            int vramAddress = value * 0x2000;
            gbMemoryMap[0x08] = &gbVram[vramAddress];
            gbMemoryMap[0x09] = &gbVram[vramAddress + 0x1000];

            gbVramBank = value;
            register_VBK = value;
        }
        return;
    } break;

    // BOOTROM disable register (also gbCgbMode enabler if value & 0x10 ?)
    case 0x50: {
        if (inBios && (value & 1)) {
            gbMemoryMap[0x00] = &gbRom[0x0000];
            if (gbHardware & 5) {
                memcpy((uint8_t*)(gbRom + 0x100), (uint8_t*)(gbMemory + 0x100), 0xF00);
            }
            inBios = false;
        }
    } break;

    // HDMA1
    case 0x51: {
        if (gbCgbMode) {
            if (value > 0x7f && value < 0xa0)
                value = 0;

            gbHdmaSource = (value << 8) | (gbHdmaSource & 0xf0);

            register_HDMA1 = value;
            return;
        }
    } break;

    // HDMA2
    case 0x52: {
        if (gbCgbMode) {
            value = value & 0xf0;

            gbHdmaSource = (gbHdmaSource & 0xff00) | (value);

            register_HDMA2 = value;
            return;
        }
    } break;

    // HDMA3
    case 0x53: {
        if (gbCgbMode) {
            value = value & 0x1f;
            gbHdmaDestination = (value << 8) | (gbHdmaDestination & 0xf0);
            gbHdmaDestination |= 0x8000;
            register_HDMA3 = value;
            return;
        }
    } break;

    // HDMA4
    case 0x54: {
        if (gbCgbMode) {
            value = value & 0xf0;
            gbHdmaDestination = (gbHdmaDestination & 0x1f00) | value;
            gbHdmaDestination |= 0x8000;
            register_HDMA4 = value;
            return;
        }
    } break;

    // HDMA5
    case 0x55: {

        if (gbCgbMode) {
            gbHdmaBytes = 16 + (value & 0x7f) * 16;
            if (gbHdmaOn) {
                if (value & 0x80) {
                    gbMemory[0xff55] = register_HDMA5 = (value & 0x7f);
                } else {
                    register_HDMA5 = 0xff;
                    gbHdmaOn = 0;
                }
            } else {
                if (value & 0x80) {
                    gbHdmaOn = 1;
                    gbMemory[0xff55] = register_HDMA5 = value & 0x7f;
                    if (gbLcdModeDelayed == 0)
                        gbDoHdma();
                } else {
                    // we need to take the time it takes to complete the transfer into
                    // account... according to GB DEV FAQs, the setup time is the same
                    // for single and double speed, but the actual transfer takes the
                    // same time
                    if (gbSpeed)
                        gbDmaTicks = 2 + 16 * ((value & 0x7f) + 1);
                    else
                        gbDmaTicks = 1 + 8 * ((value & 0x7f) + 1);

                    gbCopyMemory((gbHdmaDestination & 0x1ff0) | 0x8000,
                        gbHdmaSource & 0xfff0,
                        gbHdmaBytes);
                    gbHdmaDestination += gbHdmaBytes;
                    gbHdmaSource += gbHdmaBytes;

                    gbMemory[0xff51] = register_HDMA1 = 0xff; // = (gbHdmaSource >> 8) & 0xff;
                    gbMemory[0xff52] = register_HDMA2 = 0xff; // = gbHdmaSource & 0xf0;
                    gbMemory[0xff53] = register_HDMA3 = 0xff; // = ((gbHdmaDestination - 0x8000) >> 8) & 0x1f;
                    gbMemory[0xff54] = register_HDMA4 = 0xff; // = gbHdmaDestination & 0xf0;
                    gbMemory[0xff55] = register_HDMA5 = 0xff;
                }
            }
            return;
        }
    } break;

    // BCPS
    case 0x68: {
        if (gbCgbMode) {
            int paletteIndex = (value & 0x3f) >> 1;
            int paletteHiLo = (value & 0x01);

            gbMemory[0xff68] = value;

            gbMemory[0xff69] = (paletteHiLo ? (gbPalette[paletteIndex] >> 8) : (gbPalette[paletteIndex] & 0x00ff));
            return;
        }
    } break;

    // BCPD
    case 0x69: {
        if (gbCgbMode) {
            int v = gbMemory[0xff68];
            int paletteIndex = (v & 0x3f) >> 1;
            int paletteHiLo = (v & 0x01);

            if (gbCgbPaletteAccessValid()) {
                gbMemory[0xff69] = value;
                gbPalette[paletteIndex] = (paletteHiLo ? ((value << 8) | (gbPalette[paletteIndex] & 0xff)) : ((gbPalette[paletteIndex] & 0xff00) | (value))) & 0x7fff;
            }

            if (gbMemory[0xff68] & 0x80) {
                int index = ((gbMemory[0xff68] & 0x3f) + 1) & 0x3f;

                gbMemory[0xff68] = (gbMemory[0xff68] & 0x80) | index;
                gbMemory[0xff69] = (index & 1 ? (gbPalette[index >> 1] >> 8) : (gbPalette[index >> 1] & 0x00ff));
            }
            return;
        }
    } break;

    // OCPS
    case 0x6a: {
        if (gbCgbMode) {
            int paletteIndex = (value & 0x3f) >> 1;
            int paletteHiLo = (value & 0x01);

            paletteIndex += 32;

            gbMemory[0xff6a] = value;

            gbMemory[0xff6b] = (paletteHiLo ? (gbPalette[paletteIndex] >> 8) : (gbPalette[paletteIndex] & 0x00ff));
            return;
        }
    } break;

    // OCPD
    case 0x6b: {

        if (gbCgbMode) {
            int v = gbMemory[0xff6a];
            int paletteIndex = (v & 0x3f) >> 1;
            int paletteHiLo = (v & 0x01);

            paletteIndex += 32;

            if (gbCgbPaletteAccessValid()) {
                gbMemory[0xff6b] = value;
                gbPalette[paletteIndex] = (paletteHiLo ? ((value << 8) | (gbPalette[paletteIndex] & 0xff)) : ((gbPalette[paletteIndex] & 0xff00) | (value))) & 0x7fff;
            }

            if (gbMemory[0xff6a] & 0x80) {
                int index = ((gbMemory[0xff6a] & 0x3f) + 1) & 0x3f;

                gbMemory[0xff6a] = (gbMemory[0xff6a] & 0x80) | index;

                gbMemory[0xff6b] = (index & 1 ? (gbPalette[(index >> 1) + 32] >> 8) : (gbPalette[(index >> 1) + 32] & 0x00ff));
            }
            return;
        }
    } break;

    case 0x6c: {
        gbMemory[0xff6c] = 0xfe | value;
        return;
    }

    // SVBK
    case 0x70: {
        if (gbCgbMode) {
            value = value & 7;

            int bank = value;
            if (value == 0)
                bank = 1;

            if (bank == gbWramBank)
                return;

            int wramAddress = bank * 0x1000;
            gbMemoryMap[0x0d] = &gbWram[wramAddress];

            gbWramBank = bank;
            gbMemory[0xff70] = register_SVBK = value;
            return;
        }
    } break;

    case 0x75: {
        gbMemory[0xff75] = 0x8f | value;
        return;
    }

    case 0xff: {
        gbMemory[0xffff] = register_IE = value;
        return;
    }
    }

    if (address < 0xff80) {
        gbMemory[address] = value;
        return;
    }

    gbMemory[address] = value;
}

uint8_t gbReadMemory(uint16_t address)
{
    if (gbCheatMap[address])
        return gbCheatRead(address);

    if (address < 0x8000)
        return gbMemoryMap[address >> 12][address & 0x0fff];

    if (address < 0xa000) {
        if (gbVramReadAccessValid())
            return gbMemoryMap[address >> 12][address & 0x0fff];
        return 0xff;
    }

    // Used for the mirroring of 0xC000 in 0xE000
    if ((address >= 0xe000) && (address < 0xfe00))
        address &= ~0x2000;

    if (address < 0xc000) {
#ifndef FINAL_VERSION
        if (memorydebug) {
            log("Memory register read %04x PC=%04x\n",
                address,
                PC.W);
        }
#endif

        // for the 2kb ram limit (fixes crash in shawu's story
        // but now its sram test fails, as the it expects 8kb and not 2kb...
        // So use the 'genericflashcard' option to fix it).
        if (address <= (0xa000 + g_gbCartData.ram_mask())) {
            if (g_mapperReadRAM) {
                return g_mapperReadRAM(address);
            }
            return gbMemoryMap[address >> 12][address & 0x0fff];
        }
        return 0xff;
    }

    if (address >= 0xff00) {
        switch (address & 0x00ff) {
        case 0x00: {
            if (gbSgbMode) {
                gbSgbReadingController |= 4;
                gbSgbResetPacketState();
            }

            int b = gbMemory[0xff00];

            if ((b & 0x30) == 0x20) {
                b &= 0xf0;

                int joy = 0;
                if (gbSgbMode && gbSgbMultiplayer) {
                    switch (gbSgbNextController) {
                    case 0x0f:
                        joy = 0;
                        break;
                    case 0x0e:
                        joy = 1;
                        break;
                    case 0x0d:
                        joy = 2;
                        break;
                    case 0x0c:
                        joy = 3;
                        break;
                    default:
                        joy = 0;
                    }
                }
                int joystate = gbJoymask[joy];
                if (!(joystate & 128))
                    b |= 0x08;
                if (!(joystate & 64))
                    b |= 0x04;
                if (!(joystate & 32))
                    b |= 0x02;
                if (!(joystate & 16))
                    b |= 0x01;

                gbMemory[0xff00] = b;
            } else if ((b & 0x30) == 0x10) {
                b &= 0xf0;

                int joy = 0;
                if (gbSgbMode && gbSgbMultiplayer) {
                    switch (gbSgbNextController) {
                    case 0x0f:
                        joy = 0;
                        break;
                    case 0x0e:
                        joy = 1;
                        break;
                    case 0x0d:
                        joy = 2;
                        break;
                    case 0x0c:
                        joy = 3;
                        break;
                    default:
                        joy = 0;
                    }
                }
                int joystate = gbJoymask[joy];
                if (!(joystate & 8))
                    b |= 0x08;
                if (!(joystate & 4))
                    b |= 0x04;
                if (!(joystate & 2))
                    b |= 0x02;
                if (!(joystate & 1))
                    b |= 0x01;

                gbMemory[0xff00] = b;
            } else {
                if (gbSgbMode && gbSgbMultiplayer) {
                    gbMemory[0xff00] = 0xf0 | gbSgbNextController;
                } else {
                    gbMemory[0xff00] = 0xff;
                }
            }
        }
            return gbMemory[0xff00];
            break;
        case 0x01:
            return gbMemory[0xff01];
        case 0x02:
            return (gbMemory[0xff02]);
        case 0x03:
            log("Undocumented Memory register read %04x PC=%04x\n",
                address,
                PC.W);
            return 0xff;
        case 0x04:
            return register_DIV;
        case 0x05:
            return register_TIMA;
        case 0x06:
            return register_TMA;
        case 0x07:
            return (0xf8 | register_TAC);
        case 0x08:
        case 0x09:
        case 0x0a:
        case 0x0b:
        case 0x0c:
        case 0x0d:
        case 0x0e:
            log("Undocumented Memory register read %04x PC=%04x\n",
                address,
                PC.W);
            return 0xff;
        case 0x0f:
            return (0xe0 | gbMemory[0xff0f]);
        case 0x10:
        case 0x11:
        case 0x12:
        case 0x13:
        case 0x14:
        case 0x15:
        case 0x16:
        case 0x17:
        case 0x18:
        case 0x19:
        case 0x1a:
        case 0x1b:
        case 0x1c:
        case 0x1d:
        case 0x1e:
        case 0x1f:
        case 0x20:
        case 0x21:
        case 0x22:
        case 0x23:
        case 0x24:
        case 0x25:
        case 0x26:
        case 0x27:
        case 0x28:
        case 0x29:
        case 0x2a:
        case 0x2b:
        case 0x2c:
        case 0x2d:
        case 0x2e:
        case 0x2f:
        case 0x30:
        case 0x31:
        case 0x32:
        case 0x33:
        case 0x34:
        case 0x35:
        case 0x36:
        case 0x37:
        case 0x38:
        case 0x39:
        case 0x3a:
        case 0x3b:
        case 0x3c:
        case 0x3d:
        case 0x3e:
        case 0x3f:
            // Sound registers read
            return gbSoundRead(soundTicks, address);
        case 0x40:
            return register_LCDC;
        case 0x41:
            // This is a GB/C only bug (ie. not GBA/SP).
            if ((gbHardware & 7) && (gbLcdMode == 2) && (gbLcdModeDelayed == 1) && (!gbSpeed))
                return (0x80 | (gbMemory[0xff41] & 0xFC));
            else
                return (0x80 | gbMemory[0xff41]);
        case 0x42:
            return register_SCY;
        case 0x43:
            return register_SCX;
        case 0x44:
            if (((gbHardware & 7) && ((gbLcdMode == 1) && (gbLcdTicks == 0x71))) || (!(register_LCDC & 0x80)))
                    return 0;
            else
                return register_LY;
        case 0x45:
            return register_LYC;
        case 0x46:
            return register_DMA;
        case 0x4a:
            return register_WY;
        case 0x4b:
            return register_WX;
            case 0x4c:
                return 0xff;
        case 0x4f:
            return (0xfe | register_VBK);
        case 0x51:
            return register_HDMA1;
        case 0x52:
            return register_HDMA2;
        case 0x53:
            return register_HDMA3;
        case 0x54:
            return register_HDMA4;
        case 0x55:
            return register_HDMA5;
        case 0x68:
        case 0x6a:
            if (gbCgbMode)
                return (0x40 | gbMemory[address]);
            else
                return 0xc0;
        case 0x69:
        case 0x6b:
            if (gbCgbMode) {
                if (gbCgbPaletteAccessValid())
                    return (gbMemory[address]);
                else
                    return 0xff;
            } else
                return 0xff;
        case 0x70:
            if (gbCgbMode)
                return (0xf8 | register_SVBK);
            else
                return 0xff;
        case 0xff:
            return register_IE;
        }
    }
    // OAM not accessible during mode 2 & 3.
    if (((address >= 0xfe00) && (address < 0xfea0)) && ((((gbLcdMode | gbLcdModeDelayed) & 2) && (!(gbSpeed && (gbHardware & 0x2) && !(gbLcdModeDelayed & 2) && (gbLcdMode == 2)))) || (gbSpeed && (gbHardware & 0x2) && (gbLcdModeDelayed == 0) && (gbLcdTicksDelayed == (GBLCD_MODE_0_CLOCK_TICKS - gbSpritesTicks[299])))))
        return 0xff;

    if ((address >= 0xfea0) && (address < 0xff00)) {
        if (gbHardware & 1)
            return ((((address + ((address >> 4) - 0xfea)) >> 2) & 1) ? 0x00 : 0xff);
        else if (gbHardware & 2)
            return gbMemoryMap[address >> 12][address & 0x0fff];
        else if (gbHardware & 4)
            return ((((address + ((address >> 4) - 0xfea)) >> 2) & 1) ? 0xff : 0x00);
        else if (gbHardware & 8)
            return ((address & 0xf0) | ((address & 0xf0) >> 4));
    }

    return gbMemoryMap[address >> 12][address & 0x0fff];
}

void gbVblank_interrupt()
{
    gbCheatWrite(false); // Emulates GS codes.
    gbMemory[0xff0f] = register_IF &= 0xfe;
    gbWriteMemory(--SP.W, PC.B.B1);
    gbWriteMemory(--SP.W, PC.B.B0);
    PC.W = 0x40;
}

void gbLcd_interrupt()
{
    gbCheatWrite(false); // Emulates GS codes.
    gbMemory[0xff0f] = register_IF &= 0xfd;
    gbWriteMemory(--SP.W, PC.B.B1);
    gbWriteMemory(--SP.W, PC.B.B0);
    PC.W = 0x48;
}

void gbTimer_interrupt()
{
    gbMemory[0xff0f] = register_IF &= 0xfb;
    gbWriteMemory(--SP.W, PC.B.B1);
    gbWriteMemory(--SP.W, PC.B.B0);
    PC.W = 0x50;
}

void gbSerial_interrupt()
{
    gbMemory[0xff0f] = register_IF &= 0xf7;
    gbWriteMemory(--SP.W, PC.B.B1);
    gbWriteMemory(--SP.W, PC.B.B0);
    PC.W = 0x58;
}

void gbJoypad_interrupt()
{
    gbMemory[0xff0f] = register_IF &= 0xef;
    gbWriteMemory(--SP.W, PC.B.B1);
    gbWriteMemory(--SP.W, PC.B.B0);
    PC.W = 0x60;
}

void gbSpeedSwitch()
{
    gbBlackScreen = true;
    if (gbSpeed == 0) {
        gbSpeed = 1;
        GBLCD_MODE_0_CLOCK_TICKS = 51 * 2;
        GBLCD_MODE_1_CLOCK_TICKS = 1140 * 2;
        GBLCD_MODE_2_CLOCK_TICKS = 20 * 2;
        GBLCD_MODE_3_CLOCK_TICKS = 43 * 2;
        GBLY_INCREMENT_CLOCK_TICKS = 114 * 2;
        GBDIV_CLOCK_TICKS = 64;
        GBTIMER_MODE_0_CLOCK_TICKS = 256;
        GBTIMER_MODE_1_CLOCK_TICKS = 4;
        GBTIMER_MODE_2_CLOCK_TICKS = 16;
        GBTIMER_MODE_3_CLOCK_TICKS = 64;
        GBSERIAL_CLOCK_TICKS = 128 * 2;
        gbLcdTicks *= 2;
        gbLcdTicksDelayed *= 2;
        gbLcdTicksDelayed--;
        gbLcdLYIncrementTicks *= 2;
        gbLcdLYIncrementTicksDelayed *= 2;
        gbLcdLYIncrementTicksDelayed--;
        gbSerialTicks *= 2;
        //SOUND_CLOCK_TICKS = soundQuality * 24 * 2;
        //soundTicks *= 2;
        gbLine99Ticks = 3;
    } else {
        gbSpeed = 0;
        GBLCD_MODE_0_CLOCK_TICKS = 51;
        GBLCD_MODE_1_CLOCK_TICKS = 1140;
        GBLCD_MODE_2_CLOCK_TICKS = 20;
        GBLCD_MODE_3_CLOCK_TICKS = 43;
        GBLY_INCREMENT_CLOCK_TICKS = 114;
        GBDIV_CLOCK_TICKS = 64;
        GBTIMER_MODE_0_CLOCK_TICKS = 256;
        GBTIMER_MODE_1_CLOCK_TICKS = 4;
        GBTIMER_MODE_2_CLOCK_TICKS = 16;
        GBTIMER_MODE_3_CLOCK_TICKS = 64;
        GBSERIAL_CLOCK_TICKS = 128;
        gbLcdTicks >>= 1;
        gbLcdTicksDelayed++;
        gbLcdTicksDelayed >>= 1;
        gbLcdLYIncrementTicks >>= 1;
        gbLcdLYIncrementTicksDelayed++;
        gbLcdLYIncrementTicksDelayed >>= 1;
        gbSerialTicks /= 2;
        //SOUND_CLOCK_TICKS = soundQuality * 24;
        //soundTicks /= 2;
        gbLine99Ticks = 1;
        if (gbHardware & 8)
            gbLine99Ticks++;
    }
    gbDmaTicks += (134) * GBLY_INCREMENT_CLOCK_TICKS + (37 << (gbSpeed ? 1 : 0));
}

bool CPUIsGBBios(const char* file)
{
    if (strlen(file) > 4) {
        const char* p = strrchr(file, '.');

        if (p != NULL) {
            if (_stricmp(p, ".gbc") == 0)
                return true;
            if (_stricmp(p, ".gb") == 0)
                return true;
            if (_stricmp(p, ".bin") == 0)
                return true;
            if (_stricmp(p, ".bios") == 0)
                return true;
            if (_stricmp(p, ".rom") == 0)
                return true;
        }
    }

    return false;
}

void gbCPUInit(const char* biosFileName, bool useBiosFile)
{
    // GB/GBC/SGB only at the moment
    if (!(gbHardware & 7))
        return;

    coreOptions.useBios = false;
    if (useBiosFile) {
        int expectedSize = (gbHardware & 2) ? kCGBBiosSize : kGBBiosSize;
        int size = expectedSize;
        if (utilLoad(biosFileName,
                CPUIsGBBios,
                g_bios,
                size)) {
            if (size == expectedSize)
                coreOptions.useBios = true;
            else
                systemMessage(MSG_INVALID_BIOS_FILE_SIZE, N_("Invalid BOOTROM file size"));
        }
    }
}

void gbGetHardwareType()
{
    if (g_gbCartData.header_type() == gbCartData::RomHeaderType::kInvalid) {
        gbHardware = 0;
        return;
    }

    gbCgbMode = false;
    gbSgbMode = false;
    if ((gbEmulatorType == 0 && g_gbCartData.SupportsCGB()) ||
        gbEmulatorType == 1 || gbEmulatorType == 4) {
        gbCgbMode = true;
    }

    if ((!gbCgbMode) && (g_gbCartData.sgb_support())) {
        if (gbEmulatorType == 0 || gbEmulatorType == 2 || gbEmulatorType == 5)
            gbSgbMode = true;
    }

    gbHardware = 1; // GB
    if ((gbCgbMode && (gbEmulatorType == 0)) || (gbEmulatorType == 1))
        gbHardware = 2; // GBC
    else if ((gbSgbMode && (gbEmulatorType == 0)) || (gbEmulatorType == 2) ||
             (gbEmulatorType == 5))
        gbHardware = 4; // SGB(2)
    else if (gbEmulatorType == 4)
        gbHardware = 8; // GBA

    gbGBCColorType = 0;
    if (gbHardware & 8) // If GBA is selected, choose the GBA default settings.
        gbGBCColorType = 2; // (0 = GBC, 1 = GBA, 2 = GBASP)
}

static void gbSelectColorizationPalette()
{
    int infoIdx = 0;

    // Check if licensee is Nintendo. If not, use default palette.
    if (gbRom[0x014B] == 0x01 || (gbRom[0x014B] == 0x33 && gbRom[0x0144] == 0x30 && gbRom[0x0145] == 0x31)) {
        // Calculate the checksum over 16 title bytes.
        uint8_t checksum = 0;
        for (int i = 0; i < 16; i++) {
            checksum += gbRom[0x0134 + i];
        }

        // Check if the checksum is in the list.
        size_t idx;
        for (idx = 0; idx < sizeof(gbColorizationChecksums); idx++) {
            if (gbColorizationChecksums[idx] == checksum) {
                break;
            }
        }

        // Was the checksum found in the list?
        if (idx < sizeof(gbColorizationChecksums)) {
            // Indexes above 0x40 have to be disambiguated.
            if (idx > 0x40) {
                // No idea how that works. But it works.
                for (size_t i = idx - 0x41, j = 0; i < sizeof(gbColorizationDisambigChars); i += 14, j += 14) {
                    if (gbRom[0x0137] == gbColorizationDisambigChars[i]) {
                        infoIdx = idx + j;
                        break;
                    }
                }
            } else {
                // Lower indexes just use the index from the checksum list.
                infoIdx = idx;
            }
        }
    }
    uint8_t palette = gbColorizationPaletteInfo[infoIdx] & 0x1F;
    uint8_t flags = (gbColorizationPaletteInfo[infoIdx] & 0xE0) >> 5;

    // Normally the first palette is used as OBP0.
    // If bit 0 is zero, the third palette is used instead.
    const uint16_t* obp0 = 0;
    if (flags & 1) {
        obp0 = gbColorizationPaletteData[palette][0];
    } else {
        obp0 = gbColorizationPaletteData[palette][2];
    }

    memcpy(gbPalette + 32, obp0, sizeof(gbColorizationPaletteData[palette][0]));

    // Normally the second palette is used as OBP1.
    // If bit 1 is set, the first palette is used instead.
    // If bit 2 is zero, the third palette is used instead.
    const uint16_t* obp1 = 0;
    if (!(flags & 4)) {
        obp1 = gbColorizationPaletteData[palette][2];
    } else if (flags & 2) {
        obp1 = gbColorizationPaletteData[palette][0];
    } else {
        obp1 = gbColorizationPaletteData[palette][1];
    }

    memcpy(gbPalette + 36, obp1, sizeof(gbColorizationPaletteData[palette][0]));

    // Third palette is always used for BGP.
    memcpy(gbPalette, gbColorizationPaletteData[palette][2], sizeof(gbColorizationPaletteData[palette][0]));
}

void gbResetPalette() {
    if (!gbCgbMode && !gbSgbMode) {
        // In SGB Mode, palette initialization is done in gbSgbReset().
        memcpy(gbPalette, &systemGbPalette[gbPaletteOption * 8],
            8 * sizeof(systemGbPalette[0]));
    }
}

void gbReset()
{
#ifndef NO_LINK
    if (GetLinkMode() == LINK_GAMEBOY_IPC || GetLinkMode() == LINK_GAMEBOY_SOCKET) {
        EmuReseted = true;
        gbInitLink();
    }
#endif

    gbGetHardwareType();

    oldRegister_WY = 146;
    gbInterruptLaunched = 0;

    if (gbCgbMode) {
        if (gbVram == nullptr) {
            gbVram = (uint8_t*)calloc(1, kGBVRamSize);
            if (gbVram == nullptr) {
                return;
            }
        }
        if (gbWram == nullptr) {
            gbWram = (uint8_t*)malloc(kGBWRamSize);
            if (gbWram == nullptr) {
                return;
            }
        }
        memset(gbPalette, 0, sizeof(gbPalette));
    } else {
        if (gbVram != nullptr) {
            free(gbVram);
            gbVram = nullptr;
        }
        if (gbWram != nullptr) {
            free(gbWram);
            gbWram = nullptr;
        }
    }

    gbLYChangeHappened = false;
    gbLCDChangeHappened = false;
    gbBlackScreen = false;
    gbInterruptWait = 0;
    gbDmaTicks = 0;
    clockTicks = 0;

    // clean Wram
    // This kinda emulates the startup state of Wram on GB/C (not very accurate,
    // but way closer to the reality than filling it with 00es or FFes).
    // On GBA/GBASP, it's kinda filled with random data.
    // In all cases, most of the 2nd bank is filled with 00s.
    // The starting data are important for some 'buggy' games, like Buster Brothers or
    // Karamuchou ha Oosawagi!.
    if (gbMemory != nullptr) {
        memset(gbMemory, 0xff, 65536);
        for (int temp = 0xC000; temp < 0xE000; temp++)
            if ((temp & 0x8) ^ ((temp & 0x800) >> 8)) {
                if ((gbHardware & 0x02) && (gbGBCColorType == 0))
                    gbMemory[temp] = 0x0;
                else
                    gbMemory[temp] = 0x0f;
            }

            else
                gbMemory[temp] = 0xff;
    }

    if (gbSpeed) {
        gbSpeedSwitch();
        gbMemory[0xff4d] = 0;
    }

    // GB bios set this memory area to 0
    // Fixes Pitman (J) title screen
    if (gbHardware & 0x1) {
        memset(&gbMemory[0x8000], 0x0, 0x2000);
    }

    // clean LineBuffer
    if (gbLineBuffer != nullptr) {
        memset(gbLineBuffer, 0, kGBLineBufferSize);
    }
    // clean Pix
    if (g_pix != nullptr) {
        memset(g_pix, 0, kGBPixSize);
    }
    // clean Vram
    if (gbVram != nullptr) {
        memset(gbVram, 0, kGBVRamSize);
    }
    // clean Wram 2
    // This kinda emulates the startup state of Wram on GBC (not very accurate,
    // but way closer to the reality than filling it with 00es or FFes).
    // On GBA/GBASP, it's kinda filled with random data.
    // In all cases, most of the 2nd bank is filled with 00s.
    // The starting data are important for some 'buggy' games, like Buster Brothers or
    // Karamuchou ha Oosawagi!
    if (gbWram != nullptr) {
        for (int i = 0; i < 8; i++)
            if (i != 2)
                memcpy((uint16_t*)(gbWram + i * 0x1000), (uint16_t*)(gbMemory + 0xC000), 0x1000);
    }

    memset(gbSCYLine, 0, sizeof(gbSCYLine));
    memset(gbSCXLine, 0, sizeof(gbSCXLine));
    memset(gbBgpLine, 0xfc, sizeof(gbBgpLine));
    if (gbHardware & 5) {
        memset(gbObp0Line, 0xff, sizeof(gbObp0Line));
        memset(gbObp1Line, 0xff, sizeof(gbObp1Line));
    } else {
        memset(gbObp0Line, 0x0, sizeof(gbObp0Line));
        memset(gbObp1Line, 0x0, sizeof(gbObp1Line));
    }
    memset(gbSpritesTicks, 0x0, sizeof(gbSpritesTicks));

    SP.W = 0xfffe;
    AF.W = 0x01b0;
    BC.W = 0x0013;
    DE.W = 0x00d8;
    HL.W = 0x014d;
    PC.W = 0x0100;
    IFF = 0;
    gbInt48Signal = 0;

    register_TIMA = 0;
    register_TMA = 0;
    register_TAC = 0;
    gbMemory[0xff0f] = register_IF = 1;
    gbMemory[0xff40] = register_LCDC = 0x91;
    gbMemory[0xff47] = 0xfc;

    if (gbCgbMode)
        gbMemory[0xff4d] = 0x7e;
    else
        gbMemory[0xff4d] = 0xff;

    if (!gbCgbMode)
        gbMemory[0xff70] = gbMemory[0xff74] = 0xff;

    if (gbCgbMode)
        gbMemory[0xff56] = 0x3e;
    else
        gbMemory[0xff56] = 0xff;

    register_SCY = 0;
    register_SCX = 0;
    register_LYC = 0;
    register_DMA = 0xff;
    register_WY = 0;
    register_WX = 0;
    register_VBK = 0;
    register_HDMA1 = 0xff;
    register_HDMA2 = 0xff;
    register_HDMA3 = 0xff;
    register_HDMA4 = 0xff;
    register_HDMA5 = 0xff;
    register_SVBK = 0;
    register_IE = 0;

    if (gbCgbMode)
        gbMemory[0xff02] = 0x7c;
    else
        gbMemory[0xff02] = 0x7e;

    gbMemory[0xff03] = 0xff;
    int i;
    for (i = 0x8; i < 0xf; i++)
        gbMemory[0xff00 + i] = 0xff;

    gbMemory[0xff13] = 0xff;
    gbMemory[0xff15] = 0xff;
    gbMemory[0xff18] = 0xff;
    gbMemory[0xff1d] = 0xff;
    gbMemory[0xff1f] = 0xff;

    for (i = 0x27; i < 0x30; i++)
        gbMemory[0xff00 + i] = 0xff;

    gbMemory[0xff4c] = 0xff;
    gbMemory[0xff4e] = 0xff;
    gbMemory[0xff50] = 0xff;

    for (i = 0x57; i < 0x68; i++)
        gbMemory[0xff00 + i] = 0xff;

    for (i = 0x5d; i < 0x70; i++)
        gbMemory[0xff00 + i] = 0xff;

    gbMemory[0xff71] = 0xff;

    for (i = 0x78; i < 0x80; i++)
        gbMemory[0xff00 + i] = 0xff;

    if (gbHardware & 0xa) {

        if (gbHardware & 2) {
            AF.W = 0x1180;
            BC.W = 0x0000;
        } else {
            AF.W = 0x1100;
            BC.W = 0x0100; // GBA/SP have B = 0x01 (which means GBC & GBA/SP bootrom are different !)
        }

        gbMemory[0xff26] = 0xf1;
        if (gbCgbMode) {

            gbMemory[0xff31] = 0xff;
            gbMemory[0xff33] = 0xff;
            gbMemory[0xff35] = 0xff;
            gbMemory[0xff37] = 0xff;
            gbMemory[0xff39] = 0xff;
            gbMemory[0xff3b] = 0xff;
            gbMemory[0xff3d] = 0xff;

            gbMemory[0xff44] = register_LY = 0x90;
            gbDivTicks = 0x19 + ((gbHardware & 2) >> 1);
            gbInternalTimer = 0x58 + ((gbHardware & 2) >> 1);
            gbLcdTicks = GBLCD_MODE_1_CLOCK_TICKS - (register_LY - 0x8F) * GBLY_INCREMENT_CLOCK_TICKS + 72 + ((gbHardware & 2) >> 1);
            gbLcdLYIncrementTicks = 72 + ((gbHardware & 2) >> 1);
            gbMemory[0xff04] = register_DIV = 0x1E;
        } else {
            gbMemory[0xff44] = register_LY = 0x94;
            gbDivTicks = 0x22 + ((gbHardware & 2) >> 1);
            gbInternalTimer = 0x61 + ((gbHardware & 2) >> 1);
            gbLcdTicks = GBLCD_MODE_1_CLOCK_TICKS - (register_LY - 0x8F) * GBLY_INCREMENT_CLOCK_TICKS + 25 + ((gbHardware & 2) >> 1);
            gbLcdLYIncrementTicks = 25 + ((gbHardware & 2) >> 1);
            gbMemory[0xff04] = register_DIV = 0x26;
        }

        DE.W = 0xff56;
        HL.W = 0x000d;

        register_HDMA5 = 0xff;
        gbMemory[0xff68] = 0xc0;
        gbMemory[0xff6a] = 0xc0;

        gbMemory[0xff41] = register_STAT = 0x81;
        gbLcdMode = 1;
    } else {
        if (gbHardware & 4) {
            if (gbEmulatorType == 5)
                AF.W = 0xffb0;
            else
                AF.W = 0x01b0;
            BC.W = 0x0013;
            DE.W = 0x00d8;
            HL.W = 0x014d;
        }
        gbDivTicks = 14;
        gbInternalTimer = gbDivTicks--;
        gbMemory[0xff04] = register_DIV = 0xAB;
        gbMemory[0xff41] = register_STAT = 0x85;
        gbMemory[0xff44] = register_LY = 0x00;
        gbLcdTicks = 15;
        gbLcdLYIncrementTicks = 114 + gbLcdTicks;
        gbLcdMode = 1;
    }

    // used for the handling of the gb Boot Rom
    if ((gbHardware & 7) && (g_bios != NULL) && coreOptions.useBios && !coreOptions.skipBios) {
        if (gbHardware & 5) {
            memcpy((uint8_t*)(gbMemory), (uint8_t*)(gbRom), 0x1000);
            memcpy((uint8_t*)(gbMemory), (uint8_t*)(g_bios), kGBBiosSize);
        } else {
            memcpy((uint8_t*)(gbMemory), (uint8_t*)(g_bios), kCGBBiosSize);
            memcpy((uint8_t*)(gbMemory + 0x100), (uint8_t*)(gbRom + 0x100), 0x100);
        }
        gbWhiteScreen = 0;

        gbInternalTimer = 0x3e;
        gbDivTicks = 0x3f;
        gbMemory[0xff04] = register_DIV = 0x00;
        PC.W = 0x0000;
        register_LCDC = 0x11;
        gbScreenOn = false;
        gbLcdTicks = 0;
        gbLcdMode = 0;
        gbLcdModeDelayed = 0;
        gbMemory[0xff41] = register_STAT &= 0xfc;
        gbInt48Signal = 0;
        gbLcdLYIncrementTicks = GBLY_INCREMENT_CLOCK_TICKS;
        gbMemory[0xff6c] = 0xfe;

        inBios = true;
    } else if (gbHardware & 0xa) {
        // Set compatibility mode if it is a DMG ROM.
        const uint8_t gbcFlag = g_gbCartData.SupportsCGB() ? 0x80 : 0x00;
        gbMemory[0xff6c] = 0xfe | gbcFlag;
    }

    gbLine99Ticks = 1;
    if (gbHardware & 8)
        gbLine99Ticks++;

    gbLcdModeDelayed = gbLcdMode;
    gbLcdTicksDelayed = gbLcdTicks + 1;
    gbLcdLYIncrementTicksDelayed = gbLcdLYIncrementTicks + 1;

    gbTimerModeChange = false;
    gbTimerOnChange = false;
    gbTimerOn = 0;

    gbResetPalette();

    if (gbCgbMode) {
        // This is just to show that the starting values of the OBJ palettes are different
        // between the 3 consoles, and that they 'kinda' stay the same at each reset
        // (they can slightly change, somehow (randomly?)).
        // You can check the effects of gbGBCColorType on the "Vila Caldan Color" gbc demo.
        // Note that you could also check the Div register to check on which system the game
        // is running (GB,GBC and GBA(SP) have different startup values).
        // Unfortunatly, I don't have any SGB system, so I can't get their starting values.

        if (gbGBCColorType == 0) {
            // GBC Hardware
            std::copy(kGbGbcPalette.begin(), kGbGbcPalette.end(), gbPalette);
        } else if (gbGBCColorType == 1) {
            // GBA Hardware
            std::copy(kGbGbaPalette.begin(), kGbGbaPalette.end(), gbPalette);
        } else if (gbGBCColorType == 2) {
            // GBASP Hardware
            std::copy(kGbGbaSpPalette.begin(), kGbGbaSpPalette.end(), gbPalette);
        }

        // The CGB BIOS palette selection has to be done by VBA if BIOS is skipped.
        if (!g_gbCartData.RequiresCGB() && !inBios) {
            gbSelectColorizationPalette();
        }
    }

    GBTIMER_MODE_0_CLOCK_TICKS = 256;
    GBTIMER_MODE_1_CLOCK_TICKS = 4;
    GBTIMER_MODE_2_CLOCK_TICKS = 16;
    GBTIMER_MODE_3_CLOCK_TICKS = 64;

    GBLY_INCREMENT_CLOCK_TICKS = 114;
    gbTimerTicks = GBTIMER_MODE_0_CLOCK_TICKS;
    gbTimerClockTicks = GBTIMER_MODE_0_CLOCK_TICKS;
    gbSerialTicks = 0;
    gbSerialBits = 0;
    gbSerialOn = 0;
    gbWindowLine = -1;
    gbTimerOn = 0;
    gbTimerMode = 0;
    gbSpeed = 0;
    gbJoymask[0] = gbJoymask[1] = gbJoymask[2] = gbJoymask[3] = 0;

    if (gbCgbMode) {
        gbSpeed = 0;
        gbHdmaOn = 0;
        gbHdmaSource = 0x99d0;
        gbHdmaDestination = 0x99d0;
        gbVramBank = 0;
        gbWramBank = 1;
    }

    // used to clean the borders
    if (gbSgbMode) {
        gbSgbResetFlag = true;
        gbSgbReset();
        if (gbBorderOn)
            gbSgbRenderBorder();
        gbSgbResetFlag = false;
    }

    for (i = 0; i < 4; i++)
        gbBgp[i] = gbObp0[i] = gbObp1[i] = i;

    memset(&gbDataMBC1, 0, sizeof(gbDataMBC1));
    gbDataMBC1.mapperROMBank = 1;

    gbDataMBC2.mapperRAMEnable = 0;
    gbDataMBC2.mapperROMBank = 1;

    memset(&gbDataMBC3, 0, 6 * sizeof(int));
    gbDataMBC3.mapperROMBank = 1;

    memset(&gbDataMBC5, 0, sizeof(gbDataMBC5));
    gbDataMBC5.mapperROMBank = 1;
    gbDataMBC5.isRumbleCartridge = g_gbCartData.has_rumble();

    memset(&gbDataHuC1, 0, sizeof(gbDataHuC1));
    gbDataHuC1.mapperROMBank = 1;

    memset(&gbDataMBC7, 0, sizeof(gbDataMBC7));
    gbDataMBC7.mapperROMBank = 1;

    memset(&gbDataHuC3, 0, sizeof(gbDataHuC3));
    gbDataHuC3.mapperROMBank = 1;
    gbDataHuC3.mapperRAMValue = 1;
    gbRTCHuC3.memoryTimerRead = 1;

    memset(&gbDataTAMA5, 0, 26 * sizeof(int));
    gbDataTAMA5.mapperROMBank = 1;

    memset(&gbDataMMM01, 0, sizeof(gbDataMMM01));
    gbDataMMM01.mapperROMBank = 1;

    if (inBios) {
        gbMemoryMap[0x00] = &gbMemory[0x0000];
    } else {
        gbMemoryMap[0x00] = &gbRom[0x0000];
    }

    gbMemoryMap[0x01] = &gbRom[0x1000];
    gbMemoryMap[0x02] = &gbRom[0x2000];
    gbMemoryMap[0x03] = &gbRom[0x3000];
    gbMemoryMap[0x04] = &gbRom[0x4000];
    gbMemoryMap[0x05] = &gbRom[0x5000];
    gbMemoryMap[0x06] = &gbRom[0x6000];
    gbMemoryMap[0x07] = &gbRom[0x7000];
    if (gbCgbMode) {
        gbMemoryMap[0x08] = &gbVram[0x0000];
        gbMemoryMap[0x09] = &gbVram[0x1000];
        gbMemoryMap[0x0a] = &gbMemory[0xa000];
        gbMemoryMap[0x0b] = &gbMemory[0xb000];

        // TODO: 2019/1/15
        // Should we be using gbWram[] on $C000 as well
        // for a continouos mem block?
#ifndef __LIBRETRO__
        gbMemoryMap[0x0c] = &gbMemory[0xc000];
#else
        gbMemoryMap[0x0c] = &gbWram[0x0000];
#endif
        gbMemoryMap[0x0d] = &gbWram[0x1000];
        gbMemoryMap[0x0e] = &gbMemory[0xe000];
        gbMemoryMap[0x0f] = &gbMemory[0xf000];
    } else {
        gbMemoryMap[0x08] = &gbMemory[0x8000];
        gbMemoryMap[0x09] = &gbMemory[0x9000];
        gbMemoryMap[0x0a] = &gbMemory[0xa000];
        gbMemoryMap[0x0b] = &gbMemory[0xb000];
        gbMemoryMap[0x0c] = &gbMemory[0xc000];
        gbMemoryMap[0x0d] = &gbMemory[0xd000];
        gbMemoryMap[0x0e] = &gbMemory[0xe000];
        gbMemoryMap[0x0f] = &gbMemory[0xf000];
    }

    if (gbRam) {
        gbMemoryMap[0x0a] = &gbRam[0x0000];
        gbMemoryMap[0x0b] = &gbRam[0x1000];
    }

    gbSoundReset();

    systemSaveUpdateCounter = SYSTEM_SAVE_NOT_UPDATED;

    gbLastTime = systemGetClock();
    gbFrameCount = 0;

    gbScreenOn = true;
    gbSystemMessage = false;

    gbCheatWrite(true); // Emulates GS codes.
}

bool gbReadGSASnapshot(const char* fileName)
{
    FILE* file = utilOpenFile(fileName, "rb");

    if (!file) {
        systemMessage(MSG_CANNOT_OPEN_FILE, N_("Cannot open file %s"), fileName);
        return false;
    }

    fseek(file, 0x4, SEEK_SET);
    char buffer[16];
    char buffer2[16];
    FREAD_UNCHECKED(buffer, 1, 15, file);
    buffer[15] = 0;
    memcpy(buffer2, &gbRom[0x134], 15);
    buffer2[15] = 0;
    if (memcmp(buffer, buffer2, 15)) {
        systemMessage(MSG_CANNOT_IMPORT_SNAPSHOT_FOR,
            N_("Cannot import snapshot for %s. Current game is %s"),
            buffer,
            buffer2);
        fclose(file);
        return false;
    }
    fseek(file, 0x13, SEEK_SET);
    if (g_gbCartData.has_battery()) {
        switch (g_gbCartData.mapper_type()) {
            case gbCartData::MapperType::kMbc1:
            case gbCartData::MapperType::kMbc3:
            case gbCartData::MapperType::kMbc5:
            case gbCartData::MapperType::kMbc7:
            case gbCartData::MapperType::kHuC1:
                FREAD_UNCHECKED(gbRam, 1, g_gbCartData.ram_size(), file);
                break;
            case gbCartData::MapperType::kMbc2:
                FREAD_UNCHECKED(&gbMemory[0xa000], 1, k256B, file);
                break;
            default:
                systemMessage(MSG_UNSUPPORTED_SNAPSHOT_FILE,
                              N_("Unsupported snapshot file %s"), fileName);
                fclose(file);
                return false;
        }
    }
    fclose(file);
    gbReset();
    return true;
}

variable_desc gbSaveGameStruct[] = {
    { &PC.W, sizeof(uint16_t) },
    { &SP.W, sizeof(uint16_t) },
    { &AF.W, sizeof(uint16_t) },
    { &BC.W, sizeof(uint16_t) },
    { &DE.W, sizeof(uint16_t) },
    { &HL.W, sizeof(uint16_t) },
    { &IFF, sizeof(uint8_t) },
    { &GBLCD_MODE_0_CLOCK_TICKS, sizeof(int) },
    { &GBLCD_MODE_1_CLOCK_TICKS, sizeof(int) },
    { &GBLCD_MODE_2_CLOCK_TICKS, sizeof(int) },
    { &GBLCD_MODE_3_CLOCK_TICKS, sizeof(int) },
    { &GBDIV_CLOCK_TICKS, sizeof(int) },
    { &GBLY_INCREMENT_CLOCK_TICKS, sizeof(int) },
    { &GBTIMER_MODE_0_CLOCK_TICKS, sizeof(int) },
    { &GBTIMER_MODE_1_CLOCK_TICKS, sizeof(int) },
    { &GBTIMER_MODE_2_CLOCK_TICKS, sizeof(int) },
    { &GBTIMER_MODE_3_CLOCK_TICKS, sizeof(int) },
    { &GBSERIAL_CLOCK_TICKS, sizeof(int) },
    { &GBSYNCHRONIZE_CLOCK_TICKS, sizeof(int) },
    { &gbDivTicks, sizeof(int) },
    { &gbLcdMode, sizeof(int) },
    { &gbLcdTicks, sizeof(int) },
    { &gbLcdLYIncrementTicks, sizeof(int) },
    { &gbTimerTicks, sizeof(int) },
    { &gbTimerClockTicks, sizeof(int) },
    { &gbSerialTicks, sizeof(int) },
    { &gbSerialBits, sizeof(int) },
    { &gbInt48Signal, sizeof(int) },
    { &gbInterruptWait, sizeof(int) },
    { &gbSynchronizeTicks, sizeof(int) },
    { &gbTimerOn, sizeof(int) },
    { &gbTimerMode, sizeof(int) },
    { &gbSerialOn, sizeof(int) },
    { &gbWindowLine, sizeof(int) },
    { &gbCgbMode, sizeof(int) },
    { &gbVramBank, sizeof(int) },
    { &gbWramBank, sizeof(int) },
    { &gbHdmaSource, sizeof(int) },
    { &gbHdmaDestination, sizeof(int) },
    { &gbHdmaBytes, sizeof(int) },
    { &gbHdmaOn, sizeof(int) },
    { &gbSpeed, sizeof(int) },
    { &gbSgbMode, sizeof(int) },
    { &register_DIV, sizeof(uint8_t) },
    { &register_TIMA, sizeof(uint8_t) },
    { &register_TMA, sizeof(uint8_t) },
    { &register_TAC, sizeof(uint8_t) },
    { &register_IF, sizeof(uint8_t) },
    { &register_LCDC, sizeof(uint8_t) },
    { &register_STAT, sizeof(uint8_t) },
    { &register_SCY, sizeof(uint8_t) },
    { &register_SCX, sizeof(uint8_t) },
    { &register_LY, sizeof(uint8_t) },
    { &register_LYC, sizeof(uint8_t) },
    { &register_DMA, sizeof(uint8_t) },
    { &register_WY, sizeof(uint8_t) },
    { &register_WX, sizeof(uint8_t) },
    { &register_VBK, sizeof(uint8_t) },
    { &register_HDMA1, sizeof(uint8_t) },
    { &register_HDMA2, sizeof(uint8_t) },
    { &register_HDMA3, sizeof(uint8_t) },
    { &register_HDMA4, sizeof(uint8_t) },
    { &register_HDMA5, sizeof(uint8_t) },
    { &register_SVBK, sizeof(uint8_t) },
    { &register_IE, sizeof(uint8_t) },
    { &gbBgp[0], sizeof(uint8_t) },
    { &gbBgp[1], sizeof(uint8_t) },
    { &gbBgp[2], sizeof(uint8_t) },
    { &gbBgp[3], sizeof(uint8_t) },
    { &gbObp0[0], sizeof(uint8_t) },
    { &gbObp0[1], sizeof(uint8_t) },
    { &gbObp0[2], sizeof(uint8_t) },
    { &gbObp0[3], sizeof(uint8_t) },
    { &gbObp1[0], sizeof(uint8_t) },
    { &gbObp1[1], sizeof(uint8_t) },
    { &gbObp1[2], sizeof(uint8_t) },
    { &gbObp1[3], sizeof(uint8_t) },
    { NULL, 0 }
};

#ifndef __LIBRETRO__
static bool gbWriteSaveState(gzFile gzFile)
{

    utilWriteInt(gzFile, GBSAVE_GAME_VERSION);

    utilGzWrite(gzFile, &gbRom[0x134], 15);

    utilWriteInt(gzFile, coreOptions.useBios);
    utilWriteInt(gzFile, inBios);

    utilWriteData(gzFile, gbSaveGameStruct);

    utilGzWrite(gzFile, &IFF, 2);

    if (gbSgbMode) {
        gbSgbSaveGame(gzFile);
    }

    utilGzWrite(gzFile, &gbDataMBC1, sizeof(gbDataMBC1));
    utilGzWrite(gzFile, &gbDataMBC2, sizeof(gbDataMBC2));
    utilGzWrite(gzFile, &gbDataMBC3, sizeof(gbDataMBC3));
    utilGzWrite(gzFile, &gbDataMBC5, sizeof(gbDataMBC5));
    utilGzWrite(gzFile, &gbDataHuC1, sizeof(gbDataHuC1));
    utilGzWrite(gzFile, &gbDataHuC3, sizeof(gbDataHuC3));
    utilGzWrite(gzFile, &gbDataTAMA5, sizeof(gbDataTAMA5));
    if (gbTAMA5ram != NULL)
        utilGzWrite(gzFile, gbTAMA5ram, kTama5RamSize);
    utilGzWrite(gzFile, &gbDataMMM01, sizeof(gbDataMMM01));

    utilGzWrite(gzFile, gbPalette, sizeof(gbPalette));

    utilGzWrite(gzFile, &gbMemory[0x8000], 0x8000);

    if (g_gbCartData.HasRam()) {
        const size_t ramSize = g_gbCartData.ram_size();
        utilWriteInt(gzFile, ramSize);
        utilGzWrite(gzFile, gbRam, ramSize);
    }

    if (gbCgbMode) {
        utilGzWrite(gzFile, gbVram, kGBVRamSize);
        utilGzWrite(gzFile, gbWram, kGBWRamSize);
    }

    gbSoundSaveGame(gzFile);

    gbCheatsSaveGame(gzFile);

    utilWriteInt(gzFile, gbLcdModeDelayed);
    utilWriteInt(gzFile, gbLcdTicksDelayed);
    utilWriteInt(gzFile, gbLcdLYIncrementTicksDelayed);
    utilWriteInt(gzFile, gbSpritesTicks[299]);
    utilWriteInt(gzFile, gbTimerModeChange);
    utilWriteInt(gzFile, gbTimerOnChange);
    utilWriteInt(gzFile, gbHardware);
    utilWriteInt(gzFile, gbBlackScreen);
    utilWriteInt(gzFile, oldRegister_WY);
    utilWriteInt(gzFile, gbWindowLine);
    utilWriteInt(gzFile, inUseRegister_WY);
    utilWriteInt(gzFile, gbScreenOn);
    utilWriteInt(gzFile, 0x12345678); // end marker
    return true;
}

bool gbWriteMemSaveState(char* memory, int available, long& reserved)
{
    gzFile gzFile = utilMemGzOpen(memory, available, "w");

    if (gzFile == NULL) {
        return false;
    }

    bool res = gbWriteSaveState(gzFile);

    reserved = utilGzMemTell(gzFile) + 8;

    if (reserved >= (available))
        res = false;

    utilGzClose(gzFile);

    return res;
}

bool gbWriteSaveState(const char* name)
{
    gzFile gzFile = utilGzOpen(name, "wb");

    if (gzFile == NULL)
        return false;

    bool res = gbWriteSaveState(gzFile);

    utilGzClose(gzFile);
    return res;
}

static bool gbReadSaveState(gzFile gzFile)
{
    int version = utilReadInt(gzFile);

    if (version > GBSAVE_GAME_VERSION || version < 0) {
        systemMessage(MSG_UNSUPPORTED_VB_SGM,
            N_("Unsupported VisualBoy save game version %d"), version);
        return false;
    }

    uint8_t romname[20];

    utilGzRead(gzFile, romname, 15);

    if (memcmp(&gbRom[0x134], romname, 15) != 0) {
        systemMessage(MSG_CANNOT_LOAD_SGM_FOR,
            N_("Cannot load save game for %s. Playing %s"),
            romname, &gbRom[0x134]);
        return false;
    }

    bool ub = false;
    bool ib = false;

    if (version >= 11) {
        ub = utilReadInt(gzFile) ? true : false;
        ib = utilReadInt(gzFile) ? true : false;

        if ((ub != (bool)(coreOptions.useBios)) && (ib)) {
            if (coreOptions.useBios)
                systemMessage(MSG_SAVE_GAME_NOT_USING_BIOS,
                    N_("Save game is not using the BIOS files"));
            else
                systemMessage(MSG_SAVE_GAME_USING_BIOS,
                    N_("Save game is using the BIOS file"));
            return false;
        }
    }

    gbReset();

    inBios = ib;

    utilReadData(gzFile, gbSaveGameStruct);

    // Correct crash when loading color gameboy save in regular gameboy type.
    if (gbCgbMode) {
        if (gbVram == nullptr) {
            gbVram = (uint8_t*)calloc(1, kGBVRamSize);
            if (gbVram == nullptr) {
                return false;
            }
        }
        if (gbWram == nullptr) {
            gbWram = (uint8_t*)malloc(kGBWRamSize);
            if (gbWram == nullptr) {
                return false;
            }
        }
        memset(gbPalette, 0, sizeof(gbPalette));
    } else {
        if (gbVram != nullptr) {
            free(gbVram);
            gbVram = nullptr;
        }
        if (gbWram != nullptr) {
            free(gbWram);
            gbWram = nullptr;
        }
    }

    if (version >= GBSAVE_GAME_VERSION_7) {
        utilGzRead(gzFile, &IFF, 2);
    }

    if (gbSgbMode) {
        gbSgbReadGame(gzFile, version);
    } else {
        gbSgbMask = 0; // loading a game at the wrong time causes no display
    }
    if (version < 11)
        utilGzRead(gzFile, &gbDataMBC1, sizeof(gbDataMBC1) - sizeof(int));
    else
        utilGzRead(gzFile, &gbDataMBC1, sizeof(gbDataMBC1));
    utilGzRead(gzFile, &gbDataMBC2, sizeof(gbDataMBC2));
    utilGzRead(gzFile, &gbDataMBC3, sizeof(gbDataMBC3));
    utilGzRead(gzFile, &gbDataMBC5, sizeof(gbDataMBC5));
    utilGzRead(gzFile, &gbDataHuC1, sizeof(gbDataHuC1));
    utilGzRead(gzFile, &gbDataHuC3, sizeof(gbDataHuC3));
    if (version >= 11) {
        utilGzRead(gzFile, &gbDataTAMA5, sizeof(gbDataTAMA5));
        if (gbTAMA5ram != NULL) {
            if (coreOptions.skipSaveGameBattery) {
                utilGzSeek(gzFile, kTama5RamSize, SEEK_CUR);
            } else {
                utilGzRead(gzFile, gbTAMA5ram, kTama5RamSize);
            }
        }
        utilGzRead(gzFile, &gbDataMMM01, sizeof(gbDataMMM01));
    }

    if (version < GBSAVE_GAME_VERSION_5) {
        utilGzRead(gzFile, g_pix, 256 * 224 * sizeof(uint16_t));
    }
    memset(g_pix, 0, kGBPixSize);

    if (version < GBSAVE_GAME_VERSION_6) {
        utilGzRead(gzFile, gbPalette, 64 * sizeof(uint16_t));
    } else
        utilGzRead(gzFile, gbPalette, 128 * sizeof(uint16_t));

    if (version < 11)
        utilGzRead(gzFile, gbPalette, 128 * sizeof(uint16_t));

    // This is necessary for GB games (not GBC or SGB) to have them load with
    // the user-defined palette and not the saved palette.
    gbResetPalette();

    utilGzRead(gzFile, &gbMemory[0x8000], 0x8000);

    if (g_gbCartData.HasRam()) {
        const size_t ram_size = g_gbCartData.ram_size();
        if (version < 11)
            if (coreOptions.skipSaveGameBattery) {
                utilGzSeek(gzFile, ram_size, SEEK_CUR); //skip
            } else {
                utilGzRead(gzFile, gbRam, ram_size); //read
            }
        else {
            const size_t stateRamSize = utilReadInt(gzFile);
            if (coreOptions.skipSaveGameBattery) {
                // Skip over the save game RAM.
                utilGzSeek(gzFile, std::min(ram_size, stateRamSize), SEEK_CUR);
            } else {
                // Ovewrite the save game RAM.
                utilGzRead(gzFile, gbRam, std::min(ram_size, stateRamSize));
            }
            if (stateRamSize > ram_size) {
                utilGzSeek(gzFile, stateRamSize - ram_size, SEEK_CUR);
            }
        }
    }

    memset(gbSCYLine, register_SCY, sizeof(gbSCYLine));
    memset(gbSCXLine, register_SCX, sizeof(gbSCXLine));
    memset(gbBgpLine, (gbBgp[0] | (gbBgp[1] << 2) | (gbBgp[2] << 4) | (gbBgp[3] << 6)), sizeof(gbBgpLine));
    memset(gbObp0Line, (gbObp0[0] | (gbObp0[1] << 2) | (gbObp0[2] << 4) | (gbObp0[3] << 6)), sizeof(gbObp0Line));
    memset(gbObp1Line, (gbObp1[0] | (gbObp1[1] << 2) | (gbObp1[2] << 4) | (gbObp1[3] << 6)), sizeof(gbObp1Line));
    memset(gbSpritesTicks, 0x0, sizeof(gbSpritesTicks));

    if (inBios) {
        gbMemoryMap[0x00] = &gbMemory[0x0000];
        if (gbHardware & 5) {
            memcpy((uint8_t*)(gbMemory), (uint8_t*)(gbRom), 0x1000);
            memcpy((uint8_t*)(gbMemory), (uint8_t*)(g_bios), kGBBiosSize);
        } else if (gbHardware & 2) {
            memcpy((uint8_t*)(gbMemory), (uint8_t*)(g_bios), kCGBBiosSize);
            memcpy((uint8_t*)(gbMemory + 0x100), (uint8_t*)(gbRom + 0x100), 0x100);
        }

    } else
        gbMemoryMap[0x00] = &gbRom[0x0000];
    gbMemoryMap[0x01] = &gbRom[0x1000];
    gbMemoryMap[0x02] = &gbRom[0x2000];
    gbMemoryMap[0x03] = &gbRom[0x3000];
    gbMemoryMap[0x04] = &gbRom[0x4000];
    gbMemoryMap[0x05] = &gbRom[0x5000];
    gbMemoryMap[0x06] = &gbRom[0x6000];
    gbMemoryMap[0x07] = &gbRom[0x7000];
    gbMemoryMap[0x08] = &gbMemory[0x8000];
    gbMemoryMap[0x09] = &gbMemory[0x9000];
    gbMemoryMap[0x0a] = &gbMemory[0xa000];
    gbMemoryMap[0x0b] = &gbMemory[0xb000];
    gbMemoryMap[0x0c] = &gbMemory[0xc000];
    gbMemoryMap[0x0d] = &gbMemory[0xd000];
    gbMemoryMap[0x0e] = &gbMemory[0xe000];
    gbMemoryMap[0x0f] = &gbMemory[0xf000];

    switch (g_gbCartData.mapper_type()) {
        case gbCartData::MapperType::kNone:
        case gbCartData::MapperType::kMbc1:
            memoryUpdateMapMBC1();
            break;
        case gbCartData::MapperType::kMbc2:
            memoryUpdateMapMBC2();
            break;
        case gbCartData::MapperType::kMmm01:
            memoryUpdateMapMMM01();
            break;
        case gbCartData::MapperType::kMbc3:
            memoryUpdateMapMBC3();
            break;
        case gbCartData::MapperType::kMbc5:
            memoryUpdateMapMBC5();
            break;
        case gbCartData::MapperType::kMbc7:
            memoryUpdateMapMBC7();
            break;
        case gbCartData::MapperType::kGameShark:
            memoryUpdateMapGS3();
            break;
        case gbCartData::MapperType::kTama5:
            memoryUpdateMapTAMA5();
            break;
        case gbCartData::MapperType::kHuC3:
            memoryUpdateMapHuC3();
            break;
        case gbCartData::MapperType::kHuC1:
            memoryUpdateMapHuC1();
            break;
        case gbCartData::MapperType::kMbc6:
        case gbCartData::MapperType::kPocketCamera:
        case gbCartData::MapperType::kGameGenie:
        case gbCartData::MapperType::kUnknown:
            // Do nothing.
            break;
    }

    if (gbCgbMode) {
        utilGzRead(gzFile, gbVram, kGBVRamSize);
        utilGzRead(gzFile, gbWram, kGBWRamSize);

        int value = register_SVBK;
        if (value == 0)
            value = 1;

        gbMemoryMap[0x08] = &gbVram[register_VBK * 0x2000];
        gbMemoryMap[0x09] = &gbVram[register_VBK * 0x2000 + 0x1000];
        gbMemoryMap[0x0d] = &gbWram[value * 0x1000];
    }

    gbSoundReadGame(version, gzFile);

    if (gbCgbMode && gbSgbMode) {
        gbSgbMode = false;
    }

    if (gbBorderOn && !gbSgbMask) {
        gbSgbRenderBorder();
    }

    systemDrawScreen();

    if (version > GBSAVE_GAME_VERSION_1) {
        if (coreOptions.skipSaveGameCheats) {
            gbCheatsReadGameSkip(gzFile, version);
        } else {
            gbCheatsReadGame(gzFile, version);
        }
    }

    if (version < 11) {
        gbWriteMemory(0xff00, 0);
        gbMemory[0xff04] = register_DIV;
        gbMemory[0xff05] = register_TIMA;
        gbMemory[0xff06] = register_TMA;
        gbMemory[0xff07] = register_TAC;
        gbMemory[0xff40] = register_LCDC;
        gbMemory[0xff42] = register_SCY;
        gbMemory[0xff43] = register_SCX;
        gbMemory[0xff44] = register_LY;
        gbMemory[0xff45] = register_LYC;
        gbMemory[0xff46] = register_DMA;
        gbMemory[0xff4a] = register_WY;
        gbMemory[0xff4b] = register_WX;
        gbMemory[0xff4f] = register_VBK;
        gbMemory[0xff51] = register_HDMA1;
        gbMemory[0xff52] = register_HDMA2;
        gbMemory[0xff53] = register_HDMA3;
        gbMemory[0xff54] = register_HDMA4;
        gbMemory[0xff55] = register_HDMA5;
        gbMemory[0xff70] = register_SVBK;
        gbMemory[0xffff] = register_IE;
        GBDIV_CLOCK_TICKS = 64;

        if (gbSpeed)
            gbDivTicks /= 2;

        if ((gbLcdMode == 0) && (register_STAT & 8))
            gbInt48Signal |= 1;
        if ((gbLcdMode == 1) && (register_STAT & 0x10))
            gbInt48Signal |= 2;
        if ((gbLcdMode == 2) && (register_STAT & 0x20))
            gbInt48Signal |= 4;
        if ((register_LY == register_LYC) && (register_STAT & 0x40))
            gbInt48Signal |= 8;

        gbLcdLYIncrementTicks = GBLY_INCREMENT_CLOCK_TICKS;

        if (gbLcdMode == 2)
            gbLcdLYIncrementTicks -= GBLCD_MODE_2_CLOCK_TICKS - gbLcdTicks;
        else if (gbLcdMode == 3)
            gbLcdLYIncrementTicks -= GBLCD_MODE_2_CLOCK_TICKS + GBLCD_MODE_3_CLOCK_TICKS - gbLcdTicks;
        else if (gbLcdMode == 0)
            gbLcdLYIncrementTicks = gbLcdTicks;
        else if (gbLcdMode == 1) {
            gbLcdLYIncrementTicks = gbLcdTicks % GBLY_INCREMENT_CLOCK_TICKS;
            if (register_LY == 0x99)
                gbLcdLYIncrementTicks = gbLine99Ticks;
            else if (register_LY == 0)
                gbLcdLYIncrementTicks += GBLY_INCREMENT_CLOCK_TICKS;
        }

        gbLcdModeDelayed = gbLcdMode;
        gbLcdTicksDelayed = gbLcdTicks--;
        gbLcdLYIncrementTicksDelayed = gbLcdLYIncrementTicks--;
        gbInterruptWait = 0;
        memset(gbSpritesTicks, 0, sizeof(gbSpritesTicks));
    } else {
        gbLcdModeDelayed = utilReadInt(gzFile);
        gbLcdTicksDelayed = utilReadInt(gzFile);
        gbLcdLYIncrementTicksDelayed = utilReadInt(gzFile);
        gbSpritesTicks[299] = utilReadInt(gzFile) & 0xff;
        gbTimerModeChange = (utilReadInt(gzFile) ? true : false);
        gbTimerOnChange = (utilReadInt(gzFile) ? true : false);
        gbHardware = utilReadInt(gzFile);
        gbBlackScreen = (utilReadInt(gzFile) ? true : false);
        oldRegister_WY = utilReadInt(gzFile);
        gbWindowLine = utilReadInt(gzFile);
        inUseRegister_WY = utilReadInt(gzFile);
        gbScreenOn = (utilReadInt(gzFile) ? true : false);
    }

    if (gbSpeed)
        gbLine99Ticks *= 2;

    systemSaveUpdateCounter = SYSTEM_SAVE_NOT_UPDATED;

    if (version >= 12 && utilReadInt(gzFile) != 0x12345678) {
        // fails if something read too much/little from file
        return false;
    }

    return true;
}

bool gbReadMemSaveState(char* memory, int available)
{
    gzFile gzFile = utilMemGzOpen(memory, available, "r");

    bool res = gbReadSaveState(gzFile);

    utilGzClose(gzFile);

    return res;
}

bool gbReadSaveState(const char* name)
{
    gzFile gzFile = utilGzOpen(name, "rb");

    if (gzFile == NULL) {
        return false;
    }

    bool res = gbReadSaveState(gzFile);

    utilGzClose(gzFile);

    return res;
}

bool gbWritePNGFile(const char* fileName)
{
    if (gbBorderOn)
        return utilWritePNGFile(fileName, kSGBWidth, kSGBHeight, g_pix);
    return utilWritePNGFile(fileName, kGBWidth, kGBHeight, g_pix);
}

bool gbWriteBMPFile(const char* fileName)
{
    if (gbBorderOn)
        return utilWriteBMPFile(fileName, kSGBWidth, kSGBHeight, g_pix);
    return utilWriteBMPFile(fileName, kGBWidth, kGBHeight, g_pix);
}
#endif // !__LIBRETRO__

void gbCleanUp()
{
    if (gbRam != nullptr) {
        free(gbRam);
        gbRam = nullptr;
    }

    if (gbRom != nullptr) {
        free(gbRom);
        gbRom = nullptr;
    }

    if (g_bios != nullptr) {
        free(g_bios);
        g_bios = nullptr;
    }

    if (gbMemory != nullptr) {
        free(gbMemory);
        gbMemory = nullptr;
    }

    if (gbLineBuffer != nullptr) {
        free(gbLineBuffer);
        gbLineBuffer = nullptr;
    }

    if (g_pix != nullptr) {
        free(g_pix);
        g_pix = nullptr;
    }

    gbSgbShutdown();

    if (gbVram != nullptr) {
        free(gbVram);
        gbVram = nullptr;
    }

    if (gbWram != nullptr) {
        free(gbWram);
        gbWram = nullptr;
    }

    if (gbTAMA5ram != nullptr) {
        free(gbTAMA5ram);
        gbTAMA5ram = nullptr;
    }

    g_gbCartData = gbCartData();
    g_mapper = nullptr;
    g_mapperRAM = nullptr;
    g_mapperReadRAM = nullptr;
    g_mapperUpdateClock = nullptr;
    systemSaveUpdateCounter = SYSTEM_SAVE_NOT_UPDATED;

#if !defined(__LIBRETRO__)
    g_vbamIoVecs.clear();
#endif  // !__LIBRETRO__
}

bool gbLoadRom(const char* filename) {
    int romSize = 0;

    if (gbRom != nullptr) {
        gbCleanUp();
    }

    systemSaveUpdateCounter = SYSTEM_SAVE_NOT_UPDATED;

    gbRom = utilLoad(filename, utilIsGBImage, nullptr, romSize);
    if (!gbRom)
        return false;

    g_gbBatteryError = false;

    if (g_bios != nullptr) {
        free(g_bios);
        g_bios = nullptr;
    }
    g_bios = (uint8_t*)calloc(1, kGBBiosBufferSize);
    if (g_bios == nullptr) {
        return false;
    }

    gbMemory = (uint8_t*)malloc(65536);
    if (gbMemory == nullptr) {
        return false;
    }

    g_pix = (uint8_t*)calloc(1, kGBPixSize);
    if (g_pix == nullptr) {
        return false;
    }

    gbLineBuffer = (uint16_t*)malloc(kGBLineBufferSize);
    if (gbLineBuffer == nullptr) {
        return false;
    }

    return gbInitializeRom(romSize);
}

int gbGetNextEvent(int _clockTicks)
{
    if (register_LCDC & 0x80) {
        if (gbLcdTicks < _clockTicks)
            _clockTicks = gbLcdTicks;

        if (gbLcdTicksDelayed < _clockTicks)
            _clockTicks = gbLcdTicksDelayed;

        if (gbLcdLYIncrementTicksDelayed < _clockTicks)
            _clockTicks = gbLcdLYIncrementTicksDelayed;
    }

    if (gbLcdLYIncrementTicks < _clockTicks)
        _clockTicks = gbLcdLYIncrementTicks;

    if (gbSerialOn && (gbSerialTicks < _clockTicks))
        _clockTicks = gbSerialTicks;

    if (gbTimerOn && (((gbInternalTimer)&gbTimerMask[gbTimerMode]) + 1 < _clockTicks))
        _clockTicks = ((gbInternalTimer)&gbTimerMask[gbTimerMode]) + 1;

    //if(soundTicks && (soundTicks < _clockTicks))
    //  _clockTicks = soundTicks;

    if ((_clockTicks <= 0) || (gbInterruptWait))
        _clockTicks = 1;

    return _clockTicks;
}

void gbDrawLine()
{
    switch (systemColorDepth) {
    case 16: {
#ifdef __LIBRETRO__
        uint16_t* dest = (uint16_t*)g_pix + gbBorderLineSkip * (register_LY + gbBorderRowSkip)
            + gbBorderColumnSkip;
#else
        uint16_t* dest = (uint16_t*)g_pix + (gbBorderLineSkip + 2) * (register_LY + gbBorderRowSkip + 1)
            + gbBorderColumnSkip;
#endif
        for (size_t x = 0; x < kGBWidth;) {
            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];

            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];

            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];

            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];
            *dest++ = systemColorMap16[gbLineMix[x++]];
        }
        if (gbBorderOn)
            dest += gbBorderColumnSkip;
#ifndef __LIBRETRO__
        *dest++ = 0; // for filters that read one pixel more
#endif
    } break;

    case 24: {
        uint8_t* dest = (uint8_t*)g_pix + 3 * (gbBorderLineSkip * (register_LY + gbBorderRowSkip) + gbBorderColumnSkip);
        for (size_t x = 0; x < kGBWidth;) {
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;

            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;

            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;

            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
            *((uint32_t*)dest) = systemColorMap32[gbLineMix[x++]];
            dest += 3;
        }
    } break;

    case 32: {
#ifdef __LIBRETRO__
        uint32_t* dest = (uint32_t*)g_pix + gbBorderLineSkip * (register_LY + gbBorderRowSkip)
            + gbBorderColumnSkip;
#else
        uint32_t* dest = (uint32_t*)g_pix + (gbBorderLineSkip + 1) * (register_LY + gbBorderRowSkip + 1)
            + gbBorderColumnSkip;
#endif
        for (size_t x = 0; x < kGBWidth;) {
            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];

            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];

            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];

            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];
            *dest++ = systemColorMap32[gbLineMix[x++]];
        }
    } break;
    }
}

static void gbUpdateJoypads() {
    if (systemReadJoypads()) {
        // read joystick
        if (gbSgbMode && gbSgbMultiplayer) {
            if (gbSgbFourPlayers) {
                gbJoymask[0] = systemReadJoypad(0);
                gbJoymask[1] = systemReadJoypad(1);
                gbJoymask[2] = systemReadJoypad(2);
                gbJoymask[3] = systemReadJoypad(3);
            } else {
                gbJoymask[0] = systemReadJoypad(0);
                gbJoymask[1] = systemReadJoypad(1);
            }
        } else {
            gbJoymask[0] = systemReadJoypad(-1);
        }
    }

    if (g_gbCartData.has_sensor()) {
        systemUpdateMotionSensor();
    }
}

void gbEmulate(int ticksToStop)
{
    gbRegister tempRegister;
    uint8_t tempValue;
    int8_t offset;


    clockTicks = 0;
    gbDmaTicks = 0;

    int opcode = 0;

    int opcode1 = 0;
    int opcode2 = 0;
    bool execute = false;
    bool frameDone = false;

    gbUpdateJoypads();

    while (1) {
        uint16_t oldPCW = PC.W;

        if (IFF & 0x80) {
            if (register_LCDC & 0x80) {
                clockTicks = gbLcdTicks;
            } else
                clockTicks = 1000;

            clockTicks = gbGetNextEvent(clockTicks);

            /*if(gbLcdTicksDelayed < clockTicks)
        clockTicks = gbLcdTicksDelayed;

      if(gbLcdLYIncrementTicksDelayed < clockTicks)
        clockTicks = gbLcdLYIncrementTicksDelayed;

      if(gbLcdLYIncrementTicks < clockTicks)
        clockTicks = gbLcdLYIncrementTicks;

      if(gbSerialOn && (gbSerialTicks < clockTicks))
        clockTicks = gbSerialTicks;

      if(gbTimerOn && (((gbInternalTimer) & gbTimerMask[gbTimerMode])+1 < clockTicks))
        clockTicks = ((gbInternalTimer) & gbTimerMask[gbTimerMode])+1;

      if(soundTicks && (soundTicks < clockTicks))
        clockTicks = soundTicks;

      if ((clockTicks<=0) || (gbInterruptWait))
          clockTicks = 1;*/

        } else {

            // First we apply the clockTicks, then we execute the opcodes.
            opcode1 = 0;
            opcode2 = 0;
            execute = true;

            opcode2 = opcode1 = opcode = gbReadMemory(PC.W++);

            // If HALT state was launched while IME = 0 and (register_IF & register_IE & 0x1F),
            // PC.W is not incremented for the first byte of the next instruction.
            if (IFF & 2) {
                PC.W--;
                IFF &= ~2;
            }

            clockTicks = gbCycles[opcode];

            switch (opcode) {
            case 0xCB:
                // extended opcode
                opcode2 = opcode = gbReadMemory(PC.W++);
                clockTicks = gbCyclesCB[opcode];
                break;
            }
            gbOldClockTicks = clockTicks - 1;
            gbIntBreak = 1;
        }

        if (!emulating)
            return;

        // For 'breakpoint' support (opcode 0xFC is considered as a breakpoint)
        if ((clockTicks == 0) && execute) {
            PC.W = oldPCW;
            return;
        }

        if (!(IFF & 0x80))
            clockTicks = 1;

    gbRedoLoop:

        if (gbInterruptWait)
            gbInterruptWait = 0;
        else
            gbInterruptLaunched = 0;

        // Used for the EI/DI instruction's delay.
        if (IFF & 0x38) {
            int tempIFF = (IFF >> 4) & 3;

            if (tempIFF <= clockTicks) {
                tempIFF = 0;
                IFF |= 1;
            } else
                tempIFF -= clockTicks;
            IFF = (IFF & 0xCF) | (tempIFF << 4);

            if (IFF & 0x08)
                IFF &= 0x82;
        }

        if (register_LCDCBusy) {
            register_LCDCBusy -= clockTicks;
            if (register_LCDCBusy < 0)
                register_LCDCBusy = 0;
        }

        if (gbSgbMode) {
            if (gbSgbPacketTimeout) {
                gbSgbPacketTimeout -= clockTicks;

                if (gbSgbPacketTimeout <= 0)
                    gbSgbResetPacketState();
            }
        }

        ticksToStop -= clockTicks;
        soundTicks += clockTicks;
         if (!gbSpeed) soundTicks += clockTicks;

        // DIV register emulation
        gbDivTicks -= clockTicks;
        while (gbDivTicks <= 0) {
            gbMemory[0xff04] = ++register_DIV;
            gbDivTicks += GBDIV_CLOCK_TICKS;
        }

        if (register_LCDC & 0x80) {
            // LCD stuff

            gbLcdTicks -= clockTicks;
            gbLcdTicksDelayed -= clockTicks;
            gbLcdLYIncrementTicks -= clockTicks;
            gbLcdLYIncrementTicksDelayed -= clockTicks;

            // our counters are off, see what we need to do

            // This looks (and kinda is) complicated, however this
            // is the only way I found to emulate properly the way
            // the real hardware operates...
            while (((gbLcdTicks <= 0) && (gbLCDChangeHappened == false)) || ((gbLcdTicksDelayed <= 0) && (gbLCDChangeHappened == true)) || ((gbLcdLYIncrementTicks <= 0) && (gbLYChangeHappened == false)) || ((gbLcdLYIncrementTicksDelayed <= 0) && (gbLYChangeHappened == true))) {

                if ((gbLcdLYIncrementTicks <= 0) && (!gbLYChangeHappened)) {
                    gbLYChangeHappened = true;
                    gbMemory[0xff44] = register_LY = (register_LY + 1) % 154;

                    if (register_LY == 0x91) {
                        /* if (IFF & 0x80)
              gbScreenOn = !gbScreenOn;
            else*/ if (register_LCDC & 0x80)
                            gbScreenOn = true;
                    }

                    gbLcdLYIncrementTicks += GBLY_INCREMENT_CLOCK_TICKS;

                    if (gbLcdMode == 1) {

                        if (register_LY == 153)
                            gbLcdLYIncrementTicks -= GBLY_INCREMENT_CLOCK_TICKS - gbLine99Ticks;
                        else if (register_LY == 0)
                            gbLcdLYIncrementTicks += GBLY_INCREMENT_CLOCK_TICKS - gbLine99Ticks;
                    }

                    // GB only 'bug' : Halt state is broken one tick before LY==LYC interrupt
                    // is reflected in the registers.
                    if ((gbHardware & 5) && (IFF & 0x80) && (register_LY == register_LYC)
                        && (register_STAT & 0x40) && (register_LY != 0)) {
                        if (!((gbLcdModeDelayed != 1) && (register_LY == 0))) {
                            gbInt48Signal &= ~9;
                            gbCompareLYToLYC();
                            gbLYChangeHappened = false;
                            gbMemory[0xff41] = register_STAT;
                            gbMemory[0xff0f] = register_IF;
                        }

                        gbLcdLYIncrementTicksDelayed += GBLY_INCREMENT_CLOCK_TICKS - gbLine99Ticks + 1;
                    }
                }

                if ((gbLcdTicks <= 0) && (!gbLCDChangeHappened)) {
                    gbLCDChangeHappened = true;

                    switch (gbLcdMode) {
                    case 0: {
                        // H-Blank
                        // check if we reached the V-Blank period
                        if (register_LY == kGBHeight) {
                            // Yes, V-Blank
                            // set the LY increment counter
                            if (gbHardware & 0x5) {
                                register_IF |= 1; // V-Blank interrupt
                            }

                            gbInt48Signal &= ~6;
                            if (register_STAT & 0x10) {
                                // send LCD interrupt only if no interrupt 48h signal...
                                if ((!(gbInt48Signal & 1)) && ((!(gbInt48Signal & 8)) || (gbHardware & 0x0a))) {
                                    register_IF |= 2;
                                    gbInterruptLaunched |= 2;
                                    if (gbHardware & 0xa)
                                        gbInterruptWait = 1;
                                }
                                gbInt48Signal |= 2;
                            }
                            gbInt48Signal &= ~1;

                            gbLcdTicks += GBLCD_MODE_1_CLOCK_TICKS;
                            gbLcdMode = 1;

                        } else {
                            // go the the OAM being accessed mode
                            gbLcdTicks += GBLCD_MODE_2_CLOCK_TICKS;
                            gbLcdMode = 2;

                            gbInt48Signal &= ~6;
                            if (register_STAT & 0x20) {
                                // send LCD interrupt only if no interrupt 48h signal...
                                if (!gbInt48Signal) {
                                    register_IF |= 2;
                                    gbInterruptLaunched |= 2;
                                }
                                gbInt48Signal |= 4;
                            }
                            gbInt48Signal &= ~1;
                        }
                    } break;
                    case 1: {
                        // V-Blank
                        // next mode is OAM being accessed mode
                        gbInt48Signal &= ~5;
                        if (register_STAT & 0x20) {
                            // send LCD interrupt only if no interrupt 48h signal...
                            if (!gbInt48Signal) {
                                register_IF |= 2;
                                gbInterruptLaunched |= 2;
                                if ((gbHardware & 0xa) && (IFF & 0x80))
                                    gbInterruptWait = 1;
                            }
                            gbInt48Signal |= 4;
                        }
                        gbInt48Signal &= ~2;

                        gbLcdTicks += GBLCD_MODE_2_CLOCK_TICKS;

                        gbLcdMode = 2;
                        register_LY = 0x00;

                    } break;
                    case 2: {

                        // OAM being accessed mode
                        // next mode is OAM and VRAM in use
                        if ((gbScreenOn) && (register_LCDC & 0x80)) {
                            gbDrawSprites(false);
                            // Used to add a one tick delay when a window line is drawn.
                            //(fixes a part of Carmaggedon problem)
                            if ((register_LCDC & 0x01 || gbCgbMode) && (register_LCDC & 0x20) && (gbWindowLine != -2)) {

                                int tempinUseRegister_WY = inUseRegister_WY;
                                int tempgbWindowLine = gbWindowLine;

                                if ((tempgbWindowLine == -1) || (tempgbWindowLine > (int)kGBHeight)) {
                                    tempinUseRegister_WY = oldRegister_WY;
                                    if (register_LY > oldRegister_WY)
                                        tempgbWindowLine = 146;
                                }

                                if (register_LY >= tempinUseRegister_WY) {

                                    if (tempgbWindowLine == -1)
                                        tempgbWindowLine = 0;

                                    int wx = register_WX;
                                    wx -= 7;
                                    if (wx < 0)
                                        wx = 0;

                                    if ((wx <= 159) && (tempgbWindowLine <= 143)) {
                                        for (int i = wx; i < 300; i++)
                                            if (gbSpeed)
                                                gbSpritesTicks[i] += 3;
                                            else
                                                gbSpritesTicks[i] += 1;
                                    }
                                }
                            }
                        }

                        gbInt48Signal &= ~7;

                        gbLcdTicks += GBLCD_MODE_3_CLOCK_TICKS + gbSpritesTicks[299];
                        gbLcdMode = 3;
                    } break;
                    case 3: {
                        // OAM and VRAM in use
                        // next mode is H-Blank

                        gbInt48Signal &= ~7;
                        if (register_STAT & 0x08) {
                            // send LCD interrupt only if no interrupt 48h signal...
                            if (!(gbInt48Signal & 8)) {
                                register_IF |= 2;
                                if ((gbHardware & 0xa) && (IFF & 0x80))
                                    gbInterruptWait = 1;
                            }
                            gbInt48Signal |= 1;
                        }

                        gbLcdTicks += GBLCD_MODE_0_CLOCK_TICKS - gbSpritesTicks[299];

                        gbLcdMode = 0;

                        // No HDMA during HALT !
                        if (gbHdmaOn && (!(IFF & 0x80) || (register_IE & register_IF & 0x1f))) {
                            gbDoHdma();
                        }

                    } break;
                    }
                }

                if ((gbLcdTicksDelayed <= 0) && (gbLCDChangeHappened)) {
                    int framesToSkip = systemFrameSkip;

                    static bool speedup_throttle_set = false;
                    bool turbo_button_pressed        = (gbJoymask[0] >> 10) & 1;
#ifndef __LIBRETRO__
                    static uint32_t last_throttle;

                    if (turbo_button_pressed) {
                        if (coreOptions.speedup_frame_skip)
                            framesToSkip = coreOptions.speedup_frame_skip;
                        else {
                            if (!speedup_throttle_set && coreOptions.throttle != coreOptions.speedup_throttle) {
                                last_throttle = coreOptions.throttle;
                                soundSetThrottle(coreOptions.speedup_throttle);
                                speedup_throttle_set = true;
                            }

                            if (coreOptions.speedup_throttle_frame_skip)
                                framesToSkip += std::ceil(double(coreOptions.speedup_throttle) / 100.0) - 1;
                        }
                    }
                    else if (speedup_throttle_set) {
                        soundSetThrottle(last_throttle);
                        speedup_throttle_set = false;
                    }
#else
                    if (turbo_button_pressed)
                        framesToSkip = 9;
#endif

                    //gbLcdTicksDelayed = gbLcdTicks+1;
                    gbLCDChangeHappened = false;
                    switch (gbLcdModeDelayed) {
                    case 0: {
                        // H-Blank

                        memset(gbSCYLine, gbSCYLine[299], sizeof(gbSCYLine));
                        memset(gbSCXLine, gbSCXLine[299], sizeof(gbSCXLine));
                        memset(gbBgpLine, gbBgpLine[299], sizeof(gbBgpLine));
                        memset(gbObp0Line, gbObp0Line[299], sizeof(gbObp0Line));
                        memset(gbObp1Line, gbObp1Line[299], sizeof(gbObp1Line));
                        memset(gbSpritesTicks, gbSpritesTicks[299], sizeof(gbSpritesTicks));

                        if (gbWindowLine < 0)
                            oldRegister_WY = register_WY;
                        // check if we reached the V-Blank period
                        if (register_LY == kGBHeight) {
                            // Yes, V-Blank
                            // set the LY increment counter

                            if (register_LCDC & 0x80) {
                                if (gbHardware & 0xa) {

                                    register_IF |= 1; // V-Blank interrupt
                                    gbInterruptLaunched |= 1;
                                }
                            }

                            gbLcdTicksDelayed += GBLCD_MODE_1_CLOCK_TICKS;
                            gbLcdModeDelayed = 1;

                            gbFrameCount++;
                            systemFrame();
                            gbSoundTick(soundTicks);

                            if ((gbFrameCount % 10) == 0)
                                system10Frames();

                            if (gbFrameCount >= 60) {
                                uint32_t currentTime = systemGetClock();
                                if (currentTime != gbLastTime)
                                    systemShowSpeed(100000 / (currentTime - gbLastTime));
                                else
                                    systemShowSpeed(0);
                                gbLastTime = currentTime;
                                gbFrameCount = 0;
                            }

                            int newmask = gbJoymask[0] & 255;

                            if (newmask) {
                                gbMemory[0xff0f] = register_IF |= 16;
                            }

                            newmask = (gbJoymask[0] >> 10);

                            coreOptions.speedup = false;

                            if (newmask & 1 && !speedup_throttle_set)
                                coreOptions.speedup = true;

                            gbCapture = (newmask & 2) ? true : false;

                            if (gbCapture && !gbCapturePrevious) {
                                gbCaptureNumber++;
                                systemScreenCapture(gbCaptureNumber);
                            }
                            gbCapturePrevious = gbCapture;

                            if (gbFrameSkipCount >= framesToSkip) {

                                if (!gbSgbMask) {
                                    if (gbBorderOn)
                                        gbSgbRenderBorder();
                                    //if (gbScreenOn)
                                    systemDrawScreen();
                                    if (systemPauseOnFrame())
                                        ticksToStop = 0;
                                }
                                gbFrameSkipCount = 0;
                            } else {
                                gbFrameSkipCount++;
                                systemSendScreen();
                            }

                            frameDone = true;

                        } else {
                            // go the the OAM being accessed mode
                            gbLcdTicksDelayed += GBLCD_MODE_2_CLOCK_TICKS;
                            gbLcdModeDelayed = 2;
                            gbInt48Signal &= ~3;
                        }
                    } break;
                    case 1: {
                        // V-Blank
                        // next mode is OAM being accessed mode

                        // gbScreenOn = true;

                        oldRegister_WY = register_WY;

                        gbLcdTicksDelayed += GBLCD_MODE_2_CLOCK_TICKS;
                        gbLcdModeDelayed = 2;

                        // reset the window line
                        gbWindowLine = -1;
                    } break;
                    case 2: {
                        // OAM being accessed mode
                        // next mode is OAM and VRAM in use
                        gbLcdTicksDelayed += GBLCD_MODE_3_CLOCK_TICKS + gbSpritesTicks[299];
                        gbLcdModeDelayed = 3;
                    } break;
                    case 3: {

                        // OAM and VRAM in use
                        // next mode is H-Blank
                        if ((register_LY < kGBHeight) && (register_LCDC & 0x80) && gbScreenOn) {
                            if (!gbSgbMask) {
                                if (gbFrameSkipCount >= framesToSkip) {
                                    if (!gbBlackScreen) {
                                        gbRenderLine();
                                        gbDrawSprites(true);
                                    } else if (gbBlackScreen) {
                                        uint16_t color = gbColorOption ? gbColorFilter[0] : 0;
                                        if (!gbCgbMode)
                                            color = gbColorOption ? gbColorFilter[gbPalette[3] & 0x7FFF] : gbPalette[3] & 0x7FFF;
                                        for (size_t i = 0; i < kGBWidth; i++) {
                                            gbLineMix[i] = color;
                                            gbLineBuffer[i] = 0;
                                        }
                                    }
                                    gbDrawLine();
                                }
                            }
                        }
                        gbLcdTicksDelayed += GBLCD_MODE_0_CLOCK_TICKS - gbSpritesTicks[299];
                        gbLcdModeDelayed = 0;
                    } break;
                    }
                }

                if ((gbLcdLYIncrementTicksDelayed <= 0) && (gbLYChangeHappened == true)) {

                    gbLYChangeHappened = false;

                    if (!((gbLcdMode != 1) && (register_LY == 0))) {
                        {
                            gbInt48Signal &= ~8;
                            gbCompareLYToLYC();
                            if ((gbInt48Signal == 8) && (!((register_LY == 0) && (gbHardware & 1))))
                                gbInterruptLaunched |= 2;
                            if ((gbHardware & (gbSpeed ? 8 : 2)) && (register_LY == 0) && ((register_STAT & 0x44) == 0x44) && (gbLcdLYIncrementTicksDelayed == 0)) {
                                gbInterruptWait = 1;
                            }
                        }
                    }
                    gbLcdLYIncrementTicksDelayed += GBLY_INCREMENT_CLOCK_TICKS;

                    if (gbLcdModeDelayed == 1) {

                        if (register_LY == 153)
                            gbLcdLYIncrementTicksDelayed -= GBLY_INCREMENT_CLOCK_TICKS - gbLine99Ticks;
                        else if (register_LY == 0)
                            gbLcdLYIncrementTicksDelayed += GBLY_INCREMENT_CLOCK_TICKS - gbLine99Ticks;
                    }
                    gbMemory[0xff0f] = register_IF;
                    gbMemory[0xff41] = register_STAT;
                }
            }
            gbMemory[0xff0f] = register_IF;
            gbMemory[0xff41] = register_STAT = (register_STAT & 0xfc) | gbLcdModeDelayed;
        } else {

            // Used to update the screen with white lines when it's off.
            // (it looks strange, but it's kinda accurate :p)
            // You can try the Mario Demo Vx.x for exemple
            // (check the bottom 2 lines while moving)
            if (!gbWhiteScreen) {
                gbScreenTicks -= clockTicks;
                gbLcdLYIncrementTicks -= clockTicks;
                while (gbLcdLYIncrementTicks <= 0) {
                    register_LY = ((register_LY + 1) % 154);
                    gbLcdLYIncrementTicks += GBLY_INCREMENT_CLOCK_TICKS;
                }
                if (gbScreenTicks <= 0) {
                    gbWhiteScreen = 1;
                    uint8_t register_LYLcdOff = ((register_LY + 154) % 154);
                    for (register_LY = 0; register_LY <= 0x90; register_LY++) {
                        uint16_t color = gbColorOption ? gbColorFilter[0x7FFF] : 0x7FFF;
                        if (!gbCgbMode)
                            color = gbColorOption ? gbColorFilter[gbPalette[0] & 0x7FFF] : gbPalette[0] & 0x7FFF;
                        for (size_t i = 0; i < kGBWidth; i++) {
                            gbLineMix[i] = color;
                            gbLineBuffer[i] = 0;
                        }
                        gbDrawLine();
                    }
                    register_LY = register_LYLcdOff;
                }
            }

            if (gbWhiteScreen) {
                gbLcdLYIncrementTicks -= clockTicks;

                while (gbLcdLYIncrementTicks <= 0) {
                    register_LY = ((register_LY + 1) % 154);
                    gbLcdLYIncrementTicks += GBLY_INCREMENT_CLOCK_TICKS;
                    if (register_LY < kGBHeight) {

                        uint16_t color = gbColorOption ? gbColorFilter[0x7FFF] : 0x7FFF;
                        if (!gbCgbMode)
                            color = gbColorOption ? gbColorFilter[gbPalette[0] & 0x7FFF] : gbPalette[0] & 0x7FFF;
                        for (size_t i = 0; i < kGBWidth; i++) {
                            gbLineMix[i] = color;
                            gbLineBuffer[i] = 0;
                        }
                        gbDrawLine();
                    } else if ((register_LY == kGBHeight) && (!systemFrameSkip)) {
                        int framesToSkip = systemFrameSkip;
                        //if (coreOptions.speedup)
                        //    framesToSkip = 9; // try 6 FPS during speedup
                        if ((gbFrameSkipCount >= framesToSkip) || (gbWhiteScreen == 1)) {
                            gbWhiteScreen = 2;

                            if (!gbSgbMask) {
                                if (gbBorderOn)
                                    gbSgbRenderBorder();
                                //if (gbScreenOn)
                                systemDrawScreen();
                                if (systemPauseOnFrame())
                                    ticksToStop = 0;
                            }
                        } else {
                            systemSendScreen();
                        }

                        gbFrameCount++;

                        systemFrame();
                        gbSoundTick(soundTicks);

                        if ((gbFrameCount % 10) == 0)
                            system10Frames();

                        if (gbFrameCount >= 60) {
                            uint32_t currentTime = systemGetClock();
                            if (currentTime != gbLastTime)
                                systemShowSpeed(100000 / (currentTime - gbLastTime));
                            else
                                systemShowSpeed(0);
                            gbLastTime = currentTime;
                            gbFrameCount = 0;
                        }
                        frameDone = true;
                    }
                }
            }
        }

        gbMemory[0xff41] = register_STAT;

#ifndef NO_LINK
        // serial emulation
        gbSerialOn = (gbMemory[0xff02] & 0x80);
        static int SIOctr = 0;
        SIOctr++;
        if (SIOctr % 5)
            //Transfer Started
            if (gbSerialOn && (GetLinkMode() == LINK_GAMEBOY_IPC || GetLinkMode() == LINK_GAMEBOY_SOCKET)) {
#ifdef OLD_GB_LINK
                if (linkConnected) {
                    gbSerialTicks -= clockTicks;

                    while (gbSerialTicks <= 0) {
                        // increment number of shifted bits
                        gbSerialBits++;
                        linkProc();
                        if (gbSerialOn && (gbMemory[0xff02] & 1)) {
                            if (gbSerialBits == 8) {
                                gbSerialBits = 0;
                                gbMemory[0xff01] = 0xff;
                                gbMemory[0xff02] &= 0x7f;
                                gbSerialOn = 0;
                                gbMemory[0xff0f] = register_IF |= 8;
                                gbSerialTicks = 0;
                            }
                        }
                        gbSerialTicks += GBSERIAL_CLOCK_TICKS;
                    }
                } else {
#endif
                    if (gbMemory[0xff02] & 1) { //internal clocks (master)
                        gbSerialTicks -= clockTicks;

                        // overflow
                        while (gbSerialTicks <= 0) {
                            // shift serial byte to right and put a 1 bit in its place
                            //      gbMemory[0xff01] = 0x80 | (gbMemory[0xff01]>>1);
                            // increment number of shifted bits
                            gbSerialBits++;
                            if (gbSerialBits >= 8) {
                                // end of transmission
                                gbSerialTicks = 0;
                                gbSerialBits = 0;
                            } else
                                gbSerialTicks += GBSERIAL_CLOCK_TICKS;
                        }
                    } else //external clocks (slave)
                    {
                        gbSerialTicks -= clockTicks;

                        // overflow
                        while (gbSerialTicks <= 0) {
                            // shift serial byte to right and put a 1 bit in its place
                            //      gbMemory[0xff01] = 0x80 | (gbMemory[0xff01]>>1);
                            // increment number of shifted bits
                            gbSerialBits++;
                            if (gbSerialBits >= 8) {
                                // end of transmission
                                uint16_t dat = 0;
                                if (LinkIsWaiting)
                                    if (gbSerialFunction) { // external device
                                        gbSIO_SC = gbMemory[0xff02];
                                        if (!LinkFirstTime) {
                                            dat = (gbSerialFunction(gbMemory[0xff01]) << 8) | 1;
                                        } else //external clock not suppose to start a transfer, but there are time where both side using external clock and couldn't communicate properly
                                        {
                                            if (gbMemory)
                                                gbSerialOn = (gbMemory[0xff02] & 0x80);
                                            dat = gbLinkUpdate(gbMemory[0xff01], gbSerialOn);
                                        }
                                        gbMemory[0xff01] = (dat >> 8);
                                    } //else
                                gbSerialTicks = 0;
                                if ((dat & 1) && (gbMemory[0xff02] & 0x80)) //(dat & 1)==1 when reply data received
                                {
                                    gbMemory[0xff02] &= 0x7f;
                                    gbSerialOn = 0;
                                    gbMemory[0xff0f] = register_IF |= 8;
                                }
                                gbSerialBits = 0;
                            } else
                                gbSerialTicks += GBSERIAL_CLOCK_TICKS;
                        }
                    }
#ifdef OLD_GB_LINK
                }
#endif
            }
#else
        static int SIOctr = 0;
        SIOctr++;
        if (SIOctr % 5) {
            if (gbSerialOn) {
                if  (gbMemory[0xff02] & 1) {
                    gbSerialTicks -= clockTicks;
                    while (gbSerialTicks <= 0) {
                        gbMemory[0xff01] = (gbMemory[0xff01] << 1) | 1;
                        gbSerialBits++;
                        if (gbSerialBits >= 8) {
                            gbMemory[0xff02] &= 0x7f;
                            gbMemory[0xff0f] = register_IF |= 8;
                            gbSerialOn = 0;
                            gbSerialBits = 0;
                        } else
                            gbSerialTicks += GBSERIAL_CLOCK_TICKS;
                    }
                }
            }
        }
#endif
        // TODO: evaluate and fix this
        // On VBA-M (gb core running twice as fast?), each vblank is uses 35112 cycles.
        // on some cases no vblank is generated causing sound ticks to keep accumulating causing core to crash.
        // This forces core to flush sound buffers when expected sound ticks has passed and no frame is done yet.which then ends cpuloop
        if ((soundTicks > SOUND_CLOCK_TICKS) && !frameDone) {
            int last_st = soundTicks;
            gbSoundTick(soundTicks);
            soundTicks = (last_st - SOUND_CLOCK_TICKS);
        }

        // timer emulation

        if (gbTimerOn) {
            gbTimerTicks = ((gbInternalTimer)&gbTimerMask[gbTimerMode]) + 1 - clockTicks;

            while (gbTimerTicks <= 0) {
                register_TIMA++;
                // timer overflow!
                if ((register_TIMA & 0xff) == 0) {
                    // reload timer modulo
                    register_TIMA = register_TMA;
                    // flag interrupt
                    gbMemory[0xff0f] = register_IF |= 4;
                }
                gbTimerTicks += gbTimerClockTicks;
            }
            gbTimerOnChange = false;
            gbTimerModeChange = false;

            gbMemory[0xff05] = register_TIMA;
        }

        gbInternalTimer -= clockTicks;
        while (gbInternalTimer < 0)
            gbInternalTimer += 0x100;

        clockTicks = 0;

        if (gbIntBreak == 1) {
            gbIntBreak = 0;
            if ((register_IE & register_IF & gbInterruptLaunched & 0x3) && ((IFF & 0x81) == 1) && (!gbInterruptWait) && (execute)) {
                gbIntBreak = 2;
                PC.W = oldPCW;
                execute = false;
                gbOldClockTicks = 0;
            }
            if (gbOldClockTicks) {
                clockTicks = gbOldClockTicks;
                gbOldClockTicks = 0;
                goto gbRedoLoop;
            }
        }

        // Executes the opcode(s), and apply the instruction's remaining clockTicks (if any).
        if (execute) {
            switch (opcode1) {
            case 0xCB:
                // extended opcode
                switch (opcode2) {
#include "core/gb/internal/gbCodesCB.h"
                }
                break;
#include "core/gb/internal/gbCodes.h"
            }
            execute = false;

            if (clockTicks) {
                gbDmaTicks += clockTicks;
                clockTicks = 0;
            }
        }

        if (gbDmaTicks) {
            clockTicks = gbGetNextEvent(gbDmaTicks);

            if (clockTicks <= gbDmaTicks)
                gbDmaTicks -= clockTicks;
            else {
                clockTicks = gbDmaTicks;
                gbDmaTicks = 0;
            }

            goto gbRedoLoop;
        }

        // Remove the 'if an IE is pending' flag if IE has finished being executed.
        if ((IFF & 0x40) && !(IFF & 0x30))
            IFF &= 0x81;

        if ((register_IE & register_IF & 0x1f) && (IFF & 0x81) && (!gbInterruptWait)) {

            if (IFF & 1) {
                // Add 5 ticks for the interrupt execution time
                gbDmaTicks += 5;

                if (gbIntBreak == 2) {
                    gbDmaTicks--;
                    gbIntBreak = 0;
                }

                if (register_IF & register_IE & 1)
                    gbVblank_interrupt();
                else if (register_IF & register_IE & 2)
                    gbLcd_interrupt();
                else if (register_IF & register_IE & 4)
                    gbTimer_interrupt();
                else if (register_IF & register_IE & 8)
                    gbSerial_interrupt();
                else if (register_IF & register_IE & 16)
                    gbJoypad_interrupt();
            }

            IFF &= ~0x81;
        }

        if (IFF & 0x08)
            IFF &= ~0x79;

        // Used to apply the interrupt's execution time.
        if (gbDmaTicks) {
            clockTicks = gbGetNextEvent(gbDmaTicks);

            if (clockTicks <= gbDmaTicks)
                gbDmaTicks -= clockTicks;
            else {
                clockTicks = gbDmaTicks;
                gbDmaTicks = 0;
            }
            goto gbRedoLoop;
        }

        gbBlackScreen = false;

        if (ticksToStop <= 0 || frameDone) { // Stop loop
            return;
        }
    }
}

bool gbLoadRomData(const char* data, size_t size) {
    if (gbRom != nullptr) {
        gbCleanUp();
    }

    systemSaveUpdateCounter = SYSTEM_SAVE_NOT_UPDATED;

    gbRom = (uint8_t*)calloc(1, size);
    if (gbRom == nullptr) {
        return false;
    }

    memcpy(gbRom, data, size);

    g_gbBatteryError = false;

    if (g_bios != nullptr) {
        free(g_bios);
        g_bios = nullptr;
    }

    g_bios = (uint8_t*)calloc(1, kGBBiosBufferSize);
    if (g_bios == nullptr) {
        return false;
    }

    gbMemory = (uint8_t*)malloc(65536);
    if (gbMemory == nullptr) {
        return false;
    }

    g_pix = (uint8_t*)calloc(1, kGBPixSize);
    if (g_pix == nullptr) {
        return false;
    }

    gbLineBuffer = (uint16_t*)malloc(kGBLineBufferSize);
    if (gbLineBuffer == nullptr) {
        return false;
    }

    return gbInitializeRom(size);
}

#ifndef __LIBRETRO__
bool gbApplyPatch(const char* patchName) {
    int size = g_gbCartData.rom_size();
    if (!applyPatch(patchName, &gbRom, &size)) {
        return false;
    }

    // We should re-parse the header every time in case a patch has changed the
    // ROM header.
    return gbInitializeRom(size);
}
#endif  // __LIBRETRO__

#ifdef __LIBRETRO__
#include <stddef.h>

unsigned int gbWriteSaveState(uint8_t* data)
{
	uint8_t* orig = data;

    utilWriteIntMem(data, GBSAVE_GAME_VERSION);

    utilWriteMem(data, &gbRom[0x134], 15);

    utilWriteIntMem(data, coreOptions.useBios);
    utilWriteIntMem(data, inBios);

    utilWriteDataMem(data, gbSaveGameStruct);

    utilWriteMem(data, &IFF, 2);

    if (gbSgbMode) {
        gbSgbSaveGame(data);
    }

    utilWriteMem(data, &gbDataMBC1, sizeof(gbDataMBC1));
    utilWriteMem(data, &gbDataMBC2, sizeof(gbDataMBC2));
    utilWriteMem(data, &gbDataMBC3, sizeof(gbDataMBC3));
    utilWriteMem(data, &gbDataMBC5, sizeof(gbDataMBC5));
    utilWriteMem(data, &gbDataHuC1, sizeof(gbDataHuC1));
    utilWriteMem(data, &gbDataHuC3, sizeof(gbDataHuC3));
    if (g_gbCartData.mapper_type() == gbCartData::MapperType::kHuC3)
        utilWriteMem(data, &gbRTCHuC3, sizeof(gbRTCHuC3));
    utilWriteMem(data, &gbDataTAMA5, sizeof(gbDataTAMA5));
    if (gbTAMA5ram != nullptr)
        utilWriteMem(data, gbTAMA5ram, kTama5RamSize);
    utilWriteMem(data, &gbDataMMM01, sizeof(gbDataMMM01));

    utilWriteMem(data, gbPalette, sizeof(gbPalette));

    utilWriteMem(data, &gbMemory[0x8000], 0x8000);

    if (g_gbCartData.HasRam()) {
        utilWriteIntMem(data, g_gbCartData.ram_size());
        utilWriteMem(data, gbRam, g_gbCartData.ram_size());
    }

    if (gbCgbMode) {
        utilWriteMem(data, gbVram, kGBVRamSize);
        utilWriteMem(data, gbWram, kGBWRamSize);
    }

    gbSoundSaveGame(data);

    // We dont care about cheat saves
    // gbCheatsSaveGame(data);

    utilWriteIntMem(data, gbLcdModeDelayed);
    utilWriteIntMem(data, gbLcdTicksDelayed);
    utilWriteIntMem(data, gbLcdLYIncrementTicksDelayed);
    utilWriteIntMem(data, gbSpritesTicks[299]);
    utilWriteIntMem(data, gbTimerModeChange);
    utilWriteIntMem(data, gbTimerOnChange);
    utilWriteIntMem(data, gbHardware);
    utilWriteIntMem(data, gbBlackScreen);
    utilWriteIntMem(data, oldRegister_WY);
    utilWriteIntMem(data, gbWindowLine);
    utilWriteIntMem(data, inUseRegister_WY);
    utilWriteIntMem(data, gbScreenOn);
    utilWriteIntMem(data, 0x12345678); // end marker

	return (ptrdiff_t)data - (ptrdiff_t)orig;
}

bool gbReadSaveState(const uint8_t* data)
{
    int version = utilReadIntMem(data);

   if (version != GBSAVE_GAME_VERSION) {
        systemMessage(MSG_UNSUPPORTED_VB_SGM,
            N_("Unsupported VBA-M save game version %d"), version);
        return false;
    }

    uint8_t romname[20];

    utilReadMem(romname, data, 15);

    if (memcmp(&gbRom[0x134], romname, 15) != 0) {
        systemMessage(MSG_CANNOT_LOAD_SGM_FOR,
            N_("Cannot load save game for %s. Playing %s"),
            romname, &gbRom[0x134]);
        return false;
    }

    bool ub = false;
    bool ib = false;

    ub = utilReadIntMem(data) ? true : false;
    ib = utilReadIntMem(data) ? true : false;

    if ((ub != coreOptions.useBios) && (ib)) {
        if (coreOptions.useBios)
            systemMessage(MSG_SAVE_GAME_NOT_USING_BIOS,
                N_("Save game is not using the BIOS files"));
        else
            systemMessage(MSG_SAVE_GAME_USING_BIOS,
                N_("Save game is using the BIOS file"));
        return false;
    }

    gbReset();

    inBios = ib;

    utilReadDataMem(data, gbSaveGameStruct);

    // Correct crash when loading color gameboy save in regular gameboy type.
    if (gbCgbMode) {
        if (gbVram == nullptr) {
            gbVram = (uint8_t*)calloc(1, kGBVRamSize);
            if (gbVram == nullptr) {
                return false;
            }
        }
        if (gbWram == nullptr) {
            gbWram = (uint8_t*)malloc(kGBWRamSize);
            if (gbWram == nullptr) {
                return false;
            }
        }
        memset(gbPalette, 0, sizeof(gbPalette));
    } else {
        if (gbVram != nullptr) {
            free(gbVram);
            gbVram = nullptr;
        }
        if (gbWram != nullptr) {
            free(gbWram);
            gbWram = nullptr;
        }
    }

    utilReadMem(&IFF, data, 2);

    if (gbSgbMode) {
        gbSgbReadGame(data);
    } else {
        gbSgbMask = 0; // loading a game at the wrong time causes no display
    }

    utilReadMem(&gbDataMBC1, data, sizeof(gbDataMBC1));
    utilReadMem(&gbDataMBC2, data, sizeof(gbDataMBC2));
    utilReadMem(&gbDataMBC3, data, sizeof(gbDataMBC3));
    utilReadMem(&gbDataMBC5, data, sizeof(gbDataMBC5));
    utilReadMem(&gbDataHuC1, data, sizeof(gbDataHuC1));
    utilReadMem(&gbDataHuC3, data, sizeof(gbDataHuC3));
    if (g_gbCartData.mapper_type() == gbCartData::MapperType::kHuC3)
        utilReadMem(&gbRTCHuC3, data, sizeof(gbRTCHuC3));
    utilReadMem(&gbDataTAMA5, data, sizeof(gbDataTAMA5));
    if (gbTAMA5ram != NULL) {
        utilReadMem(gbTAMA5ram, data, kTama5RamSize);
    }
    utilReadMem(&gbDataMMM01, data, sizeof(gbDataMMM01));

    utilReadMem(gbPalette, data, sizeof(gbPalette));

    utilReadMem(&gbMemory[0x8000], data, 0x8000);

    if (g_gbCartData.HasRam()) {
        int cartRamSize = g_gbCartData.ram_size();
        int ramSize = utilReadIntMem(data);
        utilReadMem(gbRam, data, std::min(cartRamSize, ramSize)); //read
        /*if (ramSize > cartRamSize)
            utilGzSeek(gzFile, ramSize - cartRamSize, SEEK_CUR);*/ // Libretro Note: ????
    }

    memset(gbSCYLine, register_SCY, sizeof(gbSCYLine));
    memset(gbSCXLine, register_SCX, sizeof(gbSCXLine));
    memset(gbBgpLine, (gbBgp[0] | (gbBgp[1] << 2) | (gbBgp[2] << 4) | (gbBgp[3] << 6)), sizeof(gbBgpLine));
    memset(gbObp0Line, (gbObp0[0] | (gbObp0[1] << 2) | (gbObp0[2] << 4) | (gbObp0[3] << 6)), sizeof(gbObp0Line));
    memset(gbObp1Line, (gbObp1[0] | (gbObp1[1] << 2) | (gbObp1[2] << 4) | (gbObp1[3] << 6)), sizeof(gbObp1Line));
    memset(gbSpritesTicks, 0x0, sizeof(gbSpritesTicks));

    if (inBios) {
        gbMemoryMap[0x00] = &gbMemory[0x0000];
        if (gbHardware & 5) {
            memcpy((uint8_t*)(gbMemory), (uint8_t*)(gbRom), 0x1000);
            memcpy((uint8_t*)(gbMemory), (uint8_t*)(g_bios), kGBBiosSize);
        } else if (gbHardware & 2) {
            memcpy((uint8_t*)(gbMemory), (uint8_t*)(g_bios), kCGBBiosSize);
            memcpy((uint8_t*)(gbMemory + 0x100), (uint8_t*)(gbRom + 0x100), 0x100);
        }

    } else {
        gbMemoryMap[0x00] = &gbRom[0x0000];
    }

    gbMemoryMap[0x01] = &gbRom[0x1000];
    gbMemoryMap[0x02] = &gbRom[0x2000];
    gbMemoryMap[0x03] = &gbRom[0x3000];
    gbMemoryMap[0x04] = &gbRom[0x4000];
    gbMemoryMap[0x05] = &gbRom[0x5000];
    gbMemoryMap[0x06] = &gbRom[0x6000];
    gbMemoryMap[0x07] = &gbRom[0x7000];
    gbMemoryMap[0x08] = &gbMemory[0x8000];
    gbMemoryMap[0x09] = &gbMemory[0x9000];
    gbMemoryMap[0x0a] = &gbMemory[0xa000];
    gbMemoryMap[0x0b] = &gbMemory[0xb000];
    gbMemoryMap[0x0c] = &gbMemory[0xc000];
    gbMemoryMap[0x0d] = &gbMemory[0xd000];
    gbMemoryMap[0x0e] = &gbMemory[0xe000];
    gbMemoryMap[0x0f] = &gbMemory[0xf000];

    switch (g_gbCartData.mapper_type()) {
        case gbCartData::MapperType::kNone:
        case gbCartData::MapperType::kMbc1:
            memoryUpdateMapMBC1();
            break;
        case gbCartData::MapperType::kMbc2:
            memoryUpdateMapMBC2();
            break;
        case gbCartData::MapperType::kMmm01:
            memoryUpdateMapMMM01();
            break;
        case gbCartData::MapperType::kMbc3:
            memoryUpdateMapMBC3();
            break;
        case gbCartData::MapperType::kMbc5:
            memoryUpdateMapMBC5();
        case gbCartData::MapperType::kMbc7:
            memoryUpdateMapMBC7();
            break;
        case gbCartData::MapperType::kGameShark:
            memoryUpdateMapGS3();
            break;
        case gbCartData::MapperType::kTama5:
            memoryUpdateMapTAMA5();
            break;
        case gbCartData::MapperType::kHuC3:
            memoryUpdateMapHuC3();
            break;
        case gbCartData::MapperType::kHuC1:
            memoryUpdateMapHuC1();
            break;
        case gbCartData::MapperType::kMbc6:
        case gbCartData::MapperType::kPocketCamera:
        case gbCartData::MapperType::kGameGenie:
        case gbCartData::MapperType::kUnknown:
            // Do nothing.
            break;
    }

    if (gbCgbMode) {
        utilReadMem(gbVram, data, kGBVRamSize);
        utilReadMem(gbWram, data, kGBWRamSize);

        int value = register_SVBK;
        if (value == 0)
            value = 1;

        gbMemoryMap[0x08] = &gbVram[register_VBK * 0x2000];
        gbMemoryMap[0x09] = &gbVram[register_VBK * 0x2000 + 0x1000];
        gbMemoryMap[0x0c] = &gbWram[0x0000];
        gbMemoryMap[0x0d] = &gbWram[value * 0x1000];
    }

    gbSoundReadGame(data);

    if (gbCgbMode && gbSgbMode) {
        gbSgbMode = false;
    }

    if (gbBorderOn && !gbSgbMask) {
        gbSgbRenderBorder();
    }

    gbLcdModeDelayed = utilReadIntMem(data);
    gbLcdTicksDelayed = utilReadIntMem(data);
    gbLcdLYIncrementTicksDelayed = utilReadIntMem(data);
    gbSpritesTicks[299] = utilReadIntMem(data) & 0xff;
    gbTimerModeChange = (utilReadIntMem(data) ? true : false);
    gbTimerOnChange = (utilReadIntMem(data) ? true : false);
    gbHardware = utilReadIntMem(data);
    gbBlackScreen = (utilReadIntMem(data) ? true : false);
    oldRegister_WY = utilReadIntMem(data);
    gbWindowLine = utilReadIntMem(data);
    inUseRegister_WY = utilReadIntMem(data);
    gbScreenOn = (utilReadIntMem(data) ? true : false);

    if (gbSpeed)
        gbLine99Ticks *= 2;

    systemSaveUpdateCounter = SYSTEM_SAVE_NOT_UPDATED;

    if (version >= 12 && utilReadIntMem(data) != 0x12345678) {
        // fails if something read too much/little from file
        return false;
    }

    return true;
}
#endif /* __LIBRETRO__ */

struct EmulatedSystem GBSystem = {
    // emuMain
    gbEmulate,
    // emuReset
    gbReset,
    // emuCleanUp
    gbCleanUp,
#ifdef __LIBRETRO__
    NULL,               // emuReadBattery
    NULL,               // emuWriteBattery
    gbReadSaveState,    // emuReadState
    gbWriteSaveState,   // emuWriteState
    NULL,               // emuReadMemState
    NULL,               // emuWriteMemState
    NULL,               // emuWritePNG
    NULL,               // emuWriteBMP
#else    
    // emuReadBattery
    ReadBatteryFile,
    // emuWriteBattery
    WriteBatteryFile,
    // emuReadState
    gbReadSaveState,
    // emuWriteState
    gbWriteSaveState,
    // emuReadMemState
    gbReadMemSaveState,
    // emuWriteMemState
    gbWriteMemSaveState,
    // emuWritePNG
    gbWritePNGFile,
    // emuWriteBMP
    gbWriteBMPFile,
#endif /* ! __LIBRETRO__ */
    // emuUpdateCPSR
    NULL,
    // emuHasDebugger
    false,
    // emuCount
#ifdef FINAL_VERSION
    72000,
#else
    1000,
#endif
};
