/*  Copyright 2011 Guillaume Duhamel
    Copyright 2015 devMiyax

    This file is part of Yabause.

    Yabause is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    Yabause 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 Yabause; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
*/
/*
        Copyright 2019 devMiyax(smiyaxdev@gmail.com)

This file is part of YabaSanshiro.

        YabaSanshiro is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

YabaSanshiro 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 YabaSanshiro; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
*/

#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>
#include <jni.h>
#include <android/native_window.h>     // requires ndk r5 or newer
#include <android/native_window_jni.h> // requires ndk r5 or newer
#include <android/bitmap.h>
#include <android/log.h>
#include "../../android_debug_log.h"
#include "crashlytics.h"
#include "config.h"
#include "yabause.h"
#include "scsp.h"
#include "vidsoft.h"
#include "vidogl.h"
#include "peripheral.h"
#include "m68kcore.h"
#include "sh2core.h"
#include "sh2int.h"
#include "cdbase.h"
#include "cs2.h"
#include "vdp2.h"
#include "debug.h"
#include "osdcore.h"
#include "cheat.h"

#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <GLES3/gl3ext.h>
#include <pthread.h>

//#include "sndaudiotrack.h"
#ifdef HAVE_OPENSL
#include "sndopensl.h"
#endif

#include "SndOboe.h"

#include "libpng16/png.h"

#include "PlayRecorder.h"

#if HAVE_VULKAN
#include "../vulkan/VIDVulkanCInterface.h"
#include "../vulkan/VIDVulkan.h"
#include "../vulkan/Renderer.h"
#include "../vulkan/Window.h"
#else
#define VIDCORE_VULKAN 0xFF
#endif

#include "yabause_ra_integration.h"

// Test function declaration for mastery notification
extern "C" void testMasteryNotification();

JavaVM *yvm;
static jobject yabause = NULL;

static char mpegpath[256] = "\0";
static char cartpath[256] = "\0";
static char screenShotFilename[256] = "\0";
static char last_state_filename[256] = "\0";
static char current_game_path[512] = "\0";
static bool g_skip_ra_change_media = false;
static crashlytics_context_t *s_crashlytics_context = NULL;

EGLDisplay g_Display = EGL_NO_DISPLAY;
EGLSurface g_Surface = EGL_NO_SURFACE;
EGLContext g_Context = EGL_NO_CONTEXT;
EGLContext g_Context_Sub = EGL_NO_CONTEXT;
EGLSurface g_Pbuffer = EGL_NO_SURFACE;
EGLConfig g_Config = 0;
ANativeWindow *g_window = 0;
GLuint g_FrameBuffer = 0;
GLuint g_VertexBuffer = 0;
GLuint programObject = 0;
GLuint positionLoc = 0;
GLuint texCoordLoc = 0;
GLuint samplerLoc = 0;
int g_buf_width = -1;
int g_buf_height = -1;
int g_major_version = 0;
int g_minor_version = 0;
int g_minorminor_version = 0;
int g_pad_mode = 0;
int g_pad2_mode = 0;
int g_EnagleFPS = 0;
int g_CpuType = 2;
int g_VideoFilter = 0;
int g_PolygonGenerationMode = 0;
static int g_SoundEngine = 0;
static int g_resolution_mode = 0;
static int g_rbg_resolution_mode = 0;
static int g_extmemory = 1;
static int g_rotate_screen = 0;
static int g_use_compute_shader = 0;
static int g_scsp_sync_count = 1;
static int g_cpu_sync_shift = 1;
static int g_scsp_sync_time_mode = 1;
static int g_aspect_rate_mode = 0;
static int g_vdp2_new_composite = 0; // issue #22: 0 = legacy path, 1 = new per-pixel compositor
static int s_use_cpu_affinity = 1;
static int s_use_sh2_cache = 1;
int frameLimitMode = 0;

static int s_status = 0;
pthread_mutex_t g_mtxGlLock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t g_mtxFuncSync = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t g_cndFuncSync = PTHREAD_COND_INITIALIZER;

float vertices[] = {
    -1.0f, 1.0f, 0, 0,
    1.0f, 1.0f, 0, 0,
    1.0f, -1.0f, 0, 0,
    -1.0f, -1.0f, 0, 0};

// Setting Infomation From
const char *s_biospath = NULL;
const char *s_cdpath = NULL;
const char *s_buppath = NULL;
const char *s_cartpath = NULL;
char *s_playrecord_path = NULL;
char *s_playdatadir = NULL;
int s_carttype;
char s_savepath[256] = "\0";
int s_vidcoretype = VIDCORE_OGL;
int s_player2Enable = -1;

int surface_width = 0;
int surface_height = 0;

#define MAKE_PAD(a, b) ((a << 24) | (b))
void update_pad_mode();

enum RenderThreadMessage
{
    MSG_NONE = 0,
    MSG_WINDOW_SET,
    MSG_WINDOW_CHG,
    MSG_RENDER_LOOP_EXIT,
    MSG_SAVE_STATE,
    MSG_LOAD_STATE,
    MSG_SAVE_STATE_COMPRESSED,
    MSG_LOAD_STATE_COMPRESSED,
    MSG_PAUSE,
    MSG_RESUME,
    MSG_SCREENSHOT,
    MSG_OPEN_TRAY,
    MSG_CLOSE_TRAY,
    MSG_RESET,
    MSG_PLAY,
    MSG_RECORD
};

volatile int g_msg = MSG_NONE;
pthread_t _threadId;

M68K_struct *M68KCoreList[] = {
    &M68KDummy,
#ifdef HAVE_C68K
    &M68KC68K,
#endif
#ifdef HAVE_Q68
    &M68KQ68,
#endif
#ifdef HAVE_MUSASHI
    &M68KMusashi,
#endif
    NULL};

SH2Interface_struct *SH2CoreList[] = {
    &SH2Interpreter,
    &SH2DebugInterpreter,
#ifdef SH2_DYNAREC
    &SH2Dynarec,
#endif
#ifdef DYNAREC_DEVMIYAX
    &SH2Dyn,
#endif
    NULL};

PerInterface_struct *PERCoreList[] = {
    &PERDummy,
    NULL};

CDInterface *CDCoreList[] = {
    &DummyCD,
    &ISOCD,
    NULL};

SoundInterface_struct *SNDCoreList[] = {
    &SNDDummy,
//&SNDAudioTrack,
#ifdef HAVE_OPENSL
    &SNDOpenSL,
#endif
    &SNDOboe,
    NULL};

VideoInterface_struct *VIDCoreList[] = {
    &VIDDummy,
    &VIDSoft,
    &VIDOGL,
#if HAVE_VULKAN
    &CVIDVulkan,
#endif
    NULL};

#ifdef YAB_PORT_OSD
#include "nanovg/nanovg_osdcore.h"
OSD_struct *OSDCoreList[] = {
    &OSDNnovg,
#if HAVE_VULKAN
    &OSDNnovgVulkan,
#endif
    NULL};
#endif

void YuidrawSoftwareBuffer();
int saveScreenshot(const char *filename);
void saveScreenshotV(const char *filename);
GLuint LoadShader(GLenum type, const char *shaderSrc);
int destroy();

#define LOG_TAG "yabause"

/* Override printf for debug*/
int yprintf(const char *fmt, ...)
{
    int result = 0;
    va_list ap;
    va_start(ap, fmt);
    result = __android_log_vprint(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ap);
    va_end(ap);
    return result;
}

int printf(const char *fmt, ...)
{
    int result = 0;
    va_list ap;
    va_start(ap, fmt);
    result = __android_log_vprint(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ap);
    va_end(ap);
    return result;
}

class ScreenRecorder
{
public:
    void setScreenshotCallback(PlayRecorder *p)
    {
        using std::placeholders::_1;
        p->f_takeScreenshot = std::bind(&ScreenRecorder::takeScreenshot, this, _1);
    }
    void takeScreenshot(const char *fname)
    {
        ::saveScreenshot(fname);
    }
};

ScreenRecorder gsc;

#define YUI_LOG yprintf
//#define YUI_LOG

std::string shaderCachePath;
const char *YuiGetShaderCachePath(){
    return shaderCachePath.c_str();
}

const char *GetBiosPath()
{
    jclass yclass;
    jmethodID getBiosPath;
    jstring message;
    jboolean dummy;
    JNIEnv *env;
    const char *rtn;

    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return NULL;
    }

    yclass = env->GetObjectClass(yabause);
    getBiosPath = env->GetMethodID(yclass, "getBiosPath", "()Ljava/lang/String;");
    message = (jstring)env->CallObjectMethod(yabause, getBiosPath);
    if( message == nullptr ){
        rtn = NULL;
    }else if (env->GetStringLength(message) == 0)
        rtn = NULL;
    else
        rtn = env->GetStringUTFChars(message, &dummy);
    return rtn;
}

const char *GetShaderPath()
{
    jclass yclass;
    jmethodID getBiosPath;
    jstring message;
    jboolean dummy;
    JNIEnv *env;
    const char *rtn;

    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return NULL;
    }

    yclass = env->GetObjectClass(yabause);
    getBiosPath = env->GetMethodID(yclass, "getShaderPath", "()Ljava/lang/String;");
    message = (jstring)env->CallObjectMethod(yabause, getBiosPath);
    if (env->GetStringLength(message) == 0)
        rtn = NULL;
    else
        rtn = env->GetStringUTFChars(message, &dummy);
    return rtn;
}


char *GetPlayDataDir()
{
    jclass yclass;
    jmethodID mid;
    jstring message;
    jboolean dummy;
    JNIEnv *env;
    const char *buf;
    char *rtn;

    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return NULL;
    }

    yclass = env->GetObjectClass(yabause);
    mid = env->GetMethodID(yclass, "getTestPath", "()Ljava/lang/String;");
    message = (jstring)env->CallObjectMethod(yabause, mid);
    if (message == NULL)
    {
        return NULL;
    }
    if (env->GetStringLength(message) == 0)
        buf = NULL;
    else
        buf = env->GetStringUTFChars(message, &dummy);

    if (buf != NULL)
    {
        rtn = (char *)malloc(strlen(buf) + 1);
        strcpy(rtn, buf);
        env->ReleaseStringUTFChars(message, buf);
    }
    return rtn;
}

const char *GetGamePath()
{
    jclass yclass;
    jmethodID getGamePath;
    jstring message;
    jboolean dummy;
    JNIEnv *env;
    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return NULL;
    }

    yclass = env->GetObjectClass(yabause);
    getGamePath = env->GetMethodID(yclass, "getGamePath", "()Ljava/lang/String;");
    message = (jstring)env->CallObjectMethod(yabause, getGamePath);
    if (env->GetStringLength(message) == 0)
        return NULL;
    else
        return env->GetStringUTFChars(message, &dummy);
}

