/******************************************************************************
*
* Copyright (C) 2004-2017  Brian Berlin
* 
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* 
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
* 
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************
*
* FILENAME: sdl_if.c
*
* DESCRIPTION:  This intefaces the sdl driver (Simple Direct Media Layer)
*
* AUTHORS:  bberlin
*
* CHANGE LOG
*
*  VER     DATE     CHANGED BY   DESCRIPTION OF CHANGE
* ------  --------  ----------   ------------------------------------
* 0.1     07/18/04  bberlin      Creation
* 0.2     03/09/06  bberlin      Added GUI video setup
*                                Fixed D-Pad and analog axis centering issue
*                                Added debugger video setup to put in window
* 0.2.1   03/13/06  bberlin      Found problem where I was doing axis_idle[0]
*                                  instead of axis_idle[1] causing centering
*                                  issue on vertical axis
* 0.2.2   03/15/06  bberlin      Added and implemented deadzone parameter.
*                                  Also changed so that we don't cut off 5200
*                                  range in deadzone.
* 0.2.3   03/16/06  bberlin      Added ALT-F4 exits program to 'pc_poll_events'
* 0.2.3   03/19/06  bberlin      Added Pokey_sound_init to 'pc_init'
* 0.3.0   04/04/06  bberlin      Added pot_low and pot_high variables to
*                                  handle sensitivity configuration parameter
* 0.3.0   04/05/06  bberlin      Use axis_value with simulate_analog parameter
*                                Fix vertical axis deadzone issue
* 0.4.0   04/16/06  bberlin      Changed Pokey_sound_init in 'pc_init' to
*                                  Pokey_sound_update_freq
* 0.4.0   05/30/06  bberlin      Changed 'pc_poll_events' to use UI Key param
* 0.4.0   05/31/06  bberlin      Added 'pc_toggle_fullscreen' function
* 0.6.2   06/25/09  bberlin      Changed default player 2 down to s key
* 0.6.2   06/25/09  bberlin      Changed default sensitivity to 96 to fix
*                                  Galaxian with digital stick
* 0.7.0   01/27/17  bberlin      Fixed Remap of PC keypad keys, upgrade to
*                                  SDL2
* 0.7.1   02/17/17  bberlin      Reopen joysticks on init to get order right
*                                Re-order keyboard mappings for Up/Down/Right
*                                /Left being last
******************************************************************************/
#ifdef WIN32
#include <io.h>
#include <fcntl.h>
#include <windows.h>
#include <Windows.h>
#include <Wincon.h>
#endif
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <sys/stat.h>
#include "../kat5200.h"
#include "sdl_if.h"
#include "util.h"
#include "palette.h"
#include "../core/pokeysound.h"
#include "../core/pokey.h"
#include "../core/gtia.h"
#include "../gui/gui.h"
#include "../gui/filebrowser.h"
#include "input.h"
#include "logger.h"
#include "shader_gpu.h"
#ifdef __ANDROID__
#include <jni.h>
#endif
#include <SDL3_image/SDL_image.h>

#if defined(_WIN32) || defined(WIN32)
    #if !defined(S_ISDIR)
        #define S_ISDIR(m) (((m) & _S_IFDIR) == _S_IFDIR)
    #endif
#endif

#ifndef KAT_SOURCE_DIR
#define KAT_SOURCE_DIR "."
#endif

#define THRESH_VERY_LOW 0
#define THRESH_LOW 1
#define THRESH_HIGH 2
#define THRESH_VERY_HIGH 3
#define THRESH_EXTREME_HIGH 4

// #define SOUND_DEBUG 0

#ifdef SOUND_DEBUG
FILE *fp;
#endif

int pc_log_display_attributes (SDL_Window *win, SDL_Rect *usable_bounds);
void pc_audio_callback (void *userdata, Uint8 *buffer, int len);
void pc_set_sound_freq ( int freq );
static float pc_get_auto_sound_buffer_size_for_driver ( const char *driver );
static float pc_resolve_sound_buffer_size ( float buffer_size,
                                            const char *driver );
int pc_open_joystick (int index);
int pc_open_controller (int index);
void pc_set_shader ( t_video *p_video );
static void pc_apply_first_use_window_geometry ( const SDL_Rect *bounds );

extern unsigned int pokey_cpu_clock;
unsigned int tick_start;
int g_exit = 0;

/*
 * Setup functions
 */
int pc_set_game_sound ( t_sound *p_sound );

extern t_pc_input g_pc_input;
t_video g_video;
t_sound g_sound;

t_sdl_if g_pc_if;
//t_sdl_joy_conv g_pc_joy[MAX_PC_DEVICE];
static unsigned int g_joy_ids[MAX_PC_DEVICE] = {0};
static unsigned int g_joy_generation = 0;
static int g_mouse_grabbed = 0;
static t_sdl_aud_buf g_aud_buf;
int audio_id;
int shaders_supported;
static int g_mouse_trace = -1;
static int g_debugger_available = 1;

#ifdef __ANDROID__
/* MainActivity publishes visible system-bar/cutout insets from Android's UI
 * thread.  A small sequence lock keeps the two packed 32-bit halves coherent
 * on 32-bit ARM while the SDL render thread reads them without JNI. */
static SDL_AtomicU32 g_android_content_insets_sequence;
static SDL_AtomicU32 g_android_content_insets_low;
static SDL_AtomicU32 g_android_content_insets_high;
static SDL_AtomicInt g_android_content_insets_valid;
#endif

static int pc_event_is_gamepad_joystick_input (const SDL_Event *event);

static int pc_is_android_back_key ( const SDL_Event *event ) {

#ifdef __ANDROID__
    return event &&
           event->type == SDL_EVENT_KEY_DOWN &&
           ( event->key.scancode == SDL_SCANCODE_AC_BACK ||
             event->key.key == SDLK_ESCAPE );
#else
    (void)event;
    return 0;
#endif

} /* end pc_is_android_back_key */

#ifdef __ANDROID__
static int pc_android_has_pointing_mouse (void)
{
    JNIEnv *env;
    jobject activity;
    jclass clazz;
    jmethodID mid;
    int result = -1;

    env = (JNIEnv *) SDL_GetAndroidJNIEnv();
    activity = (jobject) SDL_GetAndroidActivity();
    if ( !env || !activity )
        return -1;

    clazz = (*env)->GetObjectClass(env, activity);
    if ( clazz ) {
        mid = (*env)->GetStaticMethodID(env, clazz, "hasPointingMouse", "()Z");
        if ( mid ) {
            result = (*env)->CallStaticBooleanMethod(env, clazz, mid) ? 1 : 0;
            if ( (*env)->ExceptionCheck(env) ) {
                (*env)->ExceptionClear(env);
                result = -1;
            }
        }
        else {
            (*env)->ExceptionClear(env);
        }
        (*env)->DeleteLocalRef(env, clazz);
    }
    (*env)->DeleteLocalRef(env, activity);

    return result;
}
#endif

static void pc_init_debugger_availability (void)
{
    g_debugger_available = 1;

#ifdef __ANDROID__
    /* Startup snapshot only: Android TV users must connect a pointing device
     * before launching kat5200 for the mouse-heavy debugger UI to be offered. */
    if ( pc_is_android_tv() ) {
        int has_mouse = pc_android_has_pointing_mouse();

        if ( has_mouse >= 0 )
            g_debugger_available = has_mouse;
    }
#endif
}

int pc_debugger_available (void)
{
    return g_debugger_available;
}

static int pc_event_is_gamepad_joystick_input (const SDL_Event *event) {

    if ( !event )
        return 0;

    switch ( event->type ) {
        case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
        case SDL_EVENT_JOYSTICK_BUTTON_UP:
            return SDL_IsGamepad(event->jbutton.which);
        case SDL_EVENT_JOYSTICK_AXIS_MOTION:
            return SDL_IsGamepad(event->jaxis.which);
        case SDL_EVENT_JOYSTICK_HAT_MOTION:
            return SDL_IsGamepad(event->jhat.which);
        case SDL_EVENT_JOYSTICK_BALL_MOTION:
            return SDL_IsGamepad(event->jball.which);
        default:
            return 0;
    }

} /* end pc_event_is_gamepad_joystick_input */

/* Thread globals */
static SDL_Semaphore* g_lock = NULL;
static void * g_thread_data;

static int pc_render_frame_shader_gpu ( void );
static void pc_free_shader_runtime ( void );
static int pc_resolve_shader_bundle_dir ( const char *shader_name,
        char *bundle_dir, size_t bundle_dir_size );
static void pc_log_shader_message ( const char *prefix, const char *detail );

static float pc_get_auto_sound_buffer_size_for_driver ( const char *driver ) {

    if ( driver && !strcmp(driver, "openslES") )
        return KCFG_SND_BUFFER_OPENSL_ES;
    return KCFG_SND_BUFFER_DEFAULT;
}

float pc_get_auto_sound_buffer_size ( void ) {

    return pc_get_auto_sound_buffer_size_for_driver(SDL_GetCurrentAudioDriver());
}

static float pc_resolve_sound_buffer_size ( float buffer_size,
                                            const char *driver ) {

    if ( buffer_size <= KCFG_SND_BUFFER_AUTO )
        return pc_get_auto_sound_buffer_size_for_driver(driver);
    if ( buffer_size < KCFG_SND_BUFFER_MIN )
        return KCFG_SND_BUFFER_MIN;
    if ( buffer_size > KCFG_SND_BUFFER_MAX )
        return KCFG_SND_BUFFER_MAX;
    return buffer_size;
}

static int pc_shader_string_is_true ( const char *value ) {

    if ( !value )
        return 0;

    return !strcmp(value, "1") ||
           !strcmp(value, "true") ||
           !strcmp(value, "True") ||
           !strcmp(value, "TRUE") ||
           !strcmp(value, "yes") ||
           !strcmp(value, "Yes") ||
           !strcmp(value, "YES");

} /* end pc_shader_string_is_true */

static int pc_shader_bundle_exists ( const char *bundle_dir ) {

    char filename[KAT_PATH_MAX];
    FILE *fp;

    if ( !bundle_dir || !strlen(bundle_dir) )
        return 0;

    if ( snprintf(filename, sizeof(filename), "%s/bundle.toml", bundle_dir) >=
            (int) sizeof(filename) )
        return 0;

    /* Probe with plain fopen so this matches how the bundle is actually
     * loaded (shader_bundle_load -> toml_parse_file -> fopen).  SDL_IOFromFile
     * is asset/VFS-aware and resolves relative paths differently on Android,
     * which made a bare-relative candidate pass here but then fail in the
     * loader (cwd is "/"), silently dropping the shader.  Keeping both on
     * fopen lets the relative candidate fail so the resolver falls through to
     * the absolute program-path candidate. */
    fp = fopen ( filename, "rb" );
    if ( !fp )
        return 0;

    fclose ( fp );
    return 1;

} /* end pc_shader_bundle_exists */

static int pc_try_shader_bundle_candidate ( const char *candidate_dir,
        char *bundle_dir, size_t bundle_dir_size ) {

    if ( !candidate_dir || !strlen(candidate_dir) )
        return -1;

    if ( !pc_shader_bundle_exists(candidate_dir) )
        return -1;

    if ( snprintf(bundle_dir, bundle_dir_size, "%s", candidate_dir) >=
            (int) bundle_dir_size )
        return -1;

    return 0;

} /* end pc_try_shader_bundle_candidate */

static int pc_resolve_shader_bundle_dir ( const char *shader_name,
        char *bundle_dir, size_t bundle_dir_size ) {

    char candidate[KAT_PATH_MAX];
    char relative[KAT_PATH_MAX];
    char *program_path;

    if ( !bundle_dir || bundle_dir_size == 0 )
        return -1;

    *bundle_dir = '\0';

    if ( !shader_name || !strlen(shader_name) ||
            !strcmp(shader_name, SHADER_NONE) )
        return -1;

    if ( pc_try_shader_bundle_candidate(shader_name, bundle_dir,
                bundle_dir_size) == 0 )
        return 0;

    if ( snprintf(relative, sizeof(relative), "shaders/bundles/%s",
                shader_name) >= (int) sizeof(relative) )
        return -1;

    if ( pc_try_shader_bundle_candidate(relative, bundle_dir,
                bundle_dir_size) == 0 )
        return 0;

    program_path = util_create_program_path_alloc ( relative );
    if ( program_path ) {
        if ( pc_try_shader_bundle_candidate(program_path, bundle_dir,
                    bundle_dir_size) == 0 ) {
            free ( program_path );
            return 0;
        }
        free ( program_path );
    }

    snprintf ( candidate, sizeof(candidate), "%s", relative );
    util_set_file_to_app_dir ( candidate );
    if ( pc_try_shader_bundle_candidate(candidate, bundle_dir,
                bundle_dir_size) == 0 )
        return 0;

    program_path = util_create_seed_path_alloc ( relative );
    if ( program_path ) {
        if ( pc_try_shader_bundle_candidate(program_path, bundle_dir,
                    bundle_dir_size) == 0 ) {
            free ( program_path );
            return 0;
        }
        free ( program_path );
    }

    if ( snprintf(candidate, sizeof(candidate), "%s/shaders/bundles/%s",
                KAT_DATADIR, shader_name) < (int) sizeof(candidate) &&
            pc_try_shader_bundle_candidate(candidate, bundle_dir,
                bundle_dir_size) == 0 )
        return 0;

    if ( snprintf(candidate, sizeof(candidate),
                "%s/resources/shaders/bundles/%s", KAT_SOURCE_DIR,
                shader_name) < (int) sizeof(candidate) &&
            pc_try_shader_bundle_candidate(candidate, bundle_dir,
                bundle_dir_size) == 0 )
        return 0;

    return -1;

} /* end pc_resolve_shader_bundle_dir */

static void pc_log_shader_message ( const char *prefix, const char *detail ) {

    char msg[768];

    snprintf ( msg, sizeof(msg), "%s%s%s", prefix ? prefix : "",
            detail && strlen(detail) ? " - " : "", detail ? detail : "" );
    logger_log_message ( LOG_ERROR, msg, "" );

} /* end pc_log_shader_message */

/* Consecutive shader-frame failures tolerated before we give up and fall back
   to the plain SDL renderer for the rest of the session.  A single failure is
   treated as transient (skip the frame and retry) so a momentary hiccup does
   not disable the shader permanently. */
#define SHADER_MAX_CONSEC_FAILS 30
static int g_shader_frame_fail_count = 0;

static void pc_free_shader_runtime ( void ) {

    shader_gpu_graph_free ( &g_pc_if.shader_graph );
    shader_bundle_free ( &g_pc_if.shader_bundle );
    g_pc_if.use_shader = 0;
    g_shader_frame_fail_count = 0;

} /* end pc_free_shader_runtime */

/******************************************************************************
**  Function   :  pc_is_android_tv
**
**  Objective  :  Report whether we are running on Android TV, by calling
**                MainActivity.isAndroidTv() (UiModeManager check) over JNI.
**                The result gates fixed-landscape orientation and the 10-foot
**                UI scale floor.  Returns 0 on every non-Android platform.
**
**  Parameters :  NONE
**
**  return     :  1 on Android TV, 0 otherwise
******************************************************************************/
int pc_is_android_tv (void)
{
#ifdef __ANDROID__
    /* Cached: the device type cannot change during a run, and SDL's JNI is
     * available from app start (set up before native main), so this is safe to
     * call before SDL_Init (used there to pick SDL_HINT_ORIENTATIONS). */
    static int cached = -1;

    JNIEnv *env;
    jobject activity;
    jclass clazz;
    jmethodID mid;

    int result = 0;

    if ( cached >= 0 )
        return cached;

    env = (JNIEnv *) SDL_GetAndroidJNIEnv();
    activity = (jobject) SDL_GetAndroidActivity();
    if ( !env || !activity )
        return 0;   /* JNI not ready yet: leave cached unresolved (-1), retry later */

    clazz = (*env)->GetObjectClass(env, activity);
    if ( clazz ) {
        mid = (*env)->GetStaticMethodID(env, clazz, "isAndroidTv", "()Z");
        if ( mid )
            result = (*env)->CallStaticBooleanMethod(env, clazz, mid) ? 1 : 0;
        else
            (*env)->ExceptionClear(env);   /* method missing: stay false */
        (*env)->DeleteLocalRef(env, clazz);
    }
    (*env)->DeleteLocalRef(env, activity);

    cached = result;   /* only cache once activity/JNI were actually available */
    return cached;
#else
    return 0;
#endif
}

/******************************************************************************
**  Function   :  pc_get_android_content_insets
**
**  Objective  :  Read Android's cached visible system-bar/display-cutout
**                insets. MainActivity updates this state from the UI thread;
**                the render thread performs atomic reads without JNI.
**
**  Parameters :  left, top, right, bottom - inset values in physical pixels
**
**  return     :  1 when Android supplied the insets, 0 otherwise
******************************************************************************/
int pc_get_android_content_insets (int *left, int *top, int *right, int *bottom)
{
#ifdef __ANDROID__
    Uint32 sequence_before;
    Uint32 sequence_after;
    Uint32 low;
    Uint32 high;
    Uint64 value;
    int valid;
    int attempt;

    if ( !left || !top || !right || !bottom )
        return 0;

    for ( attempt = 0; attempt < 8; ++attempt ) {
        sequence_before = SDL_GetAtomicU32(
                &g_android_content_insets_sequence);
        if ( sequence_before & 1u )
            continue;

        valid = SDL_GetAtomicInt(&g_android_content_insets_valid);
        low = SDL_GetAtomicU32(&g_android_content_insets_low);
        high = SDL_GetAtomicU32(&g_android_content_insets_high);
        sequence_after = SDL_GetAtomicU32(
                &g_android_content_insets_sequence);

        if ( sequence_before != sequence_after || (sequence_after & 1u) )
            continue;
        if ( !valid )
            return 0;

        value = ((Uint64)high << 32) | low;
        *left = (int)(value & 0xffffu);
        *top = (int)((value >> 16) & 0xffffu);
        *right = (int)((value >> 32) & 0xffffu);
        *bottom = (int)((value >> 48) & 0xffffu);
        return 1;
    }

    /* Insets are changing on the UI thread.  Keep SDL's safe-area result for
     * this frame and try the atomic snapshot again on the next one. */
    return 0;
#else
    (void)left;
    (void)top;
    (void)right;
    (void)bottom;
    return 0;
#endif
}

#ifdef __ANDROID__
/* Called by MainActivity's UI-thread OnApplyWindowInsetsListener. */
JNIEXPORT void JNICALL
Java_io_github_madjacket_kat5200_MainActivity_nativeSetContentInsetsPacked(
        JNIEnv *env, jclass cls, jlong packed)
{
    Uint64 value;

    (void)env;
    (void)cls;

    SDL_AddAtomicU32(&g_android_content_insets_sequence, 1);
    if ( packed < 0 ) {
        SDL_SetAtomicInt(&g_android_content_insets_valid, 0);
    }
    else {
        value = (Uint64)packed;
        SDL_SetAtomicU32(&g_android_content_insets_low, (Uint32)value);
        SDL_SetAtomicU32(&g_android_content_insets_high,
                         (Uint32)(value >> 32));
        SDL_SetAtomicInt(&g_android_content_insets_valid, 1);
    }
    SDL_AddAtomicU32(&g_android_content_insets_sequence, 1);
}
#endif

#ifdef __ANDROID__
/******************************************************************************
**  Function   :  pc_set_android_hints
**
**  Objective  :  Apply the Android SDL hints kat5200 relies on.  Called from
**                pc_init AND re-applied after SDL_ResetHints() in
**                pc_set_game_video (which would otherwise revert them to their
**                defaults when a ROM is launched).
**
**  Parameters :  NONE
**
**  return     :  NONE
******************************************************************************/
static void pc_set_android_hints (void)
{
    /* Deliver the hardware/navigation Back button to the app as an AC_BACK key
     * event (handled in pc_poll_events to return to the GUI while in-game)
     * instead of letting Android background us. */
    SDL_SetHint(SDL_HINT_ANDROID_TRAP_BACK_BUTTON, "1");

    /* Do NOT synthesize mouse events from touch: the GUI's widgets trigger on
     * mouse-press, so a synthetic touch-down would activate whatever a swipe
     * starts on (a ROM row, file entry, button) before it could be recognized
     * as a scroll.  The GUI drives Nuklear directly from SDL_EVENT_FINGER_*
     * (see gui.c): tap -> click on finger-up-without-movement, drag -> scroll. */
    SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");

    /* Allowed orientations.  Without this, SDL pins the activity to the
     * orientation that matches the window's aspect when it is first created, so
     * the app "sticks" to whatever it launched in and never follows the sensor
     * (overriding the manifest's fullUser).  A TV is a fixed-landscape device
     * (MainActivity locks it to landscape too), so restrict it to landscape;
     * a phone lists all four so it can rotate freely. */
    if ( pc_is_android_tv() )
        SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
    else
        SDL_SetHint(SDL_HINT_ORIENTATIONS,
                    "LandscapeLeft LandscapeRight Portrait PortraitUpsideDown");
}
#endif

/******************************************************************************
**  Function   :  pc_init
**
**  Objective  :  This function initializes the PC interface (SDL in this case)
**
**  Parameters :  NONE
**
**  return     :  NONE
**
******************************************************************************/
int pc_init (void) {

    char msg[256];

    g_video = config_get_global_cfg()->video;
    g_sound = config_get_global_cfg()->sound;

    //SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");

#ifdef __ANDROID__
    pc_set_android_hints();
#endif

    /*
     * Initialize SDL and all sub-systems
     */
    if((SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_JOYSTICK|SDL_INIT_GAMEPAD)==-1)) {
        sprintf(msg, "pc_init: SDL Initialization Error -  %s", SDL_GetError());
        logger_log_message ( LOG_ERROR, msg, "" );
        return -1;
    }
    pc_init_debugger_availability();

    {
        SDL_Rect display_bounds = { 0, 0, 0, 0 };

        pc_log_display_attributes ( 0, &display_bounds );
        pc_apply_first_use_window_geometry ( &display_bounds );
    }

    //SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "0");
    g_pc_if.surface = 0;
    g_pc_if.ntsc_surface = 0;
    g_pc_if.renderer = 0;
    g_pc_if.screen = 0;
    g_pc_if.screen_texture = 0;
    g_pc_if.ntsc_emu = 0;
    g_pc_if.use_shader = 0;
    g_pc_if.shaders_supported = 0;
    g_pc_if.gpu_device = NULL;
    g_pc_if.gpu_shader_formats = 0;
    g_pc_if.shaders_supported = 0;
    shader_bundle_init ( &g_pc_if.shader_bundle );
    shader_gpu_graph_init ( &g_pc_if.shader_graph );

    g_aud_buf.buf = 0;
    g_aud_buf.extra_buf = 0;

    /*
     * In windows, kill that extra console window
     */
#ifdef WIN32
    FreeConsole();
#endif

#ifdef SOUND_DEBUG
    fp = fopen ( "snd_dbg.txt", "w+" );
#endif

    g_lock = SDL_CreateSemaphore ( 1 );

    return 0;

} /* pc_init */

/******************************************************************************
**  Function   :  pc_log_display_attributes
**                            
**  Objective  :  This function grabs display attributes such as the name, 
**                bounds, and dpi
**
**  Parameters :  win - existing window to inspect, or NULL to create a hidden
**                     probe window
**               usable_bounds - optional output for usable display bounds
**                                                
**  return     :  0
**      
******************************************************************************/
int pc_log_display_attributes(SDL_Window* win, SDL_Rect *usable_bounds) {

    char msg[1024];
    const char* name;
    int w_valid;
    SDL_DisplayID display;
    const SDL_DisplayMode *current;
    SDL_Window* win_test;
    SDL_Rect bounds;

    if ( usable_bounds ) {
        usable_bounds->x = 0;
        usable_bounds->y = 0;
        usable_bounds->w = 0;
        usable_bounds->h = 0;
    }

    /* Create a test window to see what display is used */
    w_valid = pc_window_position_in_bounds(g_video.x, g_video.y);

    if (win)
        win_test = win;
    else {
        win_test = SDL_CreateWindow("test",
                      640, 480,
                      SDL_WINDOW_HIDDEN);

        if (!win_test) {
            sprintf(msg, "pc_log_display_attributes: SDL_CreateWindow failed - %s",
                    SDL_GetError());
            logger_log_message(LOG_ERROR, msg, "");
            return -1;
        }

        SDL_SetWindowPosition(win_test,
            w_valid ? g_video.x : SDL_WINDOWPOS_UNDEFINED,
            w_valid ? g_video.y : SDL_WINDOWPOS_UNDEFINED);
    }

    display = SDL_GetDisplayForWindow(win_test);
    if ( !display ) {
        int num_displays = 0;
        SDL_DisplayID *displays = SDL_GetDisplays(&num_displays);
        display = (displays && num_displays > 0) ? displays[0] : 0;
        SDL_free(displays);
    }

    /* Name of Display */
    name = SDL_GetDisplayName(display);
    if ( name ) {
        sprintf ( msg, "Display %u : %s", (unsigned int)display, name );
    }
    else {
        sprintf ( msg, "Display %u : ", (unsigned int)display );
    }
	logger_log_message(LOG_INFO, msg, "");

    /* Video Mode */
    current = SDL_GetCurrentDisplayMode(display);
    if ( current ) {
        sprintf ( msg, "            %d x %d @ %g Hz", current->w,
                current->h, current->refresh_rate );
	    logger_log_message(LOG_INFO, msg, "");
    }

    if ( SDL_GetDisplayUsableBounds ( display, &bounds ) ||
         SDL_GetDisplayBounds ( display, &bounds ) ) {
        sprintf ( msg, "            usable bounds x=%d y=%d w=%d h=%d",
                bounds.x, bounds.y, bounds.w, bounds.h );
        logger_log_message(LOG_INFO, msg, "");
        if ( usable_bounds )
            *usable_bounds = bounds;
    }

    /* DPI */
    float scale = SDL_GetWindowDisplayScale(win_test);
    sprintf ( msg, "            %f scale", scale);
	logger_log_message(LOG_INFO, msg, "");

    int tx, ty;
    SDL_GetWindowPosition(win_test, &tx, &ty);
    if ( win) {
        sprintf ( msg, "            x=%d y=%d", tx, 
                ty);
	    logger_log_message(LOG_INFO, msg, "");
    }

    if (!win)
        SDL_DestroyWindow(win_test);

    return 0;

} /* pc_log_display_attributes */

/******************************************************************************
**  Function   :  pc_calculate_first_use_window_geometry
**
**  Objective  :  Calculate first-run desktop window geometry from display
**                bounds.  Pure helper for unit tests; SDL probing and config
**                writes stay in pc_apply_first_use_window_geometry.
**
**  Parameters :  bounds - usable display bounds
**                w/h/x/y - calculated window size and position
**
**  return     :  0 on success, -1 for invalid input
**
******************************************************************************/
int pc_calculate_first_use_window_geometry ( const SDL_Rect *bounds,
        int *w, int *h, int *x, int *y )
{
    int calc_w;
    int calc_h;

    if ( !bounds || !w || !h || !x || !y ||
         bounds->w <= 0 || bounds->h <= 0 )
        return -1;

    calc_w = (bounds->w * 3) / 4;
    calc_h = (bounds->h * 3) / 4;

    if ( calc_w < KAT_WINDOW_MIN_WIDTH )
        calc_w = KAT_WINDOW_MIN_WIDTH;
    if ( calc_h < KAT_WINDOW_MIN_HEIGHT )
        calc_h = KAT_WINDOW_MIN_HEIGHT;

    if ( calc_w > bounds->w )
        calc_w = bounds->w;
    if ( calc_h > bounds->h )
        calc_h = bounds->h;

    *w = calc_w;
    *h = calc_h;
    *x = bounds->x + ((bounds->w - calc_w) / 2);
    *y = bounds->y + ((bounds->h - calc_h) / 2);

    return 0;

} /* pc_calculate_first_use_window_geometry */

