#include <stdlib.h>

#include <libretro.h>
#include <libretro_core_options.h>
#include "gambatte_log.h"
#include "blipper.h"
#include "cc_resampler.h"
#include "gambatte.h"
#include "gbcpalettes.h"
#include "bootloader.h"
#ifdef HAVE_NETWORK
#include "net_serial.h"
#endif

#if defined(__DJGPP__) && defined(__STRICT_ANSI__)
/* keep this above libretro-common includes */
#undef __STRICT_ANSI__
#endif

#ifndef PATH_MAX_LENGTH
#if defined(_XBOX1) || defined(_3DS) || defined(PSP) || defined(PS2) || defined(GEKKO)|| defined(WIIU) || defined(ORBIS) || defined(__PSL1GHT__) || defined(__PS3__)
#define PATH_MAX_LENGTH 512
#else
#define PATH_MAX_LENGTH 4096
#endif
#endif

#include <string/stdstring.h>
#include <file/file_path.h>
#include <streams/file_stream.h>
#include <array/rhmap.h>

#include <cassert>
#include <cstdio>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>

#ifdef _3DS
extern "C" void* linearMemAlign(size_t size, size_t alignment);
extern "C" void linearFree(void* mem);
#endif

static retro_video_refresh_t video_cb;
static retro_input_poll_t input_poll_cb;
static retro_input_state_t input_state_cb;
static retro_audio_sample_batch_t audio_batch_cb;
static retro_environment_t environ_cb;
static gambatte::video_pixel_t* video_buf;
static gambatte::GB gb;

static bool libretro_supports_option_categories = false;
static bool libretro_supports_bitmasks          = false;
static bool libretro_supports_set_variable      = false;
static unsigned libretro_msg_interface_version  = 0;
static bool libretro_supports_ff_override       = false;
static bool libretro_ff_enabled                 = false;
static bool libretro_ff_enabled_prev            = false;

#ifdef HAVE_NETWORK
static bool show_gb_link_settings = true;
#endif

/* Minimum (and default) turbo pulse train
 * is 2 frames ON, 2 frames OFF */
#define TURBO_PERIOD_MIN      4
#define TURBO_PERIOD_MAX      120
#define TURBO_PULSE_WIDTH_MIN 2
#define TURBO_PULSE_WIDTH_MAX 15

static unsigned libretro_input_state = 0;
static bool up_down_allowed          = false;
static unsigned turbo_period         = TURBO_PERIOD_MIN;
static unsigned turbo_pulse_width    = TURBO_PULSE_WIDTH_MIN;
static unsigned turbo_a_counter      = 0;
static unsigned turbo_b_counter      = 0;

static bool rom_loaded = false;

/* Frame-pacing counters used to detect frame dupes when the
 * emulator generates audio faster than video. Previously these
 * were function-local statics inside retro_run, which meant
 * they were never reset on retro_load_game / retro_reset /
 * retro_unserialize. After a state load (or game change) the
 * stale ratio between the two would cause spurious frame
 * duplication or a starved video frame for several iterations
 * until the ratio realigned. Moved to file scope so they can
 * be cleared in all the right places. */
static uint64_t libretro_samples_count = 0;
static uint64_t libretro_frames_count  = 0;

static INLINE void frame_pacing_reset(void)
{
   libretro_samples_count = 0;
   libretro_frames_count  = 0;
}

//Dual mode runs two GBCs side by side.
//Currently, they load the same ROM, take the same input, and only the left one supports SRAM, cheats, savestates, or sound.
//Can be made useful later, but for now, it's just a tech demo.
//#define DUAL_MODE

#ifdef DUAL_MODE
static gambatte::GB gb2;
#define NUM_GAMEBOYS 2
#else
#define NUM_GAMEBOYS 1
#endif

bool use_official_bootloader = false;

#define GB_SCREEN_WIDTH 160
#define VIDEO_WIDTH (GB_SCREEN_WIDTH * NUM_GAMEBOYS)
#define VIDEO_HEIGHT 144
/* Video buffer 'width' is 256, not 160 -> assume
 * there is a benefit to making this a power of 2 */
#define VIDEO_BUFF_SIZE (256 * NUM_GAMEBOYS * VIDEO_HEIGHT * sizeof(gambatte::video_pixel_t))
#define VIDEO_PITCH (256 * NUM_GAMEBOYS)
#define VIDEO_REFRESH_RATE (4194304.0 / 70224.0)

/*************************/
/* Audio Resampler START */
/*************************/

/* There are 35112 stereo sound samples in a video frame */
#define SOUND_SAMPLES_PER_FRAME   35112
/* We request 2064 samples from each call of GB::runFor() */
#define SOUND_SAMPLES_PER_RUN     2064
/* Native GB/GBC hardware audio sample rate (~2 MHz) */
#define SOUND_SAMPLE_RATE_NATIVE  (VIDEO_REFRESH_RATE * (double)SOUND_SAMPLES_PER_FRAME)

#define SOUND_SAMPLE_RATE_CC      (SOUND_SAMPLE_RATE_NATIVE / CC_DECIMATION_RATE) /* ~64k */
#define SOUND_SAMPLE_RATE_BLIPPER (SOUND_SAMPLE_RATE_NATIVE / 64) /* ~32k */

/* GB::runFor() nominally generates up to
 * (SOUND_SAMPLES_PER_RUN + 2064) samples, which
 * defines our sound buffer size
 * NOTE: This is in fact a lie - in the upstream code,
 * GB::runFor() can generate more than
 * (SOUND_SAMPLES_PER_RUN + 2064) samples, causing a
 * buffer overflow. It has been necessary to add an
 * internal hard cap/bail out in the event that
 * excess samples are detected... */
#define SOUND_BUFF_SIZE         (SOUND_SAMPLES_PER_RUN + 2064)

/* Blipper produces between 548 and 549 output samples
 * per frame. For safety, we want to keep the blip
 * buffer no more than ~50% full. (2 * 549) = 1098,
 * so add some padding and round up to (1024 + 512) */
#define BLIP_BUFFER_SIZE (1024 + 512)

static blipper_t *resampler_l = NULL;
static blipper_t *resampler_r = NULL;

static bool use_cc_resampler = false;

static int16_t *audio_out_buffer     = NULL;
static size_t audio_out_buffer_size  = 0;
static size_t audio_out_buffer_pos   = 0;
static size_t audio_batch_frames_max = (1 << 16);

static void audio_out_buffer_init(void)
{
   float sample_rate       = use_cc_resampler ?
         SOUND_SAMPLE_RATE_CC : SOUND_SAMPLE_RATE_BLIPPER;
   float samples_per_frame = sample_rate / VIDEO_REFRESH_RATE;
   size_t buffer_size      = ((size_t)samples_per_frame + 1) << 1;

   /* Create a buffer that is double the required size
    * to minimise the likelihood of resize operations
    * (core tends to produce very brief 'bursts' of high
    * sample counts depending upon the emulated content...) */
   buffer_size = (buffer_size << 1);

   audio_out_buffer        = (int16_t *)malloc(buffer_size * sizeof(int16_t));
   /* On allocation failure leave the buffer empty; subsequent
    * audio_out_buffer_write calls become no-ops (audio is
    * recoverable, crashing isn't). */
   audio_out_buffer_size   = audio_out_buffer ? buffer_size : 0;
   audio_out_buffer_pos    = 0;
   audio_batch_frames_max  = (1 << 16);
}

static void audio_out_buffer_deinit(void)
{
   if (audio_out_buffer)
      free(audio_out_buffer);

   audio_out_buffer       = NULL;
   audio_out_buffer_size  = 0;
   audio_out_buffer_pos   = 0;
   audio_batch_frames_max = (1 << 16);
}

static INLINE bool audio_out_buffer_resize(size_t num_samples)
{
   size_t buffer_capacity = (audio_out_buffer_size -
         audio_out_buffer_pos) >> 1;

   if (buffer_capacity < num_samples)
   {
      int16_t *tmp_buffer = NULL;
      size_t tmp_buffer_size;

      tmp_buffer_size = audio_out_buffer_size +
            ((num_samples - buffer_capacity) << 1);
      tmp_buffer_size = (tmp_buffer_size << 1) - (tmp_buffer_size >> 1);

      /* Use realloc so the allocator can grow in place when
       * possible. realloc(NULL, n) == malloc(n), and on failure
       * realloc returns NULL with the original block intact, so
       * we just report failure to the caller and keep using the
       * old (possibly NULL) buffer. */
      tmp_buffer = (int16_t *)realloc(audio_out_buffer,
            tmp_buffer_size * sizeof(int16_t));
      if (!tmp_buffer)
         return false;

      audio_out_buffer      = tmp_buffer;
      audio_out_buffer_size = tmp_buffer_size;
   }

   return true;
}

/* C linkage so cc_resampler.c (compiled as C) can call it.
 * cc_resampler.h declares it inside extern "C", and the previous
 * upstream layout got away with this only because cc_resampler
 * was header-only and compiled as C++ in this TU. Now that
 * cc_resampler.c is a separate translation unit, this definition
 * must use C linkage to match. */
extern "C" void audio_out_buffer_write(int16_t *samples, size_t num_samples)
{
   if (!audio_out_buffer_resize(num_samples))
      return; /* allocation failure -- drop samples rather than crash */

   memcpy(audio_out_buffer + audio_out_buffer_pos,
         samples, (num_samples << 1) * sizeof(int16_t));

   audio_out_buffer_pos += num_samples << 1;
}

static void audio_out_buffer_read_blipper(size_t num_samples)
{
   int16_t *audio_out_buffer_ptr = NULL;

   if (!audio_out_buffer_resize(num_samples))
   {
      /* Out of memory: still drain the blipper to avoid leaving
       * stale samples in its ring (which would mis-align future
       * audio with emulator state, especially after a savestate
       * load). Discard the drained data into a small scratch. */
      int16_t scratch[64];
      size_t remaining = num_samples;
      while (remaining)
      {
         size_t chunk = remaining > 32 ? 32 : remaining;
         blipper_read(resampler_l, scratch    , chunk, 2);
         blipper_read(resampler_r, scratch + 1, chunk, 2);
         remaining -= chunk;
      }
      return;
   }

   audio_out_buffer_ptr = audio_out_buffer + audio_out_buffer_pos;

   blipper_read(resampler_l, audio_out_buffer_ptr    , num_samples, 2);
   blipper_read(resampler_r, audio_out_buffer_ptr + 1, num_samples, 2);

   audio_out_buffer_pos += num_samples << 1;
}

static void audio_upload_samples(void)
{
   int16_t *audio_out_buffer_ptr = audio_out_buffer;
   size_t num_samples            = audio_out_buffer_pos >> 1;

   while (num_samples > 0)
   {
      size_t samples_to_write = (num_samples >
            audio_batch_frames_max) ?
                  audio_batch_frames_max :
                  num_samples;
      size_t samples_written = audio_batch_cb(
            audio_out_buffer_ptr, samples_to_write);

      if ((samples_written < samples_to_write) &&
          (samples_written > 0))
         audio_batch_frames_max = samples_written;

      num_samples          -= samples_to_write;
      audio_out_buffer_ptr += samples_to_write << 1;
   }

   audio_out_buffer_pos = 0;
}

static void blipper_renderaudio(const int16_t *samples, unsigned frames)
{
   if (!frames)
      return;

   blipper_push_samples(resampler_l, samples + 0, frames, 2);
   blipper_push_samples(resampler_r, samples + 1, frames, 2);
}

static void audio_resampler_deinit(void)
{
   if (resampler_l)
      blipper_free(resampler_l);

   if (resampler_r)
      blipper_free(resampler_r);

   resampler_l = NULL;
   resampler_r = NULL;

   audio_out_buffer_deinit();
}

static void audio_resampler_init(bool startup)
{
   if (use_cc_resampler)
      CC_init();
   else
   {
      resampler_l = blipper_new(32, 0.85, 6.5, 64, BLIP_BUFFER_SIZE, NULL);
      resampler_r = blipper_new(32, 0.85, 6.5, 64, BLIP_BUFFER_SIZE, NULL);

      /* It is possible for blipper_new() to fail,
       * must handle errors */
      if (!resampler_l || !resampler_r)
      {
         /* Display warning message */
         if (libretro_msg_interface_version >= 1)
         {
            struct retro_message_ext msg = {
               "Sinc resampler unsupported on this platform - using Cosine",
               2000,
               1,
               RETRO_LOG_WARN,
               RETRO_MESSAGE_TARGET_OSD,
               RETRO_MESSAGE_TYPE_NOTIFICATION,
               -1
            };
            environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE_EXT, &msg);
         }
         else
         {
            struct retro_message msg = {
               "Sinc resampler unsupported on this platform - using Cosine",
               120
            };
            environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg);
         }

         /* Force CC resampler */
         audio_resampler_deinit();
         use_cc_resampler = true;
         CC_init();

         /* Notify frontend of option value change */
         if (libretro_supports_set_variable)
         {
            struct retro_variable var = {0};
            var.key   = "gambatte_audio_resampler";
            var.value = "cc";
            environ_cb(RETRO_ENVIRONMENT_SET_VARIABLE, &var);
         }

         /* Notify frontend of sample rate change.
          * Previously this was only done when !startup, on the
          * theory that the frontend would re-query AV info after
          * retro_load_game returns. In practice the AV info has
          * already been cached by the time retro_load_game runs
          * (the frontend reads it earlier in the load sequence),
          * so the cache is wrong until the next time something
          * changes it. Always notify so the frontend's cached
          * sample rate matches reality. */
         struct retro_system_av_info av_info;
         retro_get_system_av_info(&av_info);
         environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &av_info);
         (void)startup;
      }
   }

   audio_out_buffer_init();
}