const char *GetMemoryPath()
{
    jclass yclass;
    jmethodID getMemoryPath;
    jstring message;
    jboolean dummy;
    JNIEnv *env;
    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return NULL;
    }

    yclass = env->GetObjectClass(yabause);
    getMemoryPath = env->GetMethodID(yclass, "getMemoryPath", "()Ljava/lang/String;");
    message = (jstring)env->CallObjectMethod(yabause, getMemoryPath);
    if (env->GetStringLength(message) == 0)
        return NULL;
    else
        return env->GetStringUTFChars(message, &dummy);
}

int GetCartridgeType()
{
    jclass yclass;
    jmethodID getCartridgeType;
    JNIEnv *env;
    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return -1;
    }

    yclass = env->GetObjectClass(yabause);
    getCartridgeType = env->GetMethodID(yclass, "getCartridgeType", "()I");
    return env->CallIntMethod(yabause, getCartridgeType);
}

int GetVideoInterface()
{
    jclass yclass;
    jmethodID getVideoInterface;
    JNIEnv *env;
    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return -1;
    }

    yclass = env->GetObjectClass(yabause);
    getVideoInterface = env->GetMethodID(yclass, "getVideoInterface", "()I");
    return (int)env->CallIntMethod(yabause, getVideoInterface);
}

const char *GetCartridgePath()
{
    jclass yclass;
    jmethodID getCartridgePath;
    jstring message;
    jboolean dummy;
    JNIEnv *env;
    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return NULL;
    }

    yclass = env->GetObjectClass(yabause);
    getCartridgePath = env->GetMethodID(yclass, "getCartridgePath", "()Ljava/lang/String;");
    message = (jstring)env->CallObjectMethod(yabause, getCartridgePath);
    if (env->GetStringLength(message) == 0)
        return NULL;
    else
        return env->GetStringUTFChars(message, &dummy);
}

int GetPlayer2Device()
{

    jclass yclass;
    jmethodID getPlayer2InputDevice;
    JNIEnv *env;
    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return -1;
    }

    yclass = env->GetObjectClass(yabause);
    getPlayer2InputDevice = env->GetMethodID(yclass, "getPlayer2InputDevice", "()I");
    return env->CallIntMethod(yabause, getPlayer2InputDevice);
}

extern "C" const char *GetFileDescriptorPath(const char *fileName)
{
    jclass yclass;
    jmethodID getFileDescriptorPath;
    jstring message;
    jboolean dummy;
    JNIEnv *env;
    if (yvm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        if (yvm->AttachCurrentThread(&env, NULL) != JNI_OK)
        {
            MAIN_LOGE("Failed to AttachCurrentThread");
            return NULL;
        }
    }

    jstring strj = env->NewStringUTF(fileName);

    yclass = env->GetObjectClass(yabause);
    getFileDescriptorPath = env->GetMethodID(yclass, "getFileDescriptorPath", "(Ljava/lang/String;)Ljava/lang/String;");
    message = (jstring)env->CallObjectMethod(yabause, getFileDescriptorPath, strj);

    env->DeleteLocalRef(strj);

    if (message == NULL)
    {
        return NULL;
    }

    if (env->GetStringLength(message) == 0)
        return NULL;
    else
        return env->GetStringUTFChars(message, &dummy);
}

void onBackupWrite(const char *fname, int deviceId, char *before, char *after, int size)
{

    MAIN_LOGI("onBackupWrite is called");

    jclass yclass;
    jmethodID jniOnBackupWrite;
    JNIEnv *env;

    if (yvm->AttachCurrentThread(&env, NULL) != JNI_OK)
    {
        MAIN_LOGE("Failed to AttachCurrentThread");
        return;
    }

    /*
        if (yvm->GetEnv( (void**) &env, JNI_VERSION_1_6) != JNI_OK){

            __android_log_print(ANDROID_LOG_ERROR, "yabause", "Failed to GetEnv");

            return;
        }
    */
    jbyteArray jniBefore = env->NewByteArray(size);
    if (jniBefore == NULL)
    {
        MAIN_LOGE("Failed to NewByteArray for before");
        return;
    }

    jbyte *dst = env->GetByteArrayElements(jniBefore, NULL);
    for (int i = 0; i < size; i++)
    {
        dst[i] = before[i];
    }
    env->ReleaseByteArrayElements(jniBefore, dst, 0);

    jbyteArray jniAfter = env->NewByteArray(size);
    if (jniAfter == NULL)
    {
        MAIN_LOGE("Failed to NewByteArray for after");
        return;
    }

    dst = env->GetByteArrayElements(jniAfter, NULL);
    for (int i = 0; i < size; i++)
    {
        dst[i] = after[i];
    }
    env->ReleaseByteArrayElements(jniAfter, dst, 0);

    jstring jniFname = env->NewStringUTF(fname);
    if (jniFname == NULL)
    {
        MAIN_LOGE("Failed to NewStringUTF for fname");
        return;
    }

    yclass = env->GetObjectClass(yabause);
    jniOnBackupWrite = env->GetMethodID(yclass, "onBackupWrite", "(Ljava/lang/String;I[B[B)V");
    if (jniOnBackupWrite == NULL)
    {
        MAIN_LOGE("Failed to GetMethodID for onBackupWrite");
        return;
    }
    env->CallVoidMethod(yabause, jniOnBackupWrite, jniFname, (jint)deviceId, jniBefore, jniAfter);

    if (yvm->DetachCurrentThread() != JNI_OK)
    {
        MAIN_LOGE("Failed to DetachCurrentThread");
        return;
    }

    return;
}

extern "C" void YuiErrorMsg(const char *string)
{
    // YUI_LOG("YuiErrorMsg %s",string);

    jclass yclass;
    jmethodID errorMsg;
    jstring message;
    JNIEnv *env;

    yvm->AttachCurrentThread(&env, NULL);

    YUI_LOG("YuiErrorMsg2 %s", string);

    yclass = env->GetObjectClass(yabause);
    errorMsg = env->GetMethodID(yclass, "errorMsg", "(Ljava/lang/String;)V");
    message = env->NewStringUTF(string);
    env->CallVoidMethod(yabause, errorMsg, message);
    yvm->DetachCurrentThread();
}

void *threadStartCallback(void *myself);

extern "C" void YuiSwapBuffers(void)
{
#if HAVE_VULKAN
    if (s_vidcoretype == VIDCORE_VULKAN)
    {
        VIDVulkan::getInstance()->present();
        SetOSDToggle(g_EnagleFPS);
        return;
    }
#endif
    if (g_Display == EGL_NO_DISPLAY)
    {
        return;
    }
    if (s_vidcoretype == VIDCORE_SOFT)
    {
        YuidrawSoftwareBuffer();
    }
    eglSwapBuffers(g_Display, g_Surface);
    SetOSDToggle(g_EnagleFPS);
}

GLuint LoadShader(GLenum type, const char *shaderSrc)
{
    GLuint shader;
    GLint compiled;

    // Create the shader object
    shader = glCreateShader(type);

    if (shader == 0)
        return 0;

    // Load the shader source
    glShaderSource(shader, 1, &shaderSrc, NULL);

    // Compile the shader
    glCompileShader(shader);

    // Check the compile status
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

    if (!compiled)
    {
        GLint infoLen = 0;

        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);

        if (infoLen > 1)
        {
            char *infoLog = (char *)malloc(sizeof(char) * infoLen);
            glGetShaderInfoLog(shader, infoLen, NULL, infoLog);
            YUI_LOG("Error compiling shader:\n%s\n", infoLog);
            free(infoLog);
        }

        glDeleteShader(shader);
        return 0;
    }

    return shader;
}

int YuiInitProgramForSoftwareRendering()
{
    GLbyte vShaderStr[] =
        "attribute vec4 a_position;   \n"
        "attribute vec2 a_texCoord;   \n"
        "varying vec2 v_texCoord;     \n"
        "void main()                  \n"
        "{                            \n"
        "   gl_Position = a_position; \n"
        "   v_texCoord = a_texCoord;  \n"
        "}                            \n";

    GLbyte fShaderStr[] =
        "precision mediump float;                            \n"
        "varying vec2 v_texCoord;                            \n"
        "uniform sampler2D s_texture;                        \n"
        "void main()                                         \n"
        "{                                                   \n"
        "  gl_FragColor = texture2D( s_texture, v_texCoord );\n"
        "}                                                   \n";

    GLuint vertexShader;
    GLuint fragmentShader;
    GLint linked;

    // Load the vertex/fragment shaders
    vertexShader = LoadShader(GL_VERTEX_SHADER, (char *)vShaderStr);
    fragmentShader = LoadShader(GL_FRAGMENT_SHADER, (char *)fShaderStr);

    // Create the program object
    programObject = glCreateProgram();

    if (programObject == 0)
        return 0;

    glAttachShader(programObject, vertexShader);
    glAttachShader(programObject, fragmentShader);

    // Link the program
    glLinkProgram(programObject);

    // Check the link status
    glGetProgramiv(programObject, GL_LINK_STATUS, &linked);

    if (!linked)
    {
        GLint infoLen = 0;

        glGetProgramiv(programObject, GL_INFO_LOG_LENGTH, &infoLen);

        if (infoLen > 1)
        {
            char *infoLog = (char *)malloc(sizeof(char) * infoLen);
            glGetProgramInfoLog(programObject, infoLen, NULL, infoLog);
            YUI_LOG("Error linking program:\n%s\n", infoLog);
            free(infoLog);
            return GL_FALSE;
        }

        glDeleteProgram(programObject);
        return GL_FALSE;
    }

    // Get the attribute locations
    positionLoc = glGetAttribLocation(programObject, "a_position");
    texCoordLoc = glGetAttribLocation(programObject, "a_texCoord");

    // Get the sampler location
    samplerLoc = glGetUniformLocation(programObject, "s_texture");

    glUseProgram(programObject);

    return GL_TRUE;
}