/******************************************************************************
**  Function   :  pc_apply_first_use_window_geometry
**
**  Objective  :  On first use, replace the seeded desktop window geometry with
**                a display-relative default and save it to the live config.
**
**  Parameters :  bounds - display bounds from the existing startup probe
**
**  return     :  NONE
**
******************************************************************************/
static void pc_apply_first_use_window_geometry ( const SDL_Rect *bounds )
{
#ifdef __ANDROID__
    (void)bounds;
#else
    t_config *cfg = config_get_global_cfg();
    int w;
    int h;
    int x;
    int y;
    unsigned long long mask;

    if ( !cfg || !cfg->general.first_use || cfg->video.fullscreen )
        return;

    if ( pc_calculate_first_use_window_geometry ( bounds, &w, &h, &x, &y ) )
        return;

    cfg->video.width = w;
    cfg->video.height = h;
    cfg->video.x = x;
    cfg->video.y = y;

    mask = KCFG_VIDEO_WIDTH | KCFG_VIDEO_HEIGHT | KCFG_VIDEO_X | KCFG_VIDEO_Y;
    config_save_video ( config_get_global_table(), "All", mask );

    g_video = cfg->video;
#endif

} /* pc_apply_first_use_window_geometry */

/******************************************************************************
**  Function   :  pc_get_compiled_version
**                pc_get_linked_version
**                pc_get_image_compiled_version
**                pc_get_image_linked_version
**                            
**  Objective  :  This function gets the current SDL version.
**
**  Parameters :  string to store version in (at least 26 char)
**                                                
**  return     :  string with version
**      
******************************************************************************/
char * pc_get_compiled_version ( char *v_string ) {

    int version = SDL_VERSION;

    sprintf(v_string, "Compiled with SDL %d.%d.%d", SDL_VERSIONNUM_MAJOR(version), 
            SDL_VERSIONNUM_MINOR(version), SDL_VERSIONNUM_MICRO(version) );

    return v_string;

} /* pc_get_compiled_version */

char * pc_get_linked_version ( char *v_string ) {

    int version = SDL_GetVersion();

    sprintf(v_string, "Linked with SDL %d.%d.%d", SDL_VERSIONNUM_MAJOR(version), 
            SDL_VERSIONNUM_MINOR(version), SDL_VERSIONNUM_MICRO(version) );

    return v_string;

} /* pc_get_linked_version */

char * pc_get_image_compiled_version ( char *v_string ) {

    int version = SDL_IMAGE_VERSION;

    sprintf(v_string, "Compiled with SDL_Image %d.%d.%d", SDL_VERSIONNUM_MAJOR(version), 
            SDL_VERSIONNUM_MINOR(version), SDL_VERSIONNUM_MICRO(version) );

    return v_string;

} /* pc_get_compiled_version */

char * pc_get_image_linked_version ( char *v_string ) {

    int version = IMG_Version();

    sprintf(v_string, "Linked with SDL_Image %d.%d.%d", SDL_VERSIONNUM_MAJOR(version), 
            SDL_VERSIONNUM_MINOR(version), SDL_VERSIONNUM_MICRO(version) );

    return v_string;

} /* pc_get_linked_version */

int pc_check_ctrl_joystick_use (int joy_num, t_input_common *ctrl) {

    if ( ctrl->device[0] == DEV_JOYSTICK &&
         ctrl->device_num[0] == joy_num )
        return 1;
    else if ( ctrl->combo && ctrl->device[1] == DEV_JOYSTICK &&
         ctrl->device_num[1] == joy_num )
        return 1;

    if ( ctrl->device[0] == DEV_CONTROLLER &&
         ctrl->device_num[0] == joy_num )
        return -1;
    else if ( ctrl->combo && ctrl->device[1] == DEV_CONTROLLER &&
         ctrl->device_num[1] == joy_num )
        return -1;

    return 0;

} /* pc_check_ctrl_joystick_use */

static int pc_input_chain_uses_joy_slot ( int joy_num, t_input_common *ctrl ) {

    while ( ctrl ) {
        if ( pc_check_ctrl_joystick_use ( joy_num, ctrl ) )
            return 1;
        ctrl = ctrl->next;
    }

    return 0;

} /* pc_input_chain_uses_joy_slot */

static int pc_ui_uses_joy_slot ( int joy_num ) {

    int i;
    t_ui_input *ui = &config_get_global_cfg()->input.ui;

    if ( pc_input_chain_uses_joy_slot ( joy_num, &ui->exit ) ||
         pc_input_chain_uses_joy_slot ( joy_num, &ui->back ) ||
         pc_input_chain_uses_joy_slot ( joy_num, &ui->fullscreen ) ||
         pc_input_chain_uses_joy_slot ( joy_num, &ui->screenshot ) ||
         pc_input_chain_uses_joy_slot ( joy_num, &ui->debugger ) )
        return 1;

    for ( i = 0; i < MAX_UI_STATES; ++i ) {
        if ( pc_input_chain_uses_joy_slot ( joy_num, &ui->state_load[i] ) ||
             pc_input_chain_uses_joy_slot ( joy_num, &ui->state_save[i] ) )
            return 1;
    }

    return 0;

} /* pc_ui_uses_joy_slot */

int pc_handle_unmapped_gui_button ( SDL_Event *event, int *status ) {

    int joy_num;
    unsigned int kstatus;

    if ( status )
        *status = 0;

    if ( !event || event->type != SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
         event->gbutton.button != SDL_GAMEPAD_BUTTON_EAST )
        return 0;

    kstatus = kat5200_get_status();
    if ( !(kstatus & KSTATUS_RUN) || (kstatus & KSTATUS_GUI) )
        return 0;

    joy_num = pc_get_joystick_index_from_id ( event->gbutton.which );
    if ( joy_num < 0 || joy_num >= MAX_PC_DEVICE )
        return 0;

    if ( input_pc_common_is_mapped (
                &g_pc_input.button[1][joy_num][SDL_GAMEPAD_BUTTON_EAST] ) )
        return 0;

    if ( status )
        *status = input_ui_back ( NULL, 0, 0, 0 );
    else
        input_ui_back ( NULL, 0, 0, 0 );

    return 1;

} /* pc_handle_unmapped_gui_button */

static void pc_add_joy_id(SDL_JoystickID new_id)
{
    t_config *p_cfg = config_get_global_cfg();
    t_game_input *p_input = &p_cfg->input.game;
    int tracked = 0;

    /* Ignore if already tracked (JOYSTICK_ADDED fires for gamepads too) */
    for (int i = 0; i < MAX_PC_DEVICE; i++) {
        if (g_joy_ids[i] == new_id)
            return;
    }

    /* Check if any player has this device configured by GUID */
    int target_slot = -1;
    SDL_GUID new_guid = SDL_GetJoystickGUIDForID(new_id);
    for (int i = 0; i < MAX_CONTROLLER; i++) {
        if (!p_input->players[i].joy_enable) continue;
        if (!strlen(p_input->players[i].joy_guid)) continue;
        SDL_GUID player_guid = SDL_StringToGUID(p_input->players[i].joy_guid);
        if (memcmp(&new_guid, &player_guid, sizeof(SDL_GUID)) != 0) continue;
        if (strlen(p_input->players[i].joy_serial)) {
            SDL_Joystick *joy = SDL_OpenJoystick(new_id);
            const char *serial = joy ? SDL_GetJoystickSerial(joy) : NULL;
            int match = serial && !strcmp(p_input->players[i].joy_serial, serial);
            if (joy) SDL_CloseJoystick(joy);
            if (!match) continue;
        }
        target_slot = i;
        break;
    }

    if (target_slot >= 0) {
        /* Place in the player's configured slot; move the current occupant to the next free slot */
        SDL_JoystickID displaced = g_joy_ids[target_slot];
        g_joy_ids[target_slot] = new_id;
        tracked = 1;
        if (displaced) {
            for (int i = 0; i < MAX_PC_DEVICE; i++) {
                if (!g_joy_ids[i]) {
                    g_joy_ids[i] = displaced;
                    break;
                }
            }
        }
    } else {
        /* No specific player claims it — put in first available slot */
        for (int i = 0; i < MAX_PC_DEVICE; i++) {
            if (!g_joy_ids[i]) {
                g_joy_ids[i] = new_id;
                tracked = 1;
                break;
            }
        }
    }
    if (tracked)
        g_joy_generation++;
    if (SDL_IsGamepad(new_id))
        pc_open_controller(new_id);
    else
        pc_open_joystick(new_id);
}

static void pc_remove_joy_id_and_compact(SDL_JoystickID removed_id)
{
    int found = 0;

    for (int i = 0; i < MAX_PC_DEVICE; i++) {
        if (g_joy_ids[i] == removed_id) {
            g_joy_ids[i] = 0;
            found = 1;
            break;
        }
    }
    if (!found)
        return;

    /* Compact: shift all non-zero entries toward the front */
    int dst = 0;
    for (int src = 0; src < MAX_PC_DEVICE; src++) {
        if (g_joy_ids[src])
            g_joy_ids[dst++] = g_joy_ids[src];
    }
    while (dst < MAX_PC_DEVICE)
        g_joy_ids[dst++] = 0;
    g_joy_generation++;
}

int pc_find_first_available_id(SDL_JoystickID *ids_consumed, SDL_JoystickID *ids, int total, SDL_GUID *guid)
{
    for (int i = 0; i < total; ++i) {
        if (ids_consumed[i])
            continue;
        /* When a GUID is requested only a matching device qualifies; skip
         * non-matching ones instead of falling through and grabbing the first
         * available.  The original code returned the first unconsumed device
         * regardless of GUID, so a player bound to (say) the Xbox pad still got
         * whatever enumerated first - the Fire TV remote - leaving Port 1 dead
         * on a cold start. */
        if (guid) {
            SDL_GUID joy_guid = SDL_GetJoystickGUIDForID(ids[i]);
            if (memcmp(&joy_guid, guid, sizeof(SDL_GUID)) != 0)
                continue;
        }
        ids_consumed[i] = 1;
        return i;
    }
    return -1;
}

unsigned int * pc_check_for_and_open_joysticks(void)
{
    t_config *p_cfg = config_get_global_cfg();
    t_game_input *p_input = &p_cfg->input.game;

    int id_index, total, joy_num;
    SDL_JoystickID *ids = SDL_GetJoysticks(&total);
    SDL_JoystickID *ids_consumed = calloc(total, sizeof(SDL_JoystickID));
    for ( int i = 0; i < MAX_PC_DEVICE; ++i ) {
        g_joy_ids[i] = 0;
        if((i < MAX_CONTROLLER) && !p_input->players[i].joy_enable &&
                !pc_ui_uses_joy_slot(i))
            continue;

        if ((i < MAX_CONTROLLER) && !(strlen(p_input->players[i].joy_guid))) {
            id_index = pc_find_first_available_id(ids_consumed, ids, total, NULL);
            if (id_index >= 0)
                g_joy_ids[i] = ids[id_index];
        }
        else if (i < MAX_CONTROLLER)  {
            SDL_GUID player_guid = SDL_StringToGUID(p_input->players[i].joy_guid);

            joy_num = -1;
            while(ids[++joy_num]) {
                SDL_GUID guid = SDL_GetJoystickGUIDForID(ids[joy_num]);
                if (!memcmp(&player_guid, &guid, sizeof(SDL_GUID))) {
                    if (strlen(p_input->players[i].joy_serial)) {
                        SDL_Joystick *joy = SDL_OpenJoystick(ids[joy_num]);
                        const char *serial = SDL_GetJoystickSerial(joy);
                        SDL_CloseJoystick(joy);
                        if (!strcmp(p_input->players[i].joy_serial, serial)) {
                            g_joy_ids[i] = ids[joy_num];
                            break;
                        }
                    }
                    else {
                        id_index = pc_find_first_available_id(ids_consumed, ids, total, &guid);
                        if (id_index >= 0)
                            g_joy_ids[i] = ids[id_index];
                        break;
                    }
                }
            }
        }
        if (!g_joy_ids[i]) {
            id_index = pc_find_first_available_id(ids_consumed, ids, total, NULL);
            if (id_index >= 0)
                g_joy_ids[i] = ids[id_index];
        }
        // FIXME:  check for being used?
        if (g_joy_ids[i]) {
            if (SDL_IsGamepad(g_joy_ids[i]))
                pc_open_controller(g_joy_ids[i]); 
            else
                pc_open_joystick(g_joy_ids[i]); 
        }
    }

    free(ids_consumed);
    SDL_free(ids);  /* SDL_GetJoysticks returns an SDL-allocated array */

    return g_joy_ids;
}

static void pc_apply_mouse_grab(void) {
    SDL_SetWindowMouseGrab(g_pc_if.screen, true);
    SDL_SetWindowRelativeMouseMode(g_pc_if.screen, true);
}

static int pc_mouse_trace_enabled(void) {
    const char *trace;

    if ( g_mouse_trace < 0 ) {
        trace = getenv("KAT5200_MOUSE_TRACE");
        g_mouse_trace = trace && strcmp(trace, "0") ? 1 : 0;
    }

    return g_mouse_trace;
}

/******************************************************************************
**  Function   :  pc_game_init
**                            
**  Objective  :  This function sets up the PC interface for game play
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
int pc_game_init (void) {

    int i,j, total;
    int joy_num,joy_open;
    t_config *p_cfg = config_get_global_cfg();
    t_game_input *p_input = &p_cfg->input.game;

    e_direction dir;

    /*
     * Initialize Our Window
     *    Moved to video init
     */
    SDL_SetModState(SDL_KMOD_NONE);

    /*
     * Setup for game video and sound
     */
    pc_set_game_video ( &(p_cfg->video), p_cfg->machine.tv_standard );
    pc_set_game_sound ( &(p_cfg->sound) );

    /*
     * Start up Joysticks if enabled.
     *
     * NOTE: do NOT quit/re-init the GAMEPAD subsystem here.  That was a 2017-era
     * workaround ("reopen joysticks on init to get order right") from when devices
     * were addressed by open-order index.  Under SDL3 we key off stable instance
     * ids (g_joy_ids) assigned by GUID, and hotplug is event-driven, so the
     * teardown is unnecessary -- and harmful: SDL_QuitSubSystem(GAMEPAD) emits a
     * GAMEPAD_REMOVED for every pad, which (drained later by pc_poll_input_events)
     * wipes g_joy_ids, leaving non-player UI bindings dead until a later rescan.
     * GAMEPAD stays initialized from the startup SDL_Init.
     */
    pc_check_for_and_open_joysticks();

    /*
    for ( joy_num = 0; joy_num < MAX_PC_DEVICE; joy_num++ ) {
        joy_open = 0;
        for ( i = 0; i < MAX_CONTROLLER; ++i ) {

            // Keypad
            for ( j = 0; j < 0x10; ++j ) {
                joy_open += pc_check_ctrl_joystick_use (joy_num, 
                                     &p_input->players[i].keypad[j]);
            }

            // Buttons
            joy_open += pc_check_ctrl_joystick_use (joy_num, 
                            &p_input->players[i].top_button);
            joy_open += pc_check_ctrl_joystick_use (joy_num, 
                            &p_input->players[i].bottom_button);

            // Stick
            for ( j = 0; j < MAX_DIR; ++j ) {
                joy_open += pc_check_ctrl_joystick_use (joy_num, 
                        &p_input->players[i].stick[j]);
            }
            if ( p_cfg->general.machine != MACHINE_TYPE_5200 ) {
                joy_open += pc_check_ctrl_joystick_use (joy_num, 
                    &p_input->players[i].paddles[0].left);
                joy_open += pc_check_ctrl_joystick_use (joy_num, 
                    &p_input->players[i].paddles[0].right);
                joy_open += pc_check_ctrl_joystick_use (joy_num, 
                    &p_input->players[i].paddles[1].left);
                joy_open += pc_check_ctrl_joystick_use (joy_num, 
                    &p_input->players[i].paddles[1].right);
            }

        }

        // Keyboard and 800 controls
        if ( p_cfg->general.machine != MACHINE_TYPE_5200 ) {
            for ( j = 0; j < 0x40; ++j ) {
                joy_open += pc_check_ctrl_joystick_use (joy_num, 
                                     &p_input->keyboard[j]);
            }
            joy_open += pc_check_ctrl_joystick_use (joy_num, &p_input->ctrl_key);
            joy_open += pc_check_ctrl_joystick_use (joy_num, &p_input->shift_key);
            joy_open += pc_check_ctrl_joystick_use (joy_num, &p_input->break_key);
            joy_open += pc_check_ctrl_joystick_use (joy_num, &p_input->start_key);
            joy_open += pc_check_ctrl_joystick_use (joy_num, &p_input->select_key);
            joy_open += pc_check_ctrl_joystick_use (joy_num, &p_input->option_key);
            joy_open += pc_check_ctrl_joystick_use (joy_num, &p_input->reset_key);
        }
        
        if ( joy_open > 0 )
            pc_open_joystick ( joy_num ); 
        else if ( joy_open < 0 )
            pc_open_controller ( joy_num ); 

    } /* end for each possible physical joystick */
            

    /*
     * Make sure pokey knows if we use trackball
     *   Also, grab mouse if it is used
     */
    g_mouse_grabbed = 0;
    SDL_SetWindowMouseGrab(g_pc_if.screen, false);
    SDL_SetWindowRelativeMouseMode(g_pc_if.screen, false);
    for ( i = 0; i < MAX_CONTROLLER; ++i ) {

        if ( p_input->players[i].type == CTRLR_TYPE_NONE )
            continue;

        if ( p_input->players[i].mouse_enable &&
             p_input->players[i].type == CTRLR_TYPE_JOYSTICK ) {
            for ( dir = DIR_LEFT; dir < MAX_DIR; ++dir ) {
                t_input_common *p;
                for ( p = &p_input->players[i].stick[dir]; p; p = p->next )
                    if ( p->device[0] == DEV_MOUSE ||
                         (p->combo && p->device[1] == DEV_MOUSE) ) { g_mouse_grabbed = 1; break; }
            }
        }
        else if ( p_input->players[i].mouse_enable &&
                  p_input->players[i].type == CTRLR_TYPE_TRACKBALL ) {
            for ( dir = DIR_LEFT; dir < MAX_DIR; ++dir ) {
                t_input_common *p;
                for ( p = &p_input->players[i].trackball[dir]; p; p = p->next )
                    if ( p->device[0] == DEV_MOUSE ||
                         (p->combo && p->device[1] == DEV_MOUSE) ) { g_mouse_grabbed = 1; break; }
            }
        }
        else if ( p_input->players[i].mouse_enable &&
                  p_input->players[i].type == CTRLR_TYPE_PADDLES ) {
            t_input_common *p;
            for ( p = &p_input->players[i].paddles[0].left;  p; p = p->next )
                if ( p->device[0] == DEV_MOUSE ||
                     (p->combo && p->device[1] == DEV_MOUSE) ) { g_mouse_grabbed = 1; break; }
            for ( p = &p_input->players[i].paddles[0].right; p; p = p->next )
                if ( p->device[0] == DEV_MOUSE ||
                     (p->combo && p->device[1] == DEV_MOUSE) ) { g_mouse_grabbed = 1; break; }
            for ( p = &p_input->players[i].paddles[1].left;  p; p = p->next )
                if ( p->device[0] == DEV_MOUSE ||
                     (p->combo && p->device[1] == DEV_MOUSE) ) { g_mouse_grabbed = 1; break; }
            for ( p = &p_input->players[i].paddles[1].right; p; p = p->next )
                if ( p->device[0] == DEV_MOUSE ||
                     (p->combo && p->device[1] == DEV_MOUSE) ) { g_mouse_grabbed = 1; break; }
        }

        pokey_set_controller_type ( i, CTRL_STICK );
        if ( p_input->players[i].type == CTRLR_TYPE_TRACKBALL &&
             p_cfg->general.machine == MACHINE_TYPE_5200 ) {
            pokey_set_controller_type ( i, CTRL_BALL );
        }

    } /* end for controller */

    if ( g_mouse_grabbed )
        pc_apply_mouse_grab();

    /* Re-wire PC inputs after honoring per-player mouse_enable. */
    input_set_controls();

    /*
     * Drop any input events queued while in the GUI (e.g. the gamepad fire
     * button used to launch this ROM) and force triggers released so that
     * press is not carried into the game.
     */
    pc_flush_input_events();
    gtia_reset_triggers();

    return 0;

} /* end pc_game_init */

/******************************************************************************
**  Function   :  pc_set_game_sound
**                            
**  Objective  :  This function sets up sound for emulator play
**
**  Parameters :  NONE
**                                                
**  return     :  0 on success, otherwise error
**      
******************************************************************************/
int pc_set_game_sound ( t_sound *p_sound ) {

    char msg[512];

    g_pc_if.audio_d.freq = p_sound->frequency;
    g_pc_if.audio_d.format = SDL_AUDIO_U8;
    //g_pc_if.audio_d.format = AUDIO_U16;
    //g_pc_if.audio_d.samples = p_sound->samples;
    g_pc_if.audio_d.channels = 1;
    //if (p_sound->use_callback)
    //    g_pc_if.audio_d.callback = pc_audio_callback;
    //else
    //    g_pc_if.audio_d.callback = NULL;
    //g_pc_if.audio_d.userdata = NULL;
    g_sound = *p_sound;

    if ( g_sound.enable ) {
        if (g_pc_if.audio_stream) {
            SDL_DestroyAudioStream(g_pc_if.audio_stream);
            g_pc_if.audio_stream = NULL;
        }
        g_pc_if.audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &g_pc_if.audio_d, NULL, NULL);
        //if ( (audio_id = SDL_OpenAudioDevice(NULL, 0, &g_pc_if.audio_d, &g_pc_if.audio_o, 0)) == 0 ){
        if ( !g_pc_if.audio_stream ){
            sprintf(msg, "pc_set_game_sound: Sound Initialization Error - %s", SDL_GetError());
            logger_log_message ( LOG_ERROR, msg, "" );
            return -1;
        }
    
        if ( !SDL_SetAudioStreamGain ( g_pc_if.audio_stream, g_sound.gain / 100.0f ) ) {
            sprintf(msg, "pc_set_game_sound: SetAudioStreamGain failed - %s", SDL_GetError());
            logger_log_message ( LOG_ERROR, msg, "" );
        }

        audio_id = SDL_GetAudioStreamDevice(g_pc_if.audio_stream);
        SDL_GetAudioDeviceFormat(audio_id, &g_pc_if.audio_o, &g_pc_if.audio_samples);

        {   /* Resolve an Auto (0) buffer_size to the per-backend default
             * before the thresholds are computed, then log the audio backend
             * SDL chose - mirrors the "Renderer - ..." video line.  On Android
             * this is typically openslES (older OS) or aaudio. */
            const char *drv = SDL_GetCurrentAudioDriver();

            g_sound.buffer_size =
                pc_resolve_sound_buffer_size(g_sound.buffer_size, drv);

            sprintf(msg, "Audio Driver - %s", drv ? drv : "unknown");
            logger_log_message(LOG_INFO, msg, "");
        }
        /*
         * Allocate a buffer for sound
         */
        g_aud_buf.play_pos = 0;
        g_aud_buf.write_pos = 0;
        //if (!(p_sound->use_callback)) {
            g_aud_buf.write_pos = 0;
        //}
        //else {
            g_aud_buf.write_pos = 0;
        //}
 
        pc_reset_sound_adj ( );
        pc_set_sound_freq ( g_pc_if.audio_d.freq );

        //memset(g_aud_buf.buf, g_pc_if.audio_o.silence, g_aud_buf.size*sizeof(char));
        memset(g_aud_buf.buf, SDL_GetSilenceValueForFormat(g_pc_if.audio_d.format), g_aud_buf.size*sizeof(char));

        /*
         * Log output Sound parameters
         */
        sprintf( msg, "pc_set_game_sound: Sound Initialized - Freq=%d, Samples=%d",
                 g_pc_if.audio_d.freq, g_sound.samples );
        logger_log_message ( LOG_INFO, msg, "" );

#ifdef SOUND_DEBUG
        strcat ( msg, "\n" );
        fwrite ( msg, sizeof(char), strlen(msg), fp );
#endif

        pokey_sound_update_freq ((Uint16)g_pc_if.audio_d.freq, SDL_GetSilenceValueForFormat(g_pc_if.audio_d.format));
        //Pokey_sound_update_freq (FREQ_17_APPROX, (Uint16)g_pc_if.audio_o.freq);

        SDL_ResumeAudioDevice(audio_id);
        //SDL_PauseAudioDevice(audio_id,0);
    }

    /*
     * If sound disabled, go ahead and allocate 1 so we 
     *    process events in pokey_sound_process
     */
    else {
        free(g_aud_buf.buf);
        g_aud_buf.fill_size = 1;
        g_aud_buf.curr_fill_size = 1;
        g_aud_buf.buf = malloc(1 * sizeof(unsigned char));
    }

    return 0;

} /* end pc_set_game_sound */

/******************************************************************************
**  Function   :  pc_set_sound_gain
**
**  Objective  :  Apply an output volume/gain (percent, 0..KCFG_SND_GAIN_MAX) to
**                the currently open audio stream.  Used by the GUI so a gain
**                change takes effect on the still-open stream without a re-init.
**
**  Parameters :  gain - volume percent (100 = unchanged)
**
**  return     :  NONE
**
******************************************************************************/
void pc_set_sound_gain ( int gain ) {

    if ( gain < 0 )
        gain = 0;
    if ( gain > KCFG_SND_GAIN_MAX )
        gain = KCFG_SND_GAIN_MAX;

    g_sound.gain = gain;

    if ( g_sound.enable && g_pc_if.audio_stream )
        SDL_SetAudioStreamGain ( g_pc_if.audio_stream, gain / 100.0f );

} /* end pc_set_sound_gain */

/******************************************************************************
**  Function   :  pc_update_output_rgb
**
**  Objective  :  This function maps the configured palette into ARGB8888
**                pixels for non-NTSC output blitting.
**
**  Parameters :  palette - SDL palette colors
**
**  return     :  NONE
**
******************************************************************************/
static void pc_update_output_rgb ( SDL_Color *palette ) {

    int i;
    const SDL_PixelFormatDetails *f_details =
        SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_ARGB8888);

    if ( !f_details )
        return;

    for ( i = 0; i < 256; ++i ) {
        g_pc_if.output_rgb[i] = SDL_MapRGB(f_details, NULL,
                                           palette[i].r,
                                           palette[i].g,
                                           palette[i].b);
    }

} /* end pc_update_output_rgb */

/******************************************************************************
**  Function   :  pc_set_palette
**                            
**  Objective  :  This function opens up and assigns the palette
**
**  Parameters :  NONE
**                                                
**  return     :  0
**      
******************************************************************************/
int pc_set_palette ( t_video *p_video, SDL_Color *palette, unsigned int *rgb ) {

    int i;

    for ( i = 0; i < 256; ++i ) {
        palette[i].r = (p_video->palette[i] >> 16) & 0xff;
        palette[i].g = (p_video->palette[i] >> 8) & 0xff;
        palette[i].b = p_video->palette[i] & 0xff;
        palette[i].a = 0xff;
        if ( rgb )
            rgb[i] = i;
    }

    pc_update_output_rgb ( palette );

    return 0;

} /* end pc_set_palette */

SDL_ScaleMode pc_texture_scale_mode ( int texture_scaling ) {

    switch ( texture_scaling ) {
        case TEXTURE_SCALING_SMOOTH:
            return SDL_SCALEMODE_LINEAR;
        default:
            return SDL_SCALEMODE_NEAREST;
    }
}