/***********************/
/* Audio Resampler END */
/***********************/

/***************************/
/* Palette Switching START */
/***************************/

static bool internal_palette_active    = false;
static size_t internal_palette_index   = 0;
static unsigned palette_switch_counter = 0;

/* Period in frames between palette switches
 * when holding RetroPad L/R */
#define PALETTE_SWITCH_PERIOD 30

/* Note: These must be updated if the internal
 * palette options in libretro_core_options.h
 * are changed
 * > We could count the palettes at runtime,
 *   but this adds unnecessary performance
 *   overheads and seems futile given that
 *   a number of other parameters must be
 *   hardcoded anyway... */
#define NUM_PALETTES_DEFAULT       51
#define NUM_PALETTES_TWB64_1      100
#define NUM_PALETTES_TWB64_2      100
#define NUM_PALETTES_TWB64_3      100
#define NUM_PALETTES_PIXELSHIFT_1  45
#define NUM_PALETTES_TOTAL        (NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1 + NUM_PALETTES_TWB64_2 + \
                                   NUM_PALETTES_TWB64_3 + NUM_PALETTES_PIXELSHIFT_1)

struct retro_core_option_value *palettes_default_opt_values      = NULL;
struct retro_core_option_value *palettes_twb64_1_opt_values      = NULL;
struct retro_core_option_value *palettes_twb64_2_opt_values      = NULL;
struct retro_core_option_value *palettes_twb64_3_opt_values      = NULL;
struct retro_core_option_value *palettes_pixelshift_1_opt_values = NULL;

static const char *internal_palette_labels[NUM_PALETTES_TOTAL] = {0};

static size_t *palettes_default_index_map      = NULL;
static size_t *palettes_twb64_1_index_map      = NULL;
static size_t *palettes_twb64_2_index_map      = NULL;
static size_t *palettes_twb64_3_index_map      = NULL;
static size_t *palettes_pixelshift_1_index_map = NULL;

static void parse_internal_palette_values(const char *key,
      struct retro_core_option_v2_definition *opt_defs_intl,
      size_t num_palettes, size_t palette_offset,
      struct retro_core_option_value **opt_values,
      size_t **index_map)
{
   size_t i;
   struct retro_core_option_v2_definition *opt_defs     = option_defs_us;
   struct retro_core_option_v2_definition *opt_def      = NULL;
   size_t label_index                                   = 0;
#ifndef HAVE_NO_LANGEXTRA
   struct retro_core_option_v2_definition *opt_def_intl = NULL;
#endif
   /* Find option corresponding to key */
   for (opt_def = opt_defs; !string_is_empty(opt_def->key); opt_def++)
      if (string_is_equal(opt_def->key, key))
         break;

   /* Cache option values array for fast access
    * when setting palette index */
   *opt_values = opt_def->values;

   /* Loop over all palette values for specified
    * option and:
    * - Generate palette index maps
    * - Fetch palette labels for notification
    *   purposes
    * Note: We perform no error checking here,
    * since we are operating on hardcoded structs
    * over which the core has full control */
   for (i = 0; i < num_palettes; i++)
   {
      const char *value       = opt_def->values[i].value;
      const char *value_label = NULL;

      /* Add entry to hash map
       * > Note that we have to set index+1, since
       *   a return value of 0 from RHMAP_GET_STR()
       *   indicates that the key string was not found */
      RHMAP_SET_STR((*index_map), value, i + 1);

      /* Check if we have a localised palette label */
#ifndef HAVE_NO_LANGEXTRA
      if (opt_defs_intl)
      {
         /* Find localised option corresponding to key */
         for (opt_def_intl = opt_defs_intl;
              !string_is_empty(opt_def_intl->key);
              opt_def_intl++)
         {
            if (string_is_equal(opt_def_intl->key, key))
            {
               size_t j = 0;

               /* Search for current option value */
               for (;;)
               {
                  const char *value_intl = opt_def_intl->values[j].value;

                  if (string_is_empty(value_intl))
                     break;

                  if (string_is_equal(value, value_intl))
                  {
                     /* We have a match; fetch localised label */
                     value_label = opt_def_intl->values[j].label;
                     break;
                  }

                  j++;
               }

               break;
            }
         }
      }
#endif
      /* If localised palette label is unset,
       * use value itself from option_defs_us */
      if (!value_label)
         value_label = value;

      /* Cache label for 'consolidated' palette index */
      internal_palette_labels[palette_offset + label_index++] = value_label;
   }
}

static void init_palette_switch(void)
{
   struct retro_core_option_v2_definition *opt_defs_intl = NULL;
#ifndef HAVE_NO_LANGEXTRA
   unsigned language                                     = 0;
#endif

   libretro_supports_set_variable = false;
   if (environ_cb(RETRO_ENVIRONMENT_SET_VARIABLE, NULL))
      libretro_supports_set_variable = true;

   libretro_msg_interface_version = 0;
   environ_cb(RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION,
         &libretro_msg_interface_version);

   internal_palette_active = false;
   internal_palette_index  = 0;
   palette_switch_counter  = 0;

#ifndef HAVE_NO_LANGEXTRA
   if (environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &language) &&
       (language < RETRO_LANGUAGE_LAST) &&
       (language != RETRO_LANGUAGE_ENGLISH) &&
       options_intl[language])
      opt_defs_intl = options_intl[language]->definitions;
#endif

   /* Parse palette values for each palette group
    * > Default palettes */
   parse_internal_palette_values("gambatte_gb_internal_palette",
         opt_defs_intl, NUM_PALETTES_DEFAULT,
         0,
         &palettes_default_opt_values,
         &palettes_default_index_map);
   /* > TWB64 Pack 1 palettes */
   parse_internal_palette_values("gambatte_gb_palette_twb64_1",
         opt_defs_intl, NUM_PALETTES_TWB64_1,
         NUM_PALETTES_DEFAULT,
         &palettes_twb64_1_opt_values,
         &palettes_twb64_1_index_map);
   /* > TWB64 Pack 2 palettes */
   parse_internal_palette_values("gambatte_gb_palette_twb64_2",
         opt_defs_intl, NUM_PALETTES_TWB64_2,
         NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1,
         &palettes_twb64_2_opt_values,
         &palettes_twb64_2_index_map);
   /* > TWB64 Pack 3 palettes */
   parse_internal_palette_values("gambatte_gb_palette_twb64_3",
         opt_defs_intl, NUM_PALETTES_TWB64_3,
         NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1 + NUM_PALETTES_TWB64_2,
         &palettes_twb64_3_opt_values,
         &palettes_twb64_3_index_map);
   /* > PixelShift - Pack 1 palettes */
   parse_internal_palette_values("gambatte_gb_palette_pixelshift_1",
         opt_defs_intl, NUM_PALETTES_PIXELSHIFT_1,
         NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1 + NUM_PALETTES_TWB64_2 +
         NUM_PALETTES_TWB64_3,
         &palettes_pixelshift_1_opt_values,
         &palettes_pixelshift_1_index_map);
}

static void deinit_palette_switch(void)
{
   libretro_supports_set_variable   = false;
   libretro_msg_interface_version   = 0;
   internal_palette_active          = false;
   internal_palette_index           = 0;
   palette_switch_counter           = 0;
   palettes_default_opt_values      = NULL;
   palettes_twb64_1_opt_values      = NULL;
   palettes_twb64_2_opt_values      = NULL;
   palettes_twb64_3_opt_values      = NULL;
   palettes_pixelshift_1_opt_values = NULL;

   RHMAP_FREE(palettes_default_index_map);
   RHMAP_FREE(palettes_twb64_1_index_map);
   RHMAP_FREE(palettes_twb64_2_index_map);
   RHMAP_FREE(palettes_twb64_3_index_map);
   RHMAP_FREE(palettes_pixelshift_1_index_map);
}

static void palette_switch_set_index(size_t palette_index)
{
   const char *palettes_default_value = NULL;
   const char *palettes_ext_key       = NULL;
   const char *palettes_ext_value     = NULL;
   size_t opt_index                   = 0;
   struct retro_variable var          = {0};

   if (palette_index >= NUM_PALETTES_TOTAL)
      palette_index = NUM_PALETTES_TOTAL - 1;

   /* Check which palette group the specified
    * index corresponds to */
   if (palette_index < NUM_PALETTES_DEFAULT)
   {
      /* This is a palette from the default group */
      opt_index              = palette_index;
      palettes_default_value = palettes_default_opt_values[opt_index].value;
   }
   else if (palette_index <
         NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1)
   {
      /* This is a palette from the TWB64 Pack 1 group */
      palettes_default_value = "TWB64 - Pack 1";

      opt_index              = palette_index - NUM_PALETTES_DEFAULT;
      palettes_ext_key       = "gambatte_gb_palette_twb64_1";
      palettes_ext_value     = palettes_twb64_1_opt_values[opt_index].value;
   }
   else if (palette_index <
         NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1 + NUM_PALETTES_TWB64_2)
   {
      /* This is a palette from the TWB64 Pack 2 group */
      palettes_default_value = "TWB64 - Pack 2";

      opt_index              = palette_index -
            (NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1);
      palettes_ext_key       = "gambatte_gb_palette_twb64_2";
      palettes_ext_value     = palettes_twb64_2_opt_values[opt_index].value;
   }
   else if (palette_index <
         NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1 + NUM_PALETTES_TWB64_2 +
         NUM_PALETTES_TWB64_3)
   {
      /* This is a palette from the TWB64 Pack 3 group */
      palettes_default_value = "TWB64 - Pack 3";

      opt_index              = palette_index -
            (NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1 + NUM_PALETTES_TWB64_2);
      palettes_ext_key       = "gambatte_gb_palette_twb64_3";
      palettes_ext_value     = palettes_twb64_3_opt_values[opt_index].value;
   }
   else
   {
      /* This is a palette from the PixelShift Pack 1 group */
      palettes_default_value = "PixelShift - Pack 1";

      opt_index              = palette_index -
            (NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1 + NUM_PALETTES_TWB64_2 +
             NUM_PALETTES_TWB64_3);
      palettes_ext_key       = "gambatte_gb_palette_pixelshift_1";
      palettes_ext_value     = palettes_pixelshift_1_opt_values[opt_index].value;
   }

   /* Notify frontend of option value changes */
   var.key   = "gambatte_gb_internal_palette";
   var.value = palettes_default_value;
   environ_cb(RETRO_ENVIRONMENT_SET_VARIABLE, &var);

   if (palettes_ext_key)
   {
      var.key   = palettes_ext_key;
      var.value = palettes_ext_value;
      environ_cb(RETRO_ENVIRONMENT_SET_VARIABLE, &var);
   }

   /* Display notification message */
   if (libretro_msg_interface_version >= 1)
   {
      struct retro_message_ext msg = {
         internal_palette_labels[palette_index],
         2000,
         1,
         RETRO_LOG_INFO,
         RETRO_MESSAGE_TARGET_OSD,
         RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
         -1
      };
      environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE_EXT, &msg);
   }
   else
   {
      struct retro_message msg = {
         internal_palette_labels[palette_index],
         120
      };
      environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg);
   }
}

/*************************/
/* Palette Switching END */
/*************************/

/*****************************/
/* Interframe blending START */
/*****************************/

#define LCD_RESPONSE_TIME 0.333f
/* > 'LCD Ghosting (Fast)' method does not
 *   correctly interpret the set response time,
 *   leading to an artificially subdued blur effect.
 *   We have to compensate for this by increasing
 *   the response time, hence this 'fake' value */
#define LCD_RESPONSE_TIME_FAKE 0.5f

enum frame_blend_method
{
   FRAME_BLEND_NONE = 0,
   FRAME_BLEND_MIX,
   FRAME_BLEND_LCD_GHOSTING,
   FRAME_BLEND_LCD_GHOSTING_FAST
};