void YuidrawSoftwareBuffer()
{

    int buf_width, buf_height;
    int error;

    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    if (g_FrameBuffer == 0)
    {
        glGenTextures(1, &g_FrameBuffer);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, g_FrameBuffer);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1024, 1024, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        error = glGetError();
        if (error != GL_NO_ERROR)
        {
            YUI_LOG("g_FrameBuffer gl error %04X", error);
            return;
        }
    }
    else
    {
        glBindTexture(GL_TEXTURE_2D, g_FrameBuffer);
    }

    VIDCore->GetGlSize(&buf_width, &buf_height);
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, buf_width, buf_height, GL_RGBA, GL_UNSIGNED_BYTE, dispbuffer);

    if (g_VertexBuffer == 0)
    {
        glGenBuffers(1, &g_VertexBuffer);
        glBindBuffer(GL_ARRAY_BUFFER, g_VertexBuffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        error = glGetError();
        if (error != GL_NO_ERROR)
        {
            YUI_LOG("g_VertexBuffer gl error %04X", error);
            return;
        }
    }
    else
    {
        glBindBuffer(GL_ARRAY_BUFFER, g_VertexBuffer);
    }

    if (buf_width != g_buf_width || buf_height != g_buf_height)
    {
        vertices[6] = vertices[10] = (float)buf_width / 1024.f;
        vertices[11] = vertices[15] = (float)buf_height / 1024.f;
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glVertexAttribPointer(positionLoc, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
        glVertexAttribPointer(texCoordLoc, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void *)(sizeof(float) * 2));
        glEnableVertexAttribArray(positionLoc);
        glEnableVertexAttribArray(texCoordLoc);
        g_buf_width = buf_width;
        g_buf_height = buf_height;
        error = glGetError();
        if (error != GL_NO_ERROR)
        {
            YUI_LOG("gl error %d", error);
            return;
        }
    }
    else
    {
        glBindBuffer(GL_ARRAY_BUFFER, g_VertexBuffer);
    }

    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
}

extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_initViewport(JNIEnv *jenv, jobject obj, jobject surface, int width, int height)
{
    if (surface != 0)
    {

        if (g_Surface == EGL_NO_SURFACE)
        {
            g_window = ANativeWindow_fromSurface(jenv, surface);
            surface_width = width;
            surface_height = height;
            YUI_LOG("Got window %08X %d,%d", g_window, width, height);
            g_msg = MSG_WINDOW_SET;
            g_Surface = (EGLSurface)1;
        }
        else
        {
            if (g_window != ANativeWindow_fromSurface(jenv, surface))
            {
                g_window = ANativeWindow_fromSurface(jenv, surface);
            }
            YUI_LOG("Chg window %08X %d,%d ", g_window, g_msg, width, height);
            surface_width = width;
            surface_height = height;
            g_msg = MSG_WINDOW_CHG;
            // YUI_LOG("Got window ignore %p %d,%d", g_window, g_msg,width,height );
        }
    }
    else
    {
        YUI_LOG("Releasing window");
        ANativeWindow_release(g_window);
        g_window = NULL;
    }

    return 0;
}

extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_enableBackupWriteHook()
{
    BiosSetOnBackupWrite(onBackupWrite);
    return 0;
}

static int s_isRunning = 0;
extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_isRunning()
{
    return s_isRunning;
}


extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_lockGL()
{
    pthread_mutex_lock(&g_mtxGlLock);
    return 0;
}

extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_unlockGL()
{
    pthread_mutex_unlock(&g_mtxGlLock);
    return 0;
}

extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_toggleShowFps(JNIEnv *env)
{
    printf("%s", "Java_org_uoyabause_android_YabauseRunnable_toggleShowFps");
    ToggleFPS();
    return 0;
}

extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_pause(JNIEnv *env)
{
    pthread_mutex_lock(&g_mtxGlLock);
    g_msg = MSG_PAUSE;
    pthread_mutex_unlock(&g_mtxGlLock);
    return 0;
}

extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_resume(JNIEnv *env)
{
    pthread_mutex_lock(&g_mtxGlLock);
    g_msg = MSG_RESUME;
    pthread_mutex_unlock(&g_mtxGlLock);
    return 0;
}

extern "C" JNIEXPORT void JNICALL Java_org_uoyabause_android_YabauseRunnable_openTray(JNIEnv *env)
{
    yprintf("sending MSG_OPEN_TRAY");
    pthread_mutex_lock(&g_mtxGlLock);
    g_msg = MSG_OPEN_TRAY;
    pthread_mutex_unlock(&g_mtxGlLock);
}

extern "C" JNIEXPORT void JNICALL Java_org_uoyabause_android_YabauseRunnable_closeTray(JNIEnv *env)
{
    yprintf("sending MSG_CLOSE_TRAY");
    s_cdpath = GetGamePath();
    pthread_mutex_lock(&g_mtxGlLock);
    yprintf("new cd is %s", s_cdpath);
    
    // Store current game path for RetroAchievements
    if (s_cdpath != nullptr) {
        strncpy(current_game_path, s_cdpath, sizeof(current_game_path) - 1);
        current_game_path[sizeof(current_game_path) - 1] = '\0';
    }
    
    g_msg = MSG_CLOSE_TRAY;
    pthread_mutex_unlock(&g_mtxGlLock);
}

extern "C" JNIEXPORT void JNICALL Java_org_uoyabause_android_YabauseRunnable_setSkipRaChangeMedia(JNIEnv *env, jclass type, jboolean skip)
{
    g_skip_ra_change_media = (bool)skip;
}

extern "C" JNIEXPORT jstring JNICALL Java_org_uoyabause_android_YabauseRunnable_getCurrentGameCode(JNIEnv *env)
{
    return (jstring)env->NewStringUTF((const char *)Cs2GetCurrentGmaecode());
}

extern "C" JNIEXPORT jstring JNICALL Java_org_uoyabause_android_YabauseRunnable_getCurrentGamePath(JNIEnv *env)
{
    if (strlen(current_game_path) > 0) {
        return (jstring)env->NewStringUTF(current_game_path);
    }
    return nullptr;
}

static int enableautofskip = 0;
extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_toggleFrameSkip(JNIEnv *env)
{
    enableautofskip = 1 - enableautofskip;

    printf("%s:%d", "Java_org_uoyabause_android_YabauseRunnable_toggleFrameSkip", enableautofskip);

    if (enableautofskip)
        EnableAutoFrameSkip();
    else
        DisableAutoFrameSkip();

    return 0;
}

extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_setFrameLimitMode(JNIEnv *env, jobject obj, int mode)
{
    frameLimitMode = mode;
    VDP2SetFrameLimit(mode);
    return 0;
}

