/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *   Mupen64plus - gb_cart.c                                               *
 *   Mupen64Plus homepage: https://mupen64plus.org/                        *
 *   Copyright (C) 2015 Bobby Smiles                                       *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.          *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

/* Most of the mappers information comes from
 * "The Cycle-Accurate Game Boy Docs" by AntonioND
 */

#include "gb_cart.h"

#include "api/m64p_types.h"
#include "api/callbacks.h"
#include "backends/api/rumble_backend.h"
#include "backends/api/storage_backend.h"
#include "backends/api/video_capture_backend.h"
#if 0
void cv_imshow(const char* name, unsigned int width, unsigned int height, int channels, void* data);
#else
#define cv_imshow(...)
#endif

#include <assert.h>
#include <string.h>


enum gbcart_extra_devices
{
    GED_NONE          = 0x00,
    GED_RAM           = 0x01,
    GED_BATTERY       = 0x02,
    GED_RTC           = 0x04,
    GED_RUMBLE        = 0x08,
    GED_ACCELEROMETER = 0x10,
    GED_CAMERA        = 0x20,
};

/* various helper functions for ram, rom, or MBC uses */


static void read_rom(const void* rom_storage, const struct storage_backend_interface* irom_storage, uint32_t address, uint8_t* data, size_t size)
{
    assert(size > 0);

    if (address + size > irom_storage->size(rom_storage))
    {
        DebugMessage(M64MSG_WARNING, "Out of bound read from GB ROM %04x", address);
        return;
    }

    memcpy(data, irom_storage->data(rom_storage) + address, size);
}


static void read_ram(const void* ram_storage, const struct storage_backend_interface* iram_storage, unsigned int enabled, uint32_t address, uint8_t* data, size_t size, uint8_t mask)
{
    size_t i;
    assert(size > 0);

    /* RAM has to be enabled before use */
    if (!enabled) {
        DebugMessage(M64MSG_WARNING, "Trying to read from non enabled GB RAM %04x", address);
        memset(data, 0xff, size);
        return;
    }

    /* RAM must be present */
    if (iram_storage->data(ram_storage) == NULL) {
        DebugMessage(M64MSG_WARNING, "Trying to read from absent GB RAM %04x", address);
        memset(data, 0xff, size);
        return;
    }

    if (address + size > iram_storage->size(ram_storage))
    {
        DebugMessage(M64MSG_WARNING, "Out of bound read from GB RAM %04x", address);
        return;
    }

    memcpy(data, iram_storage->data(ram_storage) + address, size);

    if (mask != UINT8_C(0xff)) {
        for (i = 0; i < size; ++i) {
            data[i] &= mask;
        }
    }
}

static void write_ram(void* ram_storage, const struct storage_backend_interface* iram_storage, unsigned int enabled, uint16_t address, const uint8_t* data, size_t size, uint8_t mask)
{
    size_t i;
    uint8_t* dst;

    assert(size > 0);

    /* RAM has to be enabled before use */
    if (!enabled) {
        DebugMessage(M64MSG_WARNING, "Trying to write to non enabled GB RAM %04x", address);
        return;
    }

    /* RAM must be present */
    if (iram_storage->data(ram_storage) == NULL) {
        DebugMessage(M64MSG_WARNING, "Trying to write to absent GB RAM %04x", address);
        return;
    }

    if (address + size > iram_storage->size(ram_storage))
    {
        DebugMessage(M64MSG_WARNING, "Out of bound write to GB RAM %04x", address);
        return;
    }

    dst = iram_storage->data(ram_storage) + address;
    memcpy(dst, data, size);

    if (mask != UINT8_C(0xff)) {
        for (i = 0; i < size; ++i) {
            dst[i] &= mask;
        }
    }
    iram_storage->save(ram_storage, address, size);
}


static void set_ram_enable(struct gb_cart* gb_cart, uint8_t value)
{
    gb_cart->ram_enable = ((value & 0x0f) == 0x0a) ? 1 : 0;
    DebugMessage(M64MSG_VERBOSE, "RAM enable = %02x", gb_cart->ram_enable);
}




static int read_gb_cart_nombc(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    switch(address >> 13)
    {
    /* 0x0000-0x7fff: ROM */
    case (0x0000 >> 13):
    case (0x2000 >> 13):
    case (0x4000 >> 13):
    case (0x6000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, address, data, size);
        break;

    /* 0xa000-0xbfff: RAM */
    case (0xa000 >> 13):
        read_ram(gb_cart->ram_storage, gb_cart->iram_storage, 1, address - 0xa000, data, size, UINT8_C(0xff));
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart read (nombc): %04x", address);
    }

    return 0;
}

static int write_gb_cart_nombc(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    switch(address >> 13)
    {
    /* 0x0000-0x7fff: ROM */
    case (0x0000 >> 13):
    case (0x2000 >> 13):
    case (0x4000 >> 13):
    case (0x6000 >> 13):
        DebugMessage(M64MSG_VERBOSE, "Trying to write to GB ROM %04x", address);
        break;

    /* 0xa000-0xbfff: RAM */
    case (0xa000 >> 13):
        write_ram(gb_cart->ram_storage, gb_cart->iram_storage, 1, address - 0xa000, data, size, UINT8_C(0xff));
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart write (nombc): %04x", address);
    }

    return 0;
}