static enum frame_blend_method frame_blend_type  = FRAME_BLEND_NONE;
static gambatte::video_pixel_t* video_buf_prev_1 = NULL;
static gambatte::video_pixel_t* video_buf_prev_2 = NULL;
static gambatte::video_pixel_t* video_buf_prev_3 = NULL;
static gambatte::video_pixel_t* video_buf_prev_4 = NULL;
static float* video_buf_acc_r                    = NULL;
static float* video_buf_acc_g                    = NULL;
static float* video_buf_acc_b                    = NULL;
static float frame_blend_response[4]             = {0.0f};
static bool frame_blend_response_set             = false;
static void (*blend_frames)(void)                = NULL;

/* > Note: The individual frame blending functions
 *   are somewhat WET (Write Everything Twice), in that
 *   we duplicate the entire nested for loop.
 *   This code is performance-critical, so we want to
 *   minimise logic in the inner loops where possible  */
static void blend_frames_mix(void)
{
   gambatte::video_pixel_t *curr = video_buf;
   gambatte::video_pixel_t *prev = video_buf_prev_1;
   size_t x, y;

   for (y = 0; y < VIDEO_HEIGHT; y++)
   {
      for (x = 0; x < VIDEO_WIDTH; x++)
      {
         /* Get colours from current + previous frames */
         gambatte::video_pixel_t rgb_curr = *(curr + x);
         gambatte::video_pixel_t rgb_prev = *(prev + x);

         /* Store colours for next frame */
         *(prev + x) = rgb_curr;

         /* Mix colours
          * > "Mixing Packed RGB Pixels Efficiently"
          *   http://blargg.8bitalley.com/info/rgb_mixing.html */
#ifdef VIDEO_RGB565
         *(curr + x) = (rgb_curr + rgb_prev + ((rgb_curr ^ rgb_prev) & 0x821)) >> 1;
#elif defined(VIDEO_ABGR1555)
         *(curr + x) = (rgb_curr + rgb_prev + ((rgb_curr ^ rgb_prev) & 0x521)) >> 1;
#else
         *(curr + x) = (rgb_curr + rgb_prev + ((rgb_curr ^ rgb_prev) & 0x10101)) >> 1;
#endif
      }

      curr += VIDEO_PITCH;
      prev += VIDEO_PITCH;
   }
}

/* Index of the oldest frame in the lcd-ghost history ring.
 * Each blend_frames_lcd_ghost call advances it by one. The slot
 * the index points to is overwritten with the current frame's
 * pixels (replacing the oldest history) and the rotation makes
 * it the most-recent history for the next call. This avoids
 * the 4 per-pixel cross-buffer writes the previous implementation
 * was doing. */
static unsigned lcd_ghost_oldest = 0;

static void blend_frames_lcd_ghost(void)
{
   /* Map "prev_N" labels onto the ring. prev_1 is the most
    * recent past frame (one frame ago); prev_4 is the oldest
    * (four frames ago). Each call we'll rotate by one. */
   gambatte::video_pixel_t * const ring[4] = {
      video_buf_prev_1, video_buf_prev_2, video_buf_prev_3, video_buf_prev_4
   };
   const unsigned i_oldest = lcd_ghost_oldest;
   gambatte::video_pixel_t *curr   = video_buf;
   gambatte::video_pixel_t *prev_4 = ring[ i_oldest         ];
   gambatte::video_pixel_t *prev_3 = ring[(i_oldest + 1) & 3];
   gambatte::video_pixel_t *prev_2 = ring[(i_oldest + 2) & 3];
   gambatte::video_pixel_t *prev_1 = ring[(i_oldest + 3) & 3];
   /* prev_4's slot is about to receive the current frame's
    * contents; it becomes the new prev_1 for the next call. */
   gambatte::video_pixel_t *dst    = prev_4;
   float *response                 = frame_blend_response;
   size_t x, y;

   for (y = 0; y < VIDEO_HEIGHT; y++)
   {
      for (x = 0; x < VIDEO_WIDTH; x++)
      {
         /* Get colours from current + previous frames */
         gambatte::video_pixel_t rgb_curr   = *(curr + x);
         gambatte::video_pixel_t rgb_prev_1 = *(prev_1 + x);
         gambatte::video_pixel_t rgb_prev_2 = *(prev_2 + x);
         gambatte::video_pixel_t rgb_prev_3 = *(prev_3 + x);
         gambatte::video_pixel_t rgb_prev_4 = *(prev_4 + x);

         /* Save the ORIGINAL current pixel into the slot that
          * just yielded its prev_4 value -- that slot becomes
          * the new prev_1 next call. One write replaces what
          * used to be four cascading pixel copies. */
         *(dst + x) = rgb_curr;

         /* Unpack colours and convert to float */
#ifdef VIDEO_RGB565
         float r_curr = static_cast<float>(rgb_curr >> 11 & 0x1F);
         float g_curr = static_cast<float>(rgb_curr >>  6 & 0x1F);
         float b_curr = static_cast<float>(rgb_curr       & 0x1F);

         float r_prev_1 = static_cast<float>(rgb_prev_1 >> 11 & 0x1F);
         float g_prev_1 = static_cast<float>(rgb_prev_1 >>  6 & 0x1F);
         float b_prev_1 = static_cast<float>(rgb_prev_1       & 0x1F);

         float r_prev_2 = static_cast<float>(rgb_prev_2 >> 11 & 0x1F);
         float g_prev_2 = static_cast<float>(rgb_prev_2 >>  6 & 0x1F);
         float b_prev_2 = static_cast<float>(rgb_prev_2       & 0x1F);

         float r_prev_3 = static_cast<float>(rgb_prev_3 >> 11 & 0x1F);
         float g_prev_3 = static_cast<float>(rgb_prev_3 >>  6 & 0x1F);
         float b_prev_3 = static_cast<float>(rgb_prev_3       & 0x1F);

         float r_prev_4 = static_cast<float>(rgb_prev_4 >> 11 & 0x1F);
         float g_prev_4 = static_cast<float>(rgb_prev_4 >>  6 & 0x1F);
         float b_prev_4 = static_cast<float>(rgb_prev_4       & 0x1F);
#elif defined(VIDEO_ABGR1555)
         float r_curr = static_cast<float>(rgb_curr       & 0x1F);
         float g_curr = static_cast<float>(rgb_curr >>  5 & 0x1F);
         float b_curr = static_cast<float>(rgb_curr >> 10 & 0x1F);

         float r_prev_1 = static_cast<float>(rgb_prev_1       & 0x1F);
         float g_prev_1 = static_cast<float>(rgb_prev_1 >>  5 & 0x1F);
         float b_prev_1 = static_cast<float>(rgb_prev_1 >> 10 & 0x1F);

         float r_prev_2 = static_cast<float>(rgb_prev_2       & 0x1F);
         float g_prev_2 = static_cast<float>(rgb_prev_2 >>  5 & 0x1F);
         float b_prev_2 = static_cast<float>(rgb_prev_2 >> 10 & 0x1F);

         float r_prev_3 = static_cast<float>(rgb_prev_3       & 0x1F);
         float g_prev_3 = static_cast<float>(rgb_prev_3 >>  5 & 0x1F);
         float b_prev_3 = static_cast<float>(rgb_prev_3 >> 10 & 0x1F);

         float r_prev_4 = static_cast<float>(rgb_prev_4       & 0x1F);
         float g_prev_4 = static_cast<float>(rgb_prev_4 >>  5 & 0x1F);
         float b_prev_4 = static_cast<float>(rgb_prev_4 >> 10 & 0x1F);
#else
         float r_curr = static_cast<float>(rgb_curr >> 16 & 0x1F);
         float g_curr = static_cast<float>(rgb_curr >>  8 & 0x1F);
         float b_curr = static_cast<float>(rgb_curr       & 0x1F);

         float r_prev_1 = static_cast<float>(rgb_prev_1 >> 16 & 0x1F);
         float g_prev_1 = static_cast<float>(rgb_prev_1 >>  8 & 0x1F);
         float b_prev_1 = static_cast<float>(rgb_prev_1       & 0x1F);

         float r_prev_2 = static_cast<float>(rgb_prev_2 >> 16 & 0x1F);
         float g_prev_2 = static_cast<float>(rgb_prev_2 >>  8 & 0x1F);
         float b_prev_2 = static_cast<float>(rgb_prev_2       & 0x1F);

         float r_prev_3 = static_cast<float>(rgb_prev_3 >> 16 & 0x1F);
         float g_prev_3 = static_cast<float>(rgb_prev_3 >>  8 & 0x1F);
         float b_prev_3 = static_cast<float>(rgb_prev_3       & 0x1F);

         float r_prev_4 = static_cast<float>(rgb_prev_4 >> 16 & 0x1F);
         float g_prev_4 = static_cast<float>(rgb_prev_4 >>  8 & 0x1F);
         float b_prev_4 = static_cast<float>(rgb_prev_4       & 0x1F);
#endif
         /* Mix colours for current frame and convert back to video_pixel_t
          * > Response time effect implemented via an exponential
          *   drop-off algorithm, taken from the 'Gameboy Classic Shader'
          *   by Harlequin:
          *      https://github.com/libretro/glsl-shaders/blob/master/handheld/shaders/gameboy/shader-files/gb-pass0.glsl */
         r_curr += (r_prev_1 - r_curr) * *response;
         r_curr += (r_prev_2 - r_curr) * *(response + 1);
         r_curr += (r_prev_3 - r_curr) * *(response + 2);
         r_curr += (r_prev_4 - r_curr) * *(response + 3);
         gambatte::video_pixel_t r_mix = static_cast<gambatte::video_pixel_t>(r_curr + 0.5f) & 0x1F;

         g_curr += (g_prev_1 - g_curr) * *response;
         g_curr += (g_prev_2 - g_curr) * *(response + 1);
         g_curr += (g_prev_3 - g_curr) * *(response + 2);
         g_curr += (g_prev_4 - g_curr) * *(response + 3);
         gambatte::video_pixel_t g_mix = static_cast<gambatte::video_pixel_t>(g_curr + 0.5f) & 0x1F;

         b_curr += (b_prev_1 - b_curr) * *response;
         b_curr += (b_prev_2 - b_curr) * *(response + 1);
         b_curr += (b_prev_3 - b_curr) * *(response + 2);
         b_curr += (b_prev_4 - b_curr) * *(response + 3);
         gambatte::video_pixel_t b_mix = static_cast<gambatte::video_pixel_t>(b_curr + 0.5f) & 0x1F;

         /* Repack colours for current frame */
#ifdef VIDEO_RGB565
         *(curr + x) = r_mix << 11 | g_mix << 6 | b_mix;
#elif defined(VIDEO_ABGR1555)
         *(curr + x) = b_mix << 10 | g_mix << 5 | r_mix;
#else
         *(curr + x) = r_mix << 16 | g_mix << 8 | b_mix;
#endif
      }

      curr   += VIDEO_PITCH;
      prev_1 += VIDEO_PITCH;
      prev_2 += VIDEO_PITCH;
      prev_3 += VIDEO_PITCH;
      prev_4 += VIDEO_PITCH;
      dst    += VIDEO_PITCH;
   }

   /* Advance ring index: the slot we just wrote (oldest before
    * the rotation) is now the freshest history. */
   lcd_ghost_oldest = (i_oldest + 3) & 3;
}