/******************************************************************************
**  Function   :  pc_reinit_video_surfaces
**
**  Objective  :  Reinitialises video surfaces, color mapping, the NTSC
**                emulator, and GTIA screen pointer without touching the
**                window or renderer.  Called by pc_set_game_video() and
**                also directly from the GUI when the user toggles NTSC mode
**                or changes palette/NTSC parameters in the Video settings menu.
**
**  Parameters :  p_video     - pointer to user video settings
**                tv_standard - emulated system NTSC or PAL
**
**  return     :  NONE
**
******************************************************************************/
void pc_reinit_video_surfaces ( t_video *p_video, int tv_standard ) {

    int vid_w, vid_h;
    int surface_h;

    /*
     * GTIA always writes 8-bit Atari color indices.  color_rgb[] is the
     * identity map used by GTIA; output_rgb[] is the ARGB8888 lookup table
     * used when presenting without NTSC filtering.
     */
    pc_set_palette ( p_video, g_pc_if.color_palette, g_pc_if.color_rgb );

    /*
     * Determine needed video resolution
     */
    if ( p_video->ntsc_artifacts ) {
        vid_w = p_video->wide_playfield ? (ATARI_NTSC_OUT_WIDTH(342)) : atari_ntsc_min_out_width;
        vid_h = 480;
    }
    else {
        vid_w = (p_video->wide_playfield ? 342:320);
        vid_h = (tv_standard==PAL ? 240 : 240);
    }

    /*
     * Setup Copying stuff
     */
    if ( p_video->ntsc_artifacts ) {
        g_pc_if.valid_video_rect.x = 0;
        g_pc_if.valid_video_rect.y = 0;
        g_pc_if.valid_video_rect.w = vid_w;
        g_pc_if.valid_video_rect.h = vid_h;
    }
    else {
        g_pc_if.valid_video_rect.x = (p_video->wide_playfield ? 21 : 32);
        g_pc_if.valid_video_rect.y = 0;
        g_pc_if.valid_video_rect.w = (p_video->wide_playfield ? 342 : 320);
        g_pc_if.valid_video_rect.h = (tv_standard==PAL ? 240 : 240);
    }

    g_pc_if.ntsc_start_x = (p_video->wide_playfield ? 21 : 32);

    /*
     * Create a line at least double scale
     *   line needs to be 448 pixels
     *  384 (wide) + 32 (hscroll) + 32 (pl width changes)
     */
    surface_h = (tv_standard==PAL ? 240 : 240);
    if ( !g_pc_if.surface ||
         g_pc_if.surface->w != 448 ||
         g_pc_if.surface->h != surface_h ||
         g_pc_if.surface->format != SDL_PIXELFORMAT_INDEX8 ) {
        SDL_DestroySurface ( g_pc_if.surface );
        g_pc_if.surface = SDL_CreateSurface(448, surface_h, SDL_PIXELFORMAT_INDEX8);
    }

    if ( g_pc_if.surface && SDL_GetSurfacePalette(g_pc_if.surface) ) {
        SDL_SetPaletteColors(SDL_GetSurfacePalette(g_pc_if.surface),
                             g_pc_if.color_palette, 0, 256);
    }

    if ( p_video->ntsc_artifacts ) {
        if ( !g_pc_if.ntsc_surface ||
             g_pc_if.ntsc_surface->w != vid_w ||
             g_pc_if.ntsc_surface->h != vid_h ||
             g_pc_if.ntsc_surface->format != SDL_PIXELFORMAT_ARGB8888 ) {
            SDL_DestroySurface ( g_pc_if.ntsc_surface );
            g_pc_if.ntsc_surface = SDL_CreateSurface(vid_w, vid_h,
                                                     SDL_PIXELFORMAT_ARGB8888);
        }
        SDL_DestroySurface ( g_pc_if.output_surface );
        g_pc_if.output_surface = NULL;
    }
    else {
        if ( !g_pc_if.output_surface ||
             g_pc_if.output_surface->w != g_pc_if.valid_video_rect.w ||
             g_pc_if.output_surface->h != g_pc_if.valid_video_rect.h ||
             g_pc_if.output_surface->format != SDL_PIXELFORMAT_ARGB8888 ) {
            SDL_DestroySurface ( g_pc_if.output_surface );
            g_pc_if.output_surface =
                SDL_CreateSurface(g_pc_if.valid_video_rect.w,
                                  g_pc_if.valid_video_rect.h,
                                  SDL_PIXELFORMAT_ARGB8888);
        }
        SDL_DestroySurface ( g_pc_if.ntsc_surface );
        g_pc_if.ntsc_surface = NULL;
    }

    /*
     * Create texture, need format to match surface that blits here
     */
    if ( g_pc_if.screen_texture )
        SDL_DestroyTexture ( g_pc_if.screen_texture );
    g_pc_if.screen_texture = SDL_CreateTexture(g_pc_if.renderer,
                                      SDL_PIXELFORMAT_ARGB8888,
                                   SDL_TEXTUREACCESS_STREAMING,
                 util_next_power_of_two (vid_w),
                 util_next_power_of_two (vid_h) );
    SDL_SetTextureBlendMode(g_pc_if.screen_texture, SDL_BLENDMODE_NONE);
    SDL_SetTextureScaleMode(g_pc_if.screen_texture,
            pc_texture_scale_mode(p_video->texture_scaling));

    /*
     * Setup NTSC filtering if on
     */
    if ( g_pc_if.ntsc_emu ) {
        free ( g_pc_if.ntsc_emu );
        g_pc_if.ntsc_emu = 0;
    }
    if ( p_video->ntsc_artifacts ) {
        g_pc_if.ntsc_emu = (atari_ntsc_t*) malloc( sizeof (atari_ntsc_t) );
        atari_ntsc_init( g_pc_if.ntsc_emu, &p_video->ntsc_setup );
    }

    /*
     * Setup GTIA drawing surface
     */
    if ( g_pc_if.surface ) {
        gtia_set_screen ( g_pc_if.surface->pixels, g_pc_if.color_rgb,
                          g_pc_if.surface->pitch,
                          SDL_BYTESPERPIXEL(g_pc_if.surface->format), 0 );
    }

    /*
     * Setup drawing parameters
     */
    g_pc_if.draw_video_rect.x = 0;
    g_pc_if.draw_video_rect.y = 0;
    g_pc_if.draw_video_rect.w = g_pc_if.valid_video_rect.w;
    g_pc_if.draw_video_rect.h = g_pc_if.valid_video_rect.h;

    if ( p_video->ntsc_artifacts && g_pc_if.ntsc_surface ) {
        g_pc_if.draw_pixel_start = g_pc_if.ntsc_surface->pixels;
        g_pc_if.draw_pitch = g_pc_if.ntsc_surface->pitch;
        g_pc_if.draw_bpp = SDL_BYTESPERPIXEL(g_pc_if.ntsc_surface->format);
    }
    else if ( g_pc_if.output_surface ) {
        g_pc_if.draw_pixel_start = g_pc_if.output_surface->pixels;
        g_pc_if.draw_pitch = g_pc_if.output_surface->pitch;
        g_pc_if.draw_bpp = SDL_BYTESPERPIXEL(g_pc_if.output_surface->format);
    }
    else {
        g_pc_if.draw_pixel_start = NULL;
        g_pc_if.draw_pitch = 0;
        g_pc_if.draw_bpp = 0;
    }

} /* end pc_reinit_video_surfaces */

/******************************************************************************
**  Function   :  pc_native_video_size
**
**  Objective  :  Compute the native (unscaled) video resolution implied by the
**                current ntsc_artifacts / wide_playfield / tv_standard settings.
**                Single source of truth shared by pc_set_game_video, the window
**                size-limit helper and the Fit-Window aspect derivation.
**
**  Parameters :  p_video     - pointer to user video settings
**                tv_standard - emulated system NTSC or PAL
**                w, h        - out: native width / height
**
**  return     :  NONE
**
******************************************************************************/
void pc_native_video_size ( t_video *p_video, int tv_standard, int *w, int *h ) {

    if ( !p_video )
        return;

    if ( p_video->ntsc_artifacts ) {
        *w = p_video->wide_playfield ? (ATARI_NTSC_OUT_WIDTH(342)) : atari_ntsc_min_out_width;
        *h = 480;
    }
    else {
        *w = (p_video->wide_playfield ? 342:320);
        *h = (tv_standard==PAL ? 240 : 240);
    }

} /* end pc_native_video_size */

/******************************************************************************
**  Function   :  pc_get_window_size_limits
**
**  Objective  :  Provide slider bounds for the Fixed Window width/height
**                controls: minimum = native video size, maximum = current
**                display mode size (falls back to a large cap if unavailable).
**
**  Parameters :  cfg                    - global config
**                min_w, min_h           - out: minimum width / height
**                max_w, max_h           - out: maximum width / height
**
**  return     :  NONE
**
******************************************************************************/
void pc_get_window_size_limits ( t_config *cfg, int *min_w, int *min_h,
                                 int *max_w, int *max_h ) {

    const SDL_DisplayMode *mode = NULL;

    pc_native_video_size ( &cfg->video, cfg->machine.tv_standard, min_w, min_h );

    if ( g_pc_if.screen ) {
        mode = SDL_GetCurrentDisplayMode ( SDL_GetDisplayForWindow ( g_pc_if.screen ) );
    }
    else {
        int num_displays;
        SDL_DisplayID *displays = SDL_GetDisplays ( &num_displays );
        if ( displays && num_displays > 0 )
            mode = SDL_GetCurrentDisplayMode ( displays[0] );
        SDL_free ( displays );
    }

    if ( mode ) {
        *max_w = mode->w;
        *max_h = mode->h;
    }
    else {
        *max_w = 3840;
        *max_h = 2160;
    }

    /* Guard against an inverted range if the display mode is somehow smaller */
    if ( *max_w < *min_w ) *max_w = *min_w;
    if ( *max_h < *min_h ) *max_h = *min_h;

} /* end pc_get_window_size_limits */

/******************************************************************************
**  Function   :  pc_set_game_video
**
**  Objective  :  This function sets up video for emulator play
**
**  Parameters :  p_video     - pointer to user video settings
**                tv_standard - emulated system NTSC or PAL
**
**  return     :  pointer to screen
**
******************************************************************************/
SDL_Window * pc_set_game_video ( t_video *p_video, int tv_standard ) {

    int vid_w, vid_h;
    char msg[256];
    unsigned long long mask;

    /*
     * Determine needed video resolution
     */
    pc_native_video_size ( p_video, tv_standard, &vid_w, &vid_h );

    if (p_video->width < vid_w)
        p_video->width = vid_w;
    if (p_video->height < vid_h)
        p_video->height = vid_h;

    /* For Android, we always use Fullscreen */
#ifdef __ANDROID__
    const SDL_DisplayMode *mode = SDL_GetCurrentDisplayMode(SDL_GetPrimaryDisplay());
    p_video->fullscreen = 1;
    if ( mode ) {
        p_video->width = mode->w;
        p_video->height = mode->h;
    }
    mask = KCFG_VIDEO_FULLSCREEN | KCFG_VIDEO_WIDTH | KCFG_VIDEO_HEIGHT;
    config_save_video ( config_get_global_table(), "All", mask );
#endif

    /*
     * Figure Out Actual
     */
    pc_set_window(p_video, p_video->width, p_video->height);
    pc_get_renderer(p_video->vsync);
    SDL_ResetHints();
#ifdef __ANDROID__
    /* SDL_ResetHints() above reverts every hint to its default, including the
     * Back-button trap, touch-mouse suppression and orientation lock set in
     * pc_init.  Re-apply them so launching a ROM does not break TV remote Back,
     * Android touch scrolling, or the fixed-landscape lock. */
    pc_set_android_hints();
#endif
    {
        SDL_RendererLogicalPresentation sdl_pres;
        switch (p_video->window_scaling) {
            case WINDOW_SCALING_STRETCH:
                sdl_pres = SDL_LOGICAL_PRESENTATION_STRETCH;
                break;
            case WINDOW_SCALING_FIT_WINDOW:
                sdl_pres = SDL_LOGICAL_PRESENTATION_STRETCH;
                break;
            default: /* WINDOW_SCALING_LETTERBOX */
                sdl_pres = SDL_LOGICAL_PRESENTATION_LETTERBOX;
                break;
        }
        SDL_SetRenderLogicalPresentation(g_pc_if.renderer, vid_w, vid_h, sdl_pres);
        if (p_video->window_scaling == WINDOW_SCALING_FIT_WINDOW) {
            int win_w, win_h;
            SDL_GetWindowSize(g_pc_if.screen, &win_w, &win_h);
            int snapped_h = (win_w * vid_h) / vid_w;
            SDL_SetWindowSize(g_pc_if.screen, win_w, snapped_h);
        }
    }

    //SDL_ShowCursor(SDL_DISABLE);
    SDL_HideCursor();

    /*
     * Setup surfaces and GTIA
     */
    pc_reinit_video_surfaces(p_video, tv_standard);

    {
        int w, h;
        const SDL_DisplayMode *tmp_mode;
        SDL_GetWindowSize(g_pc_if.screen, &w, &h);
        tmp_mode = SDL_GetWindowFullscreenMode(g_pc_if.screen);
        if (tmp_mode) {
            sprintf( msg, "pc_set_game_video: Video Initialized - %d x %d x %d",
                    w, h, SDL_BITSPERPIXEL(tmp_mode->format) );
            logger_log_message ( LOG_INFO, msg, "" );
        }
    }

    /* Resolve and initialize a precompiled SDL GPU shader bundle if enabled. */
    pc_set_shader ( p_video );

    /* Ensure letterbox bars are always black, regardless of prior GUI state */
    SDL_SetRenderDrawColor(g_pc_if.renderer, 0, 0, 0, 255);

    return g_pc_if.screen;

} /* end pc_set_game_video */

/******************************************************************************
**  Function   :  pc_set_window
**                            
**  Objective  :  This function creates or updates the main window
**
**  Parameters :  NONE
**                                                
**  return     :  0 on success, otherwise failure
**      
******************************************************************************/
SDL_Window * pc_set_window ( t_video *p_video,  int w, int h ) {

    int flags;
    int w_valid;
    SDL_DisplayID display = 0;
    char msg[50];
    char file[1101] = "kat5200.bmp";
    const SDL_DisplayMode *mode;

    /*
     * Don't allow it to set greater than current mode
     */
    if (g_pc_if.screen == 0) {
        /* Get primary display ID for initial setup */
        int num_displays;
        SDL_DisplayID *displays = SDL_GetDisplays(&num_displays);
        display = (displays && num_displays > 0) ? displays[0] : 0;
        SDL_free(displays);
    } else {
        display = SDL_GetDisplayForWindow(g_pc_if.screen);
    }
    mode = SDL_GetCurrentDisplayMode(display);
    if (mode) {
        if (mode->h < h)
            h = mode->h;
        if (mode->w < w)
            w = mode->w;
    }

    /* If this is initial setup */
    if (g_pc_if.screen == 0) {

        w_valid = pc_window_position_in_bounds(p_video->x, p_video->y);

        flags = p_video->fullscreen ? SDL_WINDOW_FULLSCREEN : 
            (p_video->fixed_window ? 0 : SDL_WINDOW_RESIZABLE);

        sprintf ( msg, "kat5200 %s", VERSION );
        /*
         * The window-icon bitmap is a static app asset shipped beside the exe
         * (Windows), not part of live data.  Other platforms still seed it into
         * the live data dir, so resolve against the program dir there.
         */
#ifdef _WIN32
        util_set_file_to_app_dir ( file );
#else
        util_set_file_to_program_dir ( file );
#endif

        g_pc_if.screen = SDL_CreateWindow(msg,
                      w, h,
                      flags);

        SDL_SetWindowPosition(g_pc_if.screen, 
            w_valid ? p_video->x : SDL_WINDOWPOS_UNDEFINED,
            w_valid ? p_video->y : SDL_WINDOWPOS_UNDEFINED);

        SDL_Surface *icon = SDL_LoadBMP(file);
        SDL_SetWindowIcon(g_pc_if.screen, icon);
        SDL_DestroySurface(icon);  /* SetWindowIcon copies pixels; surface is ours to free */

        /* Legacy GL shader detection removed; SDL GPU bundles handle shaders. */
        g_pc_if.shaders_supported = 0;

    }

    /* Else we need to update all */
    else {

        if (p_video->fullscreen) {
            SDL_SetWindowFullscreen(g_pc_if.screen, SDL_WINDOW_FULLSCREEN);
        }
        else {
            SDL_SetWindowFullscreen(g_pc_if.screen, 0);
            SDL_SetWindowSize(g_pc_if.screen, w, h);
#if SDL_VERSION_ATLEAST(2,0,5)
            SDL_SetWindowResizable(g_pc_if.screen, 
                    p_video->fixed_window ? false : true);
#endif
        }
        SDL_RenderClear(g_pc_if.renderer);
    }

    return g_pc_if.screen;

} /* end pc_set_window */

/******************************************************************************
**  Function   :  pc_set_shader
**                            
**  Objective  :  Resolve and initialize a precompiled SDL GPU shader bundle
**                for gameplay rendering.  Any failure leaves the normal SDL
**                renderer path active.
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
void pc_set_shader ( t_video *p_video ) {

    char bundle_dir[KAT_PATH_MAX];
    char msg[768];
    t_toml_node *rom_tbl;
    SDL_GPUTextureFormat color_format;

    pc_free_shader_runtime();

    if ( !p_video || !p_video->shader || !strlen(p_video->shader) ||
            !strcmp(p_video->shader, SHADER_NONE) )
        return;

    if ( !g_pc_if.gpu_device || !g_pc_if.gpu_shader_formats ) {
        pc_log_shader_message ( "SDL GPU shader unavailable",
                "renderer did not expose a GPU device" );
        return;
    }

    if ( pc_resolve_shader_bundle_dir(p_video->shader, bundle_dir,
                sizeof(bundle_dir)) ) {
        snprintf ( msg, sizeof(msg), "No precompiled shader bundle found for %s",
                p_video->shader );
        pc_log_shader_message ( "SDL GPU shader fallback", msg );
        return;
    }

    if ( shader_bundle_load(bundle_dir, &g_pc_if.shader_bundle) ) {
        snprintf ( msg, sizeof(msg), "Failed to load shader bundle metadata: %s",
                bundle_dir );
        pc_log_shader_message ( "SDL GPU shader fallback", msg );
        pc_free_shader_runtime();
        return;
    }

    config_load_shader_bundle_info ( config_get_global_table(),
            &g_pc_if.shader_bundle, 1 );
    rom_tbl = config_get_rom_table ();
    if ( rom_tbl )
        config_load_shader_bundle_info ( rom_tbl, &g_pc_if.shader_bundle, 0 );

    color_format = SDL_GetGPUSwapchainTextureFormat ( g_pc_if.gpu_device,
            g_pc_if.screen );
    if ( shader_gpu_graph_create(g_pc_if.gpu_device, &g_pc_if.shader_bundle,
                color_format, g_pc_if.draw_video_rect.w,
                g_pc_if.draw_video_rect.h, &g_pc_if.shader_graph) ) {
        pc_log_shader_message ( "SDL GPU shader fallback",
                g_pc_if.shader_graph.error );
        pc_free_shader_runtime();
        return;
    }

    g_pc_if.use_shader = 1;
    snprintf ( msg, sizeof(msg), "SDL GPU shader enabled - %s (%s, %d pass%s)",
            g_pc_if.shader_bundle.id ? g_pc_if.shader_bundle.id : p_video->shader,
            shader_gpu_format_name(g_pc_if.shader_graph.pipeline.shader_format),
            g_pc_if.shader_bundle.num_passes,
            g_pc_if.shader_bundle.num_passes == 1 ? "" : "es" );
    logger_log_message ( LOG_INFO, msg, "" );

} /* end pc_set_shader */

int pc_get_shaders_supported ( void ) {

    return g_pc_if.shaders_supported;

} /* end pc_get_shaders_supported */

SDL_GPUDevice * pc_get_gpu_device ( void ) {

    return g_pc_if.gpu_device;

} /* end pc_get_gpu_device */

SDL_GPUShaderFormat pc_get_gpu_shader_formats ( void ) {

    return g_pc_if.gpu_shader_formats;

} /* end pc_get_gpu_shader_formats */

static int pc_gpu_renderer_available ( void ) {

    int num = SDL_GetNumRenderDrivers();

    for ( int i = 0; i < num; ++i ) {
        const char *name = SDL_GetRenderDriver ( i );
        if ( name && !strcmp(name, "gpu") )
            return 1;
    }

    return 0;

} /* end pc_gpu_renderer_available */

void pc_get_shader_live_status ( char *status, size_t status_size ) {

    t_config *cfg = config_get_global_cfg ();
    const char *shader = cfg ? cfg->video.shader : NULL;
    int want_shader = shader && strlen(shader) && strcmp(shader, SHADER_NONE);

    if ( !status || !status_size )
        return;

    status[0] = '\0';

    if ( !want_shader ) {
        snprintf ( status, status_size, "Live: shader disabled." );
        return;
    }

    if ( g_pc_if.use_shader ) {
        snprintf ( status, status_size, "Live: active (%s, %d pass%s).",
                shader_gpu_format_name(g_pc_if.shader_graph.pipeline.shader_format),
                g_pc_if.shader_bundle.num_passes,
                g_pc_if.shader_bundle.num_passes == 1 ? "" : "es" );
        return;
    }

    if ( !g_pc_if.renderer ) {
        snprintf ( status, status_size, "Live: pending; renderer not ready." );
        return;
    }

    if ( !g_pc_if.gpu_device ) {
        if ( pc_gpu_renderer_available() )
            snprintf ( status, status_size,
                    "Live: inactive; restart for GPU renderer." );
        else
            snprintf ( status, status_size,
                    "Live: unavailable; SDL GPU renderer not listed." );
        return;
    }

    if ( !g_pc_if.gpu_shader_formats ) {
        snprintf ( status, status_size,
                "Live: unavailable; GPU renderer has no shader formats." );
        return;
    }

    snprintf ( status, status_size, "Live: pending; save to apply." );

} /* end pc_get_shader_live_status */

/******************************************************************************
**  Function   :  pc_resolve_shader_bundle
**
**  Objective  :  Public wrapper over the bundle-directory resolver so the GUI
**                can validate a shader selection string (shipped id or
**                absolute external bundle directory).
**
**  return     :  0 and fills bundle_dir on success, non-zero on failure
******************************************************************************/
int pc_resolve_shader_bundle ( const char *shader_name, char *bundle_dir,
        size_t bundle_dir_size ) {

    return pc_resolve_shader_bundle_dir ( shader_name, bundle_dir,
            bundle_dir_size );

} /* end pc_resolve_shader_bundle */

static int pc_path_is_dir ( const char *path ) {

    struct stat info;

    if ( !path || !strlen(path) )
        return 0;

    /* Probe with libc stat (not SDL_GetPathInfo) so this agrees with how the
     * bundle is actually opened (pc_shader_bundle_exists/toml_parse_file use
     * fopen).  SDL's filesystem layer resolves relative paths against an app
     * base on Android, so SDL_GetPathInfo accepted the relative "shaders/
     * bundles" root -- but fopen later failed on that relative path (cwd is
     * "/"), so enumeration found zero bundles.  Using stat here makes the
     * relative root candidate fail so resolution falls through to the absolute
     * program-path candidate, which both SDL_GlobDirectory and fopen handle. */
    if ( stat(path, &info) != 0 )
        return 0;

    return S_ISDIR(info.st_mode);

} /* end pc_path_is_dir */

static int pc_resolve_shader_bundle_root ( char *root, size_t root_size ) {

    char candidate[KAT_PATH_MAX];
    char *program_path;

    if ( !root || root_size == 0 )
        return -1;

    if ( pc_path_is_dir("shaders/bundles") ) {
        snprintf ( root, root_size, "%s", "shaders/bundles" );
        return 0;
    }

    program_path = util_create_program_path_alloc ( "shaders/bundles" );
    if ( program_path ) {
        if ( pc_path_is_dir(program_path) ) {
            snprintf ( root, root_size, "%s", program_path );
            free ( program_path );
            return 0;
        }
        free ( program_path );
    }

    snprintf ( candidate, sizeof(candidate), "%s", "shaders/bundles" );
    util_set_file_to_app_dir ( candidate );
    if ( pc_path_is_dir(candidate) ) {
        snprintf ( root, root_size, "%s", candidate );
        return 0;
    }

    program_path = util_create_seed_path_alloc ( "shaders/bundles" );
    if ( program_path ) {
        if ( pc_path_is_dir(program_path) ) {
            snprintf ( root, root_size, "%s", program_path );
            free ( program_path );
            return 0;
        }
        free ( program_path );
    }

    if ( snprintf(candidate, sizeof(candidate), "%s/shaders/bundles",
                KAT_DATADIR) < (int) sizeof(candidate) &&
            pc_path_is_dir(candidate) ) {
        snprintf ( root, root_size, "%s", candidate );
        return 0;
    }

    if ( snprintf(candidate, sizeof(candidate),
                "%s/resources/shaders/bundles", KAT_SOURCE_DIR) <
                (int) sizeof(candidate) && pc_path_is_dir(candidate) ) {
        snprintf ( root, root_size, "%s", candidate );
        return 0;
    }

    return -1;

} /* end pc_resolve_shader_bundle_root */

/******************************************************************************
**  Function   :  pc_enumerate_shader_bundles
**
**  Objective  :  Fill ids[] with the directory name of each shipped bundle
**                (a subdirectory of the bundles root that contains a
**                bundle.toml).  Names are heap-allocated; caller frees with
**                free().
**
**  return     :  number of bundle ids written
******************************************************************************/
int pc_enumerate_shader_bundles ( char **ids, int max ) {

    char root[KAT_PATH_MAX];
    char candidate[KAT_PATH_MAX];
    char **entries;
    int count = 0;
    int found = 0;

    if ( !ids || max <= 0 )
        return 0;

    if ( pc_resolve_shader_bundle_root(root, sizeof(root)) )
        return 0;

    entries = SDL_GlobDirectory ( root, "*", 0, &count );
    if ( !entries )
        return 0;

    for ( int i = 0; i < count && found < max; ++i ) {
        if ( !entries[i] )
            continue;

        if ( snprintf(candidate, sizeof(candidate), "%s/%s", root,
                    entries[i]) >= (int) sizeof(candidate) )
            continue;

        if ( !pc_shader_bundle_exists(candidate) )
            continue;

        ids[found] = malloc ( strlen(entries[i]) + 1 );
        if ( !ids[found] )
            break;
        strcpy ( ids[found], entries[i] );
        found++;
    }

    SDL_free ( entries );
    return found;

} /* end pc_enumerate_shader_bundles */

/******************************************************************************
**  Menu-owned GPU shader preview.  Renders the staged bundle (with its
**  current parameter values) into an offscreen GPU texture that is wrapped as
**  an SDL_Texture so the existing Nuklear preview panel can sample it.  This
**  pipeline/resource set is intentionally separate from the gameplay shader
**  runtime so editing in the menu never disturbs live playback.  Any failure
**  tears the preview down and returns NULL so the caller can fall back to the
**  unshaded CPU preview.
******************************************************************************/
static struct {
    t_shader_gpu_graph graph;
    SDL_GPUTexture *target;
    SDL_Texture *sdl_texture;
    char bundle_id[128];
    int target_w;
    int target_h;
    int source_w;
    int source_h;
    Uint32 frame_count;
    int valid;
} s_shader_preview = { 0 };

void pc_shader_preview_free ( void ) {

    if ( s_shader_preview.sdl_texture ) {
        SDL_DestroyTexture ( s_shader_preview.sdl_texture );
        s_shader_preview.sdl_texture = NULL;
    }
    if ( s_shader_preview.target && g_pc_if.gpu_device ) {
        SDL_ReleaseGPUTexture ( g_pc_if.gpu_device, s_shader_preview.target );
        s_shader_preview.target = NULL;
    }
    shader_gpu_graph_free ( &s_shader_preview.graph );
    s_shader_preview.bundle_id[0] = '\0';
    s_shader_preview.target_w = 0;
    s_shader_preview.target_h = 0;
    s_shader_preview.source_w = 0;
    s_shader_preview.source_h = 0;
    s_shader_preview.valid = 0;

} /* end pc_shader_preview_free */