static int read_gb_cart_mbc1(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    switch(address >> 13)
    {
    /* 0x0000-0x3fff: ROM bank 00 */
    case (0x0000 >> 13):
    case (0x2000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, address, data, size);
        break;

    /* 0x4000-0x7fff: ROM bank 01-7f */
    case (0x4000 >> 13):
    case (0x6000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, (address - 0x4000) + (gb_cart->rom_bank * 0x4000), data, size);
        break;

    /* 0xa000-0xbfff: RAM bank 00-03 */
    case (0xa000 >> 13):
        read_ram(gb_cart->ram_storage, gb_cart->iram_storage, gb_cart->ram_enable, (address - 0xa000) + (gb_cart->ram_bank * 0x2000), data, size, UINT8_C(0xff));
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart read (MBC1): %04x", address);
    }

    return 0;
}

static int write_gb_cart_mbc1(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    uint8_t bank;
    uint8_t value = data[size-1];

    switch(address >> 13)
    {
    /* 0x0000-0x1fff: RAM enable */
    case (0x0000 >> 13):
        set_ram_enable(gb_cart, value);
        break;

    /* 0x2000-0x3fff: ROM bank select (low 5 bits) */
    case (0x2000 >> 13):
        bank = value & 0x1f;
        gb_cart->rom_bank = (gb_cart->rom_bank & ~UINT8_C(0x1f)) | ((bank == 0) ? 1 : bank);
        DebugMessage(M64MSG_VERBOSE, "MBC1 set rom bank %02x", gb_cart->rom_bank);
        break;

    /* 0x4000-0x5fff: RAM bank / upper ROM bank select (2 bits) */
    case (0x4000 >> 13):
        bank = value & 0x3;
        if (gb_cart->mbc1_mode == 0) {
            /* ROM mode */
            gb_cart->rom_bank = (gb_cart->rom_bank & 0x1f) | (bank << 5);
        }
        else {
            /* RAM mode */
            gb_cart->ram_bank = bank;
        }
        DebugMessage(M64MSG_VERBOSE, "MBC1 set ram bank %02x", gb_cart->ram_bank);
        break;

    /* 0x6000-0x7fff: ROM/RAM mode (1 bit) */
    case (0x6000 >> 13):
        gb_cart->mbc1_mode = (value & 0x1);
        if (gb_cart->mbc1_mode == 0) {
            /* only RAM bank 0 is accessible in ROM mode */
            gb_cart->ram_bank = 0;
        } else {
            /* only ROM banks 0x01 - 0x1f are accessible in RAM mode */
            gb_cart->rom_bank &= 0x1f;
        }
        break;

    /* 0xa000-0xbfff: RAM bank 00-03 */
    case (0xa000 >> 13):
        write_ram(gb_cart->ram_storage, gb_cart->iram_storage, gb_cart->ram_enable, (address - 0xa000) + (gb_cart->ram_bank * 0x2000), data, size, UINT8_C(0xff));
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart write (MBC1): %04x", address);
    }

    return 0;
}

static int read_gb_cart_mbc2(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    switch (address >> 13)
    {
    /* 0x0000-0x3fff: ROM bank 00 */
    case (0x0000 >> 13):
    case (0x2000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, address, data, size);
        break;

    /* 0x4000-0x7fff: ROM bank 01-0f */
    case (0x4000 >> 13):
    case (0x6000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, (address - 0x4000) + (gb_cart->rom_bank * 0x4000), data, size);
        break;

    /* 0xa000-0xa1ff: internal 512x4bit RAM */
    case (0xa000 >> 13):
        read_ram(gb_cart->ram_storage, gb_cart->iram_storage, gb_cart->ram_enable, (address - 0xa000), data, size, UINT8_C(0x0f));
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart read (MBC2): %04x", address);
    }

    return 0;
}

static int write_gb_cart_mbc2(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    uint8_t bank;
    uint8_t value = data[size-1];

    switch(address >> 13)
    {
    /* 0x0000-0x1fff: RAM enable */
    case (0x0000 >> 13):
        if ((address & 0x0100) == 0) {
            set_ram_enable(gb_cart, value);
        }
        break;

    /* 0x2000-0x3fff: ROM bank select (low 4 bits) */
    case (0x2000 >> 13):
        if ((address & 0x0100) != 0) {
            bank = value & 0x0f;
            gb_cart->rom_bank = (bank == 0) ? 1 : bank;
            DebugMessage(M64MSG_VERBOSE, "MBC2 set rom bank %02x", gb_cart->rom_bank);
        }
        break;

    /* 0xa000-0xa1ff: internal 512x4bit RAM */
    case (0xa000 >> 13):
        write_ram(gb_cart->ram_storage, gb_cart->iram_storage, gb_cart->ram_enable, (address - 0xa000), data, size, UINT8_C(0x0f));
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart write (MBC2): %04x", address);
    }

    return 0;
}