static void blend_frames_lcd_ghost_fast(void)
{
   gambatte::video_pixel_t *curr = video_buf;
   float *prev_r                 = video_buf_acc_r;
   float *prev_g                 = video_buf_acc_g;
   float *prev_b                 = video_buf_acc_b;
   size_t x, y;

   for (y = 0; y < VIDEO_HEIGHT; y++)
   {
      for (x = 0; x < VIDEO_WIDTH; x++)
      {
         /* Get colours from current + previous frames */
         gambatte::video_pixel_t rgb_curr   = *(curr + x);
         float r_prev                       = *(prev_r + x);
         float g_prev                       = *(prev_g + x);
         float b_prev                       = *(prev_b + x);

         /* Unpack current colours and convert to float */
#ifdef VIDEO_RGB565
         float r_curr = static_cast<float>(rgb_curr >> 11 & 0x1F);
         float g_curr = static_cast<float>(rgb_curr >>  6 & 0x1F);
         float b_curr = static_cast<float>(rgb_curr       & 0x1F);
#elif defined(VIDEO_ABGR1555)
         float r_curr = static_cast<float>(rgb_curr       & 0x1F);
         float g_curr = static_cast<float>(rgb_curr >>  5 & 0x1F);
         float b_curr = static_cast<float>(rgb_curr >> 10 & 0x1F);
#else
         float r_curr = static_cast<float>(rgb_curr >> 16 & 0x1F);
         float g_curr = static_cast<float>(rgb_curr >>  8 & 0x1F);
         float b_curr = static_cast<float>(rgb_curr       & 0x1F);
#endif
         /* Mix colours for current frame */
         float r_mix = (r_curr * (1.0f - LCD_RESPONSE_TIME_FAKE)) + (LCD_RESPONSE_TIME_FAKE * r_prev);
         float g_mix = (g_curr * (1.0f - LCD_RESPONSE_TIME_FAKE)) + (LCD_RESPONSE_TIME_FAKE * g_prev);
         float b_mix = (b_curr * (1.0f - LCD_RESPONSE_TIME_FAKE)) + (LCD_RESPONSE_TIME_FAKE * b_prev);

         /* Store colours for next frame */
         *(prev_r + x) = r_mix;
         *(prev_g + x) = g_mix;
         *(prev_b + x) = b_mix;

         /* Convert, repack and assign colours for current frame */
#ifdef VIDEO_RGB565
         *(curr + x) =   (static_cast<gambatte::video_pixel_t>(r_mix + 0.5f) & 0x1F) << 11
                       | (static_cast<gambatte::video_pixel_t>(g_mix + 0.5f) & 0x1F) << 6
                       | (static_cast<gambatte::video_pixel_t>(b_mix + 0.5f) & 0x1F);
#elif defined(VIDEO_ABGR1555)
         *(curr + x) =   (static_cast<gambatte::video_pixel_t>(r_mix + 0.5f) & 0x1F)
                       | (static_cast<gambatte::video_pixel_t>(g_mix + 0.5f) & 0x1F) << 5
                       | (static_cast<gambatte::video_pixel_t>(b_mix + 0.5f) & 0x1F) << 10;
#else
         *(curr + x) =   (static_cast<gambatte::video_pixel_t>(r_mix + 0.5f) & 0x1F) << 16
                       | (static_cast<gambatte::video_pixel_t>(g_mix + 0.5f) & 0x1F) << 8
                       | (static_cast<gambatte::video_pixel_t>(b_mix + 0.5f) & 0x1F);
#endif
      }

      curr   += VIDEO_PITCH;
      prev_r += VIDEO_PITCH;
      prev_g += VIDEO_PITCH;
      prev_b += VIDEO_PITCH;
   }
}

static bool allocate_video_buf_prev(gambatte::video_pixel_t** buf)
{
   if (!*buf)
   {
      *buf = (gambatte::video_pixel_t*)malloc(VIDEO_BUFF_SIZE);
      if (!*buf)
         return false;
   }
   memset(*buf, 0, VIDEO_BUFF_SIZE);
   return true;
}

static bool allocate_video_buf_acc(void)
{
   size_t i;
   size_t buf_size = 256 * NUM_GAMEBOYS * VIDEO_HEIGHT * sizeof(float);

   if (!video_buf_acc_r)
   {
      video_buf_acc_r = (float*)malloc(buf_size);
      if (!video_buf_acc_r)
         return false;
   }

   if (!video_buf_acc_g)
   {
      video_buf_acc_g = (float*)malloc(buf_size);
      if (!video_buf_acc_g)
         return false;
   }

   if (!video_buf_acc_b)
   {
      video_buf_acc_b = (float*)malloc(buf_size);
      if (!video_buf_acc_b)
         return false;
   }

   /* Cannot use memset() on arrays of floats... */
   for (i = 0; i < (256 * NUM_GAMEBOYS * VIDEO_HEIGHT); i++)
   {
      video_buf_acc_r[i] = 0.0f;
      video_buf_acc_g[i] = 0.0f;
      video_buf_acc_b[i] = 0.0f;
   }
   return true;
}

static void init_frame_blending(void)
{
   blend_frames = NULL;

   /* Allocate interframe blending buffers, as required
    * NOTE: In all cases, any used buffers are 'reset'
    * to avoid drawing garbage on the next frame */
   switch (frame_blend_type)
   {
      case FRAME_BLEND_MIX:
         /* Simple 50:50 blending requires a single buffer */
         if (!allocate_video_buf_prev(&video_buf_prev_1))
            return;
         break;
      case FRAME_BLEND_LCD_GHOSTING:
         /* 'Accurate' LCD ghosting requires four buffers */
         if (!allocate_video_buf_prev(&video_buf_prev_1))
            return;
         if (!allocate_video_buf_prev(&video_buf_prev_2))
            return;
         if (!allocate_video_buf_prev(&video_buf_prev_3))
            return;
         if (!allocate_video_buf_prev(&video_buf_prev_4))
            return;
         break;
      case FRAME_BLEND_LCD_GHOSTING_FAST:
         /* 'Fast' LCD ghosting requires three (RGB)
          * 'accumulator' buffers */
         if (!allocate_video_buf_acc())
            return;
         break;
      case FRAME_BLEND_NONE:
      default:
         /* Error condition - cannot happen
          * > Just leave blend_frames() function set to NULL */
         return;
   }

   /* Set LCD ghosting response time factors,
    * if required */
   if ((frame_blend_type == FRAME_BLEND_LCD_GHOSTING) &&
       !frame_blend_response_set)
   {
      /* For the default response time of 0.333,
       * only four previous samples are required
       * since the response factor for the fifth
       * is:
       *    pow(LCD_RESPONSE_TIME, 5.0f) -> 0.00409
       * ...which is less than half a percent, and
       * therefore irrelevant.
       * If the response time were significantly
       * increased, we may need to rethink this
       * (but more samples == greater performance
       * overheads) */
      frame_blend_response[0] = LCD_RESPONSE_TIME;
      frame_blend_response[1] = std::pow(LCD_RESPONSE_TIME, 2.0f);
      frame_blend_response[2] = std::pow(LCD_RESPONSE_TIME, 3.0f);
      frame_blend_response[3] = std::pow(LCD_RESPONSE_TIME, 4.0f);

      frame_blend_response_set = true;
   }

   /* Assign frame blending function */
   switch (frame_blend_type)
   {
      case FRAME_BLEND_MIX:
         blend_frames = blend_frames_mix;
         return;
      case FRAME_BLEND_LCD_GHOSTING:
         blend_frames = blend_frames_lcd_ghost;
         return;
      case FRAME_BLEND_LCD_GHOSTING_FAST:
         blend_frames = blend_frames_lcd_ghost_fast;
         return;
      case FRAME_BLEND_NONE:
      default:
         /* Error condition - cannot happen
          * > Just leave blend_frames() function set to NULL */
         return;
   }
}

static void deinit_frame_blending(void)
{
   if (video_buf_prev_1)
   {
      free(video_buf_prev_1);
      video_buf_prev_1 = NULL;
   }

   if (video_buf_prev_2)
   {
      free(video_buf_prev_2);
      video_buf_prev_2 = NULL;
   }

   if (video_buf_prev_3)
   {
      free(video_buf_prev_3);
      video_buf_prev_3 = NULL;
   }

   if (video_buf_prev_4)
   {
      free(video_buf_prev_4);
      video_buf_prev_4 = NULL;
   }

   if (video_buf_acc_r)
   {
      free(video_buf_acc_r);
      video_buf_acc_r = NULL;
   }

   if (video_buf_acc_g)
   {
      free(video_buf_acc_g);
      video_buf_acc_g = NULL;
   }

   if (video_buf_acc_b)
   {
      free(video_buf_acc_b);
      video_buf_acc_b = NULL;
   }

   frame_blend_type         = FRAME_BLEND_NONE;
   frame_blend_response_set = false;
}

/* Zero out any allocated frame-blend history buffers without
 * deallocating them. Called from retro_unserialize so a state
 * load doesn't carry ghost trails from the pre-load timeline. */
static void reset_frame_blending_buffers(void)
{
   if (video_buf_prev_1)
      memset(video_buf_prev_1, 0, VIDEO_BUFF_SIZE);
   if (video_buf_prev_2)
      memset(video_buf_prev_2, 0, VIDEO_BUFF_SIZE);
   if (video_buf_prev_3)
      memset(video_buf_prev_3, 0, VIDEO_BUFF_SIZE);
   if (video_buf_prev_4)
      memset(video_buf_prev_4, 0, VIDEO_BUFF_SIZE);

   /* The lcd-ghost ring is content-symmetric when all slots are
    * zero, but reset the oldest index too so the rotation
    * state is identical to a fresh init -- this matters if
    * anything down the line ever cares about which physical
    * buffer holds which logical position. */
   lcd_ghost_oldest = 0;

   /* memset is unspecified for floats on exotic platforms (since
    * IEEE-754 zero is all-bits-zero on every architecture we
    * actually target this comment is paranoia, but cheap). */
   if (video_buf_acc_r || video_buf_acc_g || video_buf_acc_b)
   {
      size_t i;
      const size_t n = 256 * NUM_GAMEBOYS * VIDEO_HEIGHT;
      for (i = 0; i < n; i++)
      {
         if (video_buf_acc_r) video_buf_acc_r[i] = 0.0f;
         if (video_buf_acc_g) video_buf_acc_g[i] = 0.0f;
         if (video_buf_acc_b) video_buf_acc_b[i] = 0.0f;
      }
   }
}

static void check_frame_blend_variable(void)
{
   struct retro_variable var;
   enum frame_blend_method old_frame_blend_type = frame_blend_type;

   frame_blend_type = FRAME_BLEND_NONE;

   var.key = "gambatte_mix_frames";
   var.value = 0;

   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (!strcmp(var.value, "mix"))
         frame_blend_type = FRAME_BLEND_MIX;
      else if (!strcmp(var.value, "lcd_ghosting"))
         frame_blend_type = FRAME_BLEND_LCD_GHOSTING;
      else if (!strcmp(var.value, "lcd_ghosting_fast"))
         frame_blend_type = FRAME_BLEND_LCD_GHOSTING_FAST;
   }

   if (frame_blend_type == FRAME_BLEND_NONE)
      blend_frames = NULL;
   else if (frame_blend_type != old_frame_blend_type)
      init_frame_blending();
}

/***************************/
/* Interframe blending END */
/***************************/

/************************/
/* Rumble support START */
/************************/

static struct retro_rumble_interface rumble = {0};
static uint16_t rumble_strength_last        = 0;
static uint16_t rumble_strength_up          = 0;
static uint16_t rumble_strength_down        = 0;
static uint16_t rumble_level                = 0;
static bool rumble_active                   = false;

void cartridge_set_rumble(unsigned active)
{
   if (!rumble.set_rumble_state ||
       !rumble_level)
      return;

   if (active)
      rumble_strength_up++;
   else
      rumble_strength_down++;

   rumble_active = true;
}

static void apply_rumble(void)
{
   uint16_t strength;

   if (!rumble.set_rumble_state ||
       !rumble_level)
      return;

   strength = (rumble_strength_up > 0) ?
         (rumble_strength_up * rumble_level) /
               (rumble_strength_up + rumble_strength_down) : 0;

   rumble_strength_up   = 0;
   rumble_strength_down = 0;

   if (strength == rumble_strength_last)
      return;

   rumble.set_rumble_state(0, RETRO_RUMBLE_WEAK, strength);
   rumble.set_rumble_state(0, RETRO_RUMBLE_STRONG, strength);

   rumble_strength_last = strength;
}

static void deactivate_rumble(void)
{
   rumble_strength_up   = 0;
   rumble_strength_down = 0;
   rumble_active        = false;

   if (!rumble.set_rumble_state ||
       (rumble_strength_last == 0))
      return;

   rumble.set_rumble_state(0, RETRO_RUMBLE_WEAK, 0);
   rumble.set_rumble_state(0, RETRO_RUMBLE_STRONG, 0);

   rumble_strength_last = 0;
}

/**********************/
/* Rumble support END */
/**********************/

#ifdef HAVE_NETWORK
/* Core options 'update display' callback */
static bool update_option_visibility(void)
{
   struct retro_variable var = {0};
   bool updated              = false;
   unsigned i;

   /* If frontend supports core option categories,
    * then gambatte_show_gb_link_settings is ignored
    * and no options should be hidden */
   if (libretro_supports_option_categories)
      return false;

   var.key = "gambatte_show_gb_link_settings";
   var.value = NULL;

   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      bool show_gb_link_settings_prev = show_gb_link_settings;

      show_gb_link_settings = true;
      if (strcmp(var.value, "disabled") == 0)
         show_gb_link_settings = false;

      if (show_gb_link_settings != show_gb_link_settings_prev)
      {
         struct retro_core_option_display option_display;

         option_display.visible = show_gb_link_settings;

         option_display.key = "gambatte_gb_link_mode";
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);

         option_display.key = "gambatte_gb_link_network_port";
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);

         for (i = 0; i < 12; i++)
         {
            char key[64] = {0};

            /* Should be using std::to_string() here, but some
             * compilers don't support it... */
            sprintf(key, "%s%u",
                 "gambatte_gb_link_network_server_ip_", i + 1);

            option_display.key = key;

            environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         }

         updated = true;
      }
   }

   return updated;
}
#endif