static int pc_shader_preview_build ( t_shader_bundle *bundle, int source_w,
        int source_h, int target_w, int target_h, char *err,
        size_t err_size ) {

#ifndef SDL_PROP_TEXTURE_CREATE_GPU_TEXTURE_POINTER
    (void) bundle;
    (void) source_w;
    (void) source_h;
    (void) target_w;
    (void) target_h;

    pc_shader_preview_free ();
    snprintf ( err, err_size,
            "live shader preview requires SDL GPU texture wrapping" );
    return -1;
#else
    SDL_GPUTextureCreateInfo tex_info;
    SDL_PropertiesID props;

    pc_shader_preview_free ();

    SDL_zero ( tex_info );
    tex_info.type = SDL_GPU_TEXTURETYPE_2D;
    tex_info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
    tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET |
                     SDL_GPU_TEXTUREUSAGE_SAMPLER;
    tex_info.width = (Uint32) target_w;
    tex_info.height = (Uint32) target_h;
    tex_info.layer_count_or_depth = 1;
    tex_info.num_levels = 1;
    tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;

    s_shader_preview.target = SDL_CreateGPUTexture ( g_pc_if.gpu_device,
            &tex_info );
    if ( !s_shader_preview.target ) {
        snprintf ( err, err_size, "preview target: %s", SDL_GetError() );
        return -1;
    }

    if ( shader_gpu_graph_create(g_pc_if.gpu_device, bundle,
                SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, source_w, source_h,
                &s_shader_preview.graph) ) {
        snprintf ( err, err_size, "%s", s_shader_preview.graph.error );
        pc_shader_preview_free ();
        return -1;
    }

    if ( shader_gpu_graph_set_sizes(&s_shader_preview.graph, source_w, source_h,
                target_w, target_h) ) {
        snprintf ( err, err_size, "%s", s_shader_preview.graph.error );
        pc_shader_preview_free ();
        return -1;
    }

    props = SDL_CreateProperties ();
    if ( !props ) {
        snprintf ( err, err_size, "preview props: %s", SDL_GetError() );
        pc_shader_preview_free ();
        return -1;
    }
    SDL_SetPointerProperty ( props, SDL_PROP_TEXTURE_CREATE_GPU_TEXTURE_POINTER,
            s_shader_preview.target );
    SDL_SetNumberProperty ( props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
            SDL_PIXELFORMAT_RGBA32 );
    SDL_SetNumberProperty ( props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER,
            SDL_TEXTUREACCESS_STATIC );
    SDL_SetNumberProperty ( props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
            target_w );
    SDL_SetNumberProperty ( props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
            target_h );
    s_shader_preview.sdl_texture = SDL_CreateTextureWithProperties (
            g_pc_if.renderer, props );
    SDL_DestroyProperties ( props );

    if ( !s_shader_preview.sdl_texture ) {
        snprintf ( err, err_size, "preview wrap: %s", SDL_GetError() );
        pc_shader_preview_free ();
        return -1;
    }

    SDL_SetTextureBlendMode ( s_shader_preview.sdl_texture, SDL_BLENDMODE_NONE );
    SDL_SetTextureScaleMode ( s_shader_preview.sdl_texture, SDL_SCALEMODE_LINEAR );

    snprintf ( s_shader_preview.bundle_id, sizeof(s_shader_preview.bundle_id),
            "%s", bundle->id ? bundle->id : "" );
    s_shader_preview.target_w = target_w;
    s_shader_preview.target_h = target_h;
    s_shader_preview.source_w = source_w;
    s_shader_preview.source_h = source_h;
    s_shader_preview.frame_count = 0;
    s_shader_preview.valid = 1;

    return 0;
#endif

} /* end pc_shader_preview_build */

SDL_Texture * pc_shader_preview_update ( t_shader_bundle *bundle,
        char *err, size_t err_size ) {

    SDL_GPUCommandBuffer *command_buffer;
    SDL_GPUViewport viewport;
    int source_w, source_h, target_w, target_h, scale;

    if ( err && err_size )
        err[0] = '\0';

    if ( !bundle || bundle->num_passes < 1 ) {
        if ( err ) snprintf ( err, err_size, "shader bundle required" );
        pc_shader_preview_free ();
        return NULL;
    }

    if ( !g_pc_if.gpu_device || !g_pc_if.renderer ) {
        if ( err ) snprintf ( err, err_size,
                "live preview needs the SDL GPU renderer" );
        pc_shader_preview_free ();
        return NULL;
    }

    if ( !g_pc_if.draw_pixel_start ) {
        if ( err ) snprintf ( err, err_size, "no video frame to preview" );
        return s_shader_preview.valid ? s_shader_preview.sdl_texture : NULL;
    }

    source_w = g_pc_if.draw_video_rect.w;
    source_h = g_pc_if.draw_video_rect.h;
    if ( source_w <= 0 || source_h <= 0 )
        return NULL;

    /* Render the preview at an integer upscale so CRT-style passes show their
       scanline/mask detail; cap the target so we never allocate huge RTs. */
    scale = 3;
    while ( scale > 1 && source_h * scale > 900 )
        scale--;
    target_w = source_w * scale;
    target_h = source_h * scale;

    if ( !s_shader_preview.valid ||
            s_shader_preview.source_w != source_w ||
            s_shader_preview.source_h != source_h ||
            s_shader_preview.target_w != target_w ||
            s_shader_preview.target_h != target_h ||
            strcmp(s_shader_preview.bundle_id, bundle->id ? bundle->id : "") ) {
        if ( pc_shader_preview_build(bundle, source_w, source_h, target_w,
                    target_h, err, err_size) )
            return NULL;
    }

    /* Flush any renderer work that may still reference the wrapped texture
       from the previous GUI frame before we render into it again. */
    SDL_FlushRenderer ( g_pc_if.renderer );

    command_buffer = SDL_AcquireGPUCommandBuffer ( g_pc_if.gpu_device );
    if ( !command_buffer ) {
        if ( err ) snprintf ( err, err_size, "preview cmd buffer: %s",
                SDL_GetError() );
        return NULL;
    }

    /* Preview renders the full graph into the upscaled target with no
       letterbox, so the final viewport simply covers the whole target. */
    SDL_zero ( viewport );
    viewport.w = (float) target_w;
    viewport.h = (float) target_h;
    viewport.max_depth = 1.0f;

    /* The preview advances its own frame counter and feedback/history state so
       animated and feedback shaders keep progressing while in the menu. */
    if ( shader_gpu_graph_render_to_texture(command_buffer,
                &s_shader_preview.graph, s_shader_preview.target, &viewport,
                g_pc_if.draw_pixel_start, source_w, source_h,
                g_pc_if.draw_pitch, s_shader_preview.frame_count, 1 ) ) {
        if ( err ) snprintf ( err, err_size, "%s", s_shader_preview.graph.error );
        SDL_CancelGPUCommandBuffer ( command_buffer );
        return NULL;
    }

    if ( !SDL_SubmitGPUCommandBuffer(command_buffer) ) {
        if ( err ) snprintf ( err, err_size, "preview submit: %s",
                SDL_GetError() );
        return NULL;
    }

    shader_gpu_graph_advance ( &s_shader_preview.graph );
    s_shader_preview.frame_count++;

    return s_shader_preview.sdl_texture;

} /* end pc_shader_preview_update */

static void pc_update_gpu_renderer_info ( void ) {

    char msg[256];
    SDL_PropertiesID props;

    g_pc_if.gpu_device = NULL;
    g_pc_if.gpu_shader_formats = 0;

    if ( !g_pc_if.renderer )
        return;

    props = SDL_GetRendererProperties(g_pc_if.renderer);
    if ( !props )
        return;

    g_pc_if.gpu_device = SDL_GetPointerProperty(props,
            SDL_PROP_RENDERER_GPU_DEVICE_POINTER, NULL);

    if ( !g_pc_if.gpu_device )
        return;

    g_pc_if.gpu_shader_formats = SDL_GetGPUShaderFormats(g_pc_if.gpu_device);
    g_pc_if.shaders_supported = g_pc_if.gpu_shader_formats != 0;
    snprintf ( msg, sizeof(msg), "Renderer GPU Device - %s formats=0x%08x",
            SDL_GetGPUDeviceDriver(g_pc_if.gpu_device) ?
            SDL_GetGPUDeviceDriver(g_pc_if.gpu_device) : "unknown",
            g_pc_if.gpu_shader_formats );
    logger_log_message ( LOG_INFO, msg, "" );

} /* end pc_update_gpu_renderer_info */

/******************************************************************************
**  Function   :  pc_window_position_in_bounds
**                            
**  Objective  :  This function checks the position for being in bounds
**
**  Parameters :  NONE
**                                                
**  return     :  1 if in bounds, 0 if not
**      
******************************************************************************/
int pc_window_position_in_bounds ( int x, int y ) {

    SDL_Rect rect;
    SDL_Point point;
    bool is_in;
    int i, num_displays = 0;
    SDL_DisplayID *displays;

    if (x == 0 && y == 0) 
        return 0;

    point.x = x;
    point.y = y;

    displays = SDL_GetDisplays(&num_displays);
    if (!displays)
        return 0;

    for ( i = 0; i < num_displays; ++i ) {

        if (!SDL_GetDisplayBounds(displays[i], &rect))
            continue;

        is_in = SDL_PointInRect(&point, &rect);
        if ( is_in == true ) {
            SDL_free(displays);
            return 1;
        }
    } 

    SDL_free(displays);
    return 0;

} /* end pc_window_position_in_bounds */

/******************************************************************************
**  Function   :  pc_get_renderer
**                            
**  Objective  :  This function creates the renderer if needed and returns it
**
**  Parameters :  vsync - set if using vertical sync, otherwise zero
**                                                
**  return     :  renderer on pass, null on fail
**      
******************************************************************************/
SDL_Renderer * pc_get_renderer ( int vsync ) {

    //SDL_RendererInfo rendererInfo;
    char msg[256];
    char bundle_dir[KAT_PATH_MAX];

    if (!g_pc_if.renderer) {

        int safe_mode = config_get_safe_video_mode();

        /* Configured backend - NULL/empty/"Default" means let SDL choose */
        const char *driver = config_get_global_cfg()->video.renderer_name;
        if (driver && (!strlen(driver) || !strcmp(driver, "Default")))
            driver = NULL;

        /* --safe only gates the GPU preference below (driver is already NULL
           when no backend is pinned).  An explicit --backend still wins so
           e.g. "--safe --backend gpu" honors the requested backend. */

        /* Validate an explicit backend against SDL's list.  On a match adopt
           the canonical driver name so SDL_CreateRenderer gets the exact
           spelling; otherwise warn (creation still falls back below, so this
           never blocks boot). */
        if ( driver ) {
            const char *avail[16];
            int n = pc_get_render_drivers(avail, 16);
            int j, found = 0;
            for ( j = 0; j < n; ++j )
                if ( avail[j] && !SDL_strcasecmp(avail[j], driver) ) {
                    driver = avail[j];
                    found = 1;
                    break;
                }
            if ( !found ) {
                int len = snprintf ( msg, sizeof(msg),
                        "Backend '%s' not available; SDL lists:", driver );
                for ( j = 0; j < n && len < (int)sizeof(msg); ++j )
                    len += snprintf ( msg + len, sizeof(msg) - len, " %s",
                            avail[j] ? avail[j] : "?" );
                logger_log_message ( LOG_ERROR, msg, "" );
            }
        }

        /*
         * Shader bundles need the SDL GPU device borrowed from the renderer.
         * If the user selected a bundled shader and did not pin a backend,
         * prefer SDL's "gpu" renderer so the shader path actually activates.
         */
        if ( !safe_mode && !driver &&
                config_get_global_cfg()->video.shader &&
                strlen(config_get_global_cfg()->video.shader) &&
                strcmp(config_get_global_cfg()->video.shader, SHADER_NONE) &&
                !pc_resolve_shader_bundle_dir(
                    config_get_global_cfg()->video.shader,
                    bundle_dir, sizeof(bundle_dir)) ) {
            driver = "gpu";
        }

        /* Create Renderer */
        g_pc_if.renderer = SDL_CreateRenderer(g_pc_if.screen, driver);

        /* Fall back to SDL default if the configured driver is unavailable */
        if (!g_pc_if.renderer && driver) {
            snprintf ( msg, sizeof(msg), "Renderer '%s' unavailable (%s) - using default",
                    driver, SDL_GetError() );
            logger_log_message ( LOG_ERROR, msg, "" );
            g_pc_if.renderer = SDL_CreateRenderer(g_pc_if.screen, NULL);
        }

        if (!g_pc_if.renderer)
            return NULL;

        snprintf ( msg, sizeof(msg), "Renderer - %s", SDL_GetRendererName(g_pc_if.renderer));
        logger_log_message ( LOG_INFO, msg, "" );
        pc_update_gpu_renderer_info();

        if ( config_get_global_cfg()->video.shader &&
                strlen(config_get_global_cfg()->video.shader) &&
                strcmp(config_get_global_cfg()->video.shader, SHADER_NONE) &&
                !pc_resolve_shader_bundle_dir(
                    config_get_global_cfg()->video.shader,
                    bundle_dir, sizeof(bundle_dir)) &&
                !g_pc_if.gpu_device ) {
            logger_log_message ( LOG_ERROR,
                    "Shader bundle configured but renderer backend exposed no SDL GPU device",
                    "" );
        }
    }

    SDL_SetRenderVSync(g_pc_if.renderer, vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);

    return g_pc_if.renderer;

} /* end pc_get_renderer */

/******************************************************************************
**  Function   :  pc_get_render_drivers
**
**  Objective  :  Fill names[] with "Default" followed by each SDL render
**                driver SDL reports as available (includes "software").
**
**  Parameters :  names - array to fill (non-owned const strings)
**                max   - capacity of names
**
**  return     :  count of names written
**
******************************************************************************/
int pc_get_render_drivers ( const char **names, int max ) {

    int count = 0;
    int num, i;

    if (!names || max <= 0)
        return 0;

    names[count++] = "Default";

    num = SDL_GetNumRenderDrivers();
    for (i = 0; i < num && count < max; i++) {
        const char *name = SDL_GetRenderDriver(i);
        if (name)
            names[count++] = name;
    }

    return count;

} /* end pc_get_render_drivers */

/******************************************************************************
**  Function   :  pc_set_debugger_video
**                            
**  Objective  :  This function makes sure we are in windowed mode to see 
**                 command window
**
**  Parameters :  NONE
**                                                
**  return     :  pointer to screen
**      
******************************************************************************/
SDL_Window * pc_set_debugger_video (void) {

    SDL_SetWindowFullscreen(g_pc_if.screen, 0);

    /*
     * We need a console here for Win32 since we freed the other one
     */
#ifdef WIN32
    AllocConsole();

    HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
    int hCrt = _open_osfhandle((long)handle_out, _O_TEXT);
    FILE* hf_out = _fdopen(hCrt, "w");
    setvbuf(hf_out, NULL, _IONBF, 1);
    *stdout = *hf_out;

    HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE);
    hCrt = _open_osfhandle((long)handle_in, _O_TEXT);
    FILE* hf_in = _fdopen(hCrt, "r");
    setvbuf(hf_in, NULL, _IONBF, 128);
    *stdin = *hf_in;

#endif
    return g_pc_if.screen;

} /* end pc_set_debugger_video */

/******************************************************************************
**  Function   :  pc_get_modes
**                            
**  Objective  :  This function queries for an array of available video modes
**
**  Parameters :  bpp     - bits per pixel
**                flags   - contains desired video format flags
**                p_modes - address to store array of resolutions
**                                                
**  return     :  number of valid modes matching the bpp input
**      
******************************************************************************/
int pc_get_modes (int bpp, int monitor, SDL_DisplayMode ***p_modes) {

    SDL_DisplayMode tmp_mode;
    int mode;
    int result, i;
    int num_modes = 0;

    *p_modes = SDL_GetFullscreenDisplayModes(monitor, &num_modes);

    return num_modes;
}

/******************************************************************************
**  Function   :  pc_toggle_fullscreen
**                            
**  Objective  :  This function toggles between fullscreen and window
**
**  Parameters :  p_video - pointer to video configuration to use
**                                                
**  return     :  pointer to screen
**      
******************************************************************************/
SDL_Window * pc_toggle_fullscreen ( t_video *p_video ) {

#ifdef __ANDROID__
    (void)p_video;
    return g_pc_if.screen;
#else
    Uint32 flags;

    /* Flip relative to the current window state, then apply the new value. */
    flags = SDL_GetWindowFlags(g_pc_if.screen);
    p_video->fullscreen = ( flags & SDL_WINDOW_FULLSCREEN ) ? 0 : 1;

    config_save_video ( config_get_global_table(),  "All",
            KCFG_VIDEO_FULLSCREEN );

    pc_apply_fullscreen ( p_video );

    return g_pc_if.screen;
#endif

} /* end pc_toggle_fullscreen */

/******************************************************************************
**  Function   :  pc_apply_fullscreen
**
**  Objective  :  Apply the fullscreen setting from config to the live SDL
**                window.  Unlike pc_toggle_fullscreen this is idempotent: it
**                sets the window to match p_video->fullscreen rather than
**                flipping the current state, so it is safe to call after a
**                config Save even while the GUI is showing.
**
**  Parameters :  p_video - pointer to video configuration to apply
**
**  return     :  NONE
**
******************************************************************************/
void pc_apply_fullscreen ( t_video *p_video ) {

#ifdef __ANDROID__
    (void)p_video;
    return;
#else
    if ( !p_video || !g_pc_if.screen )
        return;

    SDL_SetWindowFullscreen ( g_pc_if.screen,
            p_video->fullscreen ? SDL_WINDOW_FULLSCREEN : 0 );
#endif

} /* end pc_apply_fullscreen */

/******************************************************************************
**  Function   :  pc_apply_window
**
**  Objective  :  Apply the Fixed Window settings (resizable state + window
**                size) from config to the live SDL window.  Like
**                pc_apply_fullscreen this is idempotent so it is safe to call
**                after a config Save while the GUI is showing.  Skipped while
**                fullscreen since the window size is owned by the display then.
**
**  Parameters :  p_video - pointer to video configuration to apply
**
**  return     :  NONE
**
******************************************************************************/
void pc_apply_window ( t_video *p_video ) {

#ifdef __ANDROID__
    (void)p_video;
    return;
#else
    int w, h;

    if ( !p_video || !g_pc_if.screen )
        return;

    /* Guard on the config intent, not the live window flag: SDL3 fullscreen
       transitions can be async, so when called right after pc_apply_fullscreen
       turns fullscreen off the WINDOW_FULLSCREEN flag may not be cleared yet. */
    if ( p_video->fullscreen )
        return;

#if SDL_VERSION_ATLEAST(2,0,5)
    SDL_SetWindowResizable ( g_pc_if.screen,
            p_video->fixed_window ? false : true );
#endif

    w = p_video->width;
    h = p_video->height;

    /* Under Fit Window the resize handler snaps height from width to preserve
       the native aspect, so derive (and persist) height the same way here. */
    if ( p_video->window_scaling == WINDOW_SCALING_FIT_WINDOW ) {
        int vid_w, vid_h;
        pc_native_video_size ( p_video, config_get_global_cfg()->machine.tv_standard,
                               &vid_w, &vid_h );
        if ( vid_w > 0 ) {
            h = (w * vid_h) / vid_w;
            p_video->height = h;
        }
    }

    SDL_SetWindowSize ( g_pc_if.screen, w, h );
#endif

} /* end pc_apply_window */

/******************************************************************************
**  Function   :  pc_set_gui_video
**                            
**  Objective  :  This function sets up video for the gui menu 
**
**  Parameters :  fullscreen - set if needing to be in fullscreen mode
**                                                
**  return     :  pointer to screen
**      
******************************************************************************/
SDL_Window * pc_set_gui_video ( int fullscreen ) {

    Uint32 flags = 0;
    char msg[100];

    flags |= g_video.fullscreen ? SDL_WINDOW_FULLSCREEN: 0;

    sprintf ( msg, "kat5200 %s", VERSION );
    g_pc_if.screen = SDL_CreateWindow(msg, 640, 480, flags);

    SDL_ShowCursor();

    return g_pc_if.screen;

} /* end pc_set_gui_video */

/******************************************************************************
**  Function   :  pc_save_screenshot
**                            
**  Objective  :  This function save a bitmap of the current output screen
**
**  Parameters :  file - filename for bitmap
**                                                
**  return     :  error code
**      
******************************************************************************/
int pc_save_screenshot (char *file) {

    SDL_Surface* saveSurface;
    SDL_Surface* infoSurface;
    SDL_Surface* renderSurface;
    infoSurface = SDL_GetWindowSurface(g_pc_if.screen);
    if (infoSurface) {
        //unsigned char *pixels = malloc(sizeof(unsigned char) * infoSurface->w * infoSurface->h * SDL_BYTESPERPIXEL(infoSurface->format));
        //unsigned char pixels[infoSurface->w * infoSurface->h * infoSurface->format->BytesPerPixel];
        SDL_Rect clip_rect;
        SDL_GetSurfaceClipRect(infoSurface, &clip_rect);
        if (!(renderSurface = SDL_RenderReadPixels(g_pc_if.renderer, &clip_rect))) {
            SDL_DestroySurface(infoSurface);
            return -1;
        } else {
            saveSurface = SDL_CreateSurfaceFrom(renderSurface->w, renderSurface->h, SDL_PIXELFORMAT_RGBA8888, renderSurface->pixels, renderSurface->pitch);
            if (!saveSurface) {
                SDL_DestroySurface(infoSurface);
                SDL_DestroySurface(renderSurface);
                return -1;
            }
            SDL_SaveBMP(saveSurface, file);
            SDL_DestroySurface(saveSurface);
        }
        SDL_DestroySurface(renderSurface);
        SDL_DestroySurface(infoSurface);
    }

    return 0;

} /* end pc_save_screenshot */

/******************************************************************************
**  Function   :  pc_start_thread
**
**  Objective  :  This function starts a new thread immediately detaching it
**                so that it runs on it's own
**
**  Parameters :  fn   - function to run
**                name - identification of the thread
**                data - input data
**
**  return     :  NONE
**
******************************************************************************/
void * pc_start_thread ( void *fn, const char *name, void *data) {

    SDL_Thread *thread;

    thread = SDL_CreateThread(fn, name, data);
    SDL_DetachThread(thread);

    return thread;

} /* end pc_start_thread */

void pc_wait_semaphore (void) {
    SDL_WaitSemaphore ( g_lock );
}

void pc_signal_semaphore (void) {
    SDL_SignalSemaphore ( g_lock );
}

void pc_set_thread_data ( void *value ) {

    SDL_WaitSemaphore ( g_lock );

    g_thread_data = value;

    SDL_SignalSemaphore ( g_lock );

    /*
    if (!thread_local_storage) {
        SDL_LockSpinlock(&tls_lock);
        if (!thread_local_storage) {
            thread_local_storage = SDL_TLSCreate();
        }
        SDL_UnlockSpinlock(&tls_lock);
    }
    SDL_SetTLS(thread_local_storage, value, 0);
    */

} /* end pc_set_thread_data */

void * pc_get_thread_data ( void ) {

    void *return_value;

    SDL_WaitSemaphore ( g_lock );

    return_value = g_thread_data;

    SDL_SignalSemaphore ( g_lock );

    return return_value;

    //return SDL_GetTLS(thread_local_storage);

} /* end pc_get_thread_data */

/******************************************************************************
**  Function   :  pc_get_internal_storage
**                            
**  Objective  :  This function returns Android internal storage directory
**
**  Parameters :  NONE
**                                                
**  return     :  directory
**      
******************************************************************************/
const char * pc_get_internal_app_storage ( void ) {

#ifdef __ANDROID__
    return SDL_GetAndroidInternalStoragePath();
#else
    return "";
#endif

} /* end pc_get_internal_app_storage */

/******************************************************************************
**  Function   :  pc_get_external_sdcard_path
**                            
**  Objective  :  This function returns Android sdcard directory
**
**  Parameters :  dir - storage for return directory
**                                                
**  return     :  directory
**      
******************************************************************************/
char * pc_get_external_sdcard_path ( char *dir ) {

    strcpy ( dir, "");

#ifdef __ANDROID__

    /* App-specific external storage: /sdcard/Android/data/<pkg>/files.  This
     * is world-visible on the SD card (drop ROMs here via a file manager or
     * `adb push`) but needs NO runtime permission and no SAF, unlike the old
     * Environment.getExternalStorageDirectory() (broad /sdcard) path. */
    const char *path = SDL_GetAndroidExternalStoragePath();
    if ( path )
        strcpy ( dir, path );

#endif

    return dir;

} /* end pc_get_external_sdpath */

/*
 * Android storage roots.
 *
 * pc_get_external_sdcard_path above only ever yields primary emulated storage,
 * so a device with a real removable microSD (e.g. Galaxy A16) had no reachable
 * SD-card location at all: the card lives on a separate volume, and every path
 * between the app directory and it is unlistable under scoped storage.
 *
 * The roots below give the file browser an explicit list to start from:
 *   - the app-specific directory on each mounted volume, which needs no
 *     permission on any API level and therefore always works; plus
 *   - the whole volume for each, once the user has granted all-files access,
 *     which is what makes an arbitrary pre-existing ROM folder reachable.
 */
#define PC_MAX_STORAGE_ROOTS 8

static char g_storage_root_path[PC_MAX_STORAGE_ROOTS][KAT_PATH_MAX];
static char g_storage_root_label[PC_MAX_STORAGE_ROOTS][48];
static int  g_storage_root_count = -1;
static int  g_storage_root_access = -1;   /* access level the cache was built at */

#ifdef __ANDROID__
/* Call a static no-arg boolean method on MainActivity.  Returns 0 when JNI or
 * the activity is not up yet, matching pc_is_android_tv's conservative default. */
static int pc_android_call_static_bool ( const char *name )
{
    JNIEnv *env;
    jobject activity;
    jclass clazz;
    jmethodID mid;
    int result = 0;

    env = (JNIEnv *) SDL_GetAndroidJNIEnv();
    activity = (jobject) SDL_GetAndroidActivity();
    if ( !env || !activity )
        return 0;

    clazz = (*env)->GetObjectClass(env, activity);
    if ( clazz ) {
        mid = (*env)->GetStaticMethodID(env, clazz, name, "()Z");
        if ( mid )
            result = (*env)->CallStaticBooleanMethod(env, clazz, mid) ? 1 : 0;
        else
            (*env)->ExceptionClear(env);
        (*env)->DeleteLocalRef(env, clazz);
    }
    (*env)->DeleteLocalRef(env, activity);

    return result;
}

static void pc_storage_add_root ( const char *path, const char *label )
{
    if ( g_storage_root_count >= PC_MAX_STORAGE_ROOTS || !path || !path[0] )
        return;

    /* Skip duplicates: a device with no removable card can report the same
     * volume twice across API levels. */
    for ( int i = 0; i < g_storage_root_count; i++ ) {
        if ( !strcmp ( g_storage_root_path[i], path ) )
            return;
    }

    snprintf ( g_storage_root_path[g_storage_root_count],
               sizeof(g_storage_root_path[0]), "%s", path );
    snprintf ( g_storage_root_label[g_storage_root_count],
               sizeof(g_storage_root_label[0]), "%s", label );
    g_storage_root_count++;
}

/* "/storage/1A2B-3C4D/Android/data/<pkg>/files" -> "/storage/1A2B-3C4D".
 * Returns 0 when the path is not an app-specific external dir. */