static int read_gb_cart_mbc3(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    switch(address >> 13)
    {
    /* 0x0000-0x3fff: ROM bank 00 */
    case (0x0000 >> 13):
    case (0x2000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, address, data, size);
        break;

    /* 0x4000-0x7fff: ROM bank 01-7f */
    case (0x4000 >> 13):
    case (0x6000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, (address - 0x4000) + (gb_cart->rom_bank * 0x4000), data, size);
        break;

    /* 0xa000-0xbfff: RAM bank 00-07 or RTC register 08-0c */
    case (0xa000 >> 13):
        switch(gb_cart->ram_bank)
        {
        /* RAM banks */
        case 0x00:
        case 0x01:
        case 0x02:
        case 0x03:
        case 0x04:
        case 0x05:
        case 0x06:
        case 0x07:
            read_ram(gb_cart->ram_storage, gb_cart->iram_storage, gb_cart->ram_enable, (address - 0xa000) + (gb_cart->ram_bank * 0x2000), data, size, UINT8_C(0xff));
            break;

        /* RTC registers */
        case 0x08:
        case 0x09:
        case 0x0a:
        case 0x0b:
        case 0x0c:
            /* RAM has to be enabled before use */
            if (!gb_cart->ram_enable) {
                DebugMessage(M64MSG_WARNING, "Trying to read from non enabled GB RAM %04x", address);
                memset(data, 0xff, size);
                break;
            }

            if (!(gb_cart->extra_devices & GED_RTC)) {
                DebugMessage(M64MSG_WARNING, "Trying to read from absent RTC %04x", address);
                memset(data, 0xff, size);
                break;
            }

            memset(data, read_mbc3_rtc_regs(&gb_cart->rtc, gb_cart->ram_bank - 0x08), size);
            break;

        default:
            DebugMessage(M64MSG_WARNING, "Unknown device mapped in RAM/RTC space: %04x", address);
        }
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart read (MBC3): %04x", address);
    }

    return 0;
}

static int write_gb_cart_mbc3(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    uint8_t bank;
    uint8_t value = data[size-1];

    switch(address >> 13)
    {
    /* 0x0000-0x1fff: RAM/RTC enable */
    case (0x0000 >> 13):
        set_ram_enable(gb_cart, value);
        break;

    /* 0x2000-0x3fff: ROM bank select */
    case (0x2000 >> 13):
        bank = value & 0x7f;
        gb_cart->rom_bank = (bank == 0) ? 1 : bank;
        DebugMessage(M64MSG_VERBOSE, "MBC3 set rom bank %02x", gb_cart->rom_bank);
        break;

    /* 0x4000-0x5fff: RAM bank / RTC register select */
    case (0x4000 >> 13):
        gb_cart->ram_bank = value;
        DebugMessage(M64MSG_VERBOSE, "MBC3 set ram bank %02x", gb_cart->ram_bank);
        break;

    /* 0x6000-0x7fff: latch clock registers */
    case (0x6000 >> 13):
        if (!(gb_cart->extra_devices & GED_RTC)) {
            DebugMessage(M64MSG_WARNING, "Trying to latch to absent RTC %04x", address);
            break;
        }

        latch_mbc3_rtc_regs(&gb_cart->rtc, value);
        break;

    /* 0xa000-0xbfff: RAM bank 00-07 or RTC register 08-0c */
    case (0xa000 >> 13):
        switch(gb_cart->ram_bank)
        {
        /* RAM banks */
        case 0x00:
        case 0x01:
        case 0x02:
        case 0x03:
        case 0x04:
        case 0x05:
        case 0x06:
        case 0x07:
            write_ram(gb_cart->ram_storage, gb_cart->iram_storage, gb_cart->ram_enable, (address - 0xa000) + (gb_cart->ram_bank * 0x2000), data, size, UINT8_C(0xff));
            break;

        /* RTC registers */
        case 0x08:
        case 0x09:
        case 0x0a:
        case 0x0b:
        case 0x0c:
            /* RAM has to be enabled before use */
            if (!gb_cart->ram_enable) {
                DebugMessage(M64MSG_WARNING, "Trying to write to non enabled GB RAM %04x", address);
                break;
            }

            if (!(gb_cart->extra_devices & GED_RTC)) {
                DebugMessage(M64MSG_WARNING, "Trying to write to absent RTC %04x", address);
                break;
            }

            write_mbc3_rtc_regs(&gb_cart->rtc, gb_cart->ram_bank - 0x08, value);
            break;

        default:
            DebugMessage(M64MSG_WARNING, "Unknwown device mapped in RAM/RTC space: %04x", address);
        }
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart write (MBC3): %04x", address);
    }

    return 0;
}