/* Fast forward override */
static void set_fastforward_override(bool fastforward)
{
   struct retro_fastforwarding_override ff_override;

   if (!libretro_supports_ff_override)
      return;

   ff_override.ratio        = -1.0f;
   ff_override.notification = true;

   if (fastforward)
   {
      ff_override.fastforward    = true;
      ff_override.inhibit_toggle = true;
   }
   else
   {
      ff_override.fastforward    = false;
      ff_override.inhibit_toggle = false;
   }

   environ_cb(RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE, &ff_override);
}

static bool file_present_in_system(const char *fname)
{
   const char *system_dir = NULL;
   char full_path[PATH_MAX_LENGTH];

   full_path[0] = '\0';

   if (string_is_empty(fname))
      return false;

   /* Get system directory */
   if (!environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &system_dir) ||
       !system_dir)
   {
      gambatte_log(RETRO_LOG_WARN,
            "No system directory defined, unable to look for '%s'.\n", fname);
      return false;
   }

   fill_pathname_join(full_path, system_dir,
         fname, sizeof(full_path));

   return path_is_valid(full_path);
}

static bool get_bootloader_from_file(void* userdata, bool isgbc, uint8_t* data, uint32_t buf_size)
{
   const char *system_dir = NULL;
   const char *bios_name  = NULL;
   RFILE *bios_file       = NULL;
   int64_t bios_size      = 0;
   int64_t bytes_read     = 0;
   char bios_path[PATH_MAX_LENGTH];

   bios_path[0] = '\0';

   if (!use_official_bootloader)
      return false;

   /* Get system directory */
   if (!environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &system_dir) ||
       !system_dir)
   {
      gambatte_log(RETRO_LOG_WARN,
            "No system directory defined, unable to look for bootloader.\n");
      return false;
   }

   /* Get BIOS type */
   if (isgbc)
   {
      bios_name = "gbc_bios.bin";
      bios_size = 0x900;
   }
   else
   {
      bios_name = "gb_bios.bin";
      bios_size = 0x100;
   }

   if (bios_size > buf_size)
      return false;

   /* Get BIOS path */
   fill_pathname_join(bios_path, system_dir,
         bios_name, sizeof(bios_path));

   /* Read BIOS file */
   bios_file = filestream_open(bios_path,
         RETRO_VFS_FILE_ACCESS_READ,
         RETRO_VFS_FILE_ACCESS_HINT_NONE);

   if (!bios_file)
      return false;

   bytes_read = filestream_read(bios_file,
         data, bios_size);
   filestream_close(bios_file);

   if (bytes_read != bios_size)
      return false;

   gambatte_log(RETRO_LOG_INFO, "Read bootloader: %s\n", bios_path);

   return true;
}

namespace input
{
   struct map { unsigned snes; unsigned gb; };
   static const map btn_map[] = {
      { RETRO_DEVICE_ID_JOYPAD_A, gambatte::InputGetter::A },
      { RETRO_DEVICE_ID_JOYPAD_B, gambatte::InputGetter::B },
      { RETRO_DEVICE_ID_JOYPAD_SELECT, gambatte::InputGetter::SELECT },
      { RETRO_DEVICE_ID_JOYPAD_START, gambatte::InputGetter::START },
      { RETRO_DEVICE_ID_JOYPAD_RIGHT, gambatte::InputGetter::RIGHT },
      { RETRO_DEVICE_ID_JOYPAD_LEFT, gambatte::InputGetter::LEFT },
      { RETRO_DEVICE_ID_JOYPAD_UP, gambatte::InputGetter::UP },
      { RETRO_DEVICE_ID_JOYPAD_DOWN, gambatte::InputGetter::DOWN },
   };
}

static void update_input_state(void)
{
   unsigned i;
   unsigned res                = 0;
   bool turbo_a                = false;
   bool turbo_b                = false;
   bool palette_prev           = false;
   bool palette_next           = false;
   bool palette_switch_enabled = (libretro_supports_set_variable &&
         internal_palette_active);

   if (libretro_supports_bitmasks)
   {
      int16_t ret = input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_MASK);
      for (i = 0; i < sizeof(input::btn_map) / sizeof(input::map); i++)
         res |= (ret & (1 << input::btn_map[i].snes)) ? input::btn_map[i].gb : 0;

      libretro_ff_enabled = libretro_supports_ff_override &&
            (ret & (1 << RETRO_DEVICE_ID_JOYPAD_R2));

      turbo_a = (ret & (1 << RETRO_DEVICE_ID_JOYPAD_X));
      turbo_b = (ret & (1 << RETRO_DEVICE_ID_JOYPAD_Y));

      if (palette_switch_enabled)
      {
         palette_prev = (bool)(ret & (1 << RETRO_DEVICE_ID_JOYPAD_L));
         palette_next = (bool)(ret & (1 << RETRO_DEVICE_ID_JOYPAD_R));
      }
   }
   else
   {
      for (i = 0; i < sizeof(input::btn_map) / sizeof(input::map); i++)
         res |= input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, input::btn_map[i].snes) ? input::btn_map[i].gb : 0;

      libretro_ff_enabled = libretro_supports_ff_override &&
            input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2);

      turbo_a = input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X);
      turbo_b = input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y);

      if (palette_switch_enabled)
      {
         palette_prev = input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L);
         palette_next = input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R);
      }
   }

   if (!up_down_allowed)
   {
      if (res & gambatte::InputGetter::UP)
         if (res & gambatte::InputGetter::DOWN)
            res &= ~(gambatte::InputGetter::UP | gambatte::InputGetter::DOWN);

      if (res & gambatte::InputGetter::LEFT)
         if (res & gambatte::InputGetter::RIGHT)
            res &= ~(gambatte::InputGetter::LEFT | gambatte::InputGetter::RIGHT);
   }

   /* Handle fast forward button */
   if (libretro_ff_enabled != libretro_ff_enabled_prev)
   {
      set_fastforward_override(libretro_ff_enabled);
      libretro_ff_enabled_prev = libretro_ff_enabled;
   }

   /* Handle turbo buttons */
   if (turbo_a)
   {
      res |= (turbo_a_counter < turbo_pulse_width) ?
            gambatte::InputGetter::A : 0;

      turbo_a_counter++;
      if (turbo_a_counter >= turbo_period)
         turbo_a_counter = 0;
   }
   else
      turbo_a_counter = 0;

   if (turbo_b)
   {
      res |= (turbo_b_counter < turbo_pulse_width) ?
            gambatte::InputGetter::B : 0;

      turbo_b_counter++;
      if (turbo_b_counter >= turbo_period)
         turbo_b_counter = 0;
   }
   else
      turbo_b_counter = 0;

   /* Handle internal palette switching */
   if (palette_prev || palette_next)
   {
      if (palette_switch_counter == 0)
      {
         size_t palette_index = internal_palette_index;

         if (palette_prev)
         {
            if (palette_index > 0)
               palette_index--;
            else
               palette_index = NUM_PALETTES_TOTAL - 1;
         }
         else /* palette_next */
         {
            if (palette_index < NUM_PALETTES_TOTAL - 1)
               palette_index++;
            else
               palette_index = 0;
         }

         palette_switch_set_index(palette_index);
      }

      palette_switch_counter++;
      if (palette_switch_counter >= PALETTE_SWITCH_PERIOD)
         palette_switch_counter = 0;
   }
   else
      palette_switch_counter = 0;

   libretro_input_state = res;
}

/* gb_input is called multiple times per frame.
 * Determine input state once per frame using
 * update_input_state(), and simply return
 * cached value here */
class SNESInput : public gambatte::InputGetter
{
   public:
      unsigned operator()()
      {
         return libretro_input_state;
      }
} static gb_input;

#ifdef HAVE_NETWORK
enum SerialMode {
   SERIAL_NONE,
   SERIAL_SERVER,
   SERIAL_CLIENT
};
static NetSerial gb_net_serial;
static SerialMode gb_serialMode = SERIAL_NONE;
static int gb_NetworkPort = 12345;
static std::string gb_NetworkClientAddr;
#endif

void retro_get_system_info(struct retro_system_info *info)
{
   info->library_name = "Gambatte";
#ifndef GIT_VERSION
#define GIT_VERSION ""
#endif
#ifdef HAVE_NETWORK
   info->library_version = "v0.5.0-netlink" GIT_VERSION;
#else
   info->library_version = "v0.5.0" GIT_VERSION;
#endif
   info->need_fullpath = false;
   info->block_extract = false;
   info->valid_extensions = "gb|gbc|dmg";
}

void retro_get_system_av_info(struct retro_system_av_info *info)
{

   info->geometry.base_width   = VIDEO_WIDTH;
   info->geometry.base_height  = VIDEO_HEIGHT;
   info->geometry.max_width    = VIDEO_WIDTH;
   info->geometry.max_height   = VIDEO_HEIGHT;
   info->geometry.aspect_ratio = (float)GB_SCREEN_WIDTH / (float)VIDEO_HEIGHT;

   info->timing.fps            = VIDEO_REFRESH_RATE;
   info->timing.sample_rate    = use_cc_resampler ?
         SOUND_SAMPLE_RATE_CC : SOUND_SAMPLE_RATE_BLIPPER;
}

static void check_system_specs(void)
{
   unsigned level = 4;
   environ_cb(RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL, &level);
}

void retro_init(void)
{
   struct retro_log_callback log;

   if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log))
      gambatte_log_set_cb(log.log);
   else
      gambatte_log_set_cb(NULL);

   // Using uint_least32_t in an audio interface expecting you to cast to short*? :( Weird stuff.
   assert(sizeof(gambatte::uint_least32_t) == sizeof(uint32_t));
   gb.setInputGetter(&gb_input);
#ifdef DUAL_MODE
   gb2.setInputGetter(&gb_input);
#endif

#ifdef _3DS
   video_buf = (gambatte::video_pixel_t*)linearMemAlign(VIDEO_BUFF_SIZE, 128);
#else
   video_buf = (gambatte::video_pixel_t*)malloc(VIDEO_BUFF_SIZE);
#endif
   /* If allocation failed, retro_load_game will fail later when
    * gb.runFor is asked to render into a NULL buffer; we don't
    * abort retro_init since the frontend may still inspect the
    * core for system info. Zero out the buffer when we have it
    * so the very first frame is deterministic regardless of
    * whether the allocator returned dirty memory. */
   if (video_buf)
      memset(video_buf, 0, VIDEO_BUFF_SIZE);

   check_system_specs();
   
   //gb/gbc bootloader support
   gb.setBootloaderGetter(get_bootloader_from_file);
#ifdef DUAL_MODE
   gb2.setBootloaderGetter(get_bootloader_from_file);
#endif

   // Initialise internal palette maps
   initPaletteMaps();

   // Initialise palette switching functionality
   init_palette_switch();

   struct retro_variable var = {0};
   var.key = "gambatte_gb_bootloader";

   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (!strcmp(var.value, "enabled"))
         use_official_bootloader = true;
      else
         use_official_bootloader = false;
   }
   else
      use_official_bootloader = false;

   libretro_supports_bitmasks = false;
   if (environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL))
      libretro_supports_bitmasks = true;

   libretro_supports_ff_override = false;
   if (environ_cb(RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE, NULL))
      libretro_supports_ff_override = true;
}

void retro_deinit(void)
{
#ifdef _3DS
   linearFree(video_buf);
#else
   free(video_buf);
#endif
   video_buf = NULL;
   deinit_frame_blending();
   audio_resampler_deinit();

   freePaletteMaps();
   deinit_palette_switch();

   if (libretro_ff_enabled)
      set_fastforward_override(false);

   libretro_supports_option_categories = false;
   libretro_supports_bitmasks          = false;
   libretro_supports_ff_override       = false;
   libretro_ff_enabled                 = false;
   libretro_ff_enabled_prev            = false;

   libretro_input_state = 0;
   up_down_allowed      = false;
   turbo_period         = TURBO_PERIOD_MIN;
   turbo_pulse_width    = TURBO_PULSE_WIDTH_MIN;
   turbo_a_counter      = 0;
   turbo_b_counter      = 0;

   deactivate_rumble();
   memset(&rumble, 0, sizeof(struct retro_rumble_interface));
   rumble_level = 0;
}