static int pc_storage_volume_root ( const char *app_dir, char *dst, size_t dst_size )
{
    const char *marker = strstr ( app_dir, "/Android/data/" );
    size_t len;

    if ( !marker )
        return 0;

    len = (size_t)( marker - app_dir );
    if ( len == 0 || len >= dst_size )
        return 0;

    memcpy ( dst, app_dir, len );
    dst[len] = '\0';
    return 1;
}

static void pc_storage_build_roots ( void )
{
    JNIEnv *env;
    jobject activity;
    jclass clazz;
    jmethodID mid;
    jobjectArray arr;
    jsize n, i;
    int has_access = pc_storage_has_full_access();

    g_storage_root_count = 0;
    g_storage_root_access = has_access;

    env = (JNIEnv *) SDL_GetAndroidJNIEnv();
    activity = (jobject) SDL_GetAndroidActivity();
    if ( !env || !activity ) {
        g_storage_root_count = -1;   /* not ready: retry on the next call */
        return;
    }

    clazz = (*env)->GetObjectClass(env, activity);
    if ( !clazz ) {
        (*env)->DeleteLocalRef(env, activity);
        g_storage_root_count = -1;
        return;
    }

    mid = (*env)->GetStaticMethodID(env, clazz, "getStorageRoots",
                                    "()[Ljava/lang/String;");
    if ( !mid ) {
        (*env)->ExceptionClear(env);
        (*env)->DeleteLocalRef(env, clazz);
        (*env)->DeleteLocalRef(env, activity);
        return;
    }

    arr = (jobjectArray)(*env)->CallStaticObjectMethod(env, clazz, mid);
    if ( arr ) {
        n = (*env)->GetArrayLength(env, arr);

        /* Whole-volume roots first when permitted: this is the entry point for
         * a ROM folder the user already has on the card. */
        if ( has_access ) {
            for ( i = 0; i < n; i++ ) {
                jstring js = (jstring)(*env)->GetObjectArrayElement(env, arr, i);
                const char *cs = js ? (*env)->GetStringUTFChars(env, js, NULL) : NULL;
                char vol[KAT_PATH_MAX];

                if ( cs && pc_storage_volume_root ( cs, vol, sizeof(vol) ) )
                    pc_storage_add_root ( vol, i == 0 ? "Internal Storage" : "SD Card" );

                if ( cs ) (*env)->ReleaseStringUTFChars(env, js, cs);
                if ( js ) (*env)->DeleteLocalRef(env, js);
            }
        }

        /* App folders always: no permission needed, and the only SD-card path
         * that works on TV devices with no all-files-access screen. */
        for ( i = 0; i < n; i++ ) {
            jstring js = (jstring)(*env)->GetObjectArrayElement(env, arr, i);
            const char *cs = js ? (*env)->GetStringUTFChars(env, js, NULL) : NULL;

            if ( cs )
                pc_storage_add_root ( cs, i == 0 ? "Internal Storage (App Folder)"
                                                 : "SD Card (App Folder)" );

            if ( cs ) (*env)->ReleaseStringUTFChars(env, js, cs);
            if ( js ) (*env)->DeleteLocalRef(env, js);
        }

        (*env)->DeleteLocalRef(env, arr);
    }
    else {
        (*env)->ExceptionClear(env);
    }

    (*env)->DeleteLocalRef(env, clazz);
    (*env)->DeleteLocalRef(env, activity);
}
#endif /* __ANDROID__ */

/******************************************************************************
**  Function   :  pc_storage_has_full_access
**
**  Objective  :  Report whether arbitrary paths outside the app's own
**                directories can be read - i.e. whether a ROM folder already
**                sitting on the SD card is reachable.  Never cached: the user
**                can toggle it in Settings while we are backgrounded.
**
**  Parameters :  NONE
**
**  return     :  1 when unrestricted path access is available, else 0
**                (always 1 off Android, where no such restriction exists)
******************************************************************************/
int pc_storage_has_full_access ( void )
{
#ifdef __ANDROID__
    return pc_android_call_static_bool ( "hasFullStorageAccess" );
#else
    return 1;
#endif
}

/******************************************************************************
**  Function   :  pc_storage_request_full_access
**
**  Objective  :  Prompt for the access above.  On API 30+ this leaves the app
**                for a Settings toggle, so it returns as soon as the prompt is
**                shown rather than when the user answers; poll
**                pc_storage_has_full_access afterwards.
**
**  Parameters :  NONE
**
**  return     :  1 if a prompt could be shown, 0 if the platform offers none
**                (common on Android TV / Fire TV builds)
******************************************************************************/
int pc_storage_request_full_access ( void )
{
#ifdef __ANDROID__
    int shown = pc_android_call_static_bool ( "requestFullStorageAccess" );

    /* Force the root list to be rebuilt: a granted permission adds the
     * whole-volume entries. */
    g_storage_root_count = -1;
    return shown;
#else
    return 1;
#endif
}

/******************************************************************************
**  Function   :  pc_get_storage_root_count
**
**  Objective  :  Number of browsable storage roots.  0 off Android, which
**                leaves the file browser's existing behaviour untouched.
**
**  Parameters :  NONE
**
**  return     :  root count
******************************************************************************/
int pc_get_storage_root_count ( void )
{
#ifdef __ANDROID__
    /* Rebuild when never built, or when the permission state changed while we
     * were backgrounded (which changes which roots exist). */
    if ( g_storage_root_count < 0 ||
         g_storage_root_access != pc_storage_has_full_access() )
        pc_storage_build_roots();

    return g_storage_root_count < 0 ? 0 : g_storage_root_count;
#else
    return 0;
#endif
}

/******************************************************************************
**  Function   :  pc_get_storage_root_path / pc_get_storage_root_label
**
**  Objective  :  Access one root built by pc_get_storage_root_count.  Storage
**                is static, so the returned pointers stay valid until the next
**                rebuild (permission change) - copy before browsing away.
**
**  Parameters :  index - 0 .. pc_get_storage_root_count()-1
**
**  return     :  path / display label, "" when out of range
******************************************************************************/
const char * pc_get_storage_root_path ( int index )
{
    if ( index < 0 || index >= pc_get_storage_root_count() )
        return "";

    return g_storage_root_path[index];
}

const char * pc_get_storage_root_label ( int index )
{
    if ( index < 0 || index >= pc_get_storage_root_count() )
        return "";

    return g_storage_root_label[index];
}

/******************************************************************************
**  Function   :  pc_exit
**                            
**  Objective  :  This function frees up the PC resources used
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
int pc_exit (void) {

    char msg[257];
    int tx, ty;

    g_exit = 1;

    if ( g_pc_if.ntsc_emu ) {
        free ( g_pc_if.ntsc_emu );
        g_pc_if.ntsc_emu = 0;
    }

    if (g_pc_if.audio_stream) {
        SDL_DestroyAudioStream(g_pc_if.audio_stream);
        g_pc_if.audio_stream = NULL;
    }

    if ( g_aud_buf.buf ) {
        free(g_aud_buf.buf);
        g_aud_buf.buf = 0;
    }

    if ( g_aud_buf.extra_buf ) {
        free(g_aud_buf.extra_buf);
        g_aud_buf.extra_buf = 0;
    }

    pc_free_shader_runtime ();

    /*
     * Free the line we used
     */
    pc_log_display_attributes ( g_pc_if.screen, NULL );
    SDL_GetWindowPosition(g_pc_if.screen, &tx, &ty);
    SDL_DestroySurface ( g_pc_if.surface );
    SDL_DestroySurface ( g_pc_if.ntsc_surface );
    SDL_DestroySurface ( g_pc_if.output_surface );
    SDL_DestroyTexture ( g_pc_if.screen_texture );
    SDL_DestroyWindow ( g_pc_if.screen );

    SDL_DestroySemaphore ( g_lock );

    /*
     * Shutdown all subsystems
     */
    SDL_Quit();

#ifdef SOUND_DEBUG
    fclose ( fp );
#endif

    if ( g_pc_if.audio_o.freq ) {
        sprintf ( msg, "Console: Avg Sound Latency = %d ms", g_aud_buf.avg_queue * 1000 / g_pc_if.audio_o.freq ); 
        logger_log_message ( LOG_INFO, msg, "" );
    }

    return 0;

} /* end pc_exit */

/* Threads need a way to detect when we are exiting */
int pc_get_exit(void) { return g_exit; }

/******************************************************************************
**  Function   :  pc_open_joystick
**                            
**  Objective  :  This function opens up a joystick device if needed
**
**  Parameters :  index - joystick index
**                                                
**  return     :  0 for success, otherwise failure
**      
******************************************************************************/
int pc_open_joystick (int ref_id) {

    char msg[256];

    //if ( (index > (MAX_PC_DEVICE-1)) || (g_pc_joy[index].joy_ptr != 0 && SDL_JoystickConnected(g_pc_joy[index].joy_ptr)) )
    //    return 0;

    SDL_Joystick *joystick = SDL_OpenJoystick ( ref_id );
    //g_pc_joy[index].joy_ptr = SDL_OpenJoystick ( index );
    //g_pc_joy[index].joy_id = SDL_GetJoystickID(g_pc_joy[index].joy_ptr);

    if (joystick) {
        sprintf ( msg, "Joystick %d : %s Initialized", ref_id, SDL_GetJoystickNameForID(ref_id) );
        logger_log_message ( LOG_INFO, msg, "" );
    }
    else { 
        sprintf(msg, "Joystick %d : Could not open!", ref_id);
        logger_log_message ( LOG_ERROR, msg, "" );
        return -1;
    }

    return 0;

} /* end pc_open_joystick */

/******************************************************************************
**  Function   :  pc_open_controller
**                            
**  Objective  :  This function opens up a joystick device if needed
**
**  Parameters :  index - joystick index
**                                                
**  return     :  0 for success, otherwise failure
**      
******************************************************************************/
int pc_open_controller (int ref_id) {

    char msg[256];

    //if ( (index > (MAX_PC_DEVICE-1)) || (g_pc_joy[index].ctlr_ptr != 0 && SDL_GamepadConnected(g_pc_joy[index].ctlr_ptr)) )
        //return 0;

    SDL_Gamepad *gpad = SDL_OpenGamepad(ref_id);
    //g_pc_joy[index].ctlr_ptr = SDL_OpenGamepad ( index );
    //g_pc_joy[index].joy_ptr = SDL_GetGamepadJoystick(g_pc_joy[index].ctlr_ptr);
    //g_pc_joy[index].joy_id = SDL_GetJoystickID(g_pc_joy[index].joy_ptr);

    if ( gpad ) {
        sprintf(msg, "Game Controller %d : %s Initialized", ref_id, SDL_GetGamepadName(gpad));
        logger_log_message ( LOG_INFO, msg, "" );
    }
    else { 
        sprintf(msg, "Game Controller %d : Could not open!", ref_id);
        logger_log_message ( LOG_ERROR, msg, "" );
        return -1;
    }

    return 0;

} /* end pc_open_controller */

/******************************************************************************
**  Function   :  pc_detect_and_open_joysticks
**                            
**  Objective  :  This function checks number of joystick and tries to open
**                all of them quietly (for GUI autodetect)
**
**  Parameters :  NONE
**                                                
**  return     :  number found
**      
******************************************************************************/
int pc_detect_and_open_joysticks (void) {

    int i,total = 0;

    SDL_QuitSubSystem ( SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD );
    //memset ( g_pc_joy, 0, sizeof(t_sdl_joy_conv)*MAX_PC_DEVICE );

    //SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");
    SDL_InitSubSystem ( SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD );

    SDL_JoystickID *ids = SDL_GetJoysticks(&total);
    SDL_free(ids);  /* only the count is used here */
    if (total > MAX_PC_DEVICE)
        total = MAX_PC_DEVICE;

    for ( i = 0; i < total; ++i ) {

        /* No GameController just yet
        if ( SDL_IsGamepad(i) ) {
            g_pc_joy[i].ctlr_ptr = SDL_OpenGamepad ( i );
            g_pc_joy[i].joy_ptr = SDL_GetGamepadJoystick(g_pc_joy[i].ctlr_ptr);
        }
        else {
        */

        {
            //g_pc_joy[i].joy_ptr = SDL_OpenJoystick(ids[i]);
        }
        //if ( SDL_GetNumJoystickAxes(g_pc_joy[i].joy_ptr) == 3)
        //    SDL_CloseJoystick ( g_pc_joy[i].joy_ptr );
        //else
            //g_pc_joy[i].joy_id = ids[i];
            //g_pc_joy[i].joy_id = SDL_GetJoystickID(g_pc_joy[i].joy_ptr);
    }

    //SDL_JoystickEventState ( SDL_ENABLE );

    return total;

} /* end pc_detect_and_open_joystick */

/******************************************************************************
**  Function   :  pc_get_joystick_index_from_id
**                            
**  Objective  :  This function gets the index from id (SDL2 update)
**
**  Parameters :  instance_id
**                                                
**  return     :  index
**      
******************************************************************************/
int pc_get_joystick_index_from_id (int instance_id) {

    int i;

    for (i = 0; i < MAX_PC_DEVICE; ++i) {
        if (g_joy_ids[i] == instance_id)
            return i;
    }

    /* Not tracked.  Return -1 rather than silently falling back to slot 0:
     * routing an unknown device's input onto player 0 hides real device-table
     * problems and steals input from / misroutes other slots.  Callers must
     * guard for the negative result. */
    return -1;

} /* end pc_get_joystick_index_from_id */

/******************************************************************************
**  Function   :  pc_set_joystick_id
**
**  Objective  :  Seed the instance-id -> index table directly.  Normally this
**                table is filled as devices connect (pc_add_joy_id); this lets
**                headless callers (e.g. unit tests) map a synthetic joystick
**                instance id to a device index without real hardware.
**
**  Parameters :  index       - device slot (0..MAX_PC_DEVICE-1)
**                instance_id - SDL joystick instance id to store
**
**  return     :  NONE
**
******************************************************************************/
void pc_set_joystick_id (int index, unsigned int instance_id) {

    if (index >= 0 && index < MAX_PC_DEVICE)
        g_joy_ids[index] = instance_id;

} /* end pc_set_joystick_id */

/******************************************************************************
**  Function   :  pc_detect_joysticks
**                            
**  Objective  :  This function checks number of joystick and returns result
**
**  Parameters :  NONE
**                                                
**  return     :  number found
**      
******************************************************************************/
int pc_detect_joysticks (void) {

    int total = 0;

    SDL_QuitSubSystem ( SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD );
    //memset ( g_pc_joy, 0, sizeof(t_sdl_joy_conv)*MAX_PC_DEVICE );

    //SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");
    SDL_InitSubSystem ( SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD );

    SDL_free(SDL_GetJoysticks(&total));
    //total = SDL_NumJoysticks();

    return total;

} /* end pc_detect_joysticks */

/******************************************************************************
**  Function   :  pc_get_joystick_name
**                            
**  Objective  :  This function returns the joystick name by index
**
**  Parameters :  index
**                                                
**  return     :  0 for success, otherwise failure
**      
******************************************************************************/
const char * pc_get_joystick_name ( int index ) {

    return SDL_GetJoystickNameForID(index); //FIXME need JoyID not index

} /* end pc_get_joystick_name */

/******************************************************************************
**  Function   :  pc_get_num_joysticks
**
**  Objective  :  This function returns the number of joysticks
**
**  Parameters :  NONE
**
**  return     :  number of joysticks currently detected
**
******************************************************************************/
int pc_get_num_joysticks ( void ) {

    int total = 0;

    SDL_free(SDL_GetJoysticks(&total));

	return total;

} /* end pc_get_num_joysticks */

int pc_get_valid_joy_count(void) {
    int count = 0;
    for (int i = 0; i < MAX_PC_DEVICE; i++)
        if (g_joy_ids[i]) count++;
    return count;
}

unsigned int pc_get_valid_joy_id(int n) {
    int count = 0;
    for (int i = 0; i < MAX_PC_DEVICE; i++) {
        if (g_joy_ids[i]) {
            if (count == n) return g_joy_ids[i];
            count++;
        }
    }
    return 0;
}

unsigned int pc_get_joy_generation(void) {
    return g_joy_generation;
}

void pc_get_joy_slot_device_types(e_dev_type *types, int max_types) {
    int limit;

    if (!types || max_types <= 0)
        return;

    for (int i = 0; i < max_types; ++i)
        types[i] = DEV_MAX;

    limit = max_types < MAX_PC_DEVICE ? max_types : MAX_PC_DEVICE;
    for (int i = 0; i < limit; ++i) {
        if (!g_joy_ids[i])
            continue;
        types[i] = SDL_IsGamepad(g_joy_ids[i]) ? DEV_CONTROLLER : DEV_JOYSTICK;
    }
}

int pc_get_joy_slot_name(int slot, char *buf, int buf_size) {
    const char *name;

    if (buf && buf_size > 0)
        buf[0] = '\0';
    if (!buf || buf_size <= 0 || slot < 0 || slot >= MAX_PC_DEVICE)
        return 0;
    if (!g_joy_ids[slot])
        return 0;

    name = SDL_GetJoystickNameForID(g_joy_ids[slot]);
    snprintf(buf, buf_size, "%s", (name && name[0]) ? name : "Unknown");
    return 1;
}

void pc_get_joy_guid_string(unsigned int joy_id, char *buf, int buf_size) {
    SDL_GUID guid = SDL_GetJoystickGUIDForID(joy_id);
    SDL_GUIDToString(guid, buf, buf_size);
}

void pc_get_joy_serial(unsigned int joy_id, char *buf, int buf_size) {
    buf[0] = '\0';
    if (!joy_id) return;
    SDL_Joystick *joy = SDL_OpenJoystick(joy_id);
    if (joy) {
        const char *s = SDL_GetJoystickSerial(joy);
        if (s) strncpy(buf, s, buf_size - 1);
        buf[buf_size - 1] = '\0';
        SDL_CloseJoystick(joy);
    }
}

void pc_get_player_joy_name(const char *guid_str, char *buf, int buf_size) {
    if (!guid_str || !guid_str[0]) {
        strncpy(buf, "N/A", buf_size - 1);
        buf[buf_size - 1] = '\0';
        return;
    }
    SDL_GUID player_guid = SDL_StringToGUID(guid_str);
    for (int i = 0; i < MAX_PC_DEVICE; i++) {
        if (!g_joy_ids[i]) continue;
        SDL_GUID g = SDL_GetJoystickGUIDForID(g_joy_ids[i]);
        if (!memcmp(&player_guid, &g, sizeof(SDL_GUID))) {
            const char *name = SDL_GetJoystickNameForID(g_joy_ids[i]);
            strncpy(buf, name ? name : "Unknown", buf_size - 1);
            buf[buf_size - 1] = '\0';
            return;
        }
    }
    strncpy(buf, "Not attached", buf_size - 1);
    buf[buf_size - 1] = '\0';
}

/******************************************************************************
**  Function   :  pc_delay
**                            
**  Objective  :  This function delays the number of ms input 
**
**  Parameters :  ms - number of milliseconds to delay
**                                                
**  return     :  NONE
**      
******************************************************************************/
void pc_delay (int ms) {

    SDL_Delay(ms);

} /* end pc_delay */

/******************************************************************************
**  Function   :  pc_delay_precise_ns
**
**  Objective  :  This function delays the number of nanoseconds input using
**                SDL's precise delay (adaptive coarse sleep then a short spin
**                for the tail).  Use this for frame pacing instead of busy-
**                waiting on the clock.
**
**  Parameters :  ns - number of nanoseconds to delay
**
**  return     :  NONE
**
******************************************************************************/
void pc_delay_precise_ns (Uint64 ns) {

    SDL_DelayPrecise(ns);

} /* end pc_delay_precise_ns */

/******************************************************************************
**  Function   :  pc_get_ticks
**                            
**  Objective  :  This function return the number of ms since SDL was started
**
**  Parameters :  NONE
**                                                
**  return     :  ms
**      
******************************************************************************/
unsigned int pc_get_ticks (void) {

    return SDL_GetTicks();

} /* end pc_get_ticks */

Uint64 pc_get_ticks_us (void) {

    return SDL_GetTicksNS() / 1000;

} /* end pc_get_ticks_us */

/******************************************************************************
**  Function   :  pc_lock_audio
**                            
**  Objective  :  This function keeps audio callback from running
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
/*
int pc_lock_audio (void) {

    SDL_LockAudioDevice(audio_id);

    return 0;

} /* end pc_lock_audio */

/******************************************************************************
**  Function   :  pc_unlock_audio
**                            
**  Objective  :  This function keeps allows audio callback to run      
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
/*
int pc_unlock_audio (void) {

    SDL_UnlockAudioDevice(audio_id);

    return 0;

} /* end pc_unlock_audio */

/******************************************************************************
**  Function   :  pc_audio_callback
**                            
**  Objective  :  This function updates audio buffers from pokey sound
**
**  Parameters :  userdata - provided by SDL
**                buffer   - We fill this up from pokey sound
**                len      - length of the buffer to fill      
**
**  return     :  NONE
**      
******************************************************************************/
void pc_audio_callback (void *userdata, Uint8 *buffer, int len) {

    int size1, size2;

    size1 = len;
    size2 = 0;

    //printf("Play Pos: %d Write Pos %d\n", g_aud_buf.play_pos, g_aud_buf.write_pos);

    if ( (g_aud_buf.play_pos + len) > g_aud_buf.size) {
        size1 = g_aud_buf.size - g_aud_buf.play_pos;
        size2 = len - size1;
    }
    memcpy (buffer, &g_aud_buf.buf[g_aud_buf.play_pos], size1);
    memcpy (&buffer[size1], g_aud_buf.buf, size2);

    if ((g_aud_buf.play_pos < g_aud_buf.write_pos) && 
       ((g_aud_buf.play_pos + len) % g_aud_buf.size) > g_aud_buf.write_pos) {
        g_aud_buf.play_pos = g_aud_buf.write_pos;
    }
    else
        g_aud_buf.play_pos = (g_aud_buf.play_pos + len) % g_aud_buf.size;

} /* end pc_audio_callback */

/******************************************************************************
**  Function   :  pc_blit_ntsc_filtered_screen
**                            
**  Objective  :  This function filters the input surface and outputs to screen
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
void pc_blit_ntsc_filtered_screen (void) {

    static int burst_change = 0;

    burst_change ^= 1;

    /*
    int y;
    if ( g_video.ntsc_setup.merge_fields )
        burst_change = 0;
    */

    /*
    atari_ntsc_blit( g_pc_if.ntsc_emu, 
                     (unsigned char *)(g_pc_if.surface->pixels) + g_pc_if.ntsc_start_x * 
                         g_pc_if.surface->format->BytesPerPixel, 
                     g_pc_if.surface->pitch,
                     g_video.widescreen ? (ATARI_NTSC_OUT_WIDTH(342)) : atari_ntsc_min_out_width,
                     g_pc_if.surface->h, 
                     (unsigned short *)((unsigned char *)(g_pc_if.filtered_surface->pixels) + 0 * 
                         g_pc_if.filtered_surface->format->BytesPerPixel + 0 * g_pc_if.filtered_surface->pitch), 
                     g_pc_if.filtered_surface->pitch );

    for ( y = g_pc_if.surface->h-1; --y >= 0; )
    {
        unsigned char const* in = (unsigned char *)(g_pc_if.filtered_surface->pixels) + (y + 0) * g_pc_if.filtered_surface->pitch;
        unsigned char* out = (unsigned char *)(g_pc_if.filtered_surface->pixels) + (y * 2 + 0) * g_pc_if.filtered_surface->pitch;
        int n;
        for ( n = g_pc_if.filtered_surface->w; n; --n )
        {
            unsigned prev = *(unsigned short*) in;
            unsigned next = *(unsigned short*) (in + g_pc_if.filtered_surface->pitch);
            unsigned mixed = prev + next + ((prev ^ next) & 0x0821);
            *(unsigned short*) out = prev;
            *(unsigned short*) (out + g_pc_if.filtered_surface->pitch) = (mixed >> 1) - (mixed >> 4 & 0x18E3);
            in += 2;
            out += 2;
        }
    }
*/

} /* end pc_blit_ntsc_filtered_screen */

/******************************************************************************
**  Function   :  pc_blit_ntsc_filtered_screen_32
**                            
**  Objective  :  This function filters the input surface and outputs to screen
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
void pc_blit_ntsc_filtered_screen_32(void) {

    static int burst_change = 0;
    int y;
    void *pixels;
    int pitch;

    if ( !g_pc_if.ntsc_emu || !g_pc_if.surface || !g_pc_if.ntsc_surface )
        return;

    //SDL_LockTexture(g_pc_if.screen_texture, NULL, &pixels, &pitch);
    pixels = g_pc_if.ntsc_surface->pixels;
    pitch = g_pc_if.ntsc_surface->pitch;

    burst_change ^= 1;

    atari_ntsc_blit_32( g_pc_if.ntsc_emu,
                     (unsigned char *)(g_pc_if.surface->pixels) + g_pc_if.ntsc_start_x *
                         SDL_BYTESPERPIXEL(g_pc_if.surface->format),
                     g_pc_if.surface->pitch,
                     g_pc_if.valid_video_rect.w,
                     g_pc_if.surface->h,
                     (unsigned int *)((unsigned char *)(pixels)), pitch );

    //SDL_UnlockTexture(g_pc_if.screen_texture);

   
    for ( y = g_pc_if.surface->h-1; --y >= 0; )
    {
        unsigned char const* in = (unsigned char *)(pixels) + 
                                  (y) * pitch;
        unsigned char* out = (unsigned char *)(pixels) + 
                             (y * 2) * pitch;
        int n;
        for ( n = g_pc_if.valid_video_rect.w; n; --n )
        {
            unsigned prev = *(unsigned int*) in;
            unsigned next = *(unsigned int*) (in + pitch);
            /* mix 16-bit rgb without losing low bits */
            unsigned mixed = prev + next + ((prev ^ next) & 0x010101);
            /* darken by 12% */
            *(unsigned int*) out = prev;
            *(unsigned int*) (out + pitch) = (mixed >> 1) - (mixed >> 4 & 0x1F1F1F);
            in += 4;
            out += 4;
        }
    }

    //SDL_UnlockTexture(g_pc_if.screen_texture);
    

} /* end pc_blit_ntsc_filtered_screen */

/******************************************************************************
**  Function   :  pc_blit_palette_screen_32
**
**  Objective  :  This function maps the INDEX8 GTIA surface to ARGB8888
**                output using the active palette.
**
**  Parameters :  NONE
**
**  return     :  NONE
**
******************************************************************************/
void pc_blit_palette_screen_32(void) {

    int x, y;

    if ( !g_pc_if.surface || !g_pc_if.output_surface )
        return;

    for ( y = 0; y < g_pc_if.valid_video_rect.h; ++y ) {
        unsigned char *src = (unsigned char *)g_pc_if.surface->pixels +
            (g_pc_if.valid_video_rect.y + y) * g_pc_if.surface->pitch +
            g_pc_if.valid_video_rect.x;
        unsigned int *dst = (unsigned int *)
            ((unsigned char *)g_pc_if.output_surface->pixels +
             y * g_pc_if.output_surface->pitch);

        for ( x = 0; x < g_pc_if.valid_video_rect.w; ++x )
            dst[x] = g_pc_if.output_rgb[src[x]];
    }

} /* end pc_blit_palette_screen_32 */

/******************************************************************************
**  Function   :  pc_refresh_video_output
**
**  Objective  :  This function refreshes the ARGB output surface from the
**                current INDEX8 GTIA frame.
**
**  Parameters :  NONE
**
**  return     :  NONE
**
******************************************************************************/
void pc_refresh_video_output(void) {

    t_video* p_video = &(config_get_global_cfg()->video);

    if ( p_video->ntsc_artifacts )
        pc_blit_ntsc_filtered_screen_32();
    else
        pc_blit_palette_screen_32();

} /* end pc_refresh_video_output */