static int read_gb_cart_mbc5(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    switch(address >> 13)
    {
    /* 0x0000-0x3fff: ROM bank 00 */
    case (0x0000 >> 13):
    case (0x2000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, address, data, size);
        break;

    /* 0x4000-0x7fff: ROM bank 00-ff (???) */
    case (0x4000 >> 13):
    case (0x6000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, (address - 0x4000) + (gb_cart->rom_bank * 0x4000), data, size);
        break;

    /* 0xa000-0xbfff: RAM bank 00-07 */
    case (0xa000 >> 13):
        read_ram(gb_cart->ram_storage, gb_cart->iram_storage, gb_cart->ram_enable, (address - 0xa000) + ((gb_cart->ram_bank & 0x7) * 0x2000), data, size, UINT8_C(0xff));
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart read (MBC5): %04x", address);
    }

    return 0;
}

static int write_gb_cart_mbc5(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    uint8_t value = data[size-1];

    switch(address >> 13)
    {
    /* 0x0000-0x1fff: RAM enable */
    case (0x0000 >> 13):
        set_ram_enable(gb_cart, value);
        break;

    /* 0x2000-0x3fff: ROM bank select */
    case (0x2000 >> 13):
        if (address < 0x3000)
        {
            gb_cart->rom_bank &= 0xff00;
            gb_cart->rom_bank |= value;
        }
        else
        {
            gb_cart->rom_bank &= 0x00ff;
            gb_cart->rom_bank |= (value & 0x01) << 8;
        }
        DebugMessage(M64MSG_VERBOSE, "MBC5 set rom bank %04x", gb_cart->rom_bank);
        break;

    /* 0x4000-0x5fff: RAM bank select */
    case (0x4000 >> 13):
        gb_cart->ram_bank = value & 0x0f;
        if (gb_cart->extra_devices & GED_RUMBLE) {
            gb_cart->irumble->exec(gb_cart->rumble, ((gb_cart->ram_bank & 0x8) == 0) ? RUMBLE_STOP : RUMBLE_START);
        }
        DebugMessage(M64MSG_VERBOSE, "MBC5 set ram bank %02x", gb_cart->ram_bank);
        break;

    /* 0xa000-0xbfff: RAM bank 00-0f */
    case (0xa000 >> 13):
        write_ram(gb_cart->ram_storage, gb_cart->iram_storage, gb_cart->ram_enable, (address - 0xa000) + ((gb_cart->ram_bank & 0x07)* 0x2000), data, size, UINT8_C(0xff));
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart write (MBC5): %04x", address);
    }

    return 0;
}

static int read_gb_cart_mbc6(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    return 0;
}

static int write_gb_cart_mbc6(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    return 0;
}

static int read_gb_cart_mbc7(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    return 0;
}

static int write_gb_cart_mbc7(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    return 0;
}

static int read_gb_cart_mmm01(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    return 0;
}

static int write_gb_cart_mmm01(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    return 0;
}


/* TODO: extract each mbc into its own module, store in gb_cart a union of them */
static uint8_t apply_dithering_matrix(const uint8_t* d, uint8_t value, unsigned int x, unsigned int y)
{
    d += ((y & 3) * 4 + (x & 3)) * 3;

    if      (value < d[0]) return 0x00; /* black */
    else if (value < d[1]) return 0x40; /* dark gray */
    else if (value < d[2]) return 0x80; /* light gray */
    return 0xc0; /* white */
}