void retro_set_environment(retro_environment_t cb)
{
   struct retro_vfs_interface_info vfs_iface_info;
   bool option_categories = false;
   environ_cb = cb;

   /* Set core options
    * An annoyance: retro_set_environment() can be called
    * multiple times, and depending upon the current frontend
    * state various environment callbacks may be disabled.
    * This means the reported 'categories_supported' status
    * may change on subsequent iterations. We therefore have
    * to record whether 'categories_supported' is true on any
    * iteration, and latch the result */
   libretro_set_core_options(environ_cb, &option_categories);
   libretro_supports_option_categories |= option_categories;

#ifdef HAVE_NETWORK
   /* If frontend supports core option categories,
    * gambatte_show_gb_link_settings is unused and
    * should be hidden */
   if (libretro_supports_option_categories)
   {
      struct retro_core_option_display option_display;

      option_display.visible = false;
      option_display.key     = "gambatte_show_gb_link_settings";

      environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY,
            &option_display);
   }
   /* If frontend does not support core option
    * categories, core options may be shown/hidden
    * at runtime. In this case, register 'update
    * display' callback, so frontend can update
    * core options menu without calling retro_run() */
   else
   {
      struct retro_core_options_update_display_callback update_display_cb;
      update_display_cb.callback = update_option_visibility;

      environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK,
            &update_display_cb);
   }
#endif

   vfs_iface_info.required_interface_version = 2;
   vfs_iface_info.iface                      = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &vfs_iface_info))
	   filestream_vfs_init(&vfs_iface_info);
}

void retro_set_video_refresh(retro_video_refresh_t cb) { video_cb = cb; }
void retro_set_audio_sample(retro_audio_sample_t) { }
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { audio_batch_cb = cb; }
void retro_set_input_poll(retro_input_poll_t cb) { input_poll_cb = cb; }
void retro_set_input_state(retro_input_state_t cb) { input_state_cb = cb; }

void retro_set_controller_port_device(unsigned, unsigned) {}

void retro_reset()
{
   /* gb.reset() now preserves battery-backed SRAM and RTC
    * automatically (matching real Game Boy behavior), so the
    * old new[]/memcpy/delete[] dance is no longer needed. */
   gb.reset();
#ifdef DUAL_MODE
   gb2.reset();
#endif

   /* A reset is not a state load, but the same per-session
    * counters that need clearing on retro_unserialize need
    * clearing here too (frame-pacing ratio is invalid after a
    * reset; blender/resampler history would otherwise carry
    * pre-reset trails). */
   frame_pacing_reset();
   reset_frame_blending_buffers();
   if (resampler_l)
      blipper_reset(resampler_l);
   if (resampler_r)
      blipper_reset(resampler_r);
   if (use_cc_resampler)
      CC_init();
   audio_out_buffer_pos = 0;
}

/* Cached serialize size. The state size is invariant for a given
 * loaded ROM (the savestate format encodes a fixed set of fields),
 * so caching it avoids reencoding the entire state through a
 * counting omemstream every time the frontend asks for the size.
 * Frontends with runahead or rewind enabled call
 * retro_serialize_size once per frame; that previously meant one
 * full state-encode of work per frame in addition to the actual
 * save. Invalidated (set to 0) on retro_load_game,
 * retro_unload_game, and retro_reset so the next call recomputes. */
static size_t serialize_size = 0;

static INLINE void invalidate_serialize_size(void)
{
   serialize_size = 0;
}

size_t retro_serialize_size(void)
{
   if (serialize_size == 0)
      serialize_size = gb.stateSize();
   return serialize_size;
}

bool retro_serialize(void *data, size_t size)
{
   if (size != retro_serialize_size())
      return false;

   gb.saveState(data);
   return true;
}

bool retro_unserialize(const void *data, size_t size)
{
   if (size != retro_serialize_size())
      return false;

   /* gb.loadState now bounds-checks against the buffer size
    * and returns false on malformed input. Treat a parser
    * failure as an unserialize failure so the frontend can
    * surface it instead of leaving the core in a half-loaded
    * state. */
   if (!gb.loadState(data, size))
      return false;

   /* The audio resampler internal state (blipper integrator,
    * CC accumulator/highpass) and the frame-blending history
    * buffers are NOT part of the savestate. If we leave them
    * carrying state from before the load, audio pops and
    * ghost-frame bleed across the load boundary -- both visible
    * during runahead/rewind/netplay rollbacks. Reset all of
    * them here so the state load is deterministic. */
   if (resampler_l)
      blipper_reset(resampler_l);
   if (resampler_r)
      blipper_reset(resampler_r);
   if (use_cc_resampler)
      CC_init();
   audio_out_buffer_pos = 0;

   /* Frame-pacing counters track audio-vs-video sample totals
    * over the lifetime of the running session; the loaded state
    * carries no such ratio so reset them too. */
   frame_pacing_reset();

   /* Zero out frame-blend history so ghost trails from the
    * pre-load timeline don't bleed into the loaded state. */
   reset_frame_blending_buffers();

   return true;
}

/* Cheats are tracked per-index so we can honor the `enabled` flag
 * and the `index` slot semantics from the libretro API. The
 * underlying gambatte::GB only has clearCheats/setGameGenie/
 * setGameShark, so each retro_cheat_set call clears all cheats
 * and re-applies the currently-enabled ones. This is O(n) per
 * call but n is small (typically <16) and frontends only call
 * it interactively when the user toggles a cheat. */
struct LibretroCheat {
   std::string code;
   bool enabled;
   bool used;
};
static std::vector<LibretroCheat> libretro_cheats;

static void apply_libretro_cheats(void)
{
   gb.clearCheats();
   for (size_t i = 0; i < libretro_cheats.size(); i++) {
      if (!libretro_cheats[i].used || !libretro_cheats[i].enabled)
         continue;
      const std::string &c = libretro_cheats[i].code;
      if (c.find('-') != std::string::npos)
         gb.setGameGenie(c);
      else
         gb.setGameShark(c);
   }
}

void retro_cheat_reset()
{
   libretro_cheats.clear();
   gb.clearCheats();
}

void retro_cheat_set(unsigned index, bool enabled, const char *code)
{
   std::string code_str(code ? code : "");
   replace(code_str.begin(), code_str.end(), '+', ';');

   if (libretro_cheats.size() <= index)
      libretro_cheats.resize(index + 1);

   libretro_cheats[index].code    = code_str;
   libretro_cheats[index].enabled = enabled;
   libretro_cheats[index].used    = true;

   apply_libretro_cheats();
}

enum gb_colorization_enable_type
{
   GB_COLORIZATION_DISABLED = 0,
   GB_COLORIZATION_AUTO     = 1,
   GB_COLORIZATION_CUSTOM   = 2,
   GB_COLORIZATION_INTERNAL = 3,
   GB_COLORIZATION_GBC      = 4,
   GB_COLORIZATION_SGB      = 5
};

static enum gb_colorization_enable_type gb_colorization_enable = GB_COLORIZATION_DISABLED;

static std::string rom_path;
static char internal_game_name[17];

static void load_custom_palette(void)
{
   const char *system_dir = NULL;
   const char *rom_file   = NULL;
   char *rom_name         = NULL;
   RFILE *palette_file    = NULL;
   unsigned line_index    = 0;
   bool path_valid        = false;
   unsigned rgb32         = 0;
   char palette_path[PATH_MAX_LENGTH];

   palette_path[0] = '\0';

   /* Get system directory */
   if (!environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &system_dir) ||
       !system_dir)
   {
      gambatte_log(RETRO_LOG_WARN,
            "No system directory defined, unable to look for custom palettes.\n");
      return;
   }

   /* Look for palette named after ROM file */
   rom_file = path_basename(rom_path.c_str());
   if (!string_is_empty(rom_file))
   {
      size_t len = (strlen(rom_file) + 1) * sizeof(char);
      rom_name = (char*)malloc(len);
      if (rom_name)
      {
         strlcpy(rom_name, rom_file, len);
         path_remove_extension(rom_name);
         if (!string_is_empty(rom_name))
         {
            fill_pathname_join_special_ext(palette_path,
                  system_dir, "palettes", rom_name, ".pal",
                  sizeof(palette_path));
            path_valid = path_is_valid(palette_path);
         }
         free(rom_name);
         rom_name = NULL;
      }
   }

   if (!path_valid)
   {
      /* Look for palette named after the internal game
       * name in the ROM header */
      fill_pathname_join_special_ext(palette_path,
            system_dir, "palettes", internal_game_name, ".pal",
            sizeof(palette_path));
      path_valid = path_is_valid(palette_path);
   }

   if (!path_valid)
   {
      /* Look for default custom palette file (default.pal) */
      fill_pathname_join_special_ext(palette_path,
            system_dir, "palettes", "default", ".pal",
            sizeof(palette_path));
      path_valid = path_is_valid(palette_path);
   }

   if (!path_valid)
      return; /* Unable to find any custom palette file */

   palette_file = filestream_open(palette_path,
         RETRO_VFS_FILE_ACCESS_READ,
         RETRO_VFS_FILE_ACCESS_HINT_NONE);

   if (!palette_file)
   {
      gambatte_log(RETRO_LOG_WARN,
            "Failed to open custom palette: %s\n", palette_path);
      return;
   }

   gambatte_log(RETRO_LOG_INFO, "Using custom palette: %s\n", palette_path);

   /* Iterate over palette file lines */
   while (!filestream_eof(palette_file))
   {
      char *line            = filestream_getline(palette_file);
      const char *value_str = NULL;

      if (!line)
         break;

      /* Remove any leading/trailing whitespace
       * > Additionally handles 'leftovers' from
       *   CRLF line terminators if palette file
       *   happens to be in DOS format */
      string_trim_whitespace(line);

      if (string_is_empty(line) || /* Skip empty lines */
          (*line == '[') ||        /* Skip ini sections */
          (*line == ';'))          /* Skip ini comments */
         goto palette_line_end;

      /* Supposed to be a typo here... */
      if (string_starts_with(line, "slectedScheme="))
         goto palette_line_end;

      /* Get substring after first '=' character */
      value_str = strchr(line, '=');
      if (!value_str ||
          string_is_empty(++value_str))
      {
         gambatte_log(RETRO_LOG_WARN,
               "Error in %s, line %u (color left as default)\n",
               palette_path, line_index);
         goto palette_line_end;
      }

      /* Extract colour value */
      rgb32 = string_to_unsigned(value_str);
      if (rgb32 == 0)
      {
         /* string_to_unsigned() will return 0 if
          * string is invalid, so perform a manual
          * validity check... */
         for (; *value_str != '\0'; value_str++)
         {
            if (*value_str != '0')
            {
               gambatte_log(RETRO_LOG_WARN,
                     "Unable to read palette color in %s, line %u (color left as default)\n",
                     palette_path, line_index);
               goto palette_line_end;
            }
         }
      }

#ifdef VIDEO_RGB565
      rgb32 = (rgb32 & 0x0000F8) >>  3 | /* blue */
              (rgb32 & 0x00FC00) >>  5 | /* green */
              (rgb32 & 0xF80000) >>  8;  /* red */
#elif defined(VIDEO_ABGR1555)
      rgb32 = (rgb32 & 0x0000F8) <<  7 | /* blue */
              (rgb32 & 0xF800)   >>  6 | /* green */
              (rgb32 & 0xF80000) >> 19;  /* red */
#endif

      if (     string_starts_with(line, "Background0="))
         gb.setDmgPaletteColor(0, 0, rgb32);
      else if (string_starts_with(line, "Background1="))
         gb.setDmgPaletteColor(0, 1, rgb32);
      else if (string_starts_with(line, "Background2="))
         gb.setDmgPaletteColor(0, 2, rgb32);       
      else if (string_starts_with(line, "Background3="))
         gb.setDmgPaletteColor(0, 3, rgb32);
      else if (string_starts_with(line, "Sprite%2010="))
         gb.setDmgPaletteColor(1, 0, rgb32);
      else if (string_starts_with(line, "Sprite%2011="))
         gb.setDmgPaletteColor(1, 1, rgb32);
      else if (string_starts_with(line, "Sprite%2012="))
         gb.setDmgPaletteColor(1, 2, rgb32);
      else if (string_starts_with(line, "Sprite%2013="))
         gb.setDmgPaletteColor(1, 3, rgb32);
      else if (string_starts_with(line, "Sprite%2020="))
         gb.setDmgPaletteColor(2, 0, rgb32);
      else if (string_starts_with(line, "Sprite%2021="))
         gb.setDmgPaletteColor(2, 1, rgb32);
      else if (string_starts_with(line, "Sprite%2022="))
         gb.setDmgPaletteColor(2, 2, rgb32);  
      else if (string_starts_with(line, "Sprite%2023="))
         gb.setDmgPaletteColor(2, 3, rgb32);
      else
         gambatte_log(RETRO_LOG_WARN,
               "Error in %s, line %u (color left as default)\n",
               palette_path, line_index);

palette_line_end:
      line_index++;
      free(line);
      line = NULL;
   }

   filestream_close(palette_file);
}