static void pc_get_shader_output_viewport ( int output_width, int output_height,
        SDL_GPUViewport *viewport ) {

    t_video *p_video = &(config_get_global_cfg()->video);
    float source_aspect;
    float output_aspect;
    float view_width;
    float view_height;
    float view_x;
    float view_y;

    if ( !viewport )
        return;

    viewport->x = 0.0f;
    viewport->y = 0.0f;
    viewport->w = (float) output_width;
    viewport->h = (float) output_height;
    viewport->min_depth = 0.0f;
    viewport->max_depth = 1.0f;

    if ( !g_pc_if.draw_video_rect.w || !g_pc_if.draw_video_rect.h ||
            !p_video || p_video->window_scaling != WINDOW_SCALING_LETTERBOX )
        return;

    source_aspect = (float) g_pc_if.draw_video_rect.w /
            (float) g_pc_if.draw_video_rect.h;
    output_aspect = (float) output_width / (float) output_height;

    if ( output_aspect > source_aspect ) {
        view_height = (float) output_height;
        view_width = view_height * source_aspect;
    }
    else {
        view_width = (float) output_width;
        view_height = view_width / source_aspect;
    }

    view_x = ((float) output_width - view_width) * 0.5f;
    view_y = ((float) output_height - view_height) * 0.5f;

    viewport->x = view_x;
    viewport->y = view_y;
    viewport->w = view_width;
    viewport->h = view_height;

} /* end pc_get_shader_output_viewport */

static int pc_render_frame_shader_gpu ( void ) {

    SDL_GPUCommandBuffer *command_buffer;
    SDL_GPUTexture *swapchain_texture;
    SDL_GPUViewport viewport;
    t_atari *p_console;
    Uint32 output_width = 0;
    Uint32 output_height = 0;

    command_buffer = SDL_AcquireGPUCommandBuffer ( g_pc_if.gpu_device );
    if ( !command_buffer ) {
        pc_log_shader_message ( "SDL GPU shader fallback",
                "Failed to acquire GPU command buffer" );
        return -1;
    }

    if ( !SDL_WaitAndAcquireGPUSwapchainTexture(command_buffer, g_pc_if.screen,
                &swapchain_texture, &output_width, &output_height) ) {
        pc_log_shader_message ( "SDL GPU shader fallback",
                SDL_GetError() );
        SDL_CancelGPUCommandBuffer ( command_buffer );
        return -1;
    }

    if ( !swapchain_texture ) {
        SDL_SubmitGPUCommandBuffer ( command_buffer );
        return 0;
    }

    pc_get_shader_output_viewport ( (int) output_width, (int) output_height,
            &viewport );

    if ( shader_gpu_graph_set_sizes(&g_pc_if.shader_graph,
                g_pc_if.draw_video_rect.w, g_pc_if.draw_video_rect.h,
                (int) viewport.w, (int) viewport.h) ) {
        pc_log_shader_message ( "SDL GPU shader fallback",
                g_pc_if.shader_graph.error );
        SDL_CancelGPUCommandBuffer ( command_buffer );
        return -1;
    }

    p_console = console_get_ptr ();
    if ( shader_gpu_graph_render_to_texture(command_buffer,
                &g_pc_if.shader_graph, swapchain_texture, &viewport,
                g_pc_if.draw_pixel_start, g_pc_if.draw_video_rect.w,
                g_pc_if.draw_video_rect.h, g_pc_if.draw_pitch,
                p_console ? p_console->frame_number : 0, 1 ) ) {
        pc_log_shader_message ( "SDL GPU shader fallback",
                g_pc_if.shader_graph.error );
        SDL_CancelGPUCommandBuffer ( command_buffer );
        return -1;
    }

    if ( !SDL_SubmitGPUCommandBuffer(command_buffer) ) {
        pc_log_shader_message ( "SDL GPU shader fallback",
                "Failed to submit GPU shader frame" );
        return -1;
    }

    /* Rotate history/feedback only after a successful submit so a failed or
       skipped frame never advances graph frame state. */
    shader_gpu_graph_advance ( &g_pc_if.shader_graph );

    return 0;

} /* end pc_render_frame_shader_gpu */

/******************************************************************************
**  Function   :  pc_blit_ntsc_filtered_screen_with_inputs
**                            
**  Objective  :  This function filters the input surface and outputs to screen
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
void pc_blit_ntsc_filtered_screen_with_inputs(atari_ntsc_t *ntsc_emu,
        SDL_Surface *surface, SDL_Rect surface_rect,
        SDL_Surface *screen, SDL_Rect screen_rect) {

    static int burst_change = 0;
    int y;


    burst_change ^= 1;

    /*
    if ( g_video.ntsc_setup.merge_fields )
        burst_change = 0;
    */

    atari_ntsc_blit( ntsc_emu, (unsigned char *)(surface->pixels) + surface_rect.x * 
                         SDL_BYTESPERPIXEL(surface->format) + surface_rect.y * surface->pitch, 
                     surface->pitch,
                      screen_rect.w, surface_rect.h, 
                     (unsigned short *)((unsigned char *)(screen->pixels) + screen_rect.x * 
                         SDL_BYTESPERPIXEL(screen->format)), 
                     screen->pitch );

    for ( y = surface_rect.h; --y >= 0; )
    {
        unsigned char const* in = (unsigned char *)(screen->pixels) + y * screen->pitch;
        unsigned char* out = (unsigned char *)(screen->pixels) + y * 2 * screen->pitch;
        int n;
        for ( n = screen->w; n; --n )
        {
            unsigned prev = *(unsigned short*) in;
            unsigned next = *(unsigned short*) (in + screen->pitch);
            /* mix 16-bit rgb without losing low bits */
            unsigned mixed = prev + next + ((prev ^ next) & 0x0821);
            /* darken by 12% */
            *(unsigned short*) out = prev;
            *(unsigned short*) (out + screen->pitch) = (mixed >> 1) - (mixed >> 4 & 0x18E3);
            in += 2;
            out += 2;
        }
    }

} /* end pc_blit_ntsc_filtered_screen_with_inputs */

/******************************************************************************
**  Function   :  pc_blit_ntsc_filtered_screen_with_inputs_32
**                            
**  Objective  :  This function filters the input surface and outputs to screen
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
void pc_blit_ntsc_filtered_screen_with_inputs_32(atari_ntsc_t *ntsc_emu,
        SDL_Surface *surface, SDL_Rect surface_rect,
        SDL_Surface *screen, SDL_Rect screen_rect) {

    static int burst_change = 0;
    int y;


    burst_change ^= 1;

    /*
    if ( g_video.ntsc_setup.merge_fields )
        burst_change = 0;
    */

    atari_ntsc_blit_32( ntsc_emu, (unsigned char *)(surface->pixels) + surface_rect.x * 
                         SDL_BYTESPERPIXEL(surface->format) + surface_rect.y * surface->pitch, 
                     surface->pitch,
                      screen_rect.w, surface_rect.h, 
                     (unsigned int *)((unsigned char *)(screen->pixels) + screen_rect.x * 
                         SDL_BYTESPERPIXEL(screen->format)), 
                     screen->pitch );

    for ( y = surface_rect.h; --y >= 0; )
    {
        unsigned char const* in = (unsigned char *)(screen->pixels) + y * screen->pitch;
        unsigned char* out = (unsigned char *)(screen->pixels) + y * 2 * screen->pitch;
        int n;
        for ( n = screen->w; n; --n )
        {
            unsigned prev = *(unsigned int*) in;
            unsigned next = *(unsigned int*) (in + screen->pitch);
            /* mix 16-bit rgb without losing low bits */
            unsigned mixed = prev + next + ((prev ^ next) & 0x0821);
            /* darken by 12% */
            *(unsigned int*) out = prev;
            *(unsigned int*) (out + screen->pitch) = (mixed >> 1) - (mixed >> 4 & 0x18E3);
            in += 4;
            out += 4;
        }
    }

} /* end pc_blit_ntsc_filtered_screen_with_inputs */

/******************************************************************************
**  Function   :  pc_check_sound_freq
**                            
**  Objective  :  This function checks how many samples per 60 frames we are
**                acually sending and adjusts the currently set frequency
**                based on that.  We check every 3 seconds
**
**  Parameters :  NONE
**                                                
**  return     :  NONE
**      
******************************************************************************/
void pc_check_sound_freq ( int count ) {

    //int new_freq;

    //g_aud_buf.samp_cnt += count;
    //g_aud_buf.frame_cnt++;

    if (!(g_aud_buf.frame_cnt % 180)) {
        //printf ( "Ticks 60 frames: %d\n", (pc_get_ticks() - tick_start)/3);
        //printf("Sound 180 frames: %d\n", g_aud_buf.samp_cnt / (g_aud_buf.frame_cnt/60));
        //new_freq = g_aud_buf.samp_cnt / (g_aud_buf.frame_cnt/60);
        //new_freq = 43000;
        //pokey_sound_update_freq ((Uint16)new_freq, g_pc_if.audio_o.silence);
        tick_start = pc_get_ticks();
    }

} /* end pc_check_sound_freq */

void pc_reset_sound_adj ( void ) {
    tick_start = pc_get_ticks();

    g_aud_buf.frame_cnt = 0;
    g_aud_buf.samp_cnt = 0;

} /* end pc_reset_sound_adj */

/******************************************************************************
**  Function   :  pc_set_sound_freq
**                            
**  Objective  :  This function checks how up to date the sample rate is 
**                 keeping us and updates when necessary
**
**  Parameters :  queue_size - size of current queue
**                                                
**  return     :  NONE
**      
******************************************************************************/
void pc_set_sound_freq ( int freq ) {

    double throttle = console_get_ptr()->throttle == 0 ? 5.0 : 
        (double)console_get_ptr()->throttle / 100.0;
          
    g_aud_buf.fill_size = freq / console_get_ptr()->frames_per_sec;
    g_aud_buf.fill_size = (int) ((double)g_aud_buf.fill_size / throttle);

    if (g_aud_buf.buf) free(g_aud_buf.buf);
    if (g_aud_buf.extra_buf) free(g_aud_buf.extra_buf);

    g_aud_buf.curr_fill_size = g_aud_buf.fill_size;

    int output_frame_size = SDL_AUDIO_BYTESIZE(g_pc_if.audio_o.format) * g_pc_if.audio_o.channels;
    int device_buffer_bytes = g_pc_if.audio_samples * output_frame_size;

    g_aud_buf.size = g_pc_if.audio_samples * 2;
    g_aud_buf.buf = malloc(g_aud_buf.size);
    g_aud_buf.extra_buf = malloc(g_pc_if.audio_samples);

    /* Set Threshold values where we adjust curr fill size */
    g_aud_buf.adj_threshold[THRESH_VERY_LOW] = device_buffer_bytes *
                                              (g_sound.buffer_size - 1 );
    g_aud_buf.adj_threshold[THRESH_LOW] = device_buffer_bytes *
                                         (g_sound.buffer_size - 0.5 );
    g_aud_buf.adj_threshold[THRESH_HIGH] = device_buffer_bytes *
                                          (g_sound.buffer_size+1);
    g_aud_buf.adj_threshold[THRESH_VERY_HIGH] = device_buffer_bytes *
                                          (g_sound.buffer_size+2);
    g_aud_buf.adj_threshold[THRESH_EXTREME_HIGH] = (g_pc_if.audio_o.freq / 2) * output_frame_size;


    /* Set our adjust rates */
    g_aud_buf.adj_rate_high = g_aud_buf.fill_size / 100;
    g_aud_buf.adj_rate_med = g_aud_buf.fill_size / 300;
    g_aud_buf.adj_rate_low = g_aud_buf.fill_size / 500;

    if (g_aud_buf.adj_rate_high == 0)
        g_aud_buf.adj_rate_high = 1;
    if (g_aud_buf.adj_rate_med == 0)
        g_aud_buf.adj_rate_med = 1;
    if (g_aud_buf.adj_rate_low == 0)
        g_aud_buf.adj_rate_low = 1;

    pokey_sound_update_freq ((Uint16)freq, SDL_GetSilenceValueForFormat(g_pc_if.audio_d.format));

    g_aud_buf.freq = freq;

} /* end pc_set_sound_freq */

/******************************************************************************
**  Function   :  pc_adjust_sound_rate
**                            
**  Objective  :  This function checks how up to date the sample rate is 
**                 keeping us and updates when necessary
**
**  Parameters :  queue_size - size of current queue
**                                                
**  return     :  NONE
**      
******************************************************************************/
void pc_adjust_sound_rate ( int queue_size ) {

    //g_aud_buf.change_frame = g_aud_buf.frame_cnt;

    g_aud_buf.samp_cnt++;
    g_aud_buf.avg_queue = (g_aud_buf.avg_queue * (g_aud_buf.samp_cnt-1) + queue_size) / g_aud_buf.samp_cnt;

    if (queue_size < (g_aud_buf.adj_threshold[THRESH_VERY_LOW])) { 
        g_aud_buf.curr_fill_size += g_aud_buf.adj_rate_med;
    }
    else if (queue_size < (g_aud_buf.adj_threshold[THRESH_LOW])) {
        //if (g_aud_buf.curr_fill_size < g_aud_buf.fill_size)
            g_aud_buf.curr_fill_size += g_aud_buf.adj_rate_low;
    }
    else if (queue_size > (g_aud_buf.adj_threshold[THRESH_VERY_HIGH])) { 
        g_aud_buf.curr_fill_size -= g_aud_buf.adj_rate_low;
    }
    else if (queue_size > (g_aud_buf.adj_threshold[THRESH_HIGH])) { 
        if (g_aud_buf.curr_fill_size > g_aud_buf.fill_size)
            g_aud_buf.curr_fill_size -= g_aud_buf.adj_rate_low;
    }

    if (g_aud_buf.curr_fill_size < 0)
        g_aud_buf.curr_fill_size = 1;
    if (g_aud_buf.curr_fill_size > g_aud_buf.size)
        g_aud_buf.curr_fill_size = g_aud_buf.size-1;

} /* end pc_adjust_sound_rate */

int pc_handle_axis_events ( t_pc_common *p_axis, int axis_value ) {

    int status = 0;
    t_pc_common *tmp_axis = p_axis;

    while ( tmp_axis ) {
        if ( !tmp_axis->engaged ) {
            tmp_axis = tmp_axis->next;
            continue;
        }

        if ( axis_value < 0 )
            tmp_axis->pos_function ( tmp_axis, tmp_axis->player, tmp_axis->direction, 0 );
        else
            tmp_axis->neg_function ( tmp_axis, tmp_axis->player, tmp_axis->direction, 0 );

        tmp_axis = tmp_axis->next;
    }

    while ( p_axis ) {

        if ( !p_axis->engaged ) {
            p_axis = p_axis->next;
            continue;
        }

        if ( axis_value < 0 ) 
            status = p_axis->neg_function ( p_axis, p_axis->player, p_axis->direction, 
                                               abs(axis_value) );
        else 
            status = p_axis->pos_function ( p_axis, p_axis->player, p_axis->direction, 
                                               abs(axis_value) );

        if ( status )
            break;

        p_axis = p_axis->next;
    }

    return status;
}

int pc_handle_ball_events ( t_pc_common *axis, int speed, float motion ) {

    int status = 0;
    t_pc_common *tmp_axis = axis;

    while ( tmp_axis ) {
        if ( !tmp_axis->engaged ) {
            tmp_axis = tmp_axis->next;
            continue;
        }

        if ( motion < 0 )
            tmp_axis->pos_function ( tmp_axis, tmp_axis->player, tmp_axis->direction, 0 );
        else
            tmp_axis->neg_function ( tmp_axis, tmp_axis->player, tmp_axis->direction, 0 );

        tmp_axis = tmp_axis->next;
    }

    while ( axis ) {

        if ( !axis->engaged ) {
            axis = axis->next;
            continue;
        }

        /* Handle axis motion */
        if ( motion < 0 ) {
            status = axis->neg_function ( axis, axis->player, axis->direction,
                                         (int)(fabsf(motion * speed) * 0.5f) );
        }
        else {
            status = axis->pos_function ( axis, axis->player, axis->direction,
                                         (int)(fabsf(motion * speed) * 0.5f) );
        }

        if ( status )
            break;

        axis = axis->next;
    }

    return status;
}

/*
 * SDL delivers several relative mouse motion events per frame, each carrying a
 * signed delta.  Just sum the raw deltas here, tag each axis with its behavior
 * (trackball vs positional), and let input_flush_mouse_motion() do the scale,
 * clamp and pokey write once after the frame's events are collected.
 */
int pc_handle_mouse_events ( t_pc_common *axis, float motion,
                             const char *trace_axis ) {

    while ( axis ) {

        if ( !axis->engaged ) {
            axis = axis->next;
            continue;
        }

        /*
         * A mouse axis is split into positive- and negative-direction nodes
         * (the other direction is input_do_nothing).  Only bindings matching
         * this motion's sign fire; the bound Atari direction decides which
         * output axis receives the accumulated motion.
         */
        void *func = ( motion < 0 ) ? axis->neg_function : axis->pos_function;
        int mode;

        if ( func == input_mouse_trackball_axis )
            mode = 1;
        else if ( func == input_mouse_axis )
            mode = 0;
        else {
            axis = axis->next;
            continue;
        }

        input_mouse_accumulate ( axis->player, axis->direction, mode,
                                 fabsf(motion) );

        if ( pc_mouse_trace_enabled() ) {
            printf("MOUSE %-1s raw=% .4f mode=%d\n",
                   trace_axis ? trace_axis : "?", motion, mode);
        }

        axis = axis->next;
    }

    return 0;
}

int pc_handle_button_down_events ( t_pc_common *p_btn ) {

    int status = 0;

    while ( p_btn ) {

        if ( !p_btn->engaged ) {
            p_btn = p_btn->next;
            continue;
        }

        status = p_btn->pos_function ( p_btn, p_btn->player, p_btn->direction, p_btn->atari_key );
        if ( status )
            break;

        p_btn = p_btn->next;
    }

    return status;
}

int pc_handle_button_up_events ( t_pc_common *p_btn ) {

    int status = 0;

    while ( p_btn ) {

        if ( !p_btn->engaged ) {
            p_btn = p_btn->next;
            continue;
        }

        status = p_btn->neg_function ( p_btn, p_btn->player, p_btn->direction, p_btn->atari_key );
        if ( status )
            break;

        p_btn = p_btn->next;
    }

    return status;
}

/******************************************************************************
**  Function   :  pc_poll_input_events
**                            
**  Objective  :  This function checks for input events specfically.  It is 
**                called by pc_poll_events.
**
**  Parameters :  event - event to check for out input from
**                                                
**  return     :  return code (-1 to exit app)
**      
******************************************************************************/
int pc_poll_input_events (SDL_Event *event) {

    int i, status=0;
    int tmp_key_mod;
    int tmp_key_sym;
    int tmp_joy_index;

    t_pc_common *p_kb;
    t_pc_common *p_hat;
    t_pc_common *p_axis;
    t_pc_ball *p_ball;
    t_pc_ball *p_ms;
    t_pc_common *p_btn;

    switch ( event->type ) {
         case SDL_EVENT_KEY_DOWN:
            tmp_key_mod = event->key.mod & 0x0fff;
            tmp_key_sym = event->key.key > 0xff ? 
                (event->key.key & 0xff) | 0x100 : event->key.key;

            p_kb = &g_pc_input.keyboard[tmp_key_sym];

            while ( p_kb ) {
                if ( p_kb->engaged && ((tmp_key_mod & p_kb->modifier) ||
                                                           !p_kb->modifier) ) {

                    if ( (status = p_kb->pos_function ( p_kb, p_kb->player, 
                                       p_kb->direction, p_kb->atari_key )) )
                        return status;
                }
                p_kb = p_kb->next;
            }

            break;
        case SDL_EVENT_KEY_UP:
            tmp_key_sym = event->key.key > 0xff ? 
                (event->key.key & 0xff) | 0x100 : event->key.key;

            p_kb = &g_pc_input.keyboard[tmp_key_sym];

            while ( p_kb ) {
                if ( p_kb->engaged && p_kb->neg_function ) {
                    p_kb->neg_function ( p_kb, p_kb->player, p_kb->direction,
                                                          p_kb->atari_key );
                }
                p_kb = p_kb->next;
            }
            break;
        case SDL_EVENT_JOYSTICK_BALL_MOTION:
            tmp_joy_index = pc_get_joystick_index_from_id(event->jball.which);
            if ( tmp_joy_index < 0 ) break;
            p_ball = &g_pc_input.trackball[tmp_joy_index][event->jball.ball];

            if ( status = pc_handle_ball_events ( &p_ball->x_axis.common, p_ball->speed,
                                                       event->jball.xrel ) )
                return status;

            status = pc_handle_ball_events ( &p_ball->y_axis.common, p_ball->speed,
                                                       event->jball.yrel );
            break;
        case SDL_EVENT_JOYSTICK_HAT_MOTION:
            tmp_joy_index = pc_get_joystick_index_from_id(event->jhat.which);
            if ( tmp_joy_index < 0 ) break;
            p_hat = g_pc_input.pov_hat[tmp_joy_index][event->jhat.hat];

            while ( p_hat ) {
                for ( i = DIR_LEFT; i < MAX_DIR; ++i ) {

                    Uint8 hat_mask;

                    switch (i) {
                        case DIR_LEFT: hat_mask = SDL_HAT_LEFT; break;
                        case DIR_RIGHT: hat_mask = SDL_HAT_RIGHT; break;
                        case DIR_UP: hat_mask = SDL_HAT_UP; break;
                        case DIR_DOWN: hat_mask = SDL_HAT_DOWN; break;
                    }
                    if ( event->jhat.value & hat_mask )
                        status = p_hat[i].pos_function ( p_hat, p_hat[i].player, p_hat[i].direction, p_hat[i].atari_key );
                    else
                        status = p_hat[i].neg_function ( p_hat, p_hat[i].player, p_hat[i].direction, p_hat[i].atari_key );

                    if ( status )
                        return status;
                }
                p_hat = p_hat->next;
            }
            break;
        case SDL_EVENT_JOYSTICK_AXIS_MOTION:
            tmp_joy_index = pc_get_joystick_index_from_id(event->jaxis.which);
            if ( tmp_joy_index < 0 ) break;
            p_axis = &g_pc_input.analog_axis[0][tmp_joy_index][event->jaxis.axis].common;

            status = pc_handle_axis_events ( p_axis, event->jaxis.value );
            break;
        case SDL_EVENT_GAMEPAD_AXIS_MOTION:
            tmp_joy_index = pc_get_joystick_index_from_id(event->gaxis.which);
            if ( tmp_joy_index < 0 ) break;
            p_axis = &g_pc_input.analog_axis[1][tmp_joy_index][event->gaxis.axis].common;

            status = pc_handle_axis_events ( p_axis, event->gaxis.value );
            break;
        case SDL_EVENT_MOUSE_MOTION:
            p_ms = &g_pc_input.mouse;

            if ( pc_mouse_trace_enabled() ) {
                printf("MOUSE event xrel=% .4f yrel=% .4f x=% .2f y=% .2f\n",
                       event->motion.xrel, event->motion.yrel,
                       event->motion.x, event->motion.y);
            }
            pc_handle_mouse_events ( &p_ms->x_axis.common, event->motion.xrel,
                                     "x" );
            pc_handle_mouse_events ( &p_ms->y_axis.common, event->motion.yrel,
                                     "y" );
            break;
        case SDL_EVENT_MOUSE_BUTTON_DOWN:
            p_btn = &g_pc_input.mouse_button[event->button.button];
            status = pc_handle_button_down_events ( p_btn );
            break;
        case SDL_EVENT_MOUSE_BUTTON_UP:
            p_btn = &g_pc_input.mouse_button[event->button.button];
            status = pc_handle_button_up_events ( p_btn );
            break;
        case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
            tmp_joy_index = pc_get_joystick_index_from_id(event->jbutton.which);
            if ( tmp_joy_index < 0 ) break;
            p_btn = &g_pc_input.button[0][tmp_joy_index][event->jbutton.button];
            status = pc_handle_button_down_events ( p_btn );
            break;
        case SDL_EVENT_JOYSTICK_BUTTON_UP:
            tmp_joy_index = pc_get_joystick_index_from_id(event->jbutton.which);
            if ( tmp_joy_index < 0 ) break;
            p_btn = &g_pc_input.button[0][tmp_joy_index][event->jbutton.button];
            status = pc_handle_button_up_events ( p_btn );
            break;
        case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
            tmp_joy_index = pc_get_joystick_index_from_id(event->gbutton.which);
            if ( tmp_joy_index < 0 ) break;
            p_btn = &g_pc_input.button[1][tmp_joy_index][event->gbutton.button];
            status = pc_handle_button_down_events ( p_btn );
            break;
        case SDL_EVENT_GAMEPAD_BUTTON_UP:
            tmp_joy_index = pc_get_joystick_index_from_id(event->gbutton.which);
            if ( tmp_joy_index < 0 ) break;
            p_btn = &g_pc_input.button[1][tmp_joy_index][event->gbutton.button];
            status = pc_handle_button_up_events ( p_btn );
            break;

    } /* end switch event type */

    return status;

} /* end pc_poll_input_events */

/******************************************************************************
**  Function   :  pc_flush_input_events
**
**  Objective  :  Discard any pending input events from the SDL queue.  Used
**                at the GUI->game handoff so the button press that launched a
**                ROM (and any held direction) is not carried into the game.
**
**  Parameters :  NONE
**
**  return     :  NONE
**
******************************************************************************/
void pc_flush_input_events (void) {

    /* Make sure the OS layer has posted everything pending first */
    SDL_PumpEvents();

    SDL_FlushEvents ( SDL_EVENT_KEY_DOWN, SDL_EVENT_KEY_UP );
    SDL_FlushEvents ( SDL_EVENT_JOYSTICK_AXIS_MOTION,
                      SDL_EVENT_JOYSTICK_BUTTON_UP );
    SDL_FlushEvents ( SDL_EVENT_GAMEPAD_AXIS_MOTION,
                      SDL_EVENT_GAMEPAD_BUTTON_UP );

} /* end pc_flush_input_events */