static void grab_pocket_cam_image(struct pocket_cam* cam)
{
    static const uint8_t pm[4][2] = {
        { 0x00, 0x01 }, /* negative */
        { 0x01, 0x00 }, /* positive */
        { 0x01, 0x02 }, /* edge detect */
        { 0x01, 0x02 }  /* edge detect */
    };

    uint8_t regs[M64282FP_REGS_COUNT];
    uint8_t tiles[16][16][16];
    uint8_t bgr[M64282FP_SENSOR_H][M64282FP_SENSOR_W][3];
    uint8_t img[M64282FP_SENSOR_H][M64282FP_SENSOR_W];
    unsigned int x, y;

    /* setup sensor regs */
    unsigned int pm_mode = (cam->regs[0] >> 1) & 0x3;
    regs[M64282FP_Z_O] = cam->regs[5];
    regs[M64282FP_N_VH_G] = cam->regs[1];
    regs[M64282FP_C_LO] = cam->regs[3];
    regs[M64282FP_C_HI] = cam->regs[2];
    regs[M64282FP_P] = pm[pm_mode][0];
    regs[M64282FP_M] = pm[pm_mode][1];
    regs[M64282FP_X] = 0x01;
    regs[M64282FP_E_I_V] = cam->regs[4];

#if 0
    DebugMessage(M64MSG_INFO, "GB cam regs:\n"
        "P=%02x M=%02x X=%02x N_VH_G=%02x C=%04x E_I_V=%02x Z_O=%02x\n"
        "%02x %02x %02x  %02x %02x %02x  %02x %02x %02x  %02x %02x %02x\n"
        "%02x %02x %02x  %02x %02x %02x  %02x %02x %02x  %02x %02x %02x\n"
        "%02x %02x %02x  %02x %02x %02x  %02x %02x %02x  %02x %02x %02x\n"
        "%02x %02x %02x  %02x %02x %02x  %02x %02x %02x  %02x %02x %02x",
        regs[M64282FP_P], regs[M64282FP_M], regs[M64282FP_X], regs[M64282FP_N_VH_G],
        regs[M64282FP_C_HI] << 8 | regs[M64282FP_C_LO], regs[M64282FP_E_I_V], regs[M64282FP_Z_O],
        cam->regs[6], cam->regs[7], cam->regs[8],
        cam->regs[9], cam->regs[10], cam->regs[11],
        cam->regs[12], cam->regs[13], cam->regs[14],
        cam->regs[15], cam->regs[16], cam->regs[17],
        cam->regs[18], cam->regs[19], cam->regs[20],
        cam->regs[21], cam->regs[22], cam->regs[23],
        cam->regs[24], cam->regs[25], cam->regs[26],
        cam->regs[27], cam->regs[28], cam->regs[29],
        cam->regs[30], cam->regs[31], cam->regs[32],
        cam->regs[33], cam->regs[34], cam->regs[35],
        cam->regs[36], cam->regs[37], cam->regs[38],
        cam->regs[39], cam->regs[40], cam->regs[41],
        cam->regs[42], cam->regs[43], cam->regs[44],
        cam->regs[45], cam->regs[46], cam->regs[47],
        cam->regs[48], cam->regs[49], cam->regs[50],
        cam->regs[51], cam->regs[52], cam->regs[53]);
#endif

    /* grab BGR image */
    if (cam->ivcap->grab_image(cam->vcap, bgr) != M64ERR_SUCCESS) {
        memset(cam->ram, UINT8_C(0xff), sizeof(tiles));
        return;
    }

    cv_imshow("bgr image", M64282FP_SENSOR_W, M64282FP_SENSOR_H, 3, bgr);

    /* convert to gray using (2R+5G+1B)/8 formula */
    const uint8_t* bgr_ptr = &bgr[0][0][0];
    uint8_t* gray_ptr = &img[0][0];
    for (y = 0; y < M64282FP_SENSOR_H; ++y) {
        for (x = 0; x < M64282FP_SENSOR_W; ++x) {
            gray_ptr[0] = (1*bgr_ptr[0] + 5*bgr_ptr[1] + 2*bgr_ptr[2]) / 8;
            bgr_ptr += 3;
            gray_ptr += 1;
        }
    }

    /* apply m64282fp processings */
    cv_imshow("gray image", M64282FP_SENSOR_W, M64282FP_SENSOR_H, 1, img);
    process_m64282fp_image(img, regs);
    cv_imshow("m64282fp", M64282FP_SENSOR_W, M64282FP_SENSOR_H, 1, img);

    /* convert to dithered GB tile format */
    memset(tiles, 0, sizeof(tiles));
    for (y = 0; y < M64282FP_SENSOR_H; ++y) {
        for (x = 0; x < M64282FP_SENSOR_W; ++x) {
            /* apply dithering matrix */
            uint8_t c = UINT8_C(0xc0) - apply_dithering_matrix(&cam->regs[6], img[y][x], x, y);
            img[y][x] = c;
            /* encode as tiles */
            uint8_t* tile_base = &tiles[y >> 3][x >> 3][(y & 7) << 1];
            if (c & 0x40) { tile_base[0] |= (1U << (7 - (7 & x))); }
            if (c & 0x80) { tile_base[1] |= (1U << (7 - (7 & x))); }
        }
    }

    cv_imshow("dithered", M64282FP_SENSOR_W, M64282FP_SENSOR_H, 1, img);

    /* copy to ram */
    memcpy(cam->ram, tiles, sizeof(tiles));
}

static void init_pocket_cam(struct pocket_cam* cam, uint8_t* ram, void* vcap, const struct video_capture_backend_interface* ivcap)
{
    cam->ram = ram;

    cam->vcap = vcap;
    cam->ivcap = ivcap;

    /* open video device */
    cam->ivcap->open(cam->vcap,  M64282FP_SENSOR_W, M64282FP_SENSOR_H);
}

static void poweron_pocket_cam(struct pocket_cam* cam)
{
    memset(cam->regs, 0, POCKET_CAM_REGS_COUNT*sizeof(cam->regs[0]));
}

static uint8_t read_pocket_cam_regs(struct pocket_cam* cam, unsigned int reg)
{
    /* All registers except reg 0 are write-only and therefore return 0x00 */
    return (reg == 0)
        ? cam->regs[0]
        : 0x00;
}

static void write_pocket_cam_regs(struct pocket_cam* cam, unsigned int reg, uint8_t value)
{
    if (reg >= POCKET_CAM_REGS_COUNT) {
#if 0 /* silence it because it happens always (2x32 bytes writes go past 0x36 registers) */
        DebugMessage(M64MSG_WARNING, "Out of range camera register write %02x = %02x",
            reg, value);
#endif
        return;
    }

    cam->regs[reg] = value;

    if (reg == 0) {
        cam->regs[reg] &= 0x7;

        if (cam->regs[reg] & 0x1) {
            grab_pocket_cam_image(cam);
            cam->regs[reg] &= ~0x01;
        }
    }
}