static void find_internal_palette(const unsigned short **palette, bool *is_gbc)
{
   const char *palette_title = NULL;
   size_t index              = 0;
   struct retro_variable var = {0};

   // Read main internal palette setting
   var.key   = "gambatte_gb_internal_palette";
   var.value = NULL;

   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      // Handle TWB64 packs
      if (string_is_equal(var.value, "TWB64 - Pack 1"))
      {
         var.key   = "gambatte_gb_palette_twb64_1";
         var.value = NULL;

         if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
            palette_title = var.value;

         // Determine 'consolidated' palette index
         if (palette_title)
            index = RHMAP_GET_STR(palettes_twb64_1_index_map, palette_title);
         if (index > 0)
            index--;
         internal_palette_index = NUM_PALETTES_DEFAULT + index;
      }
      else if (string_is_equal(var.value, "TWB64 - Pack 2"))
      {
         var.key   = "gambatte_gb_palette_twb64_2";
         var.value = NULL;

         if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
            palette_title = var.value;

         // Determine 'consolidated' palette index
         if (palette_title)
            index = RHMAP_GET_STR(palettes_twb64_2_index_map, palette_title);
         if (index > 0)
            index--;
         internal_palette_index = NUM_PALETTES_DEFAULT +
               NUM_PALETTES_TWB64_1 + index;
      }
      else if (string_is_equal(var.value, "TWB64 - Pack 3"))
      {
         var.key   = "gambatte_gb_palette_twb64_3";
         var.value = NULL;

         if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
            palette_title = var.value;

         // Determine 'consolidated' palette index
         if (palette_title)
            index = RHMAP_GET_STR(palettes_twb64_3_index_map, palette_title);
         if (index > 0)
            index--;
         internal_palette_index = NUM_PALETTES_DEFAULT +
               NUM_PALETTES_TWB64_1 + NUM_PALETTES_TWB64_2 + index;
      }
      // Handle PixelShift packs
      else if (string_is_equal(var.value, "PixelShift - Pack 1"))
      {
         var.key   = "gambatte_gb_palette_pixelshift_1";
         var.value = NULL;

         if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
            palette_title = var.value;

         // Determine 'consolidated' palette index
         if (palette_title)
            index = RHMAP_GET_STR(palettes_pixelshift_1_index_map, palette_title);
         if (index > 0)
            index--;
         internal_palette_index = NUM_PALETTES_DEFAULT + NUM_PALETTES_TWB64_1 +
               NUM_PALETTES_TWB64_2 + NUM_PALETTES_TWB64_3 + index;
      }
      else
      {
         palette_title = var.value;

         // Determine 'consolidated' palette index
         index = RHMAP_GET_STR(palettes_default_index_map, palette_title);
         if (index > 0)
            index--;
         internal_palette_index = index;
      }
   }

   // Ensure we have a valid palette title
   if (!palette_title)
   {
      palette_title          = "GBC - Grayscale";
      internal_palette_index = 8;
   }

   // Search for requested palette
   *palette = findGbcDirPal(palette_title);

   // If palette is not found (i.e. if a palette
   // is removed from the core, and a user loads
   // old core options settings), fall back to
   // black and white
   if (!(*palette))
   {
      palette_title          = "GBC - Grayscale";
      *palette               = findGbcDirPal(palette_title);
      internal_palette_index = 8;
      // No error check here - if this fails,
      // the core is entirely broken...
   }

   // Check whether this is a GBC palette
   if (!strncmp("GBC", palette_title, 3))
      *is_gbc = true;
   else
      *is_gbc = false;

   // Record that an internal palette is
   // currently in use
   internal_palette_active = true;
}

static void check_variables(bool startup)
{
   unsigned i, j;
   unsigned colorCorrectionMode    = 0;
   unsigned colorCorrection        = 0;
   struct retro_variable var       = {0};
   float colorCorrectionBrightness = 0.5f; /* central */
   unsigned darkFilterLevel        = 0;

   var.key = "gambatte_gbc_color_correction";
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (!strcmp(var.value, "GBC only"))
         colorCorrection = 1;
      else if (!strcmp(var.value, "always"))
         colorCorrection = 2;
   }
   
   var.key   = "gambatte_gbc_color_correction_mode";
   var.value = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && !strcmp(var.value, "fast"))
      colorCorrectionMode = 1;
   gb.setColorCorrectionMode(colorCorrectionMode);
   
   var.key   = "gambatte_gbc_frontlight_position";
   var.value = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (!strcmp(var.value, "above screen"))
         colorCorrectionBrightness = 1.0f;
      else if (!strcmp(var.value, "below screen"))
         colorCorrectionBrightness = 0.0f;
   }
   gb.setColorCorrectionBrightness(colorCorrectionBrightness);
   
   var.key   = "gambatte_dark_filter_level";
   var.value = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
      darkFilterLevel = static_cast<unsigned>(atoi(var.value));
   gb.setDarkFilterLevel(darkFilterLevel);

   bool old_use_cc_resampler = use_cc_resampler;
   use_cc_resampler          = false;
   var.key                   = "gambatte_audio_resampler";
   var.value                 = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) &&
       var.value && !strcmp(var.value, "cc"))
      use_cc_resampler = true;

   if (!startup && (use_cc_resampler != old_use_cc_resampler))
   {
      struct retro_system_av_info av_info;
      audio_resampler_deinit();
      audio_resampler_init(false);
      retro_get_system_av_info(&av_info);
      environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &av_info);
   }

   up_down_allowed = false;
   var.key         = "gambatte_up_down_allowed";
   var.value       = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (!strcmp(var.value, "enabled"))
         up_down_allowed = true;
      else
         up_down_allowed = false;
   }

   turbo_period      = TURBO_PERIOD_MIN;
   turbo_pulse_width = TURBO_PULSE_WIDTH_MIN;
   var.key           = "gambatte_turbo_period";
   var.value         = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      turbo_period = atoi(var.value);
      turbo_period = (turbo_period < TURBO_PERIOD_MIN) ?
            TURBO_PERIOD_MIN : turbo_period;
      turbo_period = (turbo_period > TURBO_PERIOD_MAX) ?
            TURBO_PERIOD_MAX : turbo_period;

      turbo_pulse_width = turbo_period >> 1;
      turbo_pulse_width = (turbo_pulse_width < TURBO_PULSE_WIDTH_MIN) ?
            TURBO_PULSE_WIDTH_MIN : turbo_pulse_width;
      turbo_pulse_width = (turbo_pulse_width > TURBO_PULSE_WIDTH_MAX) ?
            TURBO_PULSE_WIDTH_MAX : turbo_pulse_width;

      turbo_a_counter = 0;
      turbo_b_counter = 0;
   }

   rumble_level = 0;
   var.key      = "gambatte_rumble_level";
   var.value    = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      rumble_level = atoi(var.value);
      rumble_level = (rumble_level > 10) ? 10 : rumble_level;
      rumble_level = (rumble_level > 0)  ? ((0x1999 * rumble_level) + 0x5) : 0;
   }
   if (rumble_level == 0)
      deactivate_rumble();

   /* Interframe blending option has its own handler */
   check_frame_blend_variable();

#ifdef HAVE_NETWORK
   gb_serialMode = SERIAL_NONE;
   var.key       = "gambatte_gb_link_mode";
   var.value     = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (!strcmp(var.value, "Network Server"))
         gb_serialMode = SERIAL_SERVER;
      else if (!strcmp(var.value, "Network Client"))
         gb_serialMode = SERIAL_CLIENT;
   }

   var.key = "gambatte_gb_link_network_port";
   var.value = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) {
      gb_NetworkPort=atoi(var.value);
   }

   unsigned ip_index = 1;
   gb_NetworkClientAddr = "";

   for (i = 0; i < 4; i++)
   {
      std::string octet = "0";
      char tmp[8] = {0};

      for (j = 0; j < 3; j++)
      {
         char key[64] = {0};

         /* Should be using std::to_string() here, but some
          * compilers don't support it... */
         sprintf(key, "%s%u",
              "gambatte_gb_link_network_server_ip_", ip_index);

         var.key = key;
         var.value = NULL;

         if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
            octet += std::string(var.value);

         ip_index++;
      }

      /* Remove leading zeros
       * Should be using std::stoul() here, but some compilers
       * don't support it... */
      sprintf(tmp, "%u", atoi(octet.c_str()));
      octet = std::string(tmp);

      if (i < 3)
         octet += ".";

      gb_NetworkClientAddr += octet;
   }

   switch(gb_serialMode)
   {
      case SERIAL_SERVER:
         gb_net_serial.start(true, gb_NetworkPort, gb_NetworkClientAddr);
         gb.setSerialIO(&gb_net_serial);
         break;
      case SERIAL_CLIENT:
         gb_net_serial.start(false, gb_NetworkPort, gb_NetworkClientAddr);
         gb.setSerialIO(&gb_net_serial);
         break;
      default:
         gb_net_serial.stop();
         gb.setSerialIO(NULL);
         break;
   }

   /* Show/hide core options */
   update_option_visibility();
#endif

   internal_palette_active = false;
   var.key = "gambatte_gb_colorization";

   if (!environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || !var.value) {
      // Should really wait until the end to call setColorCorrection(),
      // but don't want to have to change the indentation of all the
      // following code... (makes it too difficult to see the changes in
      // a git diff...)
      gb.setColorCorrection(gb.isCgb() && (colorCorrection != 0));
      return;
   }
   
   if (gb.isCgb()) {
      gb.setColorCorrection(colorCorrection != 0);
      return;
   }

   // else it is a GB-mono game -> set a color palette

   if (strcmp(var.value, "disabled") == 0)
      gb_colorization_enable = GB_COLORIZATION_DISABLED;
   else if (strcmp(var.value, "auto") == 0)
      gb_colorization_enable = GB_COLORIZATION_AUTO;
   else if (strcmp(var.value, "custom") == 0)
      gb_colorization_enable = GB_COLORIZATION_CUSTOM;
   else if (strcmp(var.value, "internal") == 0)
      gb_colorization_enable = GB_COLORIZATION_INTERNAL;
   else if (strcmp(var.value, "GBC") == 0)
      gb_colorization_enable = GB_COLORIZATION_GBC;
   else if (strcmp(var.value, "SGB") == 0)
      gb_colorization_enable = GB_COLORIZATION_SGB;

   // Containers for GBC/SGB BIOS built-in palettes
   const unsigned short *gbc_bios_palette = NULL;
   const unsigned short *sgb_bios_palette = NULL;
   bool isGbcPalette = false;

   switch (gb_colorization_enable)
   {
      case GB_COLORIZATION_AUTO:
         // Automatic colourisation
         // Order of preference:
         // 1 - SGB, if more colourful than GBC
         // 2 - GBC, if more colourful than SGB
         // 3 - SGB, if no GBC palette defined
         // 4 - User-defined internal palette, if neither GBC nor SGB palettes defined
         //
         // Load GBC BIOS built-in palette
         gbc_bios_palette = findGbcTitlePal(internal_game_name);
         // Load SGB BIOS built-in palette
         sgb_bios_palette = findSgbTitlePal(internal_game_name);
         // If both GBC and SGB palettes are defined,
         // use whichever is more colourful
         if (gbc_bios_palette)
         {
            isGbcPalette = true;
            if (sgb_bios_palette)
            {
               if (gbc_bios_palette != p005 &&
                   gbc_bios_palette != p006 &&
                   gbc_bios_palette != p007 &&
                   gbc_bios_palette != p008 &&
                   gbc_bios_palette != p012 &&
                   gbc_bios_palette != p013 &&
                   gbc_bios_palette != p016 &&
                   gbc_bios_palette != p017 &&
                   gbc_bios_palette != p01B)
               {
                  // Limited color GBC palette -> use SGB equivalent
                  gbc_bios_palette = sgb_bios_palette;
                  isGbcPalette = false;
               }
            }
         }
         // If no GBC palette is defined, use SGB palette
         if (!gbc_bios_palette)
         {
            gbc_bios_palette = sgb_bios_palette;
         }
         // If neither GBC nor SGB palettes are defined, set
         // user-defined internal palette
         if (!gbc_bios_palette)
            find_internal_palette(&gbc_bios_palette, &isGbcPalette);
         break;
      case GB_COLORIZATION_CUSTOM:
         load_custom_palette();
         break;
      case GB_COLORIZATION_INTERNAL:
         find_internal_palette(&gbc_bios_palette, &isGbcPalette);
         break;
      case GB_COLORIZATION_GBC:
         // Force GBC colourisation
         gbc_bios_palette = findGbcTitlePal(internal_game_name);
         if (!gbc_bios_palette)
            gbc_bios_palette = findGbcDirPal("GBC - Dark Green"); // GBC Default
         isGbcPalette = true;
         break;
      case GB_COLORIZATION_SGB:
         // Force SGB colourisation
         gbc_bios_palette = findSgbTitlePal(internal_game_name);
         if (!gbc_bios_palette)
            gbc_bios_palette = findGbcDirPal("SGB - 1A"); // SGB Default
         break;
      default: // GB_COLORIZATION_DISABLED
         gbc_bios_palette = findGbcDirPal("GBC - Grayscale");
         break;
   }
   
   // Enable colour correction, if required
   gb.setColorCorrection((colorCorrection == 2) || ((colorCorrection == 1) && isGbcPalette));
   
   // If gambatte is using custom colourisation
   // then we have already loaded the palette.
   // In this case we can therefore skip this loop.
   if (gb_colorization_enable != GB_COLORIZATION_CUSTOM)
   {
      /* Defensive: every code path above is supposed to either
       * leave the function (CUSTOM) or fill gbc_bios_palette via
       * a hardcoded fallback like findGbcDirPal("GBC - Grayscale").
       * If a future change breaks that invariant, fail soft
       * rather than dereference a NULL pointer. */
      if (!gbc_bios_palette)
         return;

      unsigned rgb32 = 0;
      for (unsigned palnum = 0; palnum < 3; ++palnum)
      {
         for (unsigned colornum = 0; colornum < 4; ++colornum)
         {
            rgb32 = gb.gbcToRgb32(gbc_bios_palette[palnum * 4 + colornum]);
            gb.setDmgPaletteColor(palnum, colornum, rgb32);
         }
      }
   }
}