#ifdef _ANDROID_2_2_
int initEGLFunc()
{
    void *handle;
    char *error;

    handle = dlopen("libEGL.so", RTLD_LAZY);
    if (handle == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    eglGetCurrentDisplay = dlsym(handle, "eglGetCurrentDisplay");
    if (eglGetCurrentDisplay == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    eglGetCurrentSurface = dlsym(handle, "eglGetCurrentSurface");
    if (eglGetCurrentSurface == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    eglGetCurrentContext = dlsym(handle, "eglGetCurrentContext");
    if (eglGetCurrentContext == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    eglQuerySurface = dlsym(handle, "eglQuerySurface");
    if (eglQuerySurface == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    eglSwapInterval = dlsym(handle, "eglSwapInterval");
    if (eglSwapInterval == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    eglMakeCurrent = dlsym(handle, "eglMakeCurrent");
    if (eglMakeCurrent == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    eglSwapBuffers = dlsym(handle, "eglSwapBuffers");
    if (eglSwapBuffers == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    eglQueryString = dlsym(handle, "eglQueryString");
    if (eglQueryString == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    eglGetError = dlsym(handle, "eglGetError");
    if (eglGetError == NULL)
    {
        YUI_LOG(dlerror());
        return -1;
    }

    return 0;
}
#else
int initEGLFunc()
{
    return 0;
}
#endif

extern "C" jint Java_org_uoyabause_android_YabauseRunnable_record(JNIEnv *env, jobject thiz, jstring path)
{

    jboolean dummy;
    const char *cpath = env->GetStringUTFChars(path, &dummy);

    pthread_mutex_lock(&g_mtxGlLock);
    if (s_playrecord_path != NULL)
    {
        free(s_playrecord_path);
    }
    s_playrecord_path = (char *)malloc(strlen(cpath) + 1);
    strcpy(s_playrecord_path, cpath);
    g_msg = MSG_RECORD;
    pthread_mutex_unlock(&g_mtxGlLock);

    env->ReleaseStringUTFChars(path, cpath);

    pthread_mutex_lock(&g_mtxFuncSync);
    pthread_cond_wait(&g_cndFuncSync, &g_mtxFuncSync);
    pthread_mutex_unlock(&g_mtxFuncSync);

    return 0;
}

extern "C" jint Java_org_uoyabause_android_YabauseRunnable_getRecordingStatus(JNIEnv *env, jobject thiz)
{
    return PlayRecorder::getInstance()->getStatus();
}

extern "C" jint Java_org_uoyabause_android_YabauseRunnable_play(JNIEnv *env, jobject thiz, jstring path)
{

    jboolean dummy;
    const char *cpath = env->GetStringUTFChars(path, &dummy);

    pthread_mutex_lock(&g_mtxGlLock);
    if (s_playdatadir != NULL)
    {
        free(s_playdatadir);
    }
    s_playdatadir = (char *)malloc(strlen(cpath) + 1);
    strcpy(s_playdatadir, cpath);
    g_msg = MSG_PLAY;
    pthread_mutex_unlock(&g_mtxGlLock);

    env->ReleaseStringUTFChars(path, cpath);

    pthread_mutex_lock(&g_mtxFuncSync);
    pthread_cond_wait(&g_cndFuncSync, &g_mtxFuncSync);
    pthread_mutex_unlock(&g_mtxFuncSync);

    return 0;
}

extern "C" jstring Java_org_uoyabause_android_YabauseRunnable_savestate_1compress(JNIEnv *env, jobject thiz, jstring path)
{

    jboolean dummy;
    if (cdip == NULL)
        return NULL;
    const char *cpath = env->GetStringUTFChars(path, &dummy);

    pthread_mutex_lock(&g_mtxGlLock);
    strcpy(s_savepath, cpath);
    g_msg = MSG_SAVE_STATE_COMPRESSED;
    pthread_mutex_unlock(&g_mtxGlLock);

    env->ReleaseStringUTFChars(path, cpath);

    pthread_mutex_lock(&g_mtxFuncSync);
    pthread_cond_wait(&g_cndFuncSync, &g_mtxFuncSync);
    pthread_mutex_unlock(&g_mtxFuncSync);

    return env->NewStringUTF((const char *)last_state_filename);
}

extern "C" jint Java_org_uoyabause_android_YabauseRunnable_loadstate_1compress(JNIEnv *env, jobject thiz, jstring path)
{

    jboolean dummy;
    const char *cpath = env->GetStringUTFChars(path, &dummy);

    pthread_mutex_lock(&g_mtxGlLock);
    strcpy(s_savepath, cpath);
    g_msg = MSG_LOAD_STATE_COMPRESSED;
    pthread_mutex_unlock(&g_mtxGlLock);

    env->ReleaseStringUTFChars(path, cpath);

    pthread_mutex_lock(&g_mtxFuncSync);
    pthread_cond_wait(&g_cndFuncSync, &g_mtxFuncSync);
    pthread_mutex_unlock(&g_mtxFuncSync);

    return 0;
}

extern "C" jstring Java_org_uoyabause_android_YabauseRunnable_savestate(JNIEnv *env, jobject thiz, jstring path)
{

    jboolean dummy;
    if (cdip == NULL)
        return NULL;
    const char *cpath = env->GetStringUTFChars(path, &dummy);

    pthread_mutex_lock(&g_mtxGlLock);
    strcpy(s_savepath, cpath);
    g_msg = MSG_SAVE_STATE;
    pthread_mutex_unlock(&g_mtxGlLock);

    env->ReleaseStringUTFChars(path, cpath);

    pthread_mutex_lock(&g_mtxFuncSync);
    pthread_cond_wait(&g_cndFuncSync, &g_mtxFuncSync);
    pthread_mutex_unlock(&g_mtxFuncSync);

    return env->NewStringUTF((const char *)last_state_filename);
}

extern "C" jint Java_org_uoyabause_android_YabauseRunnable_loadstate(JNIEnv *env, jobject thiz, jstring path)
{

    jboolean dummy;
    const char *cpath = env->GetStringUTFChars(path, &dummy);

    pthread_mutex_lock(&g_mtxGlLock);
    strcpy(s_savepath, cpath);
    g_msg = MSG_LOAD_STATE;
    pthread_mutex_unlock(&g_mtxGlLock);

    env->ReleaseStringUTFChars(path, cpath);

    pthread_mutex_lock(&g_mtxFuncSync);
    pthread_cond_wait(&g_cndFuncSync, &g_mtxFuncSync);
    pthread_mutex_unlock(&g_mtxFuncSync);

    return 0;
}

extern "C" JNIEXPORT int JNICALL Java_org_uoyabause_android_YabauseRunnable_screenshot(JNIEnv *env, jobject thiz, jstring filename)
{
    jboolean dummy;
    const char *cpath = env->GetStringUTFChars(filename, &dummy);
    pthread_mutex_lock(&g_mtxGlLock);
    strcpy(screenShotFilename, cpath);
    g_msg = MSG_SCREENSHOT;
    pthread_mutex_unlock(&g_mtxGlLock);
    env->ReleaseStringUTFChars(filename, cpath);

    pthread_mutex_lock(&g_mtxFuncSync);
    s_status = -1;
    pthread_cond_wait(&g_cndFuncSync, &g_mtxFuncSync);
    pthread_mutex_unlock(&g_mtxFuncSync);
    return s_status;
}

extern "C" jint Java_org_uoyabause_android_YabauseRunnable_init(JNIEnv *env, jobject obj, jobject yab)
{
    int res = 0;

    if (initEGLFunc() == -1)
        return -1;

    yabause = env->NewGlobalRef(yab);
    s_biospath = GetBiosPath();
    s_cdpath = GetGamePath();
    s_buppath = GetMemoryPath();
    s_cartpath = GetCartridgePath();
    s_vidcoretype = GetVideoInterface();
    s_carttype = GetCartridgeType();
    s_player2Enable = GetPlayer2Device();
    
    // Store current game path for RetroAchievements
    if (s_cdpath != nullptr) {
        strncpy(current_game_path, s_cdpath, sizeof(current_game_path) - 1);
        current_game_path[sizeof(current_game_path) - 1] = '\0';
    }
    s_playdatadir = GetPlayDataDir();
    shaderCachePath = string(GetShaderPath());

    YUI_LOG("YabauseRunnable_init s_vidcoretype = %d", s_vidcoretype);


    pthread_attr_t tattr;
    pthread_attr_init(&tattr);
    s_isRunning = 1;
    pthread_create(&_threadId, &tattr, threadStartCallback, NULL);

    return res;
}

extern "C" int YuiRevokeOGLOnThisThread()
{

    if (s_vidcoretype == VIDCORE_VULKAN)
    {
        return 0;
    }

#if defined(YAB_ASYNC_RENDERING)
    if (!eglMakeCurrent(g_Display, g_Pbuffer, g_Pbuffer, g_Context_Sub))
    {
        YUI_LOG("eglMakeCurrent() returned error %X", eglGetError());

        // retry three times
        usleep(10000);
        if (!eglMakeCurrent(g_Display, g_Pbuffer, g_Pbuffer, g_Context_Sub))
        {
            YUI_LOG("eglMakeCurrent() returned error %X 2", eglGetError());
            usleep(10000);
            if (!eglMakeCurrent(g_Display, g_Pbuffer, g_Pbuffer, g_Context_Sub))
            {
                YUI_LOG("eglMakeCurrent() returned error %X 3", eglGetError());
                usleep(10000);
                if (!eglMakeCurrent(g_Display, g_Pbuffer, g_Pbuffer, g_Context_Sub))
                {
                    YUI_LOG("eglMakeCurrent() returned error %X 4", eglGetError());
                    usleep(10000);
                    if (!eglMakeCurrent(g_Display, g_Pbuffer, g_Pbuffer, g_Context_Sub))
                    {
                        YUI_LOG("eglMakeCurrent() returned error %X 5", eglGetError());
                        return -1;
                    }
                }
            }
        }
    }
#endif
    glDisable(GL_SCISSOR_TEST);
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    return 0;
}

extern "C" int YuiUseOGLOnThisThread()
{

    if (s_vidcoretype == VIDCORE_VULKAN)
    {
        return 0;
    }

#if defined(YAB_ASYNC_RENDERING)
    if (!eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context))
    {
        YUI_LOG("eglMakeCurrent() returned error %X", eglGetError());

        // retry three times
        usleep(10000);
        if (!eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context))
        {
            YUI_LOG("eglMakeCurrent() returned error %X 2", eglGetError());
            usleep(10000);
            if (!eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context))
            {
                YUI_LOG("eglMakeCurrent() returned error %X 3", eglGetError());
                usleep(10000);
                if (!eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context))
                {
                    YUI_LOG("eglMakeCurrent() returned error %X 4", eglGetError());
                    usleep(10000);
                    if (!eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context))
                    {
                        YUI_LOG("eglMakeCurrent() returned error %X 5", eglGetError());
                        return -1;
                    }
                }
            }
        }
    }
#endif
    glDisable(GL_SCISSOR_TEST);
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    eglSwapBuffers(g_Display, g_Surface);
    glClear(GL_COLOR_BUFFER_BIT);
    eglSwapBuffers(g_Display, g_Surface);
    return 0;
}

int initEgl(ANativeWindow *window)
{
    int i;
    int res;
    void *padbits;

    const EGLint attribs[] = {
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_BLUE_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_RED_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_DEPTH_SIZE, 24,
        EGL_STENCIL_SIZE, 8,

        EGL_NONE};

    EGLint pbuffer_attribs[] = {
        EGL_WIDTH, 8,
        EGL_HEIGHT, 8,
        // EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA,
        // EGL_TEXTURE_TARGET, EGL_TEXTURE_2D,
        // EGL_LARGEST_PBUFFER, EGL_TRUE,
        EGL_NONE};

    EGLDisplay display;
    EGLConfig config;
    EGLint numConfigs;
    EGLint format;
    EGLSurface surface;
    EGLContext context;
    EGLint width;
    EGLint height;
    int tmp;
    GLfloat ratio;
    int attrib_list[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};

    if (s_vidcoretype == VIDCORE_SOFT)
    {
        attrib_list[1] = 2;
    }

    YUI_LOG("Initializing context");

    if ((display = eglGetDisplay(EGL_DEFAULT_DISPLAY)) == EGL_NO_DISPLAY)
    {
        YUI_LOG("eglGetDisplay() returned error %X", eglGetError());
        return -1;
    }
    if (!eglInitialize(display, 0, 0))
    {
        YUI_LOG("eglInitialize() returned error %X", eglGetError());
        return -1;
    }

    if (!eglChooseConfig(display, attribs, &config, 1, &numConfigs))
    {
        YUI_LOG("eglChooseConfig() returned error %X", eglGetError());
        destroy();
        return -1;
    }

    if (!eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format))
    {
        YUI_LOG("eglGetConfigAttrib() returned error %X", eglGetError());
        destroy();
        return -1;
    }
    // YUI_LOG("ANativeWindow_setBuffersGeometry");
    // ANativeWindow_setBuffersGeometry(window, 0, 0, format);

    YUI_LOG("eglCreateContext");
    if (!(context = eglCreateContext(display, config, 0, attrib_list)))
    {
        YUI_LOG("eglCreateContext() for context returned error %d, Fall back to software vidcore mode", eglGetError());
        s_vidcoretype = VIDCORE_SOFT;
        attrib_list[1] = 2;
        if (!(context = eglCreateContext(display, config, 0, attrib_list)))
        {
            YUI_LOG("eglCreateContext() returned error %d", eglGetError());
            destroy();
            return -1;
        }
    }

    if (!(g_Context_Sub = eglCreateContext(display, config, context, attrib_list)))
    {
        YUI_LOG("eglCreateContext() for g_Context_Sub returned error %d, Fall back to software vidcore mode", eglGetError());
        s_vidcoretype = VIDCORE_SOFT;
        attrib_list[1] = 2;
        if (!(g_Context_Sub = eglCreateContext(display, config, context, attrib_list)))
        {
            YUI_LOG("eglCreateContext() returned error %d", eglGetError());
            destroy();
            return -1;
        }
    }

    YUI_LOG("eglCreateWindowSurface");
    if (!(surface = eglCreateWindowSurface(display, config, window, 0)))
    {
        YUI_LOG("eglCreateWindowSurface() returned error %X", eglGetError());
        destroy();
        return -1;
    }

    eglQuerySurface(display, surface, EGL_WIDTH, &width);
    eglQuerySurface(display, surface, EGL_HEIGHT, &height);
    // eglSurfaceAttrib(display, surface, EGL_SWAP_BEHAVIOR, EGL_BUFFER_DESTOYED);
    YUI_LOG("eglCreateWindowSurface() ok size = %d,%d", width, height);

    pbuffer_attribs[1] = ANativeWindow_getWidth(window);
    pbuffer_attribs[3] = ANativeWindow_getHeight(window);

    YUI_LOG("eglCreatePbufferSurface");
    if (!(g_Pbuffer = eglCreatePbufferSurface(display, config, pbuffer_attribs)))
    {
        YUI_LOG("eglCreatePbufferSurface() returned error %X", eglGetError());
        destroy();
        return -1;
    }

    YUI_LOG("eglMakeCurrent");
    if (!eglMakeCurrent(display, surface, surface, context))
    {
        YUI_LOG("eglMakeCurrent() returned error %X", eglGetError());
        destroy();
        return -1;
    }
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    if (!eglQuerySurface(display, surface, EGL_WIDTH, &width) ||
        !eglQuerySurface(display, surface, EGL_HEIGHT, &height))
    {
        YUI_LOG("eglQuerySurface() returned error %X", eglGetError());
        destroy();
        return -1;
    }

    g_Display = display;
    g_Surface = surface;
    g_Context = context;
    g_Config = config;

    YUI_LOG("%s", glGetString(GL_VENDOR));
    YUI_LOG("%s", glGetString(GL_RENDERER));
    YUI_LOG("%s", glGetString(GL_VERSION));
    YUI_LOG("%s", glGetString(GL_EXTENSIONS));
    YUI_LOG("%s", eglQueryString(g_Display, EGL_EXTENSIONS));

    GLint range[2];
    GLint precision[2];
    glGetShaderPrecisionFormat(GL_FRAGMENT_SHADER, GL_LOW_FLOAT, range, precision);
    YUI_LOG("GL_LOW_FLOAT [ %d - %d ][ %d - %d ]", range[0], range[1], precision[0], precision[1]);
    glGetShaderPrecisionFormat(GL_FRAGMENT_SHADER, GL_MEDIUM_FLOAT, range, precision);
    YUI_LOG("GL_MEDIUM_FLOAT [ %d - %d ][ %d - %d ]", range[0], range[1], precision[0], precision[1]);
    glGetShaderPrecisionFormat(GL_FRAGMENT_SHADER, GL_HIGH_FLOAT, range, precision);
    YUI_LOG("GL_HIGH_FLOAT [ %d - %d ][ %d - %d ]", range[0], range[1], precision[0], precision[1]);
    glGetShaderPrecisionFormat(GL_FRAGMENT_SHADER, GL_LOW_INT, range, precision);
    YUI_LOG("GL_LOW_INT [ %d - %d ][ %d - %d ]", range[0], range[1], precision[0], precision[1]);
    glGetShaderPrecisionFormat(GL_FRAGMENT_SHADER, GL_MEDIUM_INT, range, precision);
    YUI_LOG("GL_MEDIUM_INT [ %d - %d ][ %d - %d ]", range[0], range[1], precision[0], precision[1]);
    glGetShaderPrecisionFormat(GL_FRAGMENT_SHADER, GL_HIGH_INT, range, precision);
    YUI_LOG("GL_HIGH_INT [ %d - %d ][ %d - %d ]", range[0], range[1], precision[0], precision[1]);

    glViewport(0, 0, width, height);

    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    return 0;
}

int YabauseInit()
{
    int i;
    int res;
    EGLint width;
    EGLint height;

    yabauseinit_struct yinit = {};

    eglQuerySurface(g_Display, g_Surface, EGL_WIDTH, &width);
    eglQuerySurface(g_Display, g_Surface, EGL_HEIGHT, &height);

    // s_vidcoretype = VIDCORE_VULKAN;
    memset(&yinit, 0, sizeof(yinit));
    // yinit.m68kcoretype = M68KCORE_C68K;
    yinit.m68kcoretype = M68KCORE_MUSASHI;
    yinit.percoretype = PERCORE_DUMMY;
#if defined(SH2_DYNAREC) | defined(DYNAREC_DEVMIYAX)
    // g_CpuType = SH2CORE_DEBUGINTERPRETER;
    yinit.sh2coretype = g_CpuType;
#else
    yinit.sh2coretype = SH2CORE_DEFAULT;
#endif
    // s_vidcoretype = VIDCORE_DUMMY;
    yinit.vidcoretype = s_vidcoretype;
#ifdef HAVE_OPENSL
    yinit.sndcoretype = SNDCORE_OPENSL;
#else
    yinit.sndcoretype = SNDCORE_AUDIOTRACK;
#endif

    yinit.sndcoretype = SNDCORE_OBOE;

    yinit.cdcoretype = CDCORE_ISO;
    yinit.carttype = GetCartridgeType();
    yinit.regionid = 0;

    yinit.biospath = s_biospath;
    yinit.cdpath = s_cdpath;
    yinit.buppath = s_buppath;
    yinit.carttype = s_carttype;
    yinit.cartpath = s_cartpath;

    yinit.mpegpath = mpegpath;
    yinit.videoformattype = VIDEOFORMATTYPE_NTSC;
    yinit.frameskip = 0;
    yinit.framelimit = ::frameLimitMode;
    yinit.usethreads = 0;
    yinit.skip_load = 0;
    yinit.video_filter_type = g_VideoFilter;
    yinit.polygon_generation_mode = g_PolygonGenerationMode;
    yinit.use_new_scsp = g_SoundEngine;
    yinit.resolution_mode = g_resolution_mode;
    // Force extend_backup to FALSE when using external BIOS file
    // because extended backup memory is not compatible with non-builtin BIOS
    if (s_biospath != NULL) {
        yinit.extend_backup = 0;
    } else {
        yinit.extend_backup = g_extmemory;
    }
    yinit.rotate_screen = g_rotate_screen;
    yinit.scsp_sync_count_per_frame = g_scsp_sync_count;
    yinit.sync_shift = g_cpu_sync_shift;
    yinit.scsp_main_mode = g_scsp_sync_time_mode;
    yinit.rbg_resolution_mode = g_rbg_resolution_mode;
    yinit.rbg_use_compute_shader = g_use_compute_shader;
    yinit.playRecordPath = s_playdatadir;
    if (yinit.playRecordPath != NULL)
    {
        PlayRecorder *p = PlayRecorder::getInstance();
        gsc.setScreenshotCallback(p);
    }

    yinit.use_cpu_affinity = s_use_cpu_affinity;
    yinit.use_sh2_cache = s_use_sh2_cache;

    res = YabauseInit(&yinit);
    if (res != 0)
    {
        YUI_LOG("Fail to YabauseInit %d", res);
        return -1;
    }

    // Log game info to Crashlytics for crash diagnostics
    if (s_crashlytics_context != NULL) {
        if (current_game_path[0] != '\0') {
            s_crashlytics_context->set(s_crashlytics_context, "game_path", current_game_path);
        }
        char* gameName = Cs2GetCurrentGameName();
        if (gameName != NULL) {
            s_crashlytics_context->set(s_crashlytics_context, "game_name", gameName);
        }
        char* gameCode = Cs2GetCurrentGmaecode();
        if (gameCode != NULL) {
            s_crashlytics_context->set(s_crashlytics_context, "game_code", gameCode);
        }
        s_crashlytics_context->log(s_crashlytics_context, "Game loaded");
    }

    update_pad_mode();

    // ScspSetFrameAccurate(1);

    if (s_vidcoretype == VIDCORE_OGL)
    {
        OSDChangeCore(OSDCORE_NANOVG);
        for (i = 0; VIDCoreList[i] != NULL; i++)
        {
            if (VIDCoreList[i]->id == s_vidcoretype)
            {
                VIDCoreList[i]->Resize(0, 0, width, height, 1, g_aspect_rate_mode);
                glDisable(GL_SCISSOR_TEST);
                glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
                glClear(GL_COLOR_BUFFER_BIT);
                eglSwapBuffers(g_Display, g_Surface);
                glClear(GL_COLOR_BUFFER_BIT);
                eglSwapBuffers(g_Display, g_Surface);
                break;
            }
        }
    }
    else if (s_vidcoretype == VIDCORE_VULKAN)
    {
        OSDChangeCore(OSDCORE_NANOVG_VULKAN);
        for (i = 0; VIDCoreList[i] != NULL; i++)
        {
            if (VIDCoreList[i]->id == s_vidcoretype)
            {
                VIDCoreList[i]->Resize(0, 0, surface_width, surface_height, 1, g_aspect_rate_mode);
                break;
            }
        }
    }
    else
    {
        OSDChangeCore(OSDCORE_SOFT);
        if (YuiInitProgramForSoftwareRendering() != GL_TRUE)
        {
            YUI_LOG("Fail to YuiInitProgramForSoftwareRendering");
            return -1;
        }
    }

#if 0 // Mastery test notification
    // Test Mastery notification on game startup
    {
        YUI_LOG("Triggering test Mastery notification...");
        
        // Call external function to trigger mastery notification directly
        testMasteryNotification();
    }
#endif // Mastery test

    return 0;
}

int switchWindow(ANativeWindow *window)
{

    EGLint format;
    EGLSurface surface;
    EGLint width;
    EGLint height;
    int i;

    if (g_Display == EGL_NO_DISPLAY || g_Config == NULL)
        return -1;

    eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    VdpRevoke();
    if (YuiUseOGLOnThisThread() == -1)
    {
        // Don't care
    }
    eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    eglDestroySurface(g_Display, g_Surface);

    // YUI_LOG("ANativeWindow_setBuffersGeometry");
    // ANativeWindow_setBuffersGeometry(window, 0, 0, format);

    YUI_LOG("switchWindow eglCreateWindowSurface");
    if (!(surface = eglCreateWindowSurface(g_Display, g_Config, window, 0)))
    {
        YUI_LOG("eglCreateWindowSurface() returned error %X", eglGetError());
        destroy();
        return -1;
    }

    eglQuerySurface(g_Display, surface, EGL_WIDTH, &width);
    eglQuerySurface(g_Display, surface, EGL_HEIGHT, &height);
    YUI_LOG("eglCreateWindowSurface() ok size = %d,%d", width, height);
    if (eglMakeCurrent(g_Display, surface, surface, g_Context) != EGL_TRUE)
    {
        YUI_LOG("eglMakeCurrent() returned error %X", eglGetError());
    }

    for (i = 0; VIDCoreList[i] != NULL; i++)
    {
        if (VIDCoreList[i]->id == s_vidcoretype)
        {
            YUI_LOG("Resize %d,%s %d,%d", s_vidcoretype, VIDCoreList[i]->Name, width, height);
            VIDCoreList[i]->Resize(0, 0, width, height, 1, g_aspect_rate_mode);
            glDisable(GL_SCISSOR_TEST);
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            // glClear( GL_COLOR_BUFFER_BIT );
            // eglSwapBuffers(g_Display, surface);
            // glClear( GL_COLOR_BUFFER_BIT );
            // eglSwapBuffers(g_Display, surface);
            break;
        }
    }

    g_Surface = surface;
    eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    VdpResume();
    YuiRevokeOGLOnThisThread();
    return 0;
}

int destroy()
{
    YabauseDeInit();

    if (s_vidcoretype != VIDCORE_VULKAN){
        eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        eglDestroyContext(g_Display, g_Context_Sub);
        eglDestroyContext(g_Display, g_Context);
        eglDestroySurface(g_Display, g_Surface);
        eglDestroySurface(g_Display, g_Pbuffer);
        eglTerminate(g_Display);
    }

    g_window = 0;
    g_Display = EGL_NO_DISPLAY;
    g_Context_Sub = EGL_NO_CONTEXT;
    g_Context = EGL_NO_CONTEXT;
    g_Surface = EGL_NO_SURFACE;
    g_Pbuffer = EGL_NO_SURFACE;

    return 0;
}

extern "C"
{

    void
    Java_org_uoyabause_android_YabauseRunnable_deinit(JNIEnv *env)
    {
        pthread_mutex_lock(&g_mtxGlLock);
        g_msg = MSG_RENDER_LOOP_EXIT;
        pthread_mutex_unlock(&g_mtxGlLock);

        // Poll s_isRunning with timeout to prevent ANR
        // s_isRunning is set to 0 when renderLoop() exits
        const int maxWaitMs = 5000;  // 5 second total timeout
        const int pollIntervalMs = 100;  // Check every 100ms
        int waitedMs = 0;

        while (s_isRunning && waitedMs < maxWaitMs) {
            usleep(pollIntervalMs * 1000);
            waitedMs += pollIntervalMs;

            // Retry signaling halfway through timeout
            if (waitedMs == maxWaitMs / 2) {
                YUI_LOG("deinit: thread still running, retrying signal...");
            }
        }

        if (s_isRunning) {
            YUI_LOG("Error: render loop thread did not exit within %dms, skipping join", maxWaitMs);
            // Thread is stuck - skip join to prevent ANR
            // This may cause resource leaks but is better than ANR
        } else {
            // Thread has exited, safe to join (should return immediately)
            pthread_join(_threadId, NULL);
        }
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_exec(JNIEnv *env)
    {
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_reset(JNIEnv *env)
    {
        yprintf("sending MSG_RESET");
        pthread_mutex_lock(&g_mtxGlLock);
        g_msg = MSG_RESET;
        pthread_mutex_unlock(&g_mtxGlLock);
    }

    class Command
    {
    public:
        Command(int press, int pad)
        {
            this->press = press;
            this->pad = pad;
        }
        int press;
        int pad;
    };

    typedef vector<Command> vecCommand;

    vecCommand commands;

    void
    Java_org_uoyabause_android_YabauseRunnable_press(JNIEnv *env, jobject obj, jint key, jint player)
    {
        if (PlayRecorder::getInstance()->getStatus() == PlayRecorder::RECORDING)
        {
            Command a(1, MAKE_PAD(player, key));
            commands.push_back(a);
            return;
        }
        else if (PlayRecorder::getInstance()->getStatus() == PlayRecorder::PLAYING)
        {
            return;
        }

        // yprintf("press: %d,%d",player,key);
        PerKeyDown(MAKE_PAD(player, key));
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_axis(JNIEnv *env, jobject obj, jint key, jint player, jint val)
    {
        if (PlayRecorder::getInstance()->getStatus() == PlayRecorder::PLAYING)
            return;
        // yprintf("axis: %d,%d,%d",player,key,val);
        PerAxisValue(MAKE_PAD(player, key), val); // from 0 to 255
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_release(JNIEnv *env, jobject obj, jint key, jint player)
    {
        if (PlayRecorder::getInstance()->getStatus() == PlayRecorder::RECORDING)
        {
            Command a(0, MAKE_PAD(player, key));
            commands.push_back(a);
            return;
        }
        else if (PlayRecorder::getInstance()->getStatus() == PlayRecorder::PLAYING)
        {
            return;
        }
        PerKeyUp(MAKE_PAD(player, key));
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_switch_1padmode(JNIEnv *env, jobject obj, jint mode)
    {
        g_pad_mode = mode;
        if (yabause)
            s_player2Enable = GetPlayer2Device();
        update_pad_mode();
    }

    void Java_org_uoyabause_android_YabauseRunnable_switch_1padmode2(JNIEnv *env, jobject obj, jint mode)
    {
        g_pad2_mode = mode;
        if (yabause)
            s_player2Enable = GetPlayer2Device();
        update_pad_mode();
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_enableFPS(JNIEnv *env, jobject obj, jint enable)
    {
        g_EnagleFPS = enable;
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_enableExtendedMemory(JNIEnv *env, jobject obj, jint enable)
    {
        g_extmemory = enable;
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_enableRotateScreen(JNIEnv *env, jobject obj, jint enable)
    {
        g_rotate_screen = enable;
        VideoSetSetting(VDP_SETTING_ROTATE_SCREEN, g_rotate_screen);
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_enableComputeShader(JNIEnv *env, jobject obj, jint enable)
    {
        g_use_compute_shader = enable;
        VideoSetSetting(VDP_SETTING_RBG_USE_COMPUTESHADER, g_use_compute_shader);
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setCpu(JNIEnv *env, jobject obj, jint cpu)
    {
        g_CpuType = cpu;
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setFilter(JNIEnv *env, jobject obj, jint filter)
    {
        g_VideoFilter = filter;
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setSoundEngine(JNIEnv *env, jobject obj, jint sound_engine)
    {
        g_SoundEngine = sound_engine;
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setResolutionMode(JNIEnv *env, jobject obj, jint resolution_mode)
    {
        g_resolution_mode = resolution_mode;
        VideoSetSetting(VDP_SETTING_RESOLUTION_MODE, g_resolution_mode);
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setRbgResolutionMode(JNIEnv *env, jobject obj, jint resolution_mode)
    {
        g_rbg_resolution_mode = resolution_mode;
        VideoSetSetting(VDP_SETTING_RBG_RESOLUTION_MODE, g_rbg_resolution_mode);
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setScspSyncPerFrame(JNIEnv *env, jobject obj, jint scsp_sync_count)
    {
        g_scsp_sync_count = scsp_sync_count;
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setScspSyncTimeMode(JNIEnv *env, jobject obj, jint mode)
    {
        g_scsp_sync_time_mode = mode;
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setCpuSyncPerLine(JNIEnv *env, jobject obj, jint count)
    {
        switch (count)
        {
        case 1:
            g_cpu_sync_shift = 0;
            break;
        case 2:
            g_cpu_sync_shift = 1;
            break;
        case 4:
            g_cpu_sync_shift = 2;
            break;
        case 8:
            g_cpu_sync_shift = 3;
            break;
        default:
            g_cpu_sync_shift = 0;
            break;
        }
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setPolygonGenerationMode(JNIEnv *env, jobject obj, jint pgm)
    {
        g_PolygonGenerationMode = pgm;
        VideoSetSetting(VDP_SETTING_POLYGON_MODE, g_PolygonGenerationMode);
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setAspectRateMode(JNIEnv *env, jobject obj, jint ka)
    {
        g_aspect_rate_mode = ka;
        VideoSetSetting(VDP_SETTING_ASPECT_RATE_MODE, g_aspect_rate_mode);
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setVdp2NewComposite(JNIEnv *env, jobject obj, jint enable)
    {
        // issue #22: toggle the new per-pixel VDP2 compositor (Vulkan only).
        // 0 = legacy fixed-function path (default), 1 = new path. Read each
        // frame in Vdp2DrawEnd(), so it can be switched at runtime.
        g_vdp2_new_composite = enable;
        VideoSetSetting(VDP_SETTING_VDP2_NEW_COMPOSITE, g_vdp2_new_composite);
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_enableFrameskip(JNIEnv *env, jobject obj, jint enable)
    {
        if (enable)
            EnableAutoFrameSkip();
        else
            DisableAutoFrameSkip();
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_setVolume(JNIEnv *env, jobject obj, jint volume)
    {
        ScspSetVolume(volume);
    }

    void
    Java_org_uoyabause_android_YabauseRunnable_restartAudioStream(JNIEnv *env, jobject obj)
    {
        extern void SNDOboeRestartStream(void);
        SNDOboeRestartStream();
    }

    void Java_org_uoyabause_android_YabauseRunnable_setUseCpuAffinity(JNIEnv *env, jobject obj, jint mode)
    {
        s_use_cpu_affinity = mode;
    }

    void Java_org_uoyabause_android_YabauseRunnable_setUseSh2Cache(JNIEnv *env, jobject obj, jint mode)
    {
        s_use_sh2_cache = mode;
    }

    jstring Java_org_uoyabause_android_YabauseRunnable_getGameTitle(JNIEnv *env)
    {

        char *buf;

        jstring rtn;
        if (cdip == NULL)
            return NULL;
        buf = (char *)malloc(1024);
        if (buf == NULL)
            return NULL;
        if (strcmp(cdip->cdinfo, "CD-1/1") == 0)
        {
            sprintf(buf, "%s", cdip->gamename);
        }
        else
        {
            sprintf(buf, "%s(%s)", cdip->gamename, cdip->cdinfo);
        }

        rtn = env->NewStringUTF(buf);
        free(buf);
        return rtn;
    }

    void Java_org_uoyabause_android_YabauseRunnable_updateCheat(JNIEnv *env, jobject object, jobjectArray stringArray)
    {

        if (stringArray == NULL || env->GetArrayLength(stringArray) == 0)
        {
            CheatClearCodes();
            return;
        }

        int stringCount = env->GetArrayLength(stringArray);
        int i = 0;
        int index = 0;
        CheatClearCodes();
        for (i = 0; i < stringCount; i++)
        {
            jstring string = (jstring)(env->GetObjectArrayElement(stringArray, i));
            if (string == NULL)
            {
                continue;
            }
            const char *rawString = env->GetStringUTFChars(string, 0);
            // Don't forget to call `ReleaseStringUTFChars` when you're done.

            index = CheatAddARCode(rawString);
            CheatEnableCode(index);
            env->ReleaseStringUTFChars(string, rawString);
        }
        // CheatDoPatches(); will call at  Vblank-in
        return;
    }

    jstring Java_org_uoyabause_android_YabauseRunnable_getGameinfo(JNIEnv *env)
    {

        char *buf;

        jstring rtn;
        if (cdip == NULL)
            return NULL;
        buf = (char *)malloc(1024);
        if (buf == NULL)
            return NULL;
        sprintf(buf,
                "{game:{maker_id:\"%s\",product_number:\"%s\",version:\"%s\","
                "release_date:\"%s\",\"device_infomation\":\"%s\","
                "area:\"%s\",game_title:\"%s\",input_device:\"%s\"}}",
                cdip->company, cdip->itemnum, cdip->version, cdip->date, cdip->cdinfo, cdip->region, cdip->gamename, cdip->peripheral);

        rtn = env->NewStringUTF(buf);
        free(buf);
        return rtn;
    }

#if 0
void
Java_org_uoyabause_android_YabauseRunnable_screenshot( JNIEnv* env, jobject obj, jobject bitmap )
{
    u32 * buffer;
    AndroidBitmapInfo info;
    void * pixels;
    int x, y;

    AndroidBitmap_getInfo( bitmap, &info);

    AndroidBitmap_lockPixels( bitmap, &pixels);

    buffer = dispbuffer;

    for(y = 0;y < info.height;y++) {
        for(x = 0;x < info.width;x++) {
            *((uint32_t *) pixels + x) = *(buffer + x);
        }
        pixels += info.stride;
        buffer += g_buf_width;
    }

    AndroidBitmap_unlockPixels( bitmap);
}
#endif

    void log_callback(char *message)
    {
        MAIN_LOGI("%s", message);
    }

    jint JNI_OnLoad(JavaVM *vm, void *reserved)
    {
        MAIN_LOGI("JNI_OnLoad is called");

        JNIEnv *env = nullptr;
        if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK)
            return -1;
        yvm = vm;

        LogStart();
        LogChangeOutput(DEBUG_CALLBACK, (char *)log_callback);

        s_crashlytics_context = crashlytics_init();

        return JNI_VERSION_1_6;
    }

} // extern "C"

void update_pad_mode()
{
    void *padbits;

    PerPortReset();

    if (g_pad_mode == 0)
    {
        padbits = PerPadAdd(&PORTDATA1);
        PerSetKey(MAKE_PAD(0, PERPAD_UP), PERPAD_UP, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_RIGHT), PERPAD_RIGHT, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_DOWN), PERPAD_DOWN, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_LEFT), PERPAD_LEFT, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_START), PERPAD_START, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_A), PERPAD_A, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_B), PERPAD_B, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_C), PERPAD_C, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_X), PERPAD_X, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_Y), PERPAD_Y, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_Z), PERPAD_Z, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_RIGHT_TRIGGER), PERPAD_RIGHT_TRIGGER, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_LEFT_TRIGGER), PERPAD_LEFT_TRIGGER, padbits);
    }
    else if (g_pad_mode == 1)
    {

        padbits = Per3DPadAdd(&PORTDATA1);

        PerSetKey(MAKE_PAD(0, PERANALOG_AXIS1), PERANALOG_AXIS1, padbits);
        PerSetKey(MAKE_PAD(0, PERANALOG_AXIS2), PERANALOG_AXIS2, padbits);
        PerSetKey(MAKE_PAD(0, PERANALOG_AXIS3), PERANALOG_AXIS3, padbits);
        PerSetKey(MAKE_PAD(0, PERANALOG_AXIS4), PERANALOG_AXIS4, padbits);

        PerSetKey(MAKE_PAD(0, PERPAD_UP), PERPAD_UP, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_RIGHT), PERPAD_RIGHT, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_DOWN), PERPAD_DOWN, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_LEFT), PERPAD_LEFT, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_START), PERPAD_START, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_A), PERPAD_A, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_B), PERPAD_B, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_C), PERPAD_C, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_X), PERPAD_X, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_Y), PERPAD_Y, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_Z), PERPAD_Z, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_RIGHT_TRIGGER), PERPAD_RIGHT_TRIGGER, padbits);
        PerSetKey(MAKE_PAD(0, PERPAD_LEFT_TRIGGER), PERPAD_LEFT_TRIGGER, padbits);
    }

    if (s_player2Enable != -1)
    {
        if (g_pad2_mode == 0)
        {
            padbits = PerPadAdd(&PORTDATA2);
            PerSetKey(MAKE_PAD(1, PERPAD_UP), PERPAD_UP, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_RIGHT), PERPAD_RIGHT, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_DOWN), PERPAD_DOWN, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_LEFT), PERPAD_LEFT, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_START), PERPAD_START, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_A), PERPAD_A, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_B), PERPAD_B, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_C), PERPAD_C, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_X), PERPAD_X, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_Y), PERPAD_Y, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_Z), PERPAD_Z, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_RIGHT_TRIGGER), PERPAD_RIGHT_TRIGGER, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_LEFT_TRIGGER), PERPAD_LEFT_TRIGGER, padbits);
        }
        else
        {
            padbits = Per3DPadAdd(&PORTDATA2);
            PerSetKey(MAKE_PAD(1, PERANALOG_AXIS1), PERANALOG_AXIS1, padbits);
            PerSetKey(MAKE_PAD(1, PERANALOG_AXIS2), PERANALOG_AXIS2, padbits);
            PerSetKey(MAKE_PAD(1, PERANALOG_AXIS3), PERANALOG_AXIS3, padbits);
            PerSetKey(MAKE_PAD(1, PERANALOG_AXIS4), PERANALOG_AXIS4, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_UP), PERPAD_UP, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_RIGHT), PERPAD_RIGHT, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_DOWN), PERPAD_DOWN, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_LEFT), PERPAD_LEFT, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_START), PERPAD_START, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_A), PERPAD_A, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_B), PERPAD_B, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_C), PERPAD_C, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_X), PERPAD_X, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_Y), PERPAD_Y, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_Z), PERPAD_Z, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_RIGHT_TRIGGER), PERPAD_RIGHT_TRIGGER, padbits);
            PerSetKey(MAKE_PAD(1, PERPAD_LEFT_TRIGGER), PERPAD_LEFT_TRIGGER, padbits);
        }
    }
}