static int read_gb_cart_pocket_cam(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    switch(address >> 13)
    {
    /* 0x0000-0x3fff: ROM bank 00 */
    case (0x0000 >> 13):
    case (0x2000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, address, data, size);
        break;

    /* 0x4000-0x7fff: ROM bank 00-3f */
    case (0x4000 >> 13):
    case (0x6000 >> 13):
        read_rom(gb_cart->rom_storage, gb_cart->irom_storage, (address - 0x4000) + (gb_cart->rom_bank * 0x4000), data, size);
        break;

    /* 0xa000-0xbfff: RAM bank 00-0f, Camera registers & 0x10 */
    case (0xa000 >> 13):
        if (gb_cart->ram_bank & 0x10) {
            size_t i;
            for(i = 0; i < size; ++i) {
                data[i] = read_pocket_cam_regs(&gb_cart->cam, ((address+i) & 0x7f));
            }
        }
        else {
            read_ram(gb_cart->ram_storage, gb_cart->iram_storage, 1, (address - 0xa000) + (gb_cart->ram_bank * 0x2000), data, size, UINT8_C(0xff));
        }
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart read (cam): %04x", address);
    }

    return 0;
}

static int write_gb_cart_pocket_cam(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    uint8_t value = data[size-1];

    switch(address >> 13)
    {
    /* 0x0000-0x1fff: RAM write enable */
    case (0x0000 >> 13):
        set_ram_enable(gb_cart, value);
        break;

    /* 0x2000-0x3fff: ROM bank select */
    case (0x2000 >> 13):
        gb_cart->rom_bank = (value & 0x3f);
        DebugMessage(M64MSG_VERBOSE, "CAM set rom bank %04x", gb_cart->rom_bank);
        break;

    /* 0x4000-0x5fff: RAM bank select */
    case (0x4000 >> 13):
        if (value & 0x10) {
            gb_cart->ram_bank = value;
            DebugMessage(M64MSG_VERBOSE, "CAM set register bank %02x", gb_cart->ram_bank);
        }
        else {
            gb_cart->ram_bank = (value & 0x0f);
            DebugMessage(M64MSG_VERBOSE, "CAM set ram bank %02x", gb_cart->ram_bank);
        }
        break;

    /* 0xa000-0xbfff: RAM bank 00-0f, Camera registers & 0x10 */
    case (0xa000 >> 13):
        if (gb_cart->ram_bank & 0x10) {
            size_t i;
            for(i = 0; i < size; ++i) {
                write_pocket_cam_regs(&gb_cart->cam, ((address+i) & 0x7f), data[i]);
            }
        }
        else {
            write_ram(gb_cart->ram_storage, gb_cart->iram_storage, gb_cart->ram_enable, (address - 0xa000) + (gb_cart->ram_bank * 0x2000), data, size, UINT8_C(0xff));
        }
        break;

    default:
        DebugMessage(M64MSG_WARNING, "Invalid cart write (cam): %04x", address);
    }

    return 0;
}

static int read_gb_cart_bandai_tama5(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    return 0;
}

static int write_gb_cart_bandai_tama5(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    return 0;
}

static int read_gb_cart_huc1(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    return 0;
}

static int write_gb_cart_huc1(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    return 0;
}

static int read_gb_cart_huc3(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    return 0;
}

static int write_gb_cart_huc3(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    return 0;
}



struct parsed_cart_type
{
    const char* mbc;
    int (*read_gb_cart)(struct gb_cart*,uint16_t,uint8_t*,size_t);
    int (*write_gb_cart)(struct gb_cart*,uint16_t,const uint8_t*,size_t);
    unsigned int extra_devices;
};