/******************************************************************************
**  Function   :  pc_poll_events
**
**  Objective  :  This function checks for events and acts accordingly
**
**  Parameters :  NONE
**
**  return     :  return code (-1 to exit app)
**
******************************************************************************/
int pc_poll_events (void) {

    int i, status;
    SDL_Event event;
    t_video *p_video = &(config_get_global_cfg()->video);

#ifdef SOUND_DEBUG
    char msg[257];
#endif

     /*
    * Call functions to run every frame        
    */
    for ( i = 0; i < MAX_CONTROLLER; ++i ) {

        g_pc_input.frame_x_function[i] ( NULL, i, 0 );
        g_pc_input.frame_y_function[i] ( NULL, i, 0 );

    } /* end for 5200 controller */

    status = 0;

    while ( SDL_PollEvent(&event) ) {
        /*
         * SDL may queue both GAMEPAD_* and raw JOYSTICK_* input for the same
         * physical gamepad.  Let SDL deliver both, then ignore the raw side at
         * dispatch time so cN|... bindings own gamepad input and jN|... stays
         * reserved for devices SDL does not classify as gamepads.
         */
        if ( pc_event_is_gamepad_joystick_input(&event) ) {
            continue;
        }

        switch (event.type) {

            case SDL_EVENT_QUIT:
                return -1;
                break;

#ifdef __ANDROID__
            /* Android Back button (trapped via SDL_HINT_ANDROID_TRAP_BACK_BUTTON)
             * may arrive as AC_BACK or Escape depending on the TV stick/remote.
             * In-game, route it straight to UI Back so it is not dependent on
             * any player/controller binding.  While the GUI is active, leave it
             * for gui.c's navigation handling. */
            case SDL_EVENT_KEY_DOWN:
                if ( pc_is_android_back_key(&event) ) {
                    unsigned int kstatus = kat5200_get_status();

                    if ( (kstatus & KSTATUS_RUN) && !(kstatus & KSTATUS_GUI) ) {
                        if ( (status = input_ui_back ( NULL, 0, 0, 0)) )
                            return status;
                        continue;
                    }
                }
                break;
#endif

            case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
                if ( pc_handle_unmapped_gui_button ( &event, &status ) ) {
                    if ( status )
                        return status;
                    continue;
                }
                break;

            case SDL_EVENT_WINDOW_RESIZED:
                if (p_video->window_scaling == WINDOW_SCALING_FIT_WINDOW) {
                    int new_w = event.window.data1;
                    int vid_w = g_pc_if.valid_video_rect.w;
                    int vid_h = g_pc_if.valid_video_rect.h;
                    if (vid_w > 0) {
                        int snapped_h = (new_w * vid_h) / vid_w;
                        SDL_SetWindowSize(g_pc_if.screen, new_w, snapped_h);
                    }
                }
                if (!(SDL_GetWindowFlags(g_pc_if.screen) & SDL_WINDOW_FULLSCREEN)) {
                    p_video->width = event.window.data1;
                    p_video->height = event.window.data2;
                    config_save_video ( config_get_global_table(),  "All",
                            KCFG_VIDEO_HEIGHT | KCFG_VIDEO_WIDTH );
                }
                break;
            case SDL_EVENT_WINDOW_MOVED: 
                if (!(SDL_GetWindowFlags(g_pc_if.screen) & SDL_WINDOW_FULLSCREEN)) {
                    p_video->x = event.window.data1;
                    p_video->y = event.window.data2;
                    config_save_video ( config_get_global_table(),  "All",
                            KCFG_VIDEO_X | KCFG_VIDEO_Y );
                }
                break;
            case SDL_EVENT_WINDOW_FOCUS_GAINED:
                if ( g_mouse_grabbed && !(kat5200_get_status() & KSTATUS_GUI) )
                    pc_apply_mouse_grab();
                break;
            case SDL_EVENT_JOYSTICK_ADDED:
                pc_add_joy_id(event.jdevice.which);
                break;
            case SDL_EVENT_JOYSTICK_REMOVED:
                pc_remove_joy_id_and_compact(event.jdevice.which);
                break;
            case SDL_EVENT_GAMEPAD_ADDED:
                pc_add_joy_id(event.gdevice.which);
                break;
            case SDL_EVENT_GAMEPAD_REMOVED:
                pc_remove_joy_id_and_compact(event.gdevice.which);
                break;

        } /* end switch event type */

        unsigned int kstatus = kat5200_get_status();
        unsigned int gui_debug = (KSTATUS_GUI|KSTATUS_DEBUG|KSTATUS_INPUT);

        if (kstatus & KSTATUS_GUI)
            gui_push_event(&event);

        if ((kstatus & KSTATUS_RUN) && ( !(kstatus & KSTATUS_GUI) 
            || ((kstatus & gui_debug)==gui_debug) )) {
            status = pc_poll_input_events(&event);
            if (status)
                return status;
        }

    } /* end check events */

    input_flush_mouse_motion();
    {
        unsigned int kstatus = kat5200_get_status();
        if ( (kstatus & KSTATUS_RUN) && !(kstatus & KSTATUS_GUI) )
            input_auto_fire_frame();
        else
            input_auto_fire_reset();
    }

    return status;

} /* end pc_poll_events */

/******************************************************************************
**  Function   :  pc_render_frame_video_sound
**
**  Objective  :  This function renders game video and sound
**
**  Parameters :  NONE
**
**  return     :  return code (-1 to exit app)
**
******************************************************************************/
int pc_render_frame_video_sound(void) {

    SDL_FRect src_rect;

    /* Convert the INDEX8 GTIA frame to the active ARGB output. */
    pc_refresh_video_output();

    if ( g_pc_if.use_shader ) {
        if ( !pc_render_frame_shader_gpu() ) {
            /* frame submitted through SDL GPU path */
            g_shader_frame_fail_count = 0;
            pc_render_frame_sound();
            return 0;
        }

        /* Shader frame failed.  Tolerate transient errors by skipping just this
           frame and retrying next frame; only tear the runtime down (and fall
           back to the plain renderer for good) after repeated failures, so one
           hiccup does not disable the shader for the rest of the session. */
        if ( ++g_shader_frame_fail_count < SHADER_MAX_CONSEC_FAILS ) {
            pc_render_frame_sound();
            return 0;
        }

        pc_log_shader_message ( "SDL GPU shader fallback",
                "disabling shader after repeated frame failures" );
        pc_free_shader_runtime ();
        SDL_SetRenderDrawBlendMode(g_pc_if.renderer, SDL_BLENDMODE_NONE);
        SDL_RenderPoint(g_pc_if.renderer, -1, -1);
    }

    {
        src_rect.x = g_pc_if.draw_video_rect.x;
        src_rect.y = g_pc_if.draw_video_rect.y;
        src_rect.w = g_pc_if.draw_video_rect.w;
        src_rect.h = g_pc_if.draw_video_rect.h;

        SDL_UpdateTexture(g_pc_if.screen_texture, &g_pc_if.draw_video_rect,
            g_pc_if.draw_pixel_start, g_pc_if.draw_pitch);
        SDL_RenderClear(g_pc_if.renderer);
        SDL_RenderTexture(g_pc_if.renderer, g_pc_if.screen_texture,
            &src_rect, NULL);
            //&g_pc_if.draw_video_rect, NULL);
        SDL_RenderPresent(g_pc_if.renderer);
    }

    /*
     * Process sound — delegate to pc_render_frame_sound() which also handles
     * the debugger-running path (GUI+RUN without video output).
     */
    pc_render_frame_sound();

    return 0;

} /* end pc_render_frame_video_sound */

/******************************************************************************
**  Function   :  pc_render_frame_sound
**
**  Objective  :  Process POKEY sound and feed the audio stream for one frame.
**                Called in GUI+RUN mode (debugger running) so the POKEY event
**                queue is drained each frame and audio plays normally.
**
**  Parameters :  NONE
**
**  return     :  0
**
******************************************************************************/
int pc_render_frame_sound(void) {

    int clks;
    int count = 0;

#ifdef SOUND_DEBUG
    char msg[257];
#endif

    clks = console_get_ptr()->scanlines * 114;
    if (clks > pokey_cpu_clock)
        clks = pokey_cpu_clock;

    pokey_sound_process(g_aud_buf.buf + g_aud_buf.write_pos,
        g_aud_buf.curr_fill_size, clks);

    pokey_check_clock_zero_cross();

    if (g_sound.enable) {
        int queue_size;

        queue_size = SDL_GetAudioStreamAvailable(g_pc_if.audio_stream);

#ifdef SOUND_DEBUG
        sprintf(msg, "Queue Size; %d; ", queue_size);
#endif
        if (queue_size > g_aud_buf.adj_threshold[THRESH_EXTREME_HIGH]) {
#ifdef SOUND_DEBUG
            sprintf(msg, "%s CLEARED; ", msg);
#endif
            SDL_ClearAudioStream(g_pc_if.audio_stream);
            memset(g_aud_buf.extra_buf, SDL_GetSilenceValueForFormat(g_pc_if.audio_d.format), g_pc_if.audio_samples);
            SDL_PutAudioStreamData(g_pc_if.audio_stream, g_aud_buf.extra_buf, g_pc_if.audio_samples);
        }
        if (queue_size == 0) {
#ifdef SOUND_DEBUG
            sprintf(msg, "%s STARVED; ", msg);
#endif
            memset(g_aud_buf.extra_buf, SDL_GetSilenceValueForFormat(g_pc_if.audio_d.format), g_pc_if.audio_samples);
            SDL_PutAudioStreamData(g_pc_if.audio_stream, g_aud_buf.extra_buf, g_pc_if.audio_samples);
            SDL_PutAudioStreamData(g_pc_if.audio_stream, g_aud_buf.extra_buf, g_pc_if.audio_samples);
        }

        SDL_PutAudioStreamData(g_pc_if.audio_stream, g_aud_buf.buf, g_aud_buf.curr_fill_size);
        count += g_aud_buf.curr_fill_size;

#ifdef SOUND_DEBUG
        sprintf(msg, "%s %d; \n", msg, g_aud_buf.curr_fill_size);
        fwrite(msg, sizeof(char), strlen(msg), fp);
        fflush(fp);
#endif

        pc_adjust_sound_rate(queue_size);
        pc_check_sound_freq(count);
    }

    return 0;

} /* end pc_render_frame_sound */

/******************************************************************************
**  Function   :  pc_poll_debug
**                            
**  Objective  :  This function updates the screen during debug         
**
**  Parameters :  NONE
**                                                
**  return     :  return code (-1 to exit app)
**      
******************************************************************************/
int pc_poll_debug (void) {

    SDL_Event event;

    SDL_RenderPresent(g_pc_if.renderer);
    
    while ( SDL_PollEvent(&event) ) {
        switch (event.type) {

            case SDL_EVENT_QUIT: 
                return -1;
                break;
            
             case SDL_EVENT_KEY_DOWN:
                if ( event.key.key == SDLK_ESCAPE)
                    return -1;
                break;
                
        } /* end switch event type */
        
    } /* end check events */
    
    return 0;

} /* end pc_poll_debug */

Uint32 getpixel(SDL_Surface *surface, int x, int y)
{
    int bpp = SDL_BYTESPERPIXEL(surface->format);
    Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;

    switch(bpp) {
    case 1:
        return *p;

    case 2:
        return *(Uint16 *)p;

    case 3:
        if(SDL_BYTEORDER == SDL_BIG_ENDIAN)
            return p[0] << 16 | p[1] << 8 | p[2];
        else
            return p[0] | p[1] << 8 | p[2] << 16;

    case 4:
        return *(Uint32 *)p;

    default:
        return 0;    
    }
}

/******************************************************************************
**  Function   :  pc_convert_bitmap
**                            
**  Objective  :  This function converts a bitmap of colors to a palette file
**
**  Parameters :  old_file  - file of bitmap containing palette
**                new_file  - name of file to store value to
**                                                
**  return     :  Error code
**      
******************************************************************************/
int pc_convert_bitmap ( char *old_file, char *new_file ) {

    Uint8 r[100],g[100],b[100];
    Uint32 pixel;
    unsigned long red_avg, blue_avg, green_avg;
    SDL_Surface *bitmap;
    int i,x,y;
    int j,k,l,m;
    int pal[256];
    double x_size, y_size;

    bitmap = SDL_LoadBMP ( old_file );
    const SDL_PixelFormatDetails *f_details = SDL_GetPixelFormatDetails(bitmap->format);

    x_size = (double)bitmap->w/16.0;
    y_size = (double)bitmap->h/16.0;

    i = -1;
    for ( y = 0; y < 16; ++y ) {
        for ( x = 0; x < 16; ++x ) {
            l = 0;
            for ( j = 0; j < 12; j++ ) {
                for ( k = 0; k < 8; k++ ) {
                    ++l;
                    pixel = getpixel ( bitmap, (int)(x_size*x+((x_size/2)-6)+j), (int)(y_size*y+((y_size/2)-4)+k) ); 
                    SDL_GetRGB(pixel, f_details, NULL, &r[l], &g[l], &b[l]);
                }
            }

            red_avg = 0;
            for ( m = 0; m < l; ++m )
                red_avg += r[m];
            red_avg = red_avg / l;
            green_avg = 0;
            for ( m = 0; m < l; ++m )
                green_avg += g[m];
            green_avg = green_avg / l;
            blue_avg = 0;
            for ( m = 0; m < l; ++m )
                blue_avg += b[m];
            blue_avg = blue_avg / l;
            pal[++i] = (int)((red_avg<<16) + (green_avg<<8) + blue_avg);
        }
    }

    SDL_DestroySurface ( bitmap );

    config_save_palette (config_get_global_table(), new_file, pal );

    return 0;

} /* end pc_convert_bitmap */

void pc_set_common_defaults ( t_input_common *p_in ) {

    p_in->device[0] = DEV_KEYBOARD;
    p_in->part_type[0] = PART_TYPE_KEY;
    p_in->direction[0] = DIR_PRESS;
    p_in->device_num[0] = 0;
    p_in->combo = 0;
    p_in->enable = 1;
}

static void pc_add_keyboard_default ( t_input_common *p_in, int key ) {

    t_input_common *p_next;

    while ( p_in ) {
        if ( p_in->device[0] == DEV_KEYBOARD &&
             p_in->part_type[0] == PART_TYPE_KEY &&
             p_in->part_num[0] == key )
            return;
        if ( !p_in->next )
            break;
        p_in = p_in->next;
    }

    p_next = malloc ( sizeof(t_input_common) );
    if ( !p_next )
        return;

    pc_set_common_defaults ( p_next );
    p_next->part_num[0] = key;
    p_next->next = 0;
    p_in->next = p_next;

} /* end pc_add_keyboard_default */

/******************************************************************************
**  Function   :  pc_set_ui_defaults
**                            
**  Objective  :  This function sets up defaults for the ui keys
**
**  Parameters :  NONE
**                                                
**  return     :  NONE                             
**      
******************************************************************************/
void pc_set_ui_defaults ( void ) {

    int i;
    t_config *p_cfg = config_get_global_cfg();
    t_ui_input *p_ui = &(p_cfg->input.ui);

    t_input_common exit;
    t_input_common back;
    t_input_common fullscreen;
    t_input_common screenshot;
    t_input_common debugger;
    t_input_common state_load[MAX_UI_STATES];
    t_input_common state_save[MAX_UI_STATES];

    /*
     * UI keys
     */
    for ( i = 0; i < MAX_UI_STATES; ++i ) {
        pc_set_common_defaults ( &p_ui->state_load[i] );
        p_ui->state_load[i].device_num[0] = SDL_KMOD_ALT;
        p_ui->state_load[i].part_num[0] = SDLK_1 + i;

        pc_set_common_defaults ( &p_ui->state_save[i] );
        p_ui->state_save[i].device_num[0] = SDL_KMOD_SHIFT;
        p_ui->state_save[i].part_num[0] = SDLK_1 + i;
    }

    pc_set_common_defaults ( &p_ui->exit );
    p_ui->exit.device_num[0] = SDL_KMOD_ALT;
    p_ui->exit.part_num[0] = SDLK_F4;

    pc_set_common_defaults ( &p_ui->back );
    p_ui->back.device_num[0] = 0;
    p_ui->back.part_num[0] = SDLK_ESCAPE;

    pc_set_common_defaults ( &p_ui->fullscreen );
    p_ui->fullscreen.device_num[0] = SDL_KMOD_ALT;
    p_ui->fullscreen.part_num[0] = SDLK_RETURN;

    pc_set_common_defaults ( &p_ui->screenshot );
    p_ui->screenshot.device_num[0] = SDL_KMOD_ALT;
    p_ui->screenshot.part_num[0] = SDLK_S;

    pc_set_common_defaults ( &p_ui->debugger );
    p_ui->debugger.device_num[0] = 0;
    p_ui->debugger.part_num[0] = SDLK_F1;

} /* end pc_set_ui_defaults */

/******************************************************************************
**  Function   :  pc_set_controller_defaults
**                            
**  Objective  :  This function sets up defaults for the controllers
**
**  Parameters :  NONE
**                                                
**  return     :  NONE                             
**      
******************************************************************************/
void pc_set_controller_defaults ( void ) {

    int i,j;
    t_config *p_cfg = config_get_global_cfg();
    t_game_input *p_game = &(p_cfg->input.game);

    p_game->players[0].type = CTRLR_TYPE_JOYSTICK;
    p_game->players[1].type = CTRLR_TYPE_NONE;
    p_game->players[2].type = CTRLR_TYPE_NONE;
    p_game->players[3].type = CTRLR_TYPE_NONE;

    for ( i = 0; i < MAX_DIR; ++i ) {
        for ( j = 0; j < MAX_CONTROLLER; ++j ) {
            p_game->players[j].joy_deadzone = 20;
            p_game->players[j].joy_sensitivity = 100;
            p_game->players[j].mouse_speed = 3;
            p_game->players[j].auto_fire = 0;
            p_game->players[j].auto_fire_speed = 3;
            p_game->players[j].mouse_optimize = MOUSE_OPTIMIZE_PADDLES;
            p_game->players[j].mouse_enable = 0;
            p_game->players[j].keyboard_enable = 1;
            p_game->players[j].joy_enable = 1;
            p_game->players[j].dpad_enable = 1;
            p_game->players[j].digdir_analog_enable = 0;
            p_game->players[j].digdir_analog_speed = 3;
        }
    }
    p_game->players[1].keyboard_enable = 0;

    /*
     * Keypad 1
     */
    for ( i = 0; i < 0x10; ++i ) {
        pc_set_common_defaults ( &p_game->players[0].keypad[i] );
    }
    p_game->players[0].keypad[KEY_0>>1].part_num[0] = SDLK_KP_0;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_0>>1], SDLK_0 );
    p_game->players[0].keypad[KEY_1>>1].part_num[0] = SDLK_KP_1;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_1>>1], SDLK_1 );
    p_game->players[0].keypad[KEY_2>>1].part_num[0] = SDLK_KP_2;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_2>>1], SDLK_2 );
    p_game->players[0].keypad[KEY_3>>1].part_num[0] = SDLK_KP_3;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_3>>1], SDLK_3 );
    p_game->players[0].keypad[KEY_4>>1].part_num[0] = SDLK_KP_4;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_4>>1], SDLK_4 );
    p_game->players[0].keypad[KEY_5>>1].part_num[0] = SDLK_KP_5;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_5>>1], SDLK_5 );
    p_game->players[0].keypad[KEY_6>>1].part_num[0] = SDLK_KP_6;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_6>>1], SDLK_6 );
    p_game->players[0].keypad[KEY_7>>1].part_num[0] = SDLK_KP_7;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_7>>1], SDLK_7 );
    p_game->players[0].keypad[KEY_8>>1].part_num[0] = SDLK_KP_8;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_8>>1], SDLK_8 );
    p_game->players[0].keypad[KEY_9>>1].part_num[0] = SDLK_KP_9;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_9>>1], SDLK_9 );
    p_game->players[0].keypad[KEY_START>>1].part_num[0] = SDLK_F2;
    p_game->players[0].keypad[KEY_PAUSE>>1].part_num[0] = SDLK_F3;
    p_game->players[0].keypad[KEY_RESET>>1].part_num[0] = SDLK_F4;
    p_game->players[0].keypad[KEY_POUND>>1].part_num[0] = SDLK_KP_MINUS;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_POUND>>1], SDLK_MINUS );
    p_game->players[0].keypad[KEY_STAR>>1].part_num[0] = SDLK_KP_MULTIPLY;
    pc_add_keyboard_default ( &p_game->players[0].keypad[KEY_STAR>>1], SDLK_EQUALS );

    /*
     * Keypad 2
     */
    for ( i = 0; i < 0x10; ++i ) {
        pc_set_common_defaults ( &p_game->players[1].keypad[i] );
    }
    p_game->players[1].keypad[KEY_0>>1].part_num[0] = SDLK_0;
    p_game->players[1].keypad[KEY_1>>1].part_num[0] = SDLK_1;
    p_game->players[1].keypad[KEY_2>>1].part_num[0] = SDLK_2;
    p_game->players[1].keypad[KEY_3>>1].part_num[0] = SDLK_3;
    p_game->players[1].keypad[KEY_4>>1].part_num[0] = SDLK_4;
    p_game->players[1].keypad[KEY_5>>1].part_num[0] = SDLK_5;
    p_game->players[1].keypad[KEY_6>>1].part_num[0] = SDLK_6;
    p_game->players[1].keypad[KEY_7>>1].part_num[0] = SDLK_7;
    p_game->players[1].keypad[KEY_8>>1].part_num[0] = SDLK_8;
    p_game->players[1].keypad[KEY_9>>1].part_num[0] = SDLK_9;
    p_game->players[1].keypad[KEY_START>>1].part_num[0] = SDLK_F10;
    p_game->players[1].keypad[KEY_PAUSE>>1].part_num[0] = SDLK_F11;
    p_game->players[1].keypad[KEY_RESET>>1].part_num[0] = SDLK_F12;
    p_game->players[1].keypad[KEY_POUND>>1].part_num[0] = SDLK_TAB;
    p_game->players[1].keypad[KEY_STAR>>1].part_num[0] = SDLK_Q;

    /*
     * Buttons
     */
    for ( i = 0; i < MAX_CONTROLLER; ++i ) {

        pc_set_common_defaults ( &p_game->players[i].top_button );
        pc_set_common_defaults ( &p_game->players[i].bottom_button );

        if ( i == 0 ) {
            p_game->players[i].top_button.part_num[0] = SDLK_RSHIFT;
            if ( p_cfg->general.machine == MACHINE_TYPE_5200 )
                p_game->players[i].bottom_button.part_num[0] = SDLK_RETURN;
            else
                p_game->players[i].bottom_button.part_num[0] = SDLK_KP_ENTER;
        }
        else if ( i == 1 ) {
            p_game->players[i].top_button.part_num[0] = SDLK_KP_PLUS;
            p_game->players[i].bottom_button.part_num[0] = SDLK_KP_ENTER;
        }
    }

    /*
     * Pots
     */
    for ( i = 0; i < MAX_CONTROLLER; ++i ) {
        for ( j = 0; j < MAX_DIR; ++j ) {
            pc_set_common_defaults ( &p_game->players[i].stick[j] );
        }
        for ( j = 0; j < 2; ++j ) {
            pc_set_common_defaults ( &p_game->players[i].paddles[j].left );
            pc_set_common_defaults ( &p_game->players[i].paddles[j].right );
            pc_set_common_defaults ( &p_game->players[i].paddles[j].button );
        }
    }
    p_game->players[0].stick[DIR_LEFT].part_num[0] = SDLK_LEFT;
    p_game->players[0].stick[DIR_RIGHT].part_num[0] = SDLK_RIGHT;
    p_game->players[0].stick[DIR_UP].part_num[0] = SDLK_UP;
    p_game->players[0].stick[DIR_DOWN].part_num[0] = SDLK_DOWN;

    if ( p_cfg->general.machine == MACHINE_TYPE_5200 ) {
        pc_add_keyboard_default ( &p_game->players[0].stick[DIR_LEFT], SDLK_A );
        pc_add_keyboard_default ( &p_game->players[0].stick[DIR_RIGHT], SDLK_D );
        pc_add_keyboard_default ( &p_game->players[0].stick[DIR_UP], SDLK_W );
        pc_add_keyboard_default ( &p_game->players[0].stick[DIR_DOWN], SDLK_S );
    }

    p_game->players[1].stick[DIR_LEFT].part_num[0] = SDLK_KP_4;
    p_game->players[1].stick[DIR_RIGHT].part_num[0] = SDLK_KP_6;
    p_game->players[1].stick[DIR_UP].part_num[0] = SDLK_KP_8;
    p_game->players[1].stick[DIR_DOWN].part_num[0] = SDLK_KP_2;

    p_game->players[0].paddles[0].left.part_num[0] = SDLK_LEFT;
    p_game->players[0].paddles[0].right.part_num[0] = SDLK_RIGHT;
    if ( p_cfg->general.machine == MACHINE_TYPE_5200 )
        p_game->players[0].paddles[0].button.part_num[0] = SDLK_RETURN;
    else
        p_game->players[0].paddles[0].button.part_num[0] = SDLK_KP_ENTER;
    p_game->players[0].paddles[1].left.part_num[0] = SDLK_A;
    p_game->players[0].paddles[1].right.part_num[0] = SDLK_D;
    p_game->players[0].paddles[1].button.part_num[0] = SDLK_LSHIFT;

    p_game->players[1].paddles[0].left.part_num[0] = SDLK_KP_4;
    p_game->players[1].paddles[0].right.part_num[0] = SDLK_KP_6;
    p_game->players[1].paddles[0].button.part_num[0] = SDLK_KP_ENTER;
    p_game->players[1].paddles[1].left.part_num[0] = SDLK_W;
    p_game->players[1].paddles[1].right.part_num[0] = SDLK_S;
    p_game->players[1].paddles[1].button.part_num[0] = SDLK_LCTRL;

} /* end pc_set_controller_defaults */

/******************************************************************************
**  Function   :  pc_set_keyboard_defaults
**                            
**  Objective  :  This function sets up defaults for the controllers
**
**  Parameters :  NONE
**                                                
**  return     :  NONE                             
**      
******************************************************************************/
void pc_set_keyboard_defaults ( void ) {

    int i;
    t_game_input *p_game = &(config_get_global_cfg()->input.game);

    pc_set_common_defaults ( &p_game->ctrl_key );
    pc_set_common_defaults ( &p_game->shift_key );
    pc_set_common_defaults ( &p_game->reset_key );
    pc_set_common_defaults ( &p_game->break_key );
    pc_set_common_defaults ( &p_game->start_key );
    pc_set_common_defaults ( &p_game->select_key );
    pc_set_common_defaults ( &p_game->option_key );

    p_game->ctrl_key.part_num[0] = SDLK_LCTRL;
    p_game->shift_key.part_num[0] = SDLK_LSHIFT;
    p_game->reset_key.part_num[0] = SDLK_F12;
    p_game->break_key.part_num[0] = SDLK_F7;
    pc_add_keyboard_default ( &p_game->break_key, SDLK_SCROLLLOCK );
    p_game->start_key.part_num[0] = SDLK_F2;
    p_game->select_key.part_num[0] = SDLK_F3;
    p_game->option_key.part_num[0] = SDLK_F4;

    for ( i = 0; i < 0x40; ++i ) {
        pc_set_common_defaults ( &p_game->keyboard[i] );
    }

    p_game->keyboard[A800_KEY_L].part_num[0] =  SDLK_L;
    p_game->keyboard[A800_KEY_J].part_num[0] =  SDLK_J;
    p_game->keyboard[A800_KEY_SEMI].part_num[0] =  SDLK_SEMICOLON;
    p_game->keyboard[A800_KEY_K].part_num[0] =  SDLK_K;
    p_game->keyboard[A800_KEY_PLUS].part_num[0] =  SDLK_KP_PLUS;
    p_game->keyboard[A800_KEY_STAR].part_num[0] =  SDLK_KP_MULTIPLY;
    p_game->keyboard[A800_KEY_O].part_num[0] =  SDLK_O;
    p_game->keyboard[A800_KEY_P].part_num[0] =  SDLK_P;
    p_game->keyboard[A800_KEY_U].part_num[0] =  SDLK_U;
    p_game->keyboard[A800_KEY_RETURN].part_num[0] =  SDLK_RETURN;
    p_game->keyboard[A800_KEY_I].part_num[0] =  SDLK_I;
    p_game->keyboard[A800_KEY_MINUS].part_num[0] =  SDLK_MINUS;
    p_game->keyboard[A800_KEY_EQUALS].part_num[0] =  SDLK_EQUALS;
    p_game->keyboard[A800_KEY_V].part_num[0] =  SDLK_V;
    p_game->keyboard[A800_KEY_HELP].part_num[0] =  SDLK_F6;
    p_game->keyboard[A800_KEY_C].part_num[0] =  SDLK_C;
    p_game->keyboard[A800_KEY_B].part_num[0] =  SDLK_B;
    p_game->keyboard[A800_KEY_X].part_num[0] =  SDLK_X;
    p_game->keyboard[A800_KEY_Z].part_num[0] =  SDLK_Z;
    p_game->keyboard[A800_KEY_4].part_num[0] =  SDLK_4;
    p_game->keyboard[A800_KEY_3].part_num[0] =  SDLK_3;
    p_game->keyboard[A800_KEY_6].part_num[0] =  SDLK_6;
    p_game->keyboard[A800_KEY_ESC].part_num[0] =  SDLK_F2;
    p_game->keyboard[A800_KEY_5].part_num[0] =  SDLK_5;
    p_game->keyboard[A800_KEY_2].part_num[0] =  SDLK_2;
    p_game->keyboard[A800_KEY_1].part_num[0] =  SDLK_1;
    p_game->keyboard[A800_KEY_COMMA].part_num[0] =  SDLK_COMMA;
    p_game->keyboard[A800_KEY_SPACE].part_num[0] =  SDLK_SPACE;
    p_game->keyboard[A800_KEY_PERIOD].part_num[0] =  SDLK_PERIOD;
    p_game->keyboard[A800_KEY_N].part_num[0] =  SDLK_N;
    p_game->keyboard[A800_KEY_M].part_num[0] =  SDLK_M;
    p_game->keyboard[A800_KEY_SLASH].part_num[0] =  SDLK_SLASH;
    p_game->keyboard[A800_KEY_FUJI].part_num[0] =  SDLK_LGUI;
    p_game->keyboard[A800_KEY_R].part_num[0] =  SDLK_R;
    p_game->keyboard[A800_KEY_E].part_num[0] =  SDLK_E;
    p_game->keyboard[A800_KEY_Y].part_num[0] =  SDLK_Y;
    p_game->keyboard[A800_KEY_TAB].part_num[0] =  SDLK_TAB;
    p_game->keyboard[A800_KEY_T].part_num[0] =  SDLK_T;
    p_game->keyboard[A800_KEY_W].part_num[0] =  SDLK_W;
    p_game->keyboard[A800_KEY_Q].part_num[0] =  SDLK_Q;
    p_game->keyboard[A800_KEY_9].part_num[0] =  SDLK_9;
    p_game->keyboard[A800_KEY_0].part_num[0] =  SDLK_0;
    p_game->keyboard[A800_KEY_7].part_num[0] =  SDLK_7;
    p_game->keyboard[A800_KEY_BKSP].part_num[0] =  SDLK_BACKSPACE;
    p_game->keyboard[A800_KEY_8].part_num[0] =  SDLK_8;
    p_game->keyboard[A800_KEY_LESST].part_num[0] =  SDLK_LEFTBRACKET;
    p_game->keyboard[A800_KEY_MORET].part_num[0] =  SDLK_RIGHTBRACKET;
    p_game->keyboard[A800_KEY_F].part_num[0] =  SDLK_F;
    p_game->keyboard[A800_KEY_H].part_num[0] =  SDLK_H;
    p_game->keyboard[A800_KEY_D].part_num[0] =  SDLK_D;
    p_game->keyboard[A800_KEY_CAPS].part_num[0] =  SDLK_CAPSLOCK;
    p_game->keyboard[A800_KEY_G].part_num[0] =  SDLK_G;
    p_game->keyboard[A800_KEY_S].part_num[0] =  SDLK_S;
    p_game->keyboard[A800_KEY_A].part_num[0] =  SDLK_A;

} /* end pc_set_keyboard_defaults */