void renderLoop()
{
    int renderingEnabled = 1;
    int pause = 0;

#if HAVE_VULKAN
    Renderer *r = NULL;
#endif
    int initResult = 0;

    while (renderingEnabled != 0)
    {

        if (initResult == 0)
        {
            if (g_Display && pause == 0 && g_window != NULL)
            {
                if (commands.size() > 0)
                {
                    for (int i = 0; i < commands.size(); i++)
                    {
                        if (commands[i].press == 0)
                        {
                            PerKeyUp(commands[i].pad);
                        }
                        else
                        {
                            PerKeyDown(commands[i].pad);
                        }
                    }
                    commands.clear();
                }
                YabauseExec();

                YabauseRA::Integration* rai = YabauseRA::Integration::getInstance();
                if(rai!=NULL)
                {
                    rai->doFrame();
                }
                
            }
        }

        pthread_mutex_lock(&g_mtxGlLock);
        // process incoming messages
        switch (g_msg)
        {

        case MSG_WINDOW_SET:
            YUI_LOG("MSG_WINDOW_SET");
            if (s_vidcoretype == VIDCORE_VULKAN)
            {
#if HAVE_VULKAN
                if (r == NULL)
                {
                    r = new Renderer();
                    r->setNativeWindow(g_window);
                    VIDVulkan::getInstance()->setRenderer(r);
                }
#endif
                g_Display = (void *)1;
            }
            else
            {
                if (initEgl(g_window) != 0)
                {
                    destroy();
                    pthread_mutex_unlock(&g_mtxGlLock);
                    return;
                }
            }
            initResult = YabauseInit();
            break;
        case MSG_WINDOW_CHG:
            YUI_LOG("MSG_WINDOW_CHG");
            if (s_vidcoretype == VIDCORE_VULKAN)
            {
#if HAVE_VULKAN
                for (int i = 0; VIDCoreList[i] != NULL; i++)
                {
                    if (VIDCoreList[i]->id == s_vidcoretype)
                    {
                        YUI_LOG("Resize %d,%s %d,%d", s_vidcoretype, VIDCoreList[i]->Name, surface_width, surface_height);
                        r->setNativeWindow(g_window);
                        VIDCoreList[i]->Resize(0, 0, surface_width, surface_height, 1, g_aspect_rate_mode);
                        break;
                    }
                }
#endif
            }
            else
            {
                if (switchWindow(g_window) != 0)
                {
                    destroy();
                    pthread_mutex_unlock(&g_mtxGlLock);
                    return;
                }
            }

            break;
        case MSG_RENDER_LOOP_EXIT:
            YUI_LOG("MSG_RENDER_LOOP_EXIT");
            pause = 0;
            renderingEnabled = 0;
            destroy();
            pthread_mutex_unlock(&g_mtxGlLock);
            break;

        case MSG_SAVE_STATE:
        {
            int ret;
            YUI_LOG("MSG_SAVE_STATE");

            time_t t = time(NULL);
            sprintf(last_state_filename, "%s/%s_%ld.yss", s_savepath, cdip->itemnum, t);
            ret = YabSaveState(last_state_filename);

            pthread_mutex_lock(&g_mtxFuncSync);
            pthread_cond_signal(&g_cndFuncSync);
            pthread_mutex_unlock(&g_mtxFuncSync);
        }
        break;

        case MSG_LOAD_STATE:
        {
            int rtn;
            YUI_LOG("MSG_LOAD_STATE %s", s_savepath);
            rtn = YabLoadState(s_savepath);
            switch (rtn)
            {
            case 0:
                YUI_LOG("Load State: OK");
                break;
            case -1:
                YUI_LOG("Load State: File Not Found");
                break;
            case -2:
                YUI_LOG("Load State: Bad format");
                break;
            case -3:
                YUI_LOG("Load State: Bad format deep inside");
                break;
            default:
                YUI_LOG("Load State: Fail unkown");
                break;
            }
            pthread_mutex_lock(&g_mtxFuncSync);
            pthread_cond_signal(&g_cndFuncSync);
            pthread_mutex_unlock(&g_mtxFuncSync);
        }
        break;

        case MSG_SAVE_STATE_COMPRESSED:
        {
            int ret;
            time_t t = time(NULL);
            YUI_LOG("MSG_SAVE_STATE_COMPRESSED");

            sprintf(last_state_filename, "%s/%s_%ld.yss", s_savepath, cdip->itemnum, t);
            ret = YabSaveCompressedState(last_state_filename);

            pthread_mutex_lock(&g_mtxFuncSync);
            pthread_cond_signal(&g_cndFuncSync);
            pthread_mutex_unlock(&g_mtxFuncSync);
        }
        break;

        case MSG_LOAD_STATE_COMPRESSED:
        {
            int rtn;
            YUI_LOG("MSG_LOAD_STATE_COMPRESSED");
            rtn = YabLoadCompressedState(s_savepath);
            switch (rtn)
            {
            case 0:
                YUI_LOG("Load State: OK");
                break;
            case -1:
                YUI_LOG("Load State: File Not Found");
                break;
            case -2:
                YUI_LOG("Load State: Bad format");
                break;
            case -3:
                YUI_LOG("Load State: Bad format deep inside");
                break;
            default:
                YUI_LOG("Load State: Fail unkown");
                break;
            }
            pthread_mutex_lock(&g_mtxFuncSync);
            pthread_cond_signal(&g_cndFuncSync);
            pthread_mutex_unlock(&g_mtxFuncSync);
        }
        break;

        case MSG_PAUSE:
            YUI_LOG("MSG_PAUSE");
            YabFlushBackups();
            // ScspMuteAudio(SCSP_MUTE_SYSTEM);
            pause = 1;
            break;
        case MSG_RESUME:
            YUI_LOG("MSG_RESUME");
            // ScspUnMuteAudio(SCSP_MUTE_SYSTEM);
            pause = 0;
            break;
        case MSG_OPEN_TRAY:
            YUI_LOG("MSG_OPEN_TRAY");
            Cs2ForceOpenTray();
            break;
        case MSG_CLOSE_TRAY:
        {
            YUI_LOG("MSG_CLOSE_TRAY");
            Cs2ForceCloseTray(CDCORE_ISO, s_cdpath);
            if (!g_skip_ra_change_media) {
                YabauseRA::Integration* ra = YabauseRA::Integration::getInstance();
                if(ra){
                    ra->changeMedia(s_cdpath);
                }
            }
            g_skip_ra_change_media = false;
        }
            break;
        case MSG_SCREENSHOT:
        {
            YUI_LOG("MSG_SCREENSHOT");
            PlayRecorder *p = PlayRecorder::getInstance();
            if (p->getStatus() == PlayRecorder::RECORDING)
            {
                p->takeShot();
            }
            else
            {
                s_status = saveScreenshot(screenShotFilename);
            }
            pthread_mutex_lock(&g_mtxFuncSync);
            pthread_cond_signal(&g_cndFuncSync);
            pthread_mutex_unlock(&g_mtxFuncSync);
        }
        break;
        case MSG_RESET:
        {
            YabauseReset();
            YabauseRA::Integration* ra = YabauseRA::Integration::getInstance();
            if(ra){
                ra->reset();
            }
        }
        break;
        case MSG_RECORD:
        {
            PlayRecorder *p = PlayRecorder::getInstance();
            if (p->getStatus() == PlayRecorder::IDLE)
            {
                gsc.setScreenshotCallback(p);
                p->setBaseDir(s_playrecord_path);
                p->startRocord();
            }
            else if (p->getStatus() == 0)
            {
                p->stopRocord();
            }
            pthread_mutex_lock(&g_mtxFuncSync);
            pthread_cond_signal(&g_cndFuncSync);
            pthread_mutex_unlock(&g_mtxFuncSync);
        }
        break;
        case MSG_PLAY:
        {
            PlayRecorder *p = PlayRecorder::getInstance();
            if (p->getStatus() == PlayRecorder::IDLE)
            {
                gsc.setScreenshotCallback(p);
                p->startPlay(s_playdatadir, true, NULL);
            }
            pthread_mutex_lock(&g_mtxFuncSync);
            pthread_cond_signal(&g_cndFuncSync);
            pthread_mutex_unlock(&g_mtxFuncSync);
        }
        break;
        default:
            break;
        }
        g_msg = MSG_NONE;
        pthread_mutex_unlock(&g_mtxGlLock);
    }

    YUI_LOG("byebye");

    if (s_crashlytics_context != NULL)
        crashlytics_free(&s_crashlytics_context);

    s_isRunning = 0;
}