static const struct parsed_cart_type* parse_cart_type(uint8_t cart_type)
{
#define MBC(x) #x, read_gb_cart_ ## x, write_gb_cart_ ## x
    static const struct parsed_cart_type nombc_none           = { MBC(nombc),        GED_NONE };
    static const struct parsed_cart_type nombc_ram            = { MBC(nombc),        GED_RAM };
    static const struct parsed_cart_type nombc_ram_batt       = { MBC(nombc),        GED_RAM | GED_BATTERY };

    static const struct parsed_cart_type mbc1_none            = { MBC(mbc1),         GED_NONE };
    static const struct parsed_cart_type mbc1_ram             = { MBC(mbc1),         GED_RAM  };
    static const struct parsed_cart_type mbc1_ram_batt        = { MBC(mbc1),         GED_RAM | GED_BATTERY };

    static const struct parsed_cart_type mbc2_none            = { MBC(mbc2),         GED_NONE };
    static const struct parsed_cart_type mbc2_ram_batt        = { MBC(mbc2),         GED_RAM | GED_BATTERY };

    static const struct parsed_cart_type mmm01_none           = { MBC(mmm01),        GED_NONE };
    static const struct parsed_cart_type mmm01_ram            = { MBC(mmm01),        GED_RAM  };
    static const struct parsed_cart_type mmm01_ram_batt       = { MBC(mmm01),        GED_RAM | GED_BATTERY };

    static const struct parsed_cart_type mbc3_none            = { MBC(mbc3),         GED_NONE };
    static const struct parsed_cart_type mbc3_ram             = { MBC(mbc3),         GED_RAM  };
    static const struct parsed_cart_type mbc3_ram_batt        = { MBC(mbc3),         GED_RAM | GED_BATTERY };
    static const struct parsed_cart_type mbc3_batt_rtc        = { MBC(mbc3),         GED_BATTERY | GED_RTC };
    static const struct parsed_cart_type mbc3_ram_batt_rtc    = { MBC(mbc3),         GED_RAM | GED_BATTERY | GED_RTC };

    static const struct parsed_cart_type mbc5_none            = { MBC(mbc5),         GED_NONE };
    static const struct parsed_cart_type mbc5_ram             = { MBC(mbc5),         GED_RAM  };
    static const struct parsed_cart_type mbc5_ram_batt        = { MBC(mbc5),         GED_RAM | GED_BATTERY };
    static const struct parsed_cart_type mbc5_rumble          = { MBC(mbc5),         GED_RUMBLE };
    static const struct parsed_cart_type mbc5_ram_rumble      = { MBC(mbc5),         GED_RAM | GED_RUMBLE };
    static const struct parsed_cart_type mbc5_ram_batt_rumble = { MBC(mbc5),         GED_RAM | GED_BATTERY | GED_RUMBLE };

    static const struct parsed_cart_type mbc6                 = { MBC(mbc6),         GED_RAM | GED_BATTERY };

    static const struct parsed_cart_type mbc7                 = { MBC(mbc7),         GED_RAM | GED_BATTERY | GED_ACCELEROMETER };

    static const struct parsed_cart_type pocket_cam           = { MBC(pocket_cam),   GED_RAM | GED_CAMERA };

    static const struct parsed_cart_type bandai_tama5         = { MBC(bandai_tama5), GED_NONE };

    static const struct parsed_cart_type huc3                 = { MBC(huc3),         GED_NONE };

    static const struct parsed_cart_type huc1                 = { MBC(huc1),         GED_RAM | GED_BATTERY };
#undef MBC

    switch(cart_type)
    {
    case 0x00: return &nombc_none;
    case 0x01: return &mbc1_none;
    case 0x02: return &mbc1_ram;
    case 0x03: return &mbc1_ram_batt;
    /* 0x04 is unused */
    case 0x05: return &mbc2_none;
    case 0x06: return &mbc2_ram_batt;
    /* 0x07 is unused */
    case 0x08: return &nombc_ram;
    case 0x09: return &nombc_ram_batt;
    /* 0x0a is unused */
    case 0x0b: return &mmm01_none;
    case 0x0c: return &mmm01_ram;
    case 0x0d: return &mmm01_ram_batt;
    /* 0x0e is unused */
    case 0x0f: return &mbc3_batt_rtc;
    case 0x10: return &mbc3_ram_batt_rtc;
    case 0x11: return &mbc3_none;
    case 0x12: return &mbc3_ram;
    case 0x13: return &mbc3_ram_batt;
    /* 0x14-0x18 are unused */
    case 0x19: return &mbc5_none;
    case 0x1a: return &mbc5_ram;
    case 0x1b: return &mbc5_ram_batt;
    case 0x1c: return &mbc5_rumble;
    case 0x1d: return &mbc5_ram_rumble;
    case 0x1e: return &mbc5_ram_batt_rumble;
    /* 0x1f is unused */
    case 0x20: return &mbc6;
    /* 0x21 is unused */
    case 0x22: return &mbc7;
    /* 0x23-0xfb are unused */
    case 0xfc: return &pocket_cam;
    case 0xfd: return &bandai_tama5;
    case 0xfe: return &huc3;
    case 0xff: return &huc1;
    default:   return NULL;
    }
}