/******************************************************************************
**  Function   :  pc_check_for_lr_keys
**                            
**  Objective  :  This function checks for keys such as SHIFT and CTRL that
**                are on left and right side of keyboard and return the mate
**
**  Parameters :  key_value - key to check for 
**                                                
**  return     :  the key's mate on the other side
**      
******************************************************************************/
int pc_check_for_lr_keys ( int key_value ) {

    switch ( key_value ) {

        case SDLK_RSHIFT: return SDLK_LSHIFT; break;
        case SDLK_LSHIFT: return SDLK_RSHIFT; break;
        case SDLK_RCTRL:  return SDLK_LCTRL;  break;
        case SDLK_LCTRL:  return SDLK_RCTRL;  break;
        case SDLK_RALT:   return SDLK_LALT;   break;
        case SDLK_LALT:   return SDLK_RALT;   break;
        case SDLK_RGUI:  return SDLK_LGUI;  break;
        case SDLK_LGUI:  return SDLK_RGUI;  break;

        default: return 0; break;
    }

} /* pc_check_for_lf_keys */

/******************************************************************************
**  Function   :  pc_adjust_key_value
**                            
**  Objective  :  This function checks for key values above 0xff and adjusts
**                them to be in the 0x1ff range to avoid overrunning the pc
**                keyboard array
**
**  Parameters :  key_value - key to check for 
**                                                
**  return     :  adjusted value or input value
**      
******************************************************************************/
int pc_adjust_key_value ( int key_value ) {

    if (key_value > 0xff)
        return (key_value & 0xff) | 0x100;
    else
        return key_value;

} /* pc_adjust_key_value */

/******************************************************************************
**  Function   :  pc_interpret_key
**                            
**  Objective  :  This function checks the string for the key desired
**                config file
**
**  Parameters :  string - string to check for key
**                                                
**  return     :  SDL key value                   
**      
******************************************************************************/
int pc_interpret_key ( const char *string ) {

    int value = 0;

    /*
     * Get rid of leading spaces
     */
    //while ( string[0] == ' ' )
    //    string += 1;

    /*
     * Check for actual key value instead of string
     */
    value = atoi ( string );
    if ( value > 9 )
        return value;

    /*
     * Check for keypad
     */
    if ( !strcmp ( string, "kp0" ) )
        return SDLK_KP_0;

    if ( (string[0]=='k') && (string[1]=='p') ) {
        if ( strstr ( string, "multiply" ) )
            return SDLK_KP_MULTIPLY;
        else if ( strstr ( string, "divide" ) )
            return SDLK_KP_DIVIDE;
        else if ( strstr ( string, "period" ) )
            return SDLK_KP_PERIOD;
        else if ( strstr ( string, "minus" ) )
            return SDLK_KP_MINUS;
        else if ( strstr ( string, "plus" ) )
            return SDLK_KP_PLUS;
        else if ( strstr ( string, "kp_enter" ) )
            return SDLK_KP_ENTER;
        else if ( strstr ( string, "equals" ) )
            return SDLK_KP_EQUALS;
        else
            return SDLK_KP_1 + atoi ( string+2 ) - 1;
    }

    /*
     * Check for arrows, home, end
     */
    if (strstr(string, "leftbracket"))
        return SDLK_LEFTBRACKET;
    else if (strstr(string, "rightbracket"))
        return SDLK_RIGHTBRACKET;
    else if (strstr(string, "page_up"))
        return SDLK_PAGEUP;
    else if (strstr(string, "page_down"))
        return SDLK_PAGEDOWN;
    else if ( strstr ( string, "up" ) )
        return SDLK_UP;
    else if ( strstr ( string, "down" ) )
        return SDLK_DOWN;
    else if ( strstr ( string, "right" ) )
        return SDLK_RIGHT;
    else if ( strstr ( string, "left" ) )
        return SDLK_LEFT;
    else if ( strstr ( string, "insert" ) )
        return SDLK_INSERT;
    else if ( strstr ( string, "home" ) )
        return SDLK_HOME;
    else if ( strstr ( string, "end" ) )
        return SDLK_END;

    /*
     * Check for function keys
     */
    if ( (string[0] == 'f') && (strlen(string+1) > 0) ) {
        if ( atoi(string+1) )
            return (SDLK_F1-1) + atoi(string+1);
    }

    /*
     * Check for state keys
     */
    if ( strstr ( string, "numlock" ) )
        return SDLK_NUMLOCKCLEAR;
    else if ( strstr ( string, "capslock" ) )
        return SDLK_CAPSLOCK;
    else if ( strstr ( string, "scrollock" ) )
        return SDLK_SCROLLLOCK;
    else if ( strstr ( string, "rshift" ) )
        return SDLK_RSHIFT;
    else if ( strstr ( string, "lshift" ) )
        return SDLK_LSHIFT;
    else if ( strstr ( string, "rctrl" ) )
        return SDLK_RCTRL;
    else if ( strstr ( string, "lctrl" ) )
        return SDLK_LCTRL;
    else if ( strstr ( string, "ralt" ) )
        return SDLK_RALT;
    else if ( strstr ( string, "lalt" ) )
        return SDLK_LALT;
    else if ( strstr ( string, "rgui" ) )
        return SDLK_RGUI;
    else if ( strstr ( string, "lgui" ) )
        return SDLK_LGUI;

    /*
     * Check for other keys
     */
    if ( strstr ( string, "backspace" ) )
        return SDLK_BACKSPACE;
    else if ( strstr ( string, "tab" ) )
        return SDLK_TAB;
    else if ( strstr ( string, "esc" ) )
        return SDLK_ESCAPE;
    else if ( strstr ( string, "clear" ) )
        return SDLK_CLEAR;
    else if ( strstr ( string, "return" ) )
        return SDLK_RETURN;
    else if ( strstr ( string, "space" ) )
        return SDLK_SPACE;
    else if ( strstr ( string, "comma" ) )
        return SDLK_COMMA;
    else if ( strstr ( string, "minus" ) )
        return SDLK_MINUS;
    //else if ( strstr ( string, "plus" ) )
    //    return SDLK_PLUS;
    else if ( strstr ( string, "equals" ) )
        return SDLK_EQUALS;
    else if ( strstr ( string, "period" ) )
        return SDLK_PERIOD;
    else if ( strstr ( string, "semicolon" ) )
        return SDLK_SEMICOLON;
    else if ( strstr ( string, "backslash" ) )
        return SDLK_BACKSLASH;
    else if ( strstr ( string, "slash" ) )
        return SDLK_SLASH;
    else if ( strstr ( string, "delete" ) )
        return SDLK_DELETE;
    else if ( strstr ( string, "break" ) )
        return SDLK_PAUSE;

    /*
     * Check for numbers
     */
    if ( atoi(string) || string[0] == '0' ) {
        return atoi(string) + SDLK_0;
    }

    /*
     * Check for letters
     */
    if ( string[0] >= 'A' && string[0] <= 'z' )
        return tolower( string[0] );

    return string[0];

} /* pc_interpret_key */

/******************************************************************************
**  Function   :  pc_interpret_controller_button
**                            
**  Objective  :  This function converts the string to the controller button
**                for when a SDL game controller is detected
**
**  Parameters :  string - string to check for key
**                                                
**  return     :  SDL button value                   
**      
******************************************************************************/
int pc_interpret_controller_button ( const char *string ) {

    int value = 0;

    if (strstr(string, "back"))
        return SDL_GAMEPAD_BUTTON_BACK;
    else if (strstr(string, "guide"))
        return SDL_GAMEPAD_BUTTON_GUIDE;
    else if (strstr(string, "start"))
        return SDL_GAMEPAD_BUTTON_START;
    else if (strstr(string, "leftstick"))
        return SDL_GAMEPAD_BUTTON_LEFT_STICK;
    else if ( strstr ( string, "rightstick" ) )
        return SDL_GAMEPAD_BUTTON_RIGHT_STICK;
    else if ( strstr ( string, "leftshoulder" ) )
        return SDL_GAMEPAD_BUTTON_LEFT_SHOULDER;
    else if ( strstr ( string, "rightshoulder" ) )
        return SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER;
    else if ( strstr ( string, "dpad_up" ) )
        return SDL_GAMEPAD_BUTTON_DPAD_UP;
    else if ( strstr ( string, "dpad_down" ) )
        return SDL_GAMEPAD_BUTTON_DPAD_DOWN;
    else if ( strstr ( string, "dpad_left" ) )
        return SDL_GAMEPAD_BUTTON_DPAD_LEFT;
    else if ( strstr ( string, "dpad_right" ) )
        return SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
    else if ( strstr ( string, "x" ) )
        return SDL_GAMEPAD_BUTTON_WEST;
    else if ( strstr ( string, "y" ) )
        return SDL_GAMEPAD_BUTTON_NORTH;
    else if ( strstr ( string, "a" ) )
        return SDL_GAMEPAD_BUTTON_SOUTH;
    else if ( strstr ( string, "b" ) )
        return SDL_GAMEPAD_BUTTON_EAST;

    return SDL_GAMEPAD_BUTTON_WEST;

} /* pc_interpret_controller_button */

/******************************************************************************
**  Function   :  pc_interpret_controller_button_value
**                            
**  Objective  :  This function converts the value of the controller button
**                to a string for when a SDL game controller is detected
**
**  Parameters : value - SDL Controller value to get name for 
**               string - output containing gthe name
**                                                
**  return     :  string
**      
******************************************************************************/
char * pc_interpret_controller_button_value ( int value, char *string ) {

    strcpy ( string, "x" );

    if ( value == SDL_GAMEPAD_BUTTON_BACK )
        return strcpy(string, "back");
    if ( value == SDL_GAMEPAD_BUTTON_GUIDE )
        return strcpy(string, "guide");
    if ( value == SDL_GAMEPAD_BUTTON_START )
        return strcpy(string, "start");
    if ( value == SDL_GAMEPAD_BUTTON_LEFT_STICK )
        return strcpy(string, "leftstick");
    if ( value == SDL_GAMEPAD_BUTTON_RIGHT_STICK )
         return strcpy ( string, "rightstick" );
    if ( value == SDL_GAMEPAD_BUTTON_LEFT_SHOULDER )
         return strcpy ( string, "leftshoulder" );
    if ( value == SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER )
         return strcpy ( string, "rightshoulder" );
    if ( value == SDL_GAMEPAD_BUTTON_DPAD_UP )
         return strcpy ( string, "dpad_up" );
    if ( value == SDL_GAMEPAD_BUTTON_DPAD_DOWN )
         return strcpy ( string, "dpad_down" );
    if ( value == SDL_GAMEPAD_BUTTON_DPAD_LEFT )
         return strcpy ( string, "dpad_left" );
    if ( value == SDL_GAMEPAD_BUTTON_DPAD_RIGHT )
         return strcpy ( string, "dpad_right" );
    if ( value == SDL_GAMEPAD_BUTTON_WEST )
         return strcpy ( string, "x" );
    if ( value == SDL_GAMEPAD_BUTTON_NORTH )
         return strcpy ( string, "y" );
    if ( value == SDL_GAMEPAD_BUTTON_SOUTH )
         return strcpy ( string, "a" );
    if ( value == SDL_GAMEPAD_BUTTON_EAST )
         return strcpy ( string, "b" );

} /* pc_interpret_controller_button_value */

/******************************************************************************
**  Function   :  pc_interpret_controller_axis
**                            
**  Objective  :  This function converts the string to the controller axis
**                for when a SDL game controller is detected
**
**  Parameters :  string - string to check for axis value
**                                                
**  return     :  SDL axis value                   
**      
******************************************************************************/
int pc_interpret_controller_axis ( const char *string ) {

    int value = 0;

    if (strstr(string, "leftx"))
        return SDL_GAMEPAD_AXIS_LEFTX;
    else if (strstr(string, "lefty"))
        return SDL_GAMEPAD_AXIS_LEFTY;
    else if (strstr(string, "rightx"))
        return SDL_GAMEPAD_AXIS_RIGHTX;
    else if (strstr(string, "righty"))
        return SDL_GAMEPAD_AXIS_RIGHTY;
    else if ( strstr ( string, "triggerleft" ) )
        return SDL_GAMEPAD_AXIS_LEFT_TRIGGER;
    else if ( strstr ( string, "triggerright" ) )
        return SDL_GAMEPAD_AXIS_RIGHT_TRIGGER;

    return SDL_GAMEPAD_AXIS_LEFTX;

} /* pc_interpret_controller_axis */

/******************************************************************************
**  Function   :  pc_interpret_controller_axis_value
**                            
**  Objective  :  This function converts the value of the controller button
**                to a string for when a SDL game controller is detected
**
**  Parameters : value - SDL Controller value to get name for 
**               string - output containing gthe name
**                                                
**  return     :  string
**      
******************************************************************************/
char * pc_interpret_controller_axis_value ( int value, char *string ) {

    strcpy(string, "leftx");

    if ( value == SDL_GAMEPAD_AXIS_LEFTX )
        return strcpy(string, "leftx");
    if ( value == SDL_GAMEPAD_AXIS_LEFTY )
        return strcpy(string, "lefty");
    if ( value == SDL_GAMEPAD_AXIS_RIGHTX )
        return strcpy(string, "rightx");
    if ( value == SDL_GAMEPAD_AXIS_RIGHTY )
        return strcpy(string, "righty");
    if ( value == SDL_GAMEPAD_AXIS_LEFT_TRIGGER )
         return strcpy ( string, "triggerleft" );
    if ( value == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER )
         return strcpy ( string, "triggerright" );

} /* pc_interpret_controller_axis_value */

/******************************************************************************
**  Function   :  pc_interpret_key_value
**                            
**  Objective  :  This function checks the input value and outputs the key name
**
**  Parameters :  value  - SDL key value to get name for
**                string - Output containing name
**                                                
**  return     :  string                          
**      
******************************************************************************/
char * pc_interpret_key_value ( int value, char *string ) {

    *string = '\0';

    sprintf ( string, "%d", value );

    /*
     * Check for keypad
     */
    if ( (value >= SDLK_KP_1) && (value <= SDLK_KP_9) ) {
        sprintf ( string, "kp%d", (value - SDLK_KP_1 + 1) );
        return string;
    }

    if ( value == SDLK_KP_0 )
        return strcpy ( string, "kp0" );

    if ( value == SDLK_KP_MULTIPLY )
        return strcpy ( string, "kp_multiply" );
    else if ( value == SDLK_KP_DIVIDE )
        return strcpy ( string, "kp_divide" );
    else if ( value == SDLK_KP_PERIOD )
        return strcpy ( string, "kp_period" );
    else if ( value == SDLK_KP_MINUS )
        return strcpy ( string, "kp_minus" );
    else if ( value == SDLK_KP_PLUS )
        return strcpy ( string, "kp_plus" );
    else if ( value == SDLK_KP_ENTER )
        return strcpy ( string, "kp_enter" );
    else if ( value == SDLK_KP_EQUALS )
        return strcpy ( string, "kp_equals" );

    /*
     * Check for arrows, home, end
     */
    if ( value == SDLK_UP )
        return strcpy ( string, "up" );
    else if ( value == SDLK_DOWN )
        return strcpy ( string, "down" );
    else if ( value == SDLK_RIGHT )
        return strcpy ( string, "right" );
    else if ( value == SDLK_LEFT )
        return strcpy ( string, "left" );
    else if ( value == SDLK_INSERT )
        return strcpy ( string, "insert" );
    else if ( value == SDLK_HOME )
        return strcpy ( string, "home" );
    else if ( value == SDLK_END )
        return strcpy ( string, "end" );
    else if ( value == SDLK_PAGEUP )
        return strcpy ( string, "page_up" );
    else if ( value == SDLK_PAGEDOWN )
        return strcpy ( string, "page_down" );

    /*
     * Check for function keys
     */
    if ( (value >= SDLK_F1) && (value <= SDLK_F15) ) {
        sprintf ( string, "f%d", value-SDLK_F1+1 );
        return string;
    }

    /*
     * Check for state keys
     */
    if ( value == SDLK_NUMLOCKCLEAR)
        return strcpy ( string, "numlock" );
    else if ( value == SDLK_CAPSLOCK )
        return strcpy ( string, "capslock" );
    else if ( value == SDLK_SCROLLLOCK )
        return strcpy ( string, "scrollock" );
    else if ( value == SDLK_RSHIFT )
        return strcpy ( string, "rshift" );
    else if ( value == SDLK_LSHIFT )
        return strcpy ( string, "lshift" );
    else if ( value == SDLK_RCTRL )
        return strcpy ( string, "rctrl" );
    else if ( value == SDLK_LCTRL )
        return strcpy ( string, "lctrl" );
    else if ( value == SDLK_RALT )
        return strcpy ( string, "ralt" );
    else if ( value == SDLK_LALT )
        return strcpy ( string, "lalt" );
    else if ( value == SDLK_RGUI )
        return strcpy ( string, "rgui" );
    else if ( value == SDLK_LGUI )
        return strcpy ( string, "lgui" );

    /*
     * Check for other keys
     */
    if ( value == SDLK_BACKSPACE )
        return strcpy ( string, "backspace" );
    if ( value == SDLK_TAB )
        return strcpy ( string, "tab" );
    if ( value == SDLK_ESCAPE )
        return strcpy ( string, "esc" );
    if ( value == SDLK_CLEAR )
        return strcpy ( string, "clear" );
    if ( value == SDLK_RETURN )
        return strcpy ( string, "return" );
    if ( value == SDLK_SPACE )
        return strcpy ( string, "space" );
    if ( value == SDLK_COMMA )
        return strcpy ( string, "comma" );
    if ( value == SDLK_MINUS )
        return strcpy ( string, "minus" );
    //if ( value == SDLK_PLUS )
    //    return strcpy ( string, "plus" );
    if ( value == SDLK_EQUALS )
        return strcpy ( string, "equals" );
    if ( value == SDLK_PERIOD )
        return strcpy ( string, "period" );
    if ( value == SDLK_SEMICOLON )
        return strcpy ( string, "semicolon" );
    if ( value == SDLK_LEFTBRACKET )
        return strcpy ( string, "leftbracket" );
    if ( value == SDLK_RIGHTBRACKET )
        return strcpy ( string, "rightbracket" );
    if ( value == SDLK_BACKSLASH )
        return strcpy ( string, "backslash" );
    if ( value == SDLK_SLASH )
        return strcpy ( string, "slash" );
    if ( value == SDLK_DELETE )
        return strcpy ( string, "delete" );
    if ( value == SDLK_PAUSE )
        return strcpy ( string, "break" );

    /*
     * Check for numbers
     */
    if ( (value >= SDLK_0) && (value <= SDLK_9) ) {
        sprintf ( string, "%d", value-SDLK_0 );
        return string;
    }

    /*
     * Check for letters
     */
    if ( (value >= SDLK_A) && (value <= SDLK_Z) ) {
        sprintf ( string, "%c", value );
        return string;
    }

    return string;

} /* pc_interpret_key_value */

/******************************************************************************
**  Function   :  pc_interpret_mod_key
**                            
**  Objective  :  This function checks the string for the key desired
**                config file
**
**  Parameters :  string - string to check for key
**                                                
**  return     :  SDL key value                   
**      
******************************************************************************/
int pc_interpret_mod_key ( const char *string ) {

    int value = 0;

    /*
     * Get rid of leading spaces
     */
    //while ( string[0] == ' ' )
    //    string += 1;

    /*
     * Check for actual key value instead of string
     */
    value = atoi ( string );
    if ( value > 9 )
        return value;

    /*
     * Check for state keys
     */
    if ( strstr ( string, "shift" ) )
        return SDL_KMOD_SHIFT;
    else if ( strstr ( string, "ctrl" ) )
        return SDL_KMOD_CTRL;
    else if ( strstr ( string, "alt" ) )
        return SDL_KMOD_ALT;
    else if ( strstr ( string, "gui" ) )
        return SDL_KMOD_GUI;
    else if ( strstr ( string, "meta" ) )
        return SDL_KMOD_GUI;
    else if ( strstr ( string, "win" ) )
        return SDL_KMOD_GUI;
    else if ( !strcmp ( string, "0" ) )
        return 0;

    return 0;

} /* pc_interpret_mod_key */

/******************************************************************************
**  Function   :  pc_interpret_mod_key_value
**                            
**  Objective  :  This function checks the input value and outputs the key name
**
**  Parameters :  value  - SDL key value to get name for
**                string - Output containing name
**                                                
**  return     :  string                          
**      
******************************************************************************/
char * pc_interpret_mod_key_value ( int value, char *string ) {

    *string = '\0';

    sprintf ( string, "%d", value );

    if ( value == SDL_KMOD_SHIFT )
        return strcpy ( string, "shift" );
    else if ( value == SDL_KMOD_CTRL )
        return strcpy ( string, "ctrl" );
    else if ( value == SDL_KMOD_ALT )
        return strcpy ( string, "alt" );
    else if ( value == SDL_KMOD_GUI )
        return strcpy ( string, "gui" );
    else
        return strcpy ( string, "0" );

    return string;

} /* pc_interpret_mod_key_value */

char **pc_glob_directory(char *dir, char *pattern, int *count)
{
    return SDL_GlobDirectory(dir, pattern, 0, count);
}

void *pc_malloc(size_t size)
{
    return SDL_malloc(size);
}

void pc_free_memory(void *memory)
{
    SDL_free(memory);
}

char *pc_get_current_directory(void)
{
    return SDL_GetCurrentDirectory();
}

int pc_get_entry_type(int type)
{
    if (type == SDL_PATHTYPE_DIRECTORY)
        return FB_ENTRY_DIR;
    else
        return FB_ENTRY_FILE;
}

static SDL_EnumerationResult pc_add_directory_entry(void *userdata, const char *dirname, const char *fname)
{
    struct fb_dir_entries *cfg = ((struct fb_dir_entries *)userdata);
    SDL_PathInfo info;

    char *tmp_path = SDL_malloc(strlen(dirname) + strlen(fname) + 1);
    if (tmp_path) {
        sprintf(tmp_path, "%s%s", dirname, fname);
        if (SDL_GetPathInfo(tmp_path, &info)) {
            if (cfg->fb_type != MODAL_FB_DIR || info.type == SDL_PATHTYPE_DIRECTORY) {
                cfg->entries[cfg->count] = SDL_malloc(strlen(fname) + 2);
                strcpy(cfg->entries[cfg->count], fname);
                if (info.type == SDL_PATHTYPE_DIRECTORY)
                    strcat(cfg->entries[cfg->count], "/");
                cfg->entry_type[cfg->count] = pc_get_entry_type(info.type);
                cfg->count++;
            }
        }
        SDL_free(tmp_path);
    }

    return SDL_ENUM_CONTINUE;
}

static SDL_EnumerationResult pc_count_directory_entries(void *userdata, const char *dirname, const char *fname)
{
    (void)dirname;
    (void)fname;

    int *count = ((int *)userdata);
    (*count)++;

    return SDL_ENUM_CONTINUE;
}

typedef struct {
    char *name;
    int   type;
} t_sort_entry;

static int pc_compare_dir_entries(const void *a, const void *b)
{
    const t_sort_entry *ea = (const t_sort_entry *)a;
    const t_sort_entry *eb = (const t_sort_entry *)b;

    /* Directories before files */
    if (ea->type != eb->type)
        return (ea->type == FB_ENTRY_DIR) ? -1 : 1;

    /* Case-insensitive alphabetic within the same type */
    return SDL_strcasecmp(ea->name, eb->name);
}

char **pc_enumerate_directory(struct fb_dir_entries *fb_cfg)
{
    int old_count = fb_cfg->count;
    fb_cfg->count = 0;

    if (!SDL_EnumerateDirectory(fb_cfg->dir, pc_count_directory_entries, &fb_cfg->count)) {
        printf("Fail dir: %s Error: %s\n", fb_cfg->dir, SDL_GetError());
        fb_cfg->count = 0;
        return NULL;
    }

    if (fb_cfg->entries) {
        for (int i = 0; i < old_count; ++i) {
            SDL_free(fb_cfg->entries[i]);
        }
        SDL_free(fb_cfg->entries);
    }
    if (fb_cfg->entry_type)
        SDL_free(fb_cfg->entry_type);

    fb_cfg->entries = SDL_malloc(sizeof(char *) * (fb_cfg->count));
    fb_cfg->entry_type = SDL_malloc(sizeof(int) * fb_cfg->count);

    fb_cfg->count = 0;
    SDL_EnumerateDirectory(fb_cfg->dir, pc_add_directory_entry, fb_cfg);

    /* Sort: directories first then files, each group case-insensitive alpha */
    if (fb_cfg->count > 1) {
        t_sort_entry *tmp = SDL_malloc(sizeof(t_sort_entry) * fb_cfg->count);
        if (tmp) {
            for (int i = 0; i < fb_cfg->count; i++) {
                tmp[i].name = fb_cfg->entries[i];
                tmp[i].type = fb_cfg->entry_type[i];
            }
            SDL_qsort(tmp, fb_cfg->count, sizeof(t_sort_entry), pc_compare_dir_entries);
            for (int i = 0; i < fb_cfg->count; i++) {
                fb_cfg->entries[i]    = tmp[i].name;
                fb_cfg->entry_type[i] = tmp[i].type;
            }
            SDL_free(tmp);
        }
    }

    return fb_cfg->entries;
}