void *threadStartCallback(void *myself)
{
    renderLoop();
    pthread_exit(0);
    return NULL;
}

int saveScreenshot(const char *filename)
{

    int width;
    int height;
    unsigned char *buf = NULL;
    unsigned char *bufRGB = NULL;
    png_bytep *row_pointers = NULL;
    int quality = 100; // best
    FILE *outfile = NULL;
    int row_stride;
    int glerror;
    int u, v;
    int pmode;
    png_byte color_type;
    png_byte bit_depth;
    png_structp png_ptr;
    png_infop info_ptr;
    int number_of_passes;
    int rtn = -1;

    YUI_LOG("saveScreenshot %s", filename);

    if (s_vidcoretype == VIDCORE_VULKAN)
    {

        for (int i = 0; VIDCoreList[i] != NULL; i++)
        {
            if (VIDCoreList[i]->id == s_vidcoretype)
            {
                VIDCoreList[i]->GetScreenshot((void **)&buf, &width, &height);
                break;
            }
        }
    }
    else
    {

        eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);

        VdpRevoke();
        if (YuiUseOGLOnThisThread() == -1)
        {
            VdpResume();
            return -1;
        }

        eglQuerySurface(g_Display, g_Surface, EGL_WIDTH, &width);
        eglQuerySurface(g_Display, g_Surface, EGL_HEIGHT, &height);

        YUI_LOG("screen %d,%d", width, height);

        buf = (unsigned char *)malloc(width * height * 4);
        if (buf == NULL)
        {
            YUI_LOG("not enough memory\n");
            goto FINISH;
        }

        glReadBuffer(GL_BACK);
        pmode = GL_RGBA;
        glGetError();
        glReadPixels(0, 0, width, height, pmode, GL_UNSIGNED_BYTE, buf);
        if ((glerror = glGetError()) != GL_NO_ERROR)
        {
            YUI_LOG("glReadPixels %04X\n", glerror);
            goto FINISH;
        }
        for (u = 3; u < width * height * 4; u += 4)
        {
            buf[u] = 0xFF;
        }
    }

    row_pointers = (png_bytep *)malloc(sizeof(png_bytep) * height);
    for (v = 0; v < height; v++)
        row_pointers[v] = (png_byte *)&buf[(height - 1 - v) * width * 4];

    // save as png
    if ((outfile = fopen(filename, "wb")) == NULL)
    {
        YUI_LOG("can't open %s\n", filename);
        goto FINISH;
    }

    /* initialize stuff */
    png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

    if (!png_ptr)
    {
        YUI_LOG("[write_png_file] png_create_write_struct failed");
        goto FINISH;
    }

    info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr)
    {
        YUI_LOG("[write_png_file] png_create_info_struct failed");
        goto FINISH;
    }

    if (setjmp(png_jmpbuf(png_ptr)))
    {
        YUI_LOG("[write_png_file] Error during init_io");
        goto FINISH;
    }
    /* write header */
    png_init_io(png_ptr, outfile);

    if (setjmp(png_jmpbuf(png_ptr)))
    {
        YUI_LOG("[write_png_file] Error during writing header");
        goto FINISH;
    }
    bit_depth = 8;
    color_type = PNG_COLOR_TYPE_RGB_ALPHA;
    png_set_IHDR(png_ptr, info_ptr, width, height,
                 bit_depth, color_type, PNG_INTERLACE_NONE,
                 PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
    // png_set_gAMA(png_ptr, info_ptr, 1.0);
    {
        png_text text[3];
        int txt_fields = 0;
        char desc[256];

        time_t gmt;
        png_time mod_time;

        time(&gmt);
        png_convert_from_time_t(&mod_time, gmt);
        png_set_tIME(png_ptr, info_ptr, &mod_time);

        text[txt_fields].key = (png_charp) "Title";
        text[txt_fields].text = Cs2GetCurrentGmaecode();
        text[txt_fields].compression = PNG_TEXT_COMPRESSION_NONE;
        txt_fields++;

        sprintf(desc, "uoYabause Version %s\n VENDER: %s\n RENDERER: %s\n VERSION %s\n", YAB_VERSION, glGetString(GL_VENDOR), glGetString(GL_RENDERER), glGetString(GL_VERSION));
        text[txt_fields].key = (char *)"Description";
        text[txt_fields].text = desc;
        text[txt_fields].compression = PNG_TEXT_COMPRESSION_NONE;
        txt_fields++;

        png_set_text(png_ptr, info_ptr, text, txt_fields);
    }
    png_write_info(png_ptr, info_ptr);

    /* write bytes */
    if (setjmp(png_jmpbuf(png_ptr)))
    {
        YUI_LOG("[write_png_file] Error during writing bytes");
        goto FINISH;
    }
    png_write_image(png_ptr, row_pointers);

    /* end write */
    if (setjmp(png_jmpbuf(png_ptr)))
    {
        YUI_LOG("[write_png_file] Error during end of write");
        goto FINISH;
    }

    png_write_end(png_ptr, NULL);

#if 0 // JPEG Version
    struct jpeg_compress_struct cinfo;
    struct jpeg_error_mgr jerr;
    JSAMPROW row_pointer[1];

    // Convert to RGB
    bufRGB = (unsigned char *)malloc(width*height*3);
    if( bufRGB == NULL ){
        YUI_LOG("not enough memory\n");
        goto FINISH;
    }

    if( pmode == GL_RGBA ){

        for( v=(height-1); v>0; v-- ){
            unsigned char * in  = &buf[ v*width*4 ] ;
            unsigned char * out = &bufRGB[ (height-1-v)*width*3 ] ;
            for( u=0; u<width; u++ ){
                out[u*3+0] = in[u*4+0];
                out[u*3+1] = in[u*4+1];
                out[u*3+2] = in[u*4+2];
            }
        }
    }else{
        for( v=(height-1); v>0; v-- ){
            unsigned char * in  = &buf[ v*width*3 ] ;
            unsigned char * out = &bufRGB[ (height-1-v)*width*3 ] ;
            for( u=0; u<width; u++ ){
                out[u*3+0] = in[u*3+0];
                out[u*3+1] = in[u*3+1];
                out[u*3+2] = in[u*3+2];
            }
        }
    }
    // save as jpeg
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_compress(&cinfo);
    if ((outfile = fopen(filename, "wb")) == NULL) {
        YUI_LOG("can't open %s\n", filename);
        goto FINISH;
    }
    jpeg_stdio_dest(&cinfo, outfile);
    cinfo.image_width = width;
    cinfo.image_height = height;
    cinfo.input_components = 3;
    cinfo.in_color_space = JCS_RGB;
    jpeg_set_defaults(&cinfo);
    jpeg_set_quality(&cinfo, quality, TRUE);
    jpeg_start_compress(&cinfo, TRUE);
    row_stride = width * 3;
    while (cinfo.next_scanline < cinfo.image_height) {
        row_pointer[0] = & bufRGB[cinfo.next_scanline * row_stride];
        (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
    }
    jpeg_finish_compress(&cinfo);
    jpeg_destroy_compress(&cinfo);
#endif
    rtn = 0;
FINISH:
    if (outfile)
        fclose(outfile);
    if (buf)
        free(buf);
    if (bufRGB)
        free(bufRGB);
    if (row_pointers)
        free(row_pointers);
    eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    VdpResume();
    YUI_LOG("write screenshot as  %s\n", filename);
    YuiRevokeOGLOnThisThread();

    return rtn;
}

extern "C"
{

    int YabauseThread_IsUseBios()
    {
        if (s_biospath == NULL)
        {
            return 1;
        }
        return 0;
    }

    const char *YabauseThread_getBackupPath()
    {
        return s_buppath;
    }

    void YabauseThread_setUseBios(int use)
    {
    }

    char tmpbakcup[256];
    void YabauseThread_setBackupPath(const char *buf)
    {
        strcpy(tmpbakcup, buf);
        s_buppath = tmpbakcup;
    }

    void YabauseThread_resetPlaymode()
    {
        if (s_playrecord_path != NULL)
        {
            free(s_playrecord_path);
            s_playrecord_path = NULL;
        }
        s_buppath = GetMemoryPath();
    }

    void YabauseThread_coldBoot()
    {
        YabauseDeInit();
        YabauseInit();
        YabauseReset();
    }
}