void init_gb_cart(struct gb_cart* gb_cart,
        void* rom_opaque, void (*init_rom)(void* user_data, void** rom_storage, const struct storage_backend_interface** irom_storage), void (*release_rom)(void* user_data),
        void* ram_opaque, void (*init_ram)(void* user_data, size_t ram_size, void** ram_storage, const struct storage_backend_interface** iram_storage), void (*release_ram)(void* user_data),
        void* clock, const struct clock_backend_interface* iclock,
        void* rumble, const struct rumble_backend_interface* irumble,
        void* vcap, const struct video_capture_backend_interface* ivcap)
{
    const struct parsed_cart_type* type;
    void* rom_storage = NULL;
    const struct storage_backend_interface* irom_storage = NULL;
    void* ram_storage = NULL;
    const struct storage_backend_interface* iram_storage = NULL;
    struct mbc3_rtc rtc;
    struct pocket_cam cam;

    memset(&rtc, 0, sizeof(rtc));
    memset(&cam, 0, sizeof(cam));

    /* ask to load rom and initialize rom storage backend */
    init_rom(rom_opaque, &rom_storage, &irom_storage);

    /* handle no cart case */
    if (irom_storage == NULL) {
        goto no_cart;
    }

    /* check rom size */
    const uint8_t* rom_data = irom_storage->data(rom_storage);
    if (rom_data == NULL || irom_storage->size(rom_storage) < 0x8000)
    {
        DebugMessage(M64MSG_ERROR, "Invalid GB ROM file size (< 32k)");
        goto error_release_rom;
    }

    /* get and parse cart type */
    uint8_t cart_type = rom_data[0x147];
    type = parse_cart_type(cart_type);
    if (type == NULL)
    {
        DebugMessage(M64MSG_ERROR, "Invalid GB cart type (%02x)", cart_type);
        goto error_release_rom;
    }

    DebugMessage(M64MSG_INFO, "GB cart type (%02x) %s%s%s%s%s%s%s",
            cart_type,
            type->mbc,
            (type->extra_devices & GED_RAM)            ? " RAM" : "",
            (type->extra_devices & GED_BATTERY)        ? " BATT" : "",
            (type->extra_devices & GED_RTC)            ? " RTC" : "",
            (type->extra_devices & GED_RUMBLE)         ? " RUMBLE" : "",
            (type->extra_devices & GED_ACCELEROMETER)  ? " ACCEL" : "",
            (type->extra_devices & GED_CAMERA)         ? " CAM" : "");

    /* load ram (if present) */
    if (type->extra_devices & GED_RAM)
    {
        size_t ram_size = 0;
        switch(rom_data[0x149])
        {
        case 0x00: ram_size = (strcmp(type->mbc, "mbc2") == 0)
                            ? 0x200 /* MBC2 have an integrated 512x4bit RAM */
                            : 0;
                   break;
        case 0x01: ram_size =  1*0x800; break;
        case 0x02: ram_size =  4*0x800; break;
        case 0x03: ram_size = 16*0x800; break;
        case 0x04: ram_size = 64*0x800; break;
        case 0x05: ram_size = 32*0x800; break;
        }

        if (ram_size != 0)
        {
            init_ram(ram_opaque, ram_size, &ram_storage, &iram_storage);

            /* check that init_ram succeeded */
            if (iram_storage == NULL) {
                DebugMessage(M64MSG_ERROR, "Failed to initialize GB RAM");
                goto error_release_ram;
            }

            if (iram_storage->data(ram_storage) == NULL || iram_storage->size(ram_storage) != ram_size)
            {
                DebugMessage(M64MSG_ERROR, "Cannot get GB RAM (%d bytes)", (uint32_t) ram_size);
                goto error_release_ram;
            }

            DebugMessage(M64MSG_INFO, "Using a %d bytes GB RAM", (uint32_t) ram_size);
        }
    }

    /* set RTC clock (if present) */
    if (type->extra_devices & GED_RTC) {
        init_mbc3_rtc(&rtc, clock, iclock);
    }

    if (type->extra_devices & GED_CAMERA) {
        init_pocket_cam(&cam, iram_storage->data(ram_storage), vcap, ivcap);
    }

    /* update gb_cart */
    gb_cart->rom_storage = rom_storage;
    gb_cart->irom_storage = irom_storage;
    gb_cart->ram_storage = ram_storage;
    gb_cart->iram_storage = iram_storage;
    gb_cart->extra_devices = type->extra_devices;
    gb_cart->rtc = rtc;
    gb_cart->cam = cam;
    gb_cart->rumble = rumble;
    gb_cart->irumble = irumble;
    gb_cart->read_gb_cart = type->read_gb_cart;
    gb_cart->write_gb_cart = type->write_gb_cart;

    return;

error_release_ram:
    release_ram(ram_opaque);
error_release_rom:
    release_rom(rom_opaque);
no_cart:
    memset(gb_cart, 0, sizeof(*gb_cart));
}

void poweron_gb_cart(struct gb_cart* gb_cart)
{
    gb_cart->rom_bank = 1;
    gb_cart->ram_bank = 0;
    gb_cart->ram_enable = 0;
    gb_cart->mbc1_mode = 0;

    if (gb_cart->extra_devices & GED_RTC) {
        poweron_mbc3_rtc(&gb_cart->rtc);
    }

    if (gb_cart->extra_devices & GED_CAMERA) {
        poweron_pocket_cam(&gb_cart->cam);
    }

    if (gb_cart->extra_devices & GED_RUMBLE) {
        gb_cart->irumble->exec(gb_cart->rumble, RUMBLE_STOP);
    }
}

int read_gb_cart(struct gb_cart* gb_cart, uint16_t address, uint8_t* data, size_t size)
{
    return gb_cart->read_gb_cart(gb_cart, address, data, size);
}

int write_gb_cart(struct gb_cart* gb_cart, uint16_t address, const uint8_t* data, size_t size)
{
    return gb_cart->write_gb_cart(gb_cart, address, data, size);
}