bool retro_load_game(const struct retro_game_info *info)
{
   bool can_dupe = false;

   /* Defensive validation. retro_load_game previously read 16
    * header bytes at info->data + 0x134 with no size check; that
    * read happens to be safe today by transitivity through
    * Cartridge::loadROM's romsize >= 0x4000 check, but that is a
    * non-obvious invariant. Validate explicitly so a future
    * change to loadROM cannot reintroduce the OOB read. */
   if (!info || !info->data || info->size < 0x150)
   {
      gambatte_log(RETRO_LOG_ERROR, "ROM is missing or too small.\n");
      return false;
   }

   /* Reset per-session state that does not belong to the
    * cartridge or the gambatte core proper but does affect
    * libretro-visible behavior. Frontends may call
    * retro_load_game multiple times against the same loaded
    * library, so anything that persisted from a previous game
    * must be cleared here. */
   frame_pacing_reset();
   invalidate_serialize_size();
   reset_frame_blending_buffers();

   environ_cb(RETRO_ENVIRONMENT_GET_CAN_DUPE, &can_dupe);
   if (!can_dupe)
   {
      gambatte_log(RETRO_LOG_ERROR, "Cannot dupe frames!\n");
      return false;
   }

   if (environ_cb(RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE, &rumble))
      gambatte_log(RETRO_LOG_INFO, "Rumble environment supported.\n");
   else
      gambatte_log(RETRO_LOG_INFO, "Rumble environment not supported.\n");

   struct retro_input_descriptor desc[] = {
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT,   "D-Pad Left" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP,     "D-Pad Up" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN,   "D-Pad Down" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT,  "D-Pad Right" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B,      "B" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A,      "A" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y,      "Turbo B" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X,      "Turbo A" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START,  "Start" },
      { 0 },
   };

   struct retro_input_descriptor desc_ff[] = { /* ff: fast forward */
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT,   "D-Pad Left" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP,     "D-Pad Up" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN,   "D-Pad Down" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT,  "D-Pad Right" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B,      "B" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A,      "A" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y,      "Turbo B" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X,      "Turbo A" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START,  "Start" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2,     "Fast Forward" },
      { 0 },
   };

   struct retro_input_descriptor desc_ps[] = { /* ps: palette switching */
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT,   "D-Pad Left" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP,     "D-Pad Up" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN,   "D-Pad Down" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT,  "D-Pad Right" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B,      "B" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A,      "A" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y,      "Turbo B" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X,      "Turbo A" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START,  "Start" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L,      "Prev. Internal Palette" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R,      "Next Internal Palette" },
      { 0 },
   };

   struct retro_input_descriptor desc_ff_ps[] = { /* ff: fast forward, ps: palette switching */
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT,   "D-Pad Left" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP,     "D-Pad Up" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN,   "D-Pad Down" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT,  "D-Pad Right" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B,      "B" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A,      "A" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y,      "Turbo B" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X,      "Turbo A" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START,  "Start" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L,      "Prev. Internal Palette" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R,      "Next Internal Palette" },
      { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2,     "Fast Forward" },
      { 0 },
   };

   if (libretro_supports_ff_override)
   {
      if (libretro_supports_set_variable)
         environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc_ff_ps);
      else
         environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc_ff);
   }
   else
   {
      if (libretro_supports_set_variable)
         environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc_ps);
      else
         environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc);
   }

#if defined(VIDEO_RGB565) || defined(VIDEO_ABGR1555)
   enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_RGB565;
   if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt))
   {
      gambatte_log(RETRO_LOG_ERROR, "RGB565 is not supported.\n");
      return false;
   }
#else
   enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888;
   if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt))
   {
      gambatte_log(RETRO_LOG_ERROR, "XRGB8888 is not supported.\n");
      return false;
   }
#endif
   
   bool has_gbc_bootloader = file_present_in_system("gbc_bios.bin");

   unsigned flags = 0;
   struct retro_variable var = {0};
   var.key = "gambatte_gb_hwmode";
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (!strcmp(var.value, "GB"))
      {
          flags |= gambatte::GB::FORCE_DMG;
      }
      
      if (!strcmp(var.value, "GBC"))
      {
         if (has_gbc_bootloader && use_official_bootloader)
            flags |= gambatte::GB::FORCE_CGB;
      }

      if (!strcmp(var.value, "GBA"))
      {
         flags |= gambatte::GB::GBA_CGB;
         if (has_gbc_bootloader && use_official_bootloader)
            flags |= gambatte::GB::FORCE_CGB;
      }
   }

   if (gb.load(info->data, info->size, flags) != 0)
      return false;
#ifdef DUAL_MODE
   if (gb2.load(info->data, info->size, flags) != 0)
      return false;
#endif

   rom_path = info->path ? info->path : "";
   strncpy(internal_game_name, (const char*)info->data + 0x134, sizeof(internal_game_name) - 1);
   internal_game_name[sizeof(internal_game_name)-1]='\0';

   gambatte_log(RETRO_LOG_INFO, "Got internal game name: %s.\n", internal_game_name);

   check_variables(true);
   audio_resampler_init(true);

   unsigned sramlen       = gb.savedata_size();
   const uint64_t rom     = RETRO_MEMDESC_CONST;
   const uint64_t mainram = RETRO_MEMDESC_SYSTEM_RAM;
   struct retro_memory_map mmaps;

   struct retro_memory_descriptor descs[10] =
   {
      { mainram, gb.rambank0_ptr(),     0, 0xC000,          0, 0, 0x1000, NULL },
      { mainram, gb.rambank1_ptr(),     0, 0xD000,          0, 0, 0x1000, NULL },
      { mainram, gb.zeropage_ptr(),     0, 0xFF80,          0, 0, 0x0080, NULL },
      {       0, gb.vram_ptr(),         0, 0x8000,          0, 0, 0x2000, NULL },
      {       0, gb.oamram_ptr(),       0, 0xFE00, 0xFFFFFFE0, 0, 0x00A0, NULL },
      {     rom, gb.rombank0_ptr(),     0, 0x0000,          0, 0, 0x4000, NULL },
      {     rom, gb.rombank1_ptr(),     0, 0x4000,          0, 0, 0x4000, NULL },
      {       0, gb.oamram_ptr(),   0x100, 0xFF00,          0, 0, 0x0080, NULL },
      {       0, 0,                     0,      0,          0, 0,      0,    0 },
      {       0, 0,                     0,      0,          0, 0,      0,    0 }
   };

   unsigned i = 8;
   if (sramlen)
   {
      descs[i].ptr    = gb.savedata_ptr();
      descs[i].start  = 0xA000;
      descs[i].select = (size_t)~0x1FFF;
      descs[i].len    = sramlen;
      i++;
   }

   if (gb.isCgb())
   {
      descs[i].flags  = mainram;
      descs[i].ptr    = gb.rambank2_ptr();
      descs[i].start  = 0x10000;
      descs[i].select = 0xFFFFA000;
      descs[i].len    = 0x6000;
      i++;
   }

   mmaps.descriptors     = descs;
   mmaps.num_descriptors = i;   
   environ_cb(RETRO_ENVIRONMENT_SET_MEMORY_MAPS, &mmaps);
   
   bool yes = true;
   environ_cb(RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS, &yes);

   rom_loaded = true;
   return true;
}


bool retro_load_game_special(unsigned, const struct retro_game_info*, size_t) { return false; }

void retro_unload_game()
{
   rom_loaded = false;
   /* Clear per-game state so a subsequent retro_load_game with
    * a different ROM doesn't see leftovers (palette autodetect
    * keying off internal_game_name, frame-pacing ratio, cached
    * state size, residual cheat slots). */
   frame_pacing_reset();
   invalidate_serialize_size();
   reset_frame_blending_buffers();
   libretro_cheats.clear();
   memset(internal_game_name, 0, sizeof(internal_game_name));
   rom_path.clear();
}

unsigned retro_get_region() { return RETRO_REGION_NTSC; }

void *retro_get_memory_data(unsigned id)
{
   if (rom_loaded) switch (id)
   {
      case RETRO_MEMORY_SAVE_RAM:
         return gb.savedata_ptr();
      case RETRO_MEMORY_RTC:
         return gb.rtcdata_ptr();
      case RETRO_MEMORY_SYSTEM_RAM:
         /* Really ugly hack here, relies upon 
          * libgambatte/src/memory/memptrs.cpp MemPtrs::reset not
          * realizing that that memchunk hack is ugly, or 
          * otherwise getting rearranged. */
         return gb.rambank0_ptr();
   }

   return 0;
}

size_t retro_get_memory_size(unsigned id)
{
   if (rom_loaded) switch (id)
   {
      case RETRO_MEMORY_SAVE_RAM:
         return gb.savedata_size();
      case RETRO_MEMORY_RTC:
         return gb.rtcdata_size();
      case RETRO_MEMORY_SYSTEM_RAM:
         /* This is rather hacky too... it relies upon 
          * libgambatte/src/memory/cartridge.cpp not changing
          * the call to memptrs.reset, but this is 
          * probably mostly safe.
          *
          * GBC will probably not get a
          * hardware upgrade anytime soon. */
         return (gb.isCgb() ? 8 : 2) * 0x1000ul;
   }

   return 0;
}

void retro_run()
{
   /* libretro_samples_count and libretro_frames_count are
    * file-scope so they can be reset in retro_load_game,
    * retro_reset, and retro_unserialize. */

   input_poll_cb();
   update_input_state();

   uint64_t expected_frames = libretro_samples_count / SOUND_SAMPLES_PER_FRAME;
   if (libretro_frames_count < expected_frames) // Detect frame dupes.
   {
      video_cb(NULL, VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_PITCH * sizeof(gambatte::video_pixel_t));
      libretro_frames_count++;
      return;
   }

   union
   {
      gambatte::uint_least32_t u32[SOUND_BUFF_SIZE];
      int16_t i16[2 * SOUND_BUFF_SIZE];
   } static sound_buf;
   unsigned samples = SOUND_SAMPLES_PER_RUN;

   while (gb.runFor(video_buf, VIDEO_PITCH, sound_buf.u32, SOUND_BUFF_SIZE, samples) == -1)
   {
      if (use_cc_resampler)
         CC_renderaudio((audio_frame_t*)sound_buf.u32, samples);
      else
      {
         blipper_renderaudio(sound_buf.i16, samples);

         unsigned read_avail = blipper_read_avail(resampler_l);
         if (read_avail >= (BLIP_BUFFER_SIZE >> 1))
            audio_out_buffer_read_blipper(read_avail);
      }

      libretro_samples_count += samples;
      samples = SOUND_SAMPLES_PER_RUN;
   }
#ifdef DUAL_MODE
   while (gb2.runFor(video_buf + GB_SCREEN_WIDTH, VIDEO_PITCH, sound_buf.u32, samples) == -1) {}
#endif

   /* Perform interframe blending, if required */
   if (blend_frames)
      blend_frames();

   video_cb(video_buf, VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_PITCH * sizeof(gambatte::video_pixel_t));

   if (use_cc_resampler)
      CC_renderaudio((audio_frame_t*)sound_buf.u32, samples);
   else
   {
      blipper_renderaudio(sound_buf.i16, samples);

      unsigned read_avail = blipper_read_avail(resampler_l);
      audio_out_buffer_read_blipper(read_avail);
   }
   libretro_samples_count += samples;
   audio_upload_samples();

   /* Apply any 'pending' rumble effects */
   if (rumble_active)
      apply_rumble();

   libretro_frames_count++;

   bool updated = false;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated)
      check_variables(false);
}

unsigned retro_api_version() { return RETRO_API_VERSION; }

