#ifdef PLATFORM_PICO_4

#include <assert.h>
#include <math.h>
#include <stdbool.h>


#include "data.h"
#include "geometry.h"
#include "global.h"
#include "glsl.h"
#include "input.h"
#include "opengl.h"
#include "settings.h"
#include "shader.h"
#include "texture.h"
#include "ui.h"
#include "xr.h"

#include <array>
#include <dirent.h>
#include <list>
#include <sys/system_properties.h>
#include <thread>

#include <time.h>
#include <unistd.h>
#include <dirent.h>  // for opendir/closedir
#include <pthread.h>
#include <malloc.h>                     // for memalign
#include <dlfcn.h>                      // for dlopen
#include <sys/prctl.h>                  // for prctl( PR_SET_NAME )
#include <sys/stat.h>                   // for gettid
#include <sys/syscall.h>                // for syscall
#include <android/log.h>                // for __android_log_print
#include <android/input.h>              // for AKEYCODE_ etc.
#include <android/window.h>             // for AWINDOW_FLAG_KEEP_SCREEN_ON
#include <android/native_window_jni.h>  // for native window JNI
#include <android_native_app_glue.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>

#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>

typedef struct {
  JavaVM* vm;        // Java Virtual Machine
  JNIEnv* env;       // Thread specific environment
  jobject activity;  // Java activity object
} Java_t;


#define BIT(x) (1 << (x))

#if !defined(GL_EXT_framebuffer_multisample)
typedef void (*PFNGLRENDERBUFFERSTORAGEMULTISAMPLEEXTPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
  GLsizei height);
#endif


// GL_EXT_disjoint_timer_query without _EXT
#if !defined(GL_TIMESTAMP)
#define GL_QUERY_COUNTER_BITS GL_QUERY_COUNTER_BITS_EXT
#define GL_TIME_ELAPSED GL_TIME_ELAPSED_EXT
#define GL_TIMESTAMP GL_TIMESTAMP_EXT
#define GL_GPU_DISJOINT GL_GPU_DISJOINT_EXT
#endif

// GL_EXT_buffer_storage without _EXT
#if !defined(GL_BUFFER_STORAGE_FLAGS)
#define GL_MAP_READ_BIT 0x0001                          // GL_MAP_READ_BIT_EXT
#define GL_MAP_WRITE_BIT 0x0002                         // GL_MAP_WRITE_BIT_EXT
#define GL_MAP_PERSISTENT_BIT 0x0040                    // GL_MAP_PERSISTENT_BIT_EXT
#define GL_MAP_COHERENT_BIT 0x0080                      // GL_MAP_COHERENT_BIT_EXT
#define GL_DYNAMIC_STORAGE_BIT 0x0100                   // GL_DYNAMIC_STORAGE_BIT_EXT
#define GL_CLIENT_STORAGE_BIT 0x0200                    // GL_CLIENT_STORAGE_BIT_EXT
#define GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT 0x00004000  // GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT
#define GL_BUFFER_IMMUTABLE_STORAGE 0x821F              // GL_BUFFER_IMMUTABLE_STORAGE_EXT
#define GL_BUFFER_STORAGE_FLAGS 0x8220                  // GL_BUFFER_STORAGE_FLAGS_EXT
#endif

typedef void(GL_APIENTRY* PFNGLBUFFERSTORAGEEXTPROC)(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags);
typedef void(GL_APIENTRY* PFNGLTEXSTORAGE3DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
  GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);

typedef struct {
  int dummy;
} ksDriverInstance;


typedef enum {
  KS_GPU_QUEUE_PROPERTY_GRAPHICS = BIT(0),
  KS_GPU_QUEUE_PROPERTY_COMPUTE = BIT(1),
  KS_GPU_QUEUE_PROPERTY_TRANSFER = BIT(2)
} ksGpuQueueProperty;

typedef enum { KS_GPU_QUEUE_PRIORITY_LOW, KS_GPU_QUEUE_PRIORITY_MEDIUM, KS_GPU_QUEUE_PRIORITY_HIGH } ksGpuQueuePriority;

#define MAX_QUEUES 16

typedef struct {
  int queueCount;                                  // number of queues
  ksGpuQueueProperty queueProperties;              // desired queue family properties
  ksGpuQueuePriority queuePriorities[MAX_QUEUES];  // individual queue priorities
} ksGpuQueueInfo;

typedef struct {
  ksDriverInstance* instance;
  ksGpuQueueInfo queueInfo;
} ksGpuDevice;

typedef enum {
  KS_GPU_SURFACE_COLOR_FORMAT_R5G6B5,
  KS_GPU_SURFACE_COLOR_FORMAT_B5G6R5,
  KS_GPU_SURFACE_COLOR_FORMAT_R8G8B8A8,
  KS_GPU_SURFACE_COLOR_FORMAT_B8G8R8A8,
  KS_GPU_SURFACE_COLOR_FORMAT_MAX
} ksGpuSurfaceColorFormat;

typedef enum {
  KS_GPU_SURFACE_DEPTH_FORMAT_NONE,
  KS_GPU_SURFACE_DEPTH_FORMAT_D16,
  KS_GPU_SURFACE_DEPTH_FORMAT_D24,
  KS_GPU_SURFACE_DEPTH_FORMAT_MAX
} ksGpuSurfaceDepthFormat;

typedef enum {
  KS_GPU_SAMPLE_COUNT_1 = 1,
  KS_GPU_SAMPLE_COUNT_2 = 2,
  KS_GPU_SAMPLE_COUNT_4 = 4,
  KS_GPU_SAMPLE_COUNT_8 = 8,
  KS_GPU_SAMPLE_COUNT_16 = 16,
  KS_GPU_SAMPLE_COUNT_32 = 32,
  KS_GPU_SAMPLE_COUNT_64 = 64,
} ksGpuSampleCount;

typedef struct {
  const ksGpuDevice* device;
  EGLDisplay display;
  EGLConfig config;
  EGLSurface tinySurface;
  EGLSurface mainSurface;
  EGLContext context;
} ksGpuContext;

typedef struct {
  unsigned char redBits;
  unsigned char greenBits;
  unsigned char blueBits;
  unsigned char alphaBits;
  unsigned char colorBits;
  unsigned char depthBits;
} ksGpuSurfaceBits;

void ksGpuContext_SetCurrent(ksGpuContext* context);

typedef struct {
  bool keyInput[256];
  bool mouseInput[8];
  int mouseInputX[8];
  int mouseInputY[8];
} ksGpuWindowInput;

typedef uint64_t ksNanoseconds;

typedef struct {
  ksGpuDevice device;
  ksGpuContext context;
  ksGpuSurfaceColorFormat colorFormat;
  ksGpuSurfaceDepthFormat depthFormat;
  ksGpuSampleCount sampleCount;
  int windowWidth;
  int windowHeight;
  int windowSwapInterval;
  float windowRefreshRate;
  bool windowFullscreen;
  bool windowActive;
  bool windowExit;
  ksGpuWindowInput input;
  ksNanoseconds lastSwapTime;

  EGLDisplay display;
  EGLint majorVersion;
  EGLint minorVersion;
  struct android_app* app;
  Java_t java;
  ANativeWindow* nativeWindow;
  bool resumed;
} ksGpuWindow;

bool ksGpuWindow_Create(ksGpuWindow* window, ksDriverInstance* instance, const ksGpuQueueInfo* queueInfo, int queueIndex,
  ksGpuSurfaceColorFormat colorFormat, ksGpuSurfaceDepthFormat depthFormat, ksGpuSampleCount sampleCount,
  int width, int height, bool fullscreen);

//#ifdef __cplusplus
//}
//#endif







//#include "gfxwrapper_opengl.h"

static void Print(const char* format, ...) {
  char buffer[4096];
  va_list args;
  va_start(args, format);
  vsnprintf(buffer, 4096, format, args);
  va_end(args);

  __android_log_print(ANDROID_LOG_VERBOSE, "atw", "%s", buffer);
}

static void Error(const char* format, ...) {
  char buffer[4096];
  va_list args;
  va_start(args, format);
  vsnprintf(buffer, 4096, format, args);
  va_end(args);

  __android_log_print(ANDROID_LOG_ERROR, "atw", "%s", buffer);
  // Without exiting, the application will likely crash.
  if (format != NULL) {
    exit(0);
  }
}

typedef struct {
  FILE* fp;
  ksNanoseconds* frameCpuTimes;
  ksNanoseconds* frameGpuTimes;
  int frameCount;
  int frame;
} ksFrameLog;

__thread ksFrameLog* threadFrameLog;

static ksFrameLog* ksFrameLog_Get() {
  ksFrameLog* l = threadFrameLog;
  if (l == NULL) {
    l = (ksFrameLog*)malloc(sizeof(ksFrameLog));
    memset(l, 0, sizeof(ksFrameLog));
    threadFrameLog = l;
  }
  return l;
}

static void ksFrameLog_Write(const char* fileName, const int lineNumber, const char* function) {
  ksFrameLog* l = ksFrameLog_Get();
  if (l != NULL && l->fp != NULL) {
    if (l->frame < l->frameCount) {
      fprintf(l->fp, "%s(%d): %s\r\n", fileName, lineNumber, function);
    }
  }
}

/*
================================================================================================================================

OpenGL error checking.

================================================================================================================================
*/

//#if defined(_DEBUG)
//#define GL(func)                                 \
//    func;                                        \
//    ksFrameLog_Write(__FILE__, __LINE__, #func); \
//    GlCheckErrors(#func);
//#else
//#define GL(func) func;
//#endif

#if defined(_DEBUG)
#define EGL(func)                                                  \
    ksFrameLog_Write(__FILE__, __LINE__, #func);                   \
    if (func == EGL_FALSE) {                                       \
        Error(#func " failed: %s", EglErrorString(eglGetError())); \
    }
#else
#define EGL(func)                                                  \
    if (func == EGL_FALSE) {                                       \
        Error(#func " failed: %s", EglErrorString(eglGetError())); \
    }
#endif

static const char* EglErrorString(const EGLint error) {
  switch (error) {
  case EGL_SUCCESS:
    return "EGL_SUCCESS";
  case EGL_NOT_INITIALIZED:
    return "EGL_NOT_INITIALIZED";
  case EGL_BAD_ACCESS:
    return "EGL_BAD_ACCESS";
  case EGL_BAD_ALLOC:
    return "EGL_BAD_ALLOC";
  case EGL_BAD_ATTRIBUTE:
    return "EGL_BAD_ATTRIBUTE";
  case EGL_BAD_CONTEXT:
    return "EGL_BAD_CONTEXT";
  case EGL_BAD_CONFIG:
    return "EGL_BAD_CONFIG";
  case EGL_BAD_CURRENT_SURFACE:
    return "EGL_BAD_CURRENT_SURFACE";
  case EGL_BAD_DISPLAY:
    return "EGL_BAD_DISPLAY";
  case EGL_BAD_SURFACE:
    return "EGL_BAD_SURFACE";
  case EGL_BAD_MATCH:
    return "EGL_BAD_MATCH";
  case EGL_BAD_PARAMETER:
    return "EGL_BAD_PARAMETER";
  case EGL_BAD_NATIVE_PIXMAP:
    return "EGL_BAD_NATIVE_PIXMAP";
  case EGL_BAD_NATIVE_WINDOW:
    return "EGL_BAD_NATIVE_WINDOW";
  case EGL_CONTEXT_LOST:
    return "EGL_CONTEXT_LOST";
  default:
    return "unknown";
  }
}

static const char* GlErrorString(GLenum error) {
  switch (error) {
  case GL_NO_ERROR:
    return "GL_NO_ERROR";
  case GL_INVALID_ENUM:
    return "GL_INVALID_ENUM";
  case GL_INVALID_VALUE:
    return "GL_INVALID_VALUE";
  case GL_INVALID_OPERATION:
    return "GL_INVALID_OPERATION";
  case GL_INVALID_FRAMEBUFFER_OPERATION:
    return "GL_INVALID_FRAMEBUFFER_OPERATION";
  case GL_OUT_OF_MEMORY:
    return "GL_OUT_OF_MEMORY";
  default:
    return "unknown";
  }
}

static void GlCheckErrors(const char* function) {
  for (int i = 0; i < 10; i++) {
    const GLenum error = glGetError();
    if (error == GL_NO_ERROR) {
      break;
    }
    Error("GL error: %s: %s", function, GlErrorString(error));
  }
}

/*
================================================================================================================================

OpenGL extensions.

================================================================================================================================
*/

typedef struct {
  bool timer_query;                       // GL_ARB_timer_query, GL_EXT_disjoint_timer_query
  bool texture_clamp_to_border;           // GL_EXT_texture_border_clamp, GL_OES_texture_border_clamp
  bool buffer_storage;                    // GL_ARB_buffer_storage
  bool multi_sampled_storage;             // GL_ARB_texture_storage_multisample
  bool multi_view;                        // GL_OVR_multiview, GL_OVR_multiview2
  bool multi_sampled_resolve;             // GL_EXT_multisampled_render_to_texture
  bool multi_view_multi_sampled_resolve;  // GL_OVR_multiview_multisampled_render_to_texture

  int texture_clamp_to_border_id;
} ksOpenGLExtensions;

ksOpenGLExtensions glExtensions;

/*
================================
Get proc address / extensions
================================
*/

void (*GetExtension(const char* functionName))() { return eglGetProcAddress(functionName); }

GLint glGetInteger(GLenum pname) {
  GLint i;
  GL(glGetIntegerv(pname, &i));
  return i;
}

//static bool GlCheckExtension(const char* extension) {
//  GL(const GLint numExtensions = glGetInteger(GL_NUM_EXTENSIONS));
//  for (GLint i = 0; i < numExtensions; i++) {
//    GL(const GLubyte* str = glGetStringi(GL_EXTENSIONS, i));
//    if (strcmp((const char*)str, extension) == 0) {
//      return true;
//    }
//  }
//  return false;
//}

typedef void(GL_APIENTRY* PFNGLBUFFERSTORAGEEXTPROC)(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags);
typedef void(GL_APIENTRY* PFNGLTEXSTORAGE3DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
  GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);

/*
================================================================================================================================

GPU Device.

================================================================================================================================
*/

bool ksGpuDevice_Create(ksGpuDevice* device, ksDriverInstance* instance, const ksGpuQueueInfo* queueInfo) {
  /*
          Use an extensions to select the appropriate device:
          https://www.opengl.org/registry/specs/NV/gpu_affinity.txt
          https://www.opengl.org/registry/specs/AMD/wgl_gpu_association.txt
          https://www.opengl.org/registry/specs/AMD/glx_gpu_association.txt

          On Linux configure each GPU to use a separate X screen and then select
          the X screen to render to.
  */

  memset(device, 0, sizeof(ksGpuDevice));

  device->instance = instance;
  device->queueInfo = *queueInfo;

  return true;
}

/*
================================================================================================================================

GPU Context.

================================================================================================================================
*/

ksGpuSurfaceBits ksGpuContext_BitsForSurfaceFormat(const ksGpuSurfaceColorFormat colorFormat,
  const ksGpuSurfaceDepthFormat depthFormat) {
  ksGpuSurfaceBits bits;
  bits.redBits = ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_R8G8B8A8)
    ? 8
    : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_B8G8R8A8)
      ? 8
      : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_R5G6B5)
        ? 5
        : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_B5G6R5) ? 5 : 8))));
  bits.greenBits = ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_R8G8B8A8)
    ? 8
    : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_B8G8R8A8)
      ? 8
      : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_R5G6B5)
        ? 6
        : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_B5G6R5) ? 6 : 8))));
  bits.blueBits = ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_R8G8B8A8)
    ? 8
    : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_B8G8R8A8)
      ? 8
      : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_R5G6B5)
        ? 5
        : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_B5G6R5) ? 5 : 8))));
  bits.alphaBits = ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_R8G8B8A8)
    ? 8
    : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_B8G8R8A8)
      ? 8
      : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_R5G6B5)
        ? 0
        : ((colorFormat == KS_GPU_SURFACE_COLOR_FORMAT_B5G6R5) ? 0 : 8))));
  bits.colorBits = bits.redBits + bits.greenBits + bits.blueBits + bits.alphaBits;
  bits.depthBits =
    ((depthFormat == KS_GPU_SURFACE_DEPTH_FORMAT_D16) ? 16 : ((depthFormat == KS_GPU_SURFACE_DEPTH_FORMAT_D24) ? 24 : 0));
  return bits;
}

static bool ksGpuContext_CreateForSurface(ksGpuContext* context, const ksGpuDevice* device, const int queueIndex,
  const ksGpuSurfaceColorFormat colorFormat, const ksGpuSurfaceDepthFormat depthFormat,
  const ksGpuSampleCount sampleCount, EGLDisplay display) {
  context->device = device;

  context->display = display;

  // Do NOT use eglChooseConfig, because the Android EGL code pushes in multisample
  // flags in eglChooseConfig when the user has selected the "force 4x MSAA" option in
  // settings, and that is completely wasted on the time warped frontbuffer.
  const int MAX_CONFIGS = 1024;
  EGLConfig configs[MAX_CONFIGS];
  EGLint numConfigs = 0;
  EGL(eglGetConfigs(display, configs, MAX_CONFIGS, &numConfigs));

  const ksGpuSurfaceBits bits = ksGpuContext_BitsForSurfaceFormat(colorFormat, depthFormat);

  const EGLint configAttribs[] = { EGL_RED_SIZE, bits.greenBits, EGL_GREEN_SIZE, bits.redBits, EGL_BLUE_SIZE, bits.blueBits,
                                  EGL_ALPHA_SIZE, bits.alphaBits, EGL_DEPTH_SIZE, bits.depthBits,
    // EGL_STENCIL_SIZE,	0,
                            EGL_SAMPLE_BUFFERS, (sampleCount > KS_GPU_SAMPLE_COUNT_1), EGL_SAMPLES,
                            (sampleCount > KS_GPU_SAMPLE_COUNT_1) ? sampleCount : 0, EGL_NONE };

  context->config = 0;
  for (int i = 0; i < numConfigs; i++) {
    EGLint value = 0;

    eglGetConfigAttrib(display, configs[i], EGL_RENDERABLE_TYPE, &value);
    if ((value & EGL_OPENGL_ES3_BIT) != EGL_OPENGL_ES3_BIT) {
      continue;
    }

    // Without EGL_KHR_surfaceless_context, the config needs to support both pbuffers and window surfaces.
    eglGetConfigAttrib(display, configs[i], EGL_SURFACE_TYPE, &value);
    if ((value & (EGL_WINDOW_BIT | EGL_PBUFFER_BIT)) != (EGL_WINDOW_BIT | EGL_PBUFFER_BIT)) {
      continue;
    }

    int j = 0;
    for (; configAttribs[j] != EGL_NONE; j += 2) {
      eglGetConfigAttrib(display, configs[i], configAttribs[j], &value);
      if (value != configAttribs[j + 1]) {
        break;
      }
    }
    if (configAttribs[j] == EGL_NONE) {
      context->config = configs[i];
      break;
    }
  }
  if (context->config == 0) {
    Error("Failed to find EGLConfig");
    return false;
  }

  EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE, EGL_NONE, EGL_NONE };
  // Use the default priority if KS_GPU_QUEUE_PRIORITY_MEDIUM is selected.
  const ksGpuQueuePriority priority = device->queueInfo.queuePriorities[queueIndex];
  if (priority != KS_GPU_QUEUE_PRIORITY_MEDIUM) {
    contextAttribs[2] = EGL_CONTEXT_PRIORITY_LEVEL_IMG;
    contextAttribs[3] = (priority == KS_GPU_QUEUE_PRIORITY_LOW) ? EGL_CONTEXT_PRIORITY_LOW_IMG : EGL_CONTEXT_PRIORITY_HIGH_IMG;
  }
  context->context = eglCreateContext(display, context->config, EGL_NO_CONTEXT, contextAttribs);
  if (context->context == EGL_NO_CONTEXT) {
    Error("eglCreateContext() failed: %s", EglErrorString(eglGetError()));
    return false;
  }

  const EGLint surfaceAttribs[] = { EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE };
  context->tinySurface = eglCreatePbufferSurface(display, context->config, surfaceAttribs);
  if (context->tinySurface == EGL_NO_SURFACE) {
    Error("eglCreatePbufferSurface() failed: %s", EglErrorString(eglGetError()));
    eglDestroyContext(display, context->context);
    context->context = EGL_NO_CONTEXT;
    return false;
  }
  context->mainSurface = context->tinySurface;

  return true;
}

void ksGpuContext_SetCurrent(ksGpuContext* context) {
  EGL(eglMakeCurrent(context->display, context->mainSurface, context->mainSurface, context->context));
}

/*
================================================================================================================================

GPU Window.

================================================================================================================================
*/

static void app_handle_cmd2(struct android_app* app, int32_t cmd) {
  ksGpuWindow* window = (ksGpuWindow*)app->userData;

  switch (cmd) {
    // There is no APP_CMD_CREATE. The ANativeActivity creates the
    // application thread from onCreate(). The application thread
    // then calls android_main().
  case APP_CMD_START: {
    Print("onStart()");
    Print("    APP_CMD_START");
    break;
  }
  case APP_CMD_RESUME: {
    Print("onResume()");
    Print("    APP_CMD_RESUME");
    window->resumed = true;
    break;
  }
  case APP_CMD_PAUSE: {
    Print("onPause()");
    Print("    APP_CMD_PAUSE");
    window->resumed = false;
    break;
  }
  case APP_CMD_STOP: {
    Print("onStop()");
    Print("    APP_CMD_STOP");
    break;
  }
  case APP_CMD_DESTROY: {
    Print("onDestroy()");
    Print("    APP_CMD_DESTROY");
    window->nativeWindow = NULL;
    break;
  }
  case APP_CMD_INIT_WINDOW: {
    Print("surfaceCreated()");
    Print("    APP_CMD_INIT_WINDOW");
    window->nativeWindow = app->window;
    break;
  }
  case APP_CMD_TERM_WINDOW: {
    Print("surfaceDestroyed()");
    Print("    APP_CMD_TERM_WINDOW");
    window->nativeWindow = NULL;
    break;
  }
  }
}

typedef enum { MOUSE_LEFT = 0, MOUSE_RIGHT = 1 } ksMouseButton;

static int32_t app_handle_input(struct android_app* app, AInputEvent* event) {
  ksGpuWindow* window = (ksGpuWindow*)app->userData;

  const int type = AInputEvent_getType(event);
  if (type == AINPUT_EVENT_TYPE_KEY) {
    int keyCode = AKeyEvent_getKeyCode(event);
    const int action = AKeyEvent_getAction(event);
    if (action == AKEY_EVENT_ACTION_DOWN) {
      // Translate controller input to useful keys.
      switch (keyCode) {
      case AKEYCODE_BUTTON_A:
        keyCode = AKEYCODE_Q;
        break;
      case AKEYCODE_BUTTON_B:
        keyCode = AKEYCODE_W;
        break;
      case AKEYCODE_BUTTON_X:
        keyCode = AKEYCODE_E;
        break;
      case AKEYCODE_BUTTON_Y:
        keyCode = AKEYCODE_M;
        break;
      case AKEYCODE_BUTTON_START:
        keyCode = AKEYCODE_L;
        break;
      case AKEYCODE_BUTTON_SELECT:
        keyCode = AKEYCODE_ESCAPE;
        break;
      }
      if (keyCode >= 0 && keyCode < 256) {
        window->input.keyInput[keyCode] = true;
        return 1;
      }
    }
    return 0;
  }
  else if (type == AINPUT_EVENT_TYPE_MOTION) {
    const int source = AInputEvent_getSource(event);
    // Events with source == AINPUT_SOURCE_TOUCHSCREEN come from the phone's builtin touch screen.
    // Events with source == AINPUT_SOURCE_MOUSE come from the trackpad on the right side of the GearVR.
    if (source == AINPUT_SOURCE_TOUCHSCREEN || source == AINPUT_SOURCE_MOUSE) {
      const int action = AKeyEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK;
      const float x = AMotionEvent_getRawX(event, 0);
      const float y = AMotionEvent_getRawY(event, 0);
      if (action == AMOTION_EVENT_ACTION_UP) {
        window->input.mouseInput[MOUSE_LEFT] = true;
        window->input.mouseInputX[MOUSE_LEFT] = (int)x;
        window->input.mouseInputY[MOUSE_LEFT] = (int)y;
        return 1;
      }
      return 0;
    }
  }
  return 0;
}

static float GetDisplayRefreshRate(const Java_t* java) {
  // Retrieve Context.WINDOW_SERVICE.

  jclass contextClass = (*java->env).FindClass("android/content/Context");
  jfieldID field_WINDOW_SERVICE = (*java->env).GetStaticFieldID(contextClass, "WINDOW_SERVICE", "Ljava/lang/String;");
  jobject WINDOW_SERVICE = (*java->env).GetStaticObjectField(contextClass, field_WINDOW_SERVICE);
  (*java->env).DeleteLocalRef(contextClass);

  // WindowManager windowManager = (WindowManager) activity.getSystemService( Context.WINDOW_SERVICE );
  const jclass activityClass = (*java->env).GetObjectClass(java->activity);
  const jmethodID getSystemServiceMethodId =
    (*java->env).GetMethodID(activityClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
  const jobject windowManager =
    (*java->env).CallObjectMethod(java->activity, getSystemServiceMethodId, WINDOW_SERVICE);
  (*java->env).DeleteLocalRef(activityClass);

  // Display display = windowManager.getDefaultDisplay();
  const jclass windowManagerClass = (*java->env).GetObjectClass(windowManager);
  const jmethodID getDefaultDisplayMethodId =
    (*java->env).GetMethodID(windowManagerClass, "getDefaultDisplay", "()Landroid/view/Display;");
  const jobject display = (*java->env).CallObjectMethod(windowManager, getDefaultDisplayMethodId);
  (*java->env).DeleteLocalRef(windowManagerClass);

  // float refreshRate = display.getRefreshRate();
  const jclass displayClass = (*java->env).GetObjectClass(display);
  const jmethodID getRefreshRateMethodId = (*java->env).GetMethodID(displayClass, "getRefreshRate", "()F");
  const float refreshRate = (*java->env).CallFloatMethod(display, getRefreshRateMethodId);
  (*java->env).DeleteLocalRef(displayClass);

  (*java->env).DeleteLocalRef(display);
  (*java->env).DeleteLocalRef(windowManager);
  (*java->env).DeleteLocalRef(WINDOW_SERVICE);

  return refreshRate;
}

struct android_app* global_app;


static ksNanoseconds GetTimeNanoseconds()
{
  static ksNanoseconds timeBase = 0;

  struct timespec ts;
  clock_gettime(CLOCK_MONOTONIC, &ts);

  if (timeBase == 0)
  {
    timeBase = (ksNanoseconds)ts.tv_sec * 1000ULL * 1000ULL * 1000ULL + ts.tv_nsec;
  }

  return (ksNanoseconds)ts.tv_sec * 1000ULL * 1000ULL * 1000ULL + ts.tv_nsec - timeBase;
}

struct AndroidAppState {
  //ANativeWindow* NativeWindow = nullptr;
  bool Resumed = false;
  //OpenXrProgram* program;
};

static void app_handle_cmd(struct android_app* app, int32_t cmd) {
  AndroidAppState* appState = (AndroidAppState*)app->userData;

  switch (cmd) {
    // There is no APP_CMD_CREATE. The ANativeActivity creates the
    // application thread from onCreate(). The application thread
    // then calls android_main().
  case APP_CMD_START: {
    //Log::Write(Log::Level::Info, __FILE__, __LINE__, "onStart()");
    break;
  }
  case APP_CMD_RESUME: {
    //Log::Write(Log::Level::Info, __FILE__, __LINE__, "onResume()");
    appState->Resumed = true;
    //if (appState->program) {
    //}
    break;
  }
  case APP_CMD_PAUSE: {
    //Log::Write(Log::Level::Info, __FILE__, __LINE__, "onPause()");
    appState->Resumed = false;
    //if (appState->program) {
    //}
    break;
  }
  case APP_CMD_STOP: {
    //Log::Write(Log::Level::Info, __FILE__, __LINE__, "onStop()");
    break;
  }
  case APP_CMD_DESTROY: {
    //Log::Write(Log::Level::Info, __FILE__, __LINE__, "onDestroy()");
    //appState->NativeWindow = NULL;
    break;
  }
  case APP_CMD_INIT_WINDOW: {
    //Log::Write(Log::Level::Info, __FILE__, __LINE__, "surfaceCreated()");
    //appState->NativeWindow = app->window;
    break;
  }
  case APP_CMD_TERM_WINDOW: {
    //Log::Write(Log::Level::Info, __FILE__, __LINE__, "surfaceDestroyed()");
    //appState->NativeWindow = NULL;
    break;
  }
  }
}

bool ksGpuWindow_Create(ksGpuWindow* window, ksDriverInstance* instance, const ksGpuQueueInfo* queueInfo, const int queueIndex,
  const ksGpuSurfaceColorFormat colorFormat, const ksGpuSurfaceDepthFormat depthFormat,
  const ksGpuSampleCount sampleCount, const int width, const int height, const bool fullscreen) {
  memset(window, 0, sizeof(ksGpuWindow));

  window->colorFormat = colorFormat;
  window->depthFormat = depthFormat;
  window->sampleCount = sampleCount;
  window->windowWidth = width;
  window->windowHeight = height;
  window->windowSwapInterval = 1;
  window->windowRefreshRate = 60.0f;
  window->windowFullscreen = true;
  window->windowActive = false;
  window->windowExit = false;
  window->lastSwapTime = GetTimeNanoseconds();

  window->app = global_app;
  window->nativeWindow = NULL;
  window->resumed = false;

  if (window->app != NULL) {
    window->app->userData = window;
    window->app->onAppCmd = app_handle_cmd2;
    window->app->onInputEvent = app_handle_input;
    window->java.vm = window->app->activity->vm;
    (*window->java.vm).AttachCurrentThread(&window->java.env, NULL);
    window->java.activity = window->app->activity->clazz;

    window->windowRefreshRate = GetDisplayRefreshRate(&window->java);

    // Keep the display on and bright.
    // Also make sure there is only one "HWC" next to the "FB TARGET" (adb shell dumpsys SurfaceFlinger).
    ANativeActivity_setWindowFlags(window->app->activity, AWINDOW_FLAG_FULLSCREEN | AWINDOW_FLAG_KEEP_SCREEN_ON, 0);
  }

  window->display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
  EGL(eglInitialize(window->display, &window->majorVersion, &window->minorVersion));

  ksGpuDevice_Create(&window->device, instance, queueInfo);
  ksGpuContext_CreateForSurface(&window->context, &window->device, queueIndex, colorFormat, depthFormat, sampleCount,
    window->display);
  ksGpuContext_SetCurrent(&window->context);

  return true;
}






























extern void init();
extern void tick();
extern void fini();

#define HAND_COUNT 2


















#define MATH_PI 3.14159265358979323846f

#define DEFAULT_NEAR_Z 0.015625f  // exact floating point representation
#define INFINITE_FAR_Z 0.0f

static const XrColor4f XrColorRed = { 1.0f, 0.0f, 0.0f, 1.0f };
static const XrColor4f XrColorGreen = { 0.0f, 1.0f, 0.0f, 1.0f };
static const XrColor4f XrColorBlue = { 0.0f, 0.0f, 1.0f, 1.0f };
static const XrColor4f XrColorYellow = { 1.0f, 1.0f, 0.0f, 1.0f };
static const XrColor4f XrColorPurple = { 1.0f, 0.0f, 1.0f, 1.0f };
static const XrColor4f XrColorCyan = { 0.0f, 1.0f, 1.0f, 1.0f };
static const XrColor4f XrColorLightGrey = { 0.7f, 0.7f, 0.7f, 1.0f };
static const XrColor4f XrColorDarkGrey = { 0.3f, 0.3f, 0.3f, 1.0f };

enum GraphicsAPI { GRAPHICS_VULKAN, GRAPHICS_OPENGL, GRAPHICS_OPENGL_ES, GRAPHICS_D3D };

// Column-major, pre-multiplied. This type does not exist in the OpenXR API and is provided for convenience.
struct XrMatrix4x4f {
  float m[16];
};

inline static float XrRcpSqrt(const float x) {
  const float SMALLEST_NON_DENORMAL = 1.1754943508222875e-038f;  // ( 1U << 23 )
  const float rcp = (x >= SMALLEST_NON_DENORMAL) ? 1.0f / sqrtf(x) : 1.0f;
  return rcp;
}

inline static void XrVector3f_Add(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) {
  result->x = a->x + b->x;
  result->y = a->y + b->y;
  result->z = a->z + b->z;
}

inline static void XrVector3f_Sub(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) {
  result->x = a->x - b->x;
  result->y = a->y - b->y;
  result->z = a->z - b->z;
}

inline static void XrVector3f_Min(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) {
  result->x = (a->x < b->x) ? a->x : b->x;
  result->y = (a->y < b->y) ? a->y : b->y;
  result->z = (a->z < b->z) ? a->z : b->z;
}

inline static void XrVector3f_Max(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) {
  result->x = (a->x > b->x) ? a->x : b->x;
  result->y = (a->y > b->y) ? a->y : b->y;
  result->z = (a->z > b->z) ? a->z : b->z;
}

inline static void XrVector3f_Decay(XrVector3f* result, const XrVector3f* a, const float value) {
  result->x = (fabsf(a->x) > value) ? ((a->x > 0.0f) ? (a->x - value) : (a->x + value)) : 0.0f;
  result->y = (fabsf(a->y) > value) ? ((a->y > 0.0f) ? (a->y - value) : (a->y + value)) : 0.0f;
  result->z = (fabsf(a->z) > value) ? ((a->z > 0.0f) ? (a->z - value) : (a->z + value)) : 0.0f;
}

inline static void XrVector3f_Lerp(XrVector3f* result, const XrVector3f* a, const XrVector3f* b, const float fraction) {
  result->x = a->x + fraction * (b->x - a->x);
  result->y = a->y + fraction * (b->y - a->y);
  result->z = a->z + fraction * (b->z - a->z);
}

inline static void XrVector3f_Scale(XrVector3f* result, const XrVector3f* a, const float scaleFactor) {
  result->x = a->x * scaleFactor;
  result->y = a->y * scaleFactor;
  result->z = a->z * scaleFactor;
}

inline static float XrVector3f_Dot(const XrVector3f* a, const XrVector3f* b) { return a->x * b->x + a->y * b->y + a->z * b->z; }

// Compute cross product, which generates a normal vector.
// Direction vector can be determined by right-hand rule: Pointing index finder in
// direction a and middle finger in direction b, thumb will point in Cross(a, b).
inline static void XrVector3f_Cross(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) {
  result->x = a->y * b->z - a->z * b->y;
  result->y = a->z * b->x - a->x * b->z;
  result->x = a->x * b->y - a->y * b->x;
}

inline static void XrVector3f_Normalize(XrVector3f* v) {
  const float lengthRcp = XrRcpSqrt(v->x * v->x + v->y * v->y + v->z * v->z);
  v->x *= lengthRcp;
  v->y *= lengthRcp;
  v->z *= lengthRcp;
}

inline static float XrVector3f_Length(const XrVector3f* v) { return sqrtf(v->x * v->x + v->y * v->y + v->z * v->z); }

inline static void XrQuaternionf_CreateFromAxisAngle(XrQuaternionf* result, const XrVector3f* axis, const float angleInRadians) {
  float s = sinf(angleInRadians / 2.0f);
  float lengthRcp = XrRcpSqrt(axis->x * axis->x + axis->y * axis->y + axis->z * axis->z);
  result->x = s * axis->x * lengthRcp;
  result->y = s * axis->y * lengthRcp;
  result->z = s * axis->z * lengthRcp;
  result->w = cosf(angleInRadians / 2.0f);
}

inline static void XrQuaternionf_Lerp(XrQuaternionf* result, const XrQuaternionf* a, const XrQuaternionf* b, const float fraction) {
  const float s = a->x * b->x + a->y * b->y + a->z * b->z + a->w * b->w;
  const float fa = 1.0f - fraction;
  const float fb = (s < 0.0f) ? -fraction : fraction;
  const float x = a->x * fa + b->x * fb;
  const float y = a->y * fa + b->y * fb;
  const float z = a->z * fa + b->z * fb;
  const float w = a->w * fa + b->w * fb;
  const float lengthRcp = XrRcpSqrt(x * x + y * y + z * z + w * w);
  result->x = x * lengthRcp;
  result->y = y * lengthRcp;
  result->z = z * lengthRcp;
  result->w = w * lengthRcp;
}

inline static void XrQuaternionf_Multiply(XrQuaternionf* result, const XrQuaternionf* a, const XrQuaternionf* b) {
  result->x = (b->w * a->x) + (b->x * a->w) + (b->y * a->z) - (b->z * a->y);
  result->y = (b->w * a->y) - (b->x * a->z) + (b->y * a->w) + (b->z * a->x);
  result->z = (b->w * a->z) + (b->x * a->y) - (b->y * a->x) + (b->z * a->w);
  result->w = (b->w * a->w) - (b->x * a->x) - (b->y * a->y) - (b->z * a->z);
}

// Use left-multiplication to accumulate transformations.
inline static void XrMatrix4x4f_Multiply(XrMatrix4x4f* result, const XrMatrix4x4f* a, const XrMatrix4x4f* b) {
  result->m[0] = a->m[0] * b->m[0] + a->m[4] * b->m[1] + a->m[8] * b->m[2] + a->m[12] * b->m[3];
  result->m[1] = a->m[1] * b->m[0] + a->m[5] * b->m[1] + a->m[9] * b->m[2] + a->m[13] * b->m[3];
  result->m[2] = a->m[2] * b->m[0] + a->m[6] * b->m[1] + a->m[10] * b->m[2] + a->m[14] * b->m[3];
  result->m[3] = a->m[3] * b->m[0] + a->m[7] * b->m[1] + a->m[11] * b->m[2] + a->m[15] * b->m[3];

  result->m[4] = a->m[0] * b->m[4] + a->m[4] * b->m[5] + a->m[8] * b->m[6] + a->m[12] * b->m[7];
  result->m[5] = a->m[1] * b->m[4] + a->m[5] * b->m[5] + a->m[9] * b->m[6] + a->m[13] * b->m[7];
  result->m[6] = a->m[2] * b->m[4] + a->m[6] * b->m[5] + a->m[10] * b->m[6] + a->m[14] * b->m[7];
  result->m[7] = a->m[3] * b->m[4] + a->m[7] * b->m[5] + a->m[11] * b->m[6] + a->m[15] * b->m[7];

  result->m[8] = a->m[0] * b->m[8] + a->m[4] * b->m[9] + a->m[8] * b->m[10] + a->m[12] * b->m[11];
  result->m[9] = a->m[1] * b->m[8] + a->m[5] * b->m[9] + a->m[9] * b->m[10] + a->m[13] * b->m[11];
  result->m[10] = a->m[2] * b->m[8] + a->m[6] * b->m[9] + a->m[10] * b->m[10] + a->m[14] * b->m[11];
  result->m[11] = a->m[3] * b->m[8] + a->m[7] * b->m[9] + a->m[11] * b->m[10] + a->m[15] * b->m[11];

  result->m[12] = a->m[0] * b->m[12] + a->m[4] * b->m[13] + a->m[8] * b->m[14] + a->m[12] * b->m[15];
  result->m[13] = a->m[1] * b->m[12] + a->m[5] * b->m[13] + a->m[9] * b->m[14] + a->m[13] * b->m[15];
  result->m[14] = a->m[2] * b->m[12] + a->m[6] * b->m[13] + a->m[10] * b->m[14] + a->m[14] * b->m[15];
  result->m[15] = a->m[3] * b->m[12] + a->m[7] * b->m[13] + a->m[11] * b->m[14] + a->m[15] * b->m[15];
}

// Creates the transpose of the given matrix.
inline static void XrMatrix4x4f_Transpose(XrMatrix4x4f* result, const XrMatrix4x4f* src) {
  result->m[0] = src->m[0];
  result->m[1] = src->m[4];
  result->m[2] = src->m[8];
  result->m[3] = src->m[12];

  result->m[4] = src->m[1];
  result->m[5] = src->m[5];
  result->m[6] = src->m[9];
  result->m[7] = src->m[13];

  result->m[8] = src->m[2];
  result->m[9] = src->m[6];
  result->m[10] = src->m[10];
  result->m[11] = src->m[14];

  result->m[12] = src->m[3];
  result->m[13] = src->m[7];
  result->m[14] = src->m[11];
  result->m[15] = src->m[15];
}

// Returns a 3x3 minor of a 4x4 matrix.
inline static float XrMatrix4x4f_Minor(const XrMatrix4x4f* matrix, int r0, int r1, int r2, int c0, int c1, int c2) {
  return matrix->m[4 * r0 + c0] *
    (matrix->m[4 * r1 + c1] * matrix->m[4 * r2 + c2] - matrix->m[4 * r2 + c1] * matrix->m[4 * r1 + c2]) -
    matrix->m[4 * r0 + c1] *
    (matrix->m[4 * r1 + c0] * matrix->m[4 * r2 + c2] - matrix->m[4 * r2 + c0] * matrix->m[4 * r1 + c2]) +
    matrix->m[4 * r0 + c2] *
    (matrix->m[4 * r1 + c0] * matrix->m[4 * r2 + c1] - matrix->m[4 * r2 + c0] * matrix->m[4 * r1 + c1]);
}

// Calculates the inverse of a 4x4 matrix.
inline static void XrMatrix4x4f_Invert(XrMatrix4x4f* result, const XrMatrix4x4f* src) {
  const float rcpDet =
    1.0f / (src->m[0] * XrMatrix4x4f_Minor(src, 1, 2, 3, 1, 2, 3) - src->m[1] * XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 2, 3) +
      src->m[2] * XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 1, 3) - src->m[3] * XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 1, 2));

  result->m[0] = XrMatrix4x4f_Minor(src, 1, 2, 3, 1, 2, 3) * rcpDet;
  result->m[1] = -XrMatrix4x4f_Minor(src, 0, 2, 3, 1, 2, 3) * rcpDet;
  result->m[2] = XrMatrix4x4f_Minor(src, 0, 1, 3, 1, 2, 3) * rcpDet;
  result->m[3] = -XrMatrix4x4f_Minor(src, 0, 1, 2, 1, 2, 3) * rcpDet;
  result->m[4] = -XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 2, 3) * rcpDet;
  result->m[5] = XrMatrix4x4f_Minor(src, 0, 2, 3, 0, 2, 3) * rcpDet;
  result->m[6] = -XrMatrix4x4f_Minor(src, 0, 1, 3, 0, 2, 3) * rcpDet;
  result->m[7] = XrMatrix4x4f_Minor(src, 0, 1, 2, 0, 2, 3) * rcpDet;
  result->m[8] = XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 1, 3) * rcpDet;
  result->m[9] = -XrMatrix4x4f_Minor(src, 0, 2, 3, 0, 1, 3) * rcpDet;
  result->m[10] = XrMatrix4x4f_Minor(src, 0, 1, 3, 0, 1, 3) * rcpDet;
  result->m[11] = -XrMatrix4x4f_Minor(src, 0, 1, 2, 0, 1, 3) * rcpDet;
  result->m[12] = -XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 1, 2) * rcpDet;
  result->m[13] = XrMatrix4x4f_Minor(src, 0, 2, 3, 0, 1, 2) * rcpDet;
  result->m[14] = -XrMatrix4x4f_Minor(src, 0, 1, 3, 0, 1, 2) * rcpDet;
  result->m[15] = XrMatrix4x4f_Minor(src, 0, 1, 2, 0, 1, 2) * rcpDet;
}

// Calculates the inverse of a rigid body transform.
inline static void XrMatrix4x4f_InvertRigidBody(XrMatrix4x4f* result, const XrMatrix4x4f* src) {
  result->m[0] = src->m[0];
  result->m[1] = src->m[4];
  result->m[2] = src->m[8];
  result->m[3] = 0.0f;
  result->m[4] = src->m[1];
  result->m[5] = src->m[5];
  result->m[6] = src->m[9];
  result->m[7] = 0.0f;
  result->m[8] = src->m[2];
  result->m[9] = src->m[6];
  result->m[10] = src->m[10];
  result->m[11] = 0.0f;
  result->m[12] = -(src->m[0] * src->m[12] + src->m[1] * src->m[13] + src->m[2] * src->m[14]);
  result->m[13] = -(src->m[4] * src->m[12] + src->m[5] * src->m[13] + src->m[6] * src->m[14]);
  result->m[14] = -(src->m[8] * src->m[12] + src->m[9] * src->m[13] + src->m[10] * src->m[14]);
  result->m[15] = 1.0f;
}

// Creates an identity matrix.
inline static void XrMatrix4x4f_CreateIdentity(XrMatrix4x4f* result) {
  result->m[0] = 1.0f;
  result->m[1] = 0.0f;
  result->m[2] = 0.0f;
  result->m[3] = 0.0f;
  result->m[4] = 0.0f;
  result->m[5] = 1.0f;
  result->m[6] = 0.0f;
  result->m[7] = 0.0f;
  result->m[8] = 0.0f;
  result->m[9] = 0.0f;
  result->m[10] = 1.0f;
  result->m[11] = 0.0f;
  result->m[12] = 0.0f;
  result->m[13] = 0.0f;
  result->m[14] = 0.0f;
  result->m[15] = 1.0f;
}

// Creates a translation matrix.
inline static void XrMatrix4x4f_CreateTranslation(XrMatrix4x4f* result, const float x, const float y, const float z) {
  result->m[0] = 1.0f;
  result->m[1] = 0.0f;
  result->m[2] = 0.0f;
  result->m[3] = 0.0f;
  result->m[4] = 0.0f;
  result->m[5] = 1.0f;
  result->m[6] = 0.0f;
  result->m[7] = 0.0f;
  result->m[8] = 0.0f;
  result->m[9] = 0.0f;
  result->m[10] = 1.0f;
  result->m[11] = 0.0f;
  result->m[12] = x;
  result->m[13] = y;
  result->m[14] = z;
  result->m[15] = 1.0f;
}

// Creates a rotation matrix.
// If -Z=forward, +Y=up, +X=right, then degreesX=pitch, degreesY=yaw, degreesZ=roll.
inline static void XrMatrix4x4f_CreateRotation(XrMatrix4x4f* result, const float degreesX, const float degreesY,
  const float degreesZ) {
  const float sinX = sinf(degreesX * (MATH_PI / 180.0f));
  const float cosX = cosf(degreesX * (MATH_PI / 180.0f));
  const XrMatrix4x4f rotationX = { {1, 0, 0, 0, 0, cosX, sinX, 0, 0, -sinX, cosX, 0, 0, 0, 0, 1} };
  const float sinY = sinf(degreesY * (MATH_PI / 180.0f));
  const float cosY = cosf(degreesY * (MATH_PI / 180.0f));
  const XrMatrix4x4f rotationY = { {cosY, 0, -sinY, 0, 0, 1, 0, 0, sinY, 0, cosY, 0, 0, 0, 0, 1} };
  const float sinZ = sinf(degreesZ * (MATH_PI / 180.0f));
  const float cosZ = cosf(degreesZ * (MATH_PI / 180.0f));
  const XrMatrix4x4f rotationZ = { {cosZ, sinZ, 0, 0, -sinZ, cosZ, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1} };
  XrMatrix4x4f rotationXY;
  XrMatrix4x4f_Multiply(&rotationXY, &rotationY, &rotationX);
  XrMatrix4x4f_Multiply(result, &rotationZ, &rotationXY);
}

// Creates a scale matrix.
inline static void XrMatrix4x4f_CreateScale(XrMatrix4x4f* result, const float x, const float y, const float z) {
  result->m[0] = x;
  result->m[1] = 0.0f;
  result->m[2] = 0.0f;
  result->m[3] = 0.0f;
  result->m[4] = 0.0f;
  result->m[5] = y;
  result->m[6] = 0.0f;
  result->m[7] = 0.0f;
  result->m[8] = 0.0f;
  result->m[9] = 0.0f;
  result->m[10] = z;
  result->m[11] = 0.0f;
  result->m[12] = 0.0f;
  result->m[13] = 0.0f;
  result->m[14] = 0.0f;
  result->m[15] = 1.0f;
}

// Creates a matrix from a quaternion.
inline static void XrMatrix4x4f_CreateFromQuaternion(XrMatrix4x4f* result, const XrQuaternionf* quat) {
  const float x2 = quat->x + quat->x;
  const float y2 = quat->y + quat->y;
  const float z2 = quat->z + quat->z;

  const float xx2 = quat->x * x2;
  const float yy2 = quat->y * y2;
  const float zz2 = quat->z * z2;

  const float yz2 = quat->y * z2;
  const float wx2 = quat->w * x2;
  const float xy2 = quat->x * y2;
  const float wz2 = quat->w * z2;
  const float xz2 = quat->x * z2;
  const float wy2 = quat->w * y2;

  result->m[0] = 1.0f - yy2 - zz2;
  result->m[1] = xy2 + wz2;
  result->m[2] = xz2 - wy2;
  result->m[3] = 0.0f;

  result->m[4] = xy2 - wz2;
  result->m[5] = 1.0f - xx2 - zz2;
  result->m[6] = yz2 + wx2;
  result->m[7] = 0.0f;

  result->m[8] = xz2 + wy2;
  result->m[9] = yz2 - wx2;
  result->m[10] = 1.0f - xx2 - yy2;
  result->m[11] = 0.0f;

  result->m[12] = 0.0f;
  result->m[13] = 0.0f;
  result->m[14] = 0.0f;
  result->m[15] = 1.0f;
}

// Creates a combined translation(rotation(scale(object))) matrix.
inline static void XrMatrix4x4f_CreateTranslationRotationScale(XrMatrix4x4f* result, const XrVector3f* translation,
  const XrQuaternionf* rotation, const XrVector3f* scale) {
  XrMatrix4x4f scaleMatrix;
  XrMatrix4x4f_CreateScale(&scaleMatrix, scale->x, scale->y, scale->z);

  XrMatrix4x4f rotationMatrix;
  XrMatrix4x4f_CreateFromQuaternion(&rotationMatrix, rotation);

  XrMatrix4x4f translationMatrix;
  XrMatrix4x4f_CreateTranslation(&translationMatrix, translation->x, translation->y, translation->z);

  XrMatrix4x4f combinedMatrix;
  XrMatrix4x4f_Multiply(&combinedMatrix, &rotationMatrix, &scaleMatrix);
  XrMatrix4x4f_Multiply(result, &translationMatrix, &combinedMatrix);
}

// Creates a projection matrix based on the specified dimensions.
// The projection matrix transforms -Z=forward, +Y=up, +X=right to the appropriate clip space for the graphics API.
// The far plane is placed at infinity if farZ <= nearZ.
// An infinite projection matrix is preferred for rasterization because, except for
// things *right* up against the near plane, it always provides better precision:
//              "Tightening the Precision of Perspective Rendering"
//              Paul Upchurch, Mathieu Desbrun
//              Journal of Graphics Tools, Volume 16, Issue 1, 2012
inline static void XrMatrix4x4f_CreateProjection(XrMatrix4x4f* result, GraphicsAPI graphicsApi, const float tanAngleLeft,
  const float tanAngleRight, const float tanAngleUp, float const tanAngleDown,
  const float nearZ, const float farZ) {
  const float tanAngleWidth = tanAngleRight - tanAngleLeft;

  // Set to tanAngleDown - tanAngleUp for a clip space with positive Y down (Vulkan).
  // Set to tanAngleUp - tanAngleDown for a clip space with positive Y up (OpenGL / D3D / Metal).
  const float tanAngleHeight = graphicsApi == GRAPHICS_VULKAN ? (tanAngleDown - tanAngleUp) : (tanAngleUp - tanAngleDown);

  // Set to nearZ for a [-1,1] Z clip space (OpenGL / OpenGL ES).
  // Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal).
  const float offsetZ = (graphicsApi == GRAPHICS_OPENGL || graphicsApi == GRAPHICS_OPENGL_ES) ? nearZ : 0;

  if (farZ <= nearZ) {
    // place the far plane at infinity
    result->m[0] = 2 / tanAngleWidth;
    result->m[4] = 0;
    result->m[8] = (tanAngleRight + tanAngleLeft) / tanAngleWidth;
    result->m[12] = 0;

    result->m[1] = 0;
    result->m[5] = 2 / tanAngleHeight;
    result->m[9] = (tanAngleUp + tanAngleDown) / tanAngleHeight;
    result->m[13] = 0;

    result->m[2] = 0;
    result->m[6] = 0;
    result->m[10] = -1;
    result->m[14] = -(nearZ + offsetZ);

    result->m[3] = 0;
    result->m[7] = 0;
    result->m[11] = -1;
    result->m[15] = 0;
  }
  else {
    // normal projection
    result->m[0] = 2 / tanAngleWidth;
    result->m[4] = 0;
    result->m[8] = (tanAngleRight + tanAngleLeft) / tanAngleWidth;
    result->m[12] = 0;

    result->m[1] = 0;
    result->m[5] = 2 / tanAngleHeight;
    result->m[9] = (tanAngleUp + tanAngleDown) / tanAngleHeight;
    result->m[13] = 0;

    result->m[2] = 0;
    result->m[6] = 0;
    result->m[10] = -(farZ + offsetZ) / (farZ - nearZ);
    result->m[14] = -(farZ * (nearZ + offsetZ)) / (farZ - nearZ);

    result->m[3] = 0;
    result->m[7] = 0;
    result->m[11] = -1;
    result->m[15] = 0;
  }
}

// Creates a projection matrix based on the specified FOV.
inline static void XrMatrix4x4f_CreateProjectionFov(XrMatrix4x4f* result, GraphicsAPI graphicsApi, const XrFovf fov,
  const float nearZ, const float farZ) {
  const float tanLeft = tanf(fov.angleLeft);
  const float tanRight = tanf(fov.angleRight);

  const float tanDown = tanf(fov.angleDown);
  const float tanUp = tanf(fov.angleUp);

  XrMatrix4x4f_CreateProjection(result, graphicsApi, tanLeft, tanRight, tanUp, tanDown, nearZ, farZ);
}

// Creates a matrix that transforms the -1 to 1 cube to cover the given 'mins' and 'maxs' transformed with the given 'matrix'.
inline static void XrMatrix4x4f_CreateOffsetScaleForBounds(XrMatrix4x4f* result, const XrMatrix4x4f* matrix, const XrVector3f* mins,
  const XrVector3f* maxs) {
  const XrVector3f offset = { (maxs->x + mins->x) * 0.5f, (maxs->y + mins->y) * 0.5f, (maxs->z + mins->z) * 0.5f };
  const XrVector3f scale = { (maxs->x - mins->x) * 0.5f, (maxs->y - mins->y) * 0.5f, (maxs->z - mins->z) * 0.5f };

  result->m[0] = matrix->m[0] * scale.x;
  result->m[1] = matrix->m[1] * scale.x;
  result->m[2] = matrix->m[2] * scale.x;
  result->m[3] = matrix->m[3] * scale.x;

  result->m[4] = matrix->m[4] * scale.y;
  result->m[5] = matrix->m[5] * scale.y;
  result->m[6] = matrix->m[6] * scale.y;
  result->m[7] = matrix->m[7] * scale.y;

  result->m[8] = matrix->m[8] * scale.z;
  result->m[9] = matrix->m[9] * scale.z;
  result->m[10] = matrix->m[10] * scale.z;
  result->m[11] = matrix->m[11] * scale.z;

  result->m[12] = matrix->m[12] + matrix->m[0] * offset.x + matrix->m[4] * offset.y + matrix->m[8] * offset.z;
  result->m[13] = matrix->m[13] + matrix->m[1] * offset.x + matrix->m[5] * offset.y + matrix->m[9] * offset.z;
  result->m[14] = matrix->m[14] + matrix->m[2] * offset.x + matrix->m[6] * offset.y + matrix->m[10] * offset.z;
  result->m[15] = matrix->m[15] + matrix->m[3] * offset.x + matrix->m[7] * offset.y + matrix->m[11] * offset.z;
}

// Returns true if the given matrix is affine.
inline static bool XrMatrix4x4f_IsAffine(const XrMatrix4x4f* matrix, const float epsilon) {
  return fabsf(matrix->m[3]) <= epsilon && fabsf(matrix->m[7]) <= epsilon && fabsf(matrix->m[11]) <= epsilon &&
    fabsf(matrix->m[15] - 1.0f) <= epsilon;
}

// Returns true if the given matrix is orthogonal.
inline static bool XrMatrix4x4f_IsOrthogonal(const XrMatrix4x4f* matrix, const float epsilon) {
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
      if (i != j) {
        if (fabsf(matrix->m[4 * i + 0] * matrix->m[4 * j + 0] + matrix->m[4 * i + 1] * matrix->m[4 * j + 1] +
          matrix->m[4 * i + 2] * matrix->m[4 * j + 2]) > epsilon) {
          return false;
        }
        if (fabsf(matrix->m[4 * 0 + i] * matrix->m[4 * 0 + j] + matrix->m[4 * 1 + i] * matrix->m[4 * 1 + j] +
          matrix->m[4 * 2 + i] * matrix->m[4 * 2 + j]) > epsilon) {
          return false;
        }
      }
    }
  }
  return true;
}

// Returns true if the given matrix is orthonormal.
inline static bool XrMatrix4x4f_IsOrthonormal(const XrMatrix4x4f* matrix, const float epsilon) {
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
      const float kd = (i == j) ? 1.0f : 0.0f;  // Kronecker delta
      if (fabsf(kd - (matrix->m[4 * i + 0] * matrix->m[4 * j + 0] + matrix->m[4 * i + 1] * matrix->m[4 * j + 1] +
        matrix->m[4 * i + 2] * matrix->m[4 * j + 2])) > epsilon) {
        return false;
      }
      if (fabsf(kd - (matrix->m[4 * 0 + i] * matrix->m[4 * 0 + j] + matrix->m[4 * 1 + i] * matrix->m[4 * 1 + j] +
        matrix->m[4 * 2 + i] * matrix->m[4 * 2 + j])) > epsilon) {
        return false;
      }
    }
  }
  return true;
}

// Returns true if the given matrix is a rigid body transform.
inline static bool XrMatrix4x4f_IsRigidBody(const XrMatrix4x4f* matrix, const float epsilon) {
  return XrMatrix4x4f_IsAffine(matrix, epsilon) && XrMatrix4x4f_IsOrthonormal(matrix, epsilon);
}

// Get the translation from a combined translation(rotation(scale(object))) matrix.
inline static void XrMatrix4x4f_GetTranslation(XrVector3f* result, const XrMatrix4x4f* src) {
  assert(XrMatrix4x4f_IsAffine(src, 1e-4f));
  assert(XrMatrix4x4f_IsOrthogonal(src, 1e-4f));

  result->x = src->m[12];
  result->y = src->m[13];
  result->z = src->m[14];
}

// Get the rotation from a combined translation(rotation(scale(object))) matrix.
inline static void XrMatrix4x4f_GetRotation(XrQuaternionf* result, const XrMatrix4x4f* src) {
  assert(XrMatrix4x4f_IsAffine(src, 1e-4f));
  assert(XrMatrix4x4f_IsOrthogonal(src, 1e-4f));

  const float rcpScaleX = XrRcpSqrt(src->m[0] * src->m[0] + src->m[1] * src->m[1] + src->m[2] * src->m[2]);
  const float rcpScaleY = XrRcpSqrt(src->m[4] * src->m[4] + src->m[5] * src->m[5] + src->m[6] * src->m[6]);
  const float rcpScaleZ = XrRcpSqrt(src->m[8] * src->m[8] + src->m[9] * src->m[9] + src->m[10] * src->m[10]);
  const float m[9] = { src->m[0] * rcpScaleX, src->m[1] * rcpScaleX, src->m[2] * rcpScaleX,
                      src->m[4] * rcpScaleY, src->m[5] * rcpScaleY, src->m[6] * rcpScaleY,
                      src->m[8] * rcpScaleZ, src->m[9] * rcpScaleZ, src->m[10] * rcpScaleZ };
  if (m[0 * 3 + 0] + m[1 * 3 + 1] + m[2 * 3 + 2] > 0.0f) {
    float t = +m[0 * 3 + 0] + m[1 * 3 + 1] + m[2 * 3 + 2] + 1.0f;
    float s = XrRcpSqrt(t) * 0.5f;
    result->w = s * t;
    result->z = (m[0 * 3 + 1] - m[1 * 3 + 0]) * s;
    result->y = (m[2 * 3 + 0] - m[0 * 3 + 2]) * s;
    result->x = (m[1 * 3 + 2] - m[2 * 3 + 1]) * s;
  }
  else if (m[0 * 3 + 0] > m[1 * 3 + 1] && m[0 * 3 + 0] > m[2 * 3 + 2]) {
    float t = +m[0 * 3 + 0] - m[1 * 3 + 1] - m[2 * 3 + 2] + 1.0f;
    float s = XrRcpSqrt(t) * 0.5f;
    result->x = s * t;
    result->y = (m[0 * 3 + 1] + m[1 * 3 + 0]) * s;
    result->z = (m[2 * 3 + 0] + m[0 * 3 + 2]) * s;
    result->w = (m[1 * 3 + 2] - m[2 * 3 + 1]) * s;
  }
  else if (m[1 * 3 + 1] > m[2 * 3 + 2]) {
    float t = -m[0 * 3 + 0] + m[1 * 3 + 1] - m[2 * 3 + 2] + 1.0f;
    float s = XrRcpSqrt(t) * 0.5f;
    result->y = s * t;
    result->x = (m[0 * 3 + 1] + m[1 * 3 + 0]) * s;
    result->w = (m[2 * 3 + 0] - m[0 * 3 + 2]) * s;
    result->z = (m[1 * 3 + 2] + m[2 * 3 + 1]) * s;
  }
  else {
    float t = -m[0 * 3 + 0] - m[1 * 3 + 1] + m[2 * 3 + 2] + 1.0f;
    float s = XrRcpSqrt(t) * 0.5f;
    result->z = s * t;
    result->w = (m[0 * 3 + 1] - m[1 * 3 + 0]) * s;
    result->x = (m[2 * 3 + 0] + m[0 * 3 + 2]) * s;
    result->y = (m[1 * 3 + 2] + m[2 * 3 + 1]) * s;
  }
}

// Get the scale from a combined translation(rotation(scale(object))) matrix.
inline static void XrMatrix4x4f_GetScale(XrVector3f* result, const XrMatrix4x4f* src) {
  assert(XrMatrix4x4f_IsAffine(src, 1e-4f));
  assert(XrMatrix4x4f_IsOrthogonal(src, 1e-4f));

  result->x = sqrtf(src->m[0] * src->m[0] + src->m[1] * src->m[1] + src->m[2] * src->m[2]);
  result->y = sqrtf(src->m[4] * src->m[4] + src->m[5] * src->m[5] + src->m[6] * src->m[6]);
  result->z = sqrtf(src->m[8] * src->m[8] + src->m[9] * src->m[9] + src->m[10] * src->m[10]);
}

// Transforms a 3D vector.
inline static void XrMatrix4x4f_TransformVector3f(XrVector3f* result, const XrMatrix4x4f* m, const XrVector3f* v) {
  const float w = m->m[3] * v->x + m->m[7] * v->y + m->m[11] * v->z + m->m[15];
  const float rcpW = 1.0f / w;
  result->x = (m->m[0] * v->x + m->m[4] * v->y + m->m[8] * v->z + m->m[12]) * rcpW;
  result->y = (m->m[1] * v->x + m->m[5] * v->y + m->m[9] * v->z + m->m[13]) * rcpW;
  result->z = (m->m[2] * v->x + m->m[6] * v->y + m->m[10] * v->z + m->m[14]) * rcpW;
}

// Transforms a 4D vector.
inline static void XrMatrix4x4f_TransformVector4f(XrVector4f* result, const XrMatrix4x4f* m, const XrVector4f* v) {
  result->x = m->m[0] * v->x + m->m[4] * v->y + m->m[8] * v->z + m->m[12] * v->w;
  result->y = m->m[1] * v->x + m->m[5] * v->y + m->m[9] * v->z + m->m[13] * v->w;
  result->z = m->m[2] * v->x + m->m[6] * v->y + m->m[10] * v->z + m->m[14] * v->w;
  result->w = m->m[3] * v->x + m->m[7] * v->y + m->m[11] * v->z + m->m[15] * v->w;
}

// Transforms the 'mins' and 'maxs' bounds with the given 'matrix'.
inline static void XrMatrix4x4f_TransformBounds(XrVector3f* resultMins, XrVector3f* resultMaxs, const XrMatrix4x4f* matrix,
  const XrVector3f* mins, const XrVector3f* maxs) {
  assert(XrMatrix4x4f_IsAffine(matrix, 1e-4f));

  const XrVector3f center = { (mins->x + maxs->x) * 0.5f, (mins->y + maxs->y) * 0.5f, (mins->z + maxs->z) * 0.5f };
  const XrVector3f extents = { maxs->x - center.x, maxs->y - center.y, maxs->z - center.z };
  const XrVector3f newCenter = { matrix->m[0] * center.x + matrix->m[4] * center.y + matrix->m[8] * center.z + matrix->m[12],
                                matrix->m[1] * center.x + matrix->m[5] * center.y + matrix->m[9] * center.z + matrix->m[13],
                                matrix->m[2] * center.x + matrix->m[6] * center.y + matrix->m[10] * center.z + matrix->m[14] };
  const XrVector3f newExtents = {
          fabsf(extents.x * matrix->m[0]) + fabsf(extents.y * matrix->m[4]) + fabsf(extents.z * matrix->m[8]),
          fabsf(extents.x * matrix->m[1]) + fabsf(extents.y * matrix->m[5]) + fabsf(extents.z * matrix->m[9]),
          fabsf(extents.x * matrix->m[2]) + fabsf(extents.y * matrix->m[6]) + fabsf(extents.z * matrix->m[10]) };
  XrVector3f_Sub(resultMins, &newCenter, &newExtents);
  XrVector3f_Add(resultMaxs, &newCenter, &newExtents);
}

// Returns true if the 'mins' and 'maxs' bounds is completely off to one side of the projection matrix.
inline static bool XrMatrix4x4f_CullBounds(const XrMatrix4x4f* mvp, const XrVector3f* mins, const XrVector3f* maxs) {
  if (maxs->x <= mins->x && maxs->y <= mins->y && maxs->z <= mins->z) {
    return false;
  }

  XrVector4f c[8];
  for (int i = 0; i < 8; i++) {
    const XrVector4f corner = { (i & 1) != 0 ? maxs->x : mins->x, (i & 2) != 0 ? maxs->y : mins->y,
                               (i & 4) != 0 ? maxs->z : mins->z, 1.0f };
    XrMatrix4x4f_TransformVector4f(&c[i], mvp, &corner);
  }

  int i;
  for (i = 0; i < 8; i++) {
    if (c[i].x > -c[i].w) {
      break;
    }
  }
  if (i == 8) {
    return true;
  }
  for (i = 0; i < 8; i++) {
    if (c[i].x < c[i].w) {
      break;
    }
  }
  if (i == 8) {
    return true;
  }

  for (i = 0; i < 8; i++) {
    if (c[i].y > -c[i].w) {
      break;
    }
  }
  if (i == 8) {
    return true;
  }
  for (i = 0; i < 8; i++) {
    if (c[i].y < c[i].w) {
      break;
    }
  }
  if (i == 8) {
    return true;
  }
  for (i = 0; i < 8; i++) {
    if (c[i].z > -c[i].w) {
      break;
    }
  }
  if (i == 8) {
    return true;
  }
  for (i = 0; i < 8; i++) {
    if (c[i].z < c[i].w) {
      break;
    }
  }
  return i == 8;
}







struct Swapchain {
  XrSwapchain handle;
  int32_t width;
  int32_t height;
};


#define CHK_STRINGIFY(x) #x
#define TOSTRING(x) CHK_STRINGIFY(x)
#define FILE_AND_LINE __FILE__ ":" TOSTRING(__LINE__)

[[noreturn]] inline void Throw(std::string failureMessage, const char* originator = nullptr, const char* sourceLocation = nullptr) {
  ASSERT(false);
}

#define THROW(msg) Throw(msg, nullptr, FILE_AND_LINE);
#define CHECK(exp)                                      \
    {                                                   \
        if (!(exp)) {                                   \
            Throw("Check failed", #exp, FILE_AND_LINE); \
        }                                               \
    }
#define CHECK_MSG(exp, msg)                  \
    {                                        \
        if (!(exp)) {                        \
            Throw(msg, #exp, FILE_AND_LINE); \
        }                                    \
    }

[[noreturn]] inline void ThrowXrResult(XrResult res, const char* originator = nullptr, const char* sourceLocation = nullptr) {
  ASSERT(false);
}

inline XrResult CheckXrResult(XrResult res, const char* originator = nullptr, const char* sourceLocation = nullptr) {
  if (XR_FAILED(res)) {
    ThrowXrResult(res, originator, sourceLocation);
  }

  return res;
}






#define THROW_XR(xr, cmd) ThrowXrResult(xr, #cmd, FILE_AND_LINE);
#define OXR(cmd) CheckXrResult(cmd, #cmd, FILE_AND_LINE);
#define CHECK_XRRESULT(res, cmdStr) CheckXrResult(res, cmdStr, FILE_AND_LINE);

#define PFN_DECLARE(pfn) PFN_##pfn pfn = nullptr
#define PFN_INITIALIZE(pfn) OXR(xrGetInstanceProcAddr(m_instance, #pfn, (PFN_xrVoidFunction*)(&pfn)))


class IApplication;

typedef struct Extentions_tag {
  //XR_FB_display_refresh_rate
  PFN_DECLARE(xrEnumerateDisplayRefreshRatesFB);
  PFN_DECLARE(xrGetDisplayRefreshRateFB);
  PFN_DECLARE(xrRequestDisplayRefreshRateFB);

  bool activePassthrough;     //XR_FB_passthrough
  bool isSupportEyeTracking;  //eye tracking
  bool activeEyeTracking;

  Extentions_tag() : activePassthrough(true), isSupportEyeTracking(false), activeEyeTracking(false) {}

  void initialize(XrInstance m_instance) {
    //XR_FB_display_refresh_rate
    PFN_INITIALIZE(xrEnumerateDisplayRefreshRatesFB);
    PFN_INITIALIZE(xrGetDisplayRefreshRateFB);
    PFN_INITIALIZE(xrRequestDisplayRefreshRateFB);
  }
}Extentions;

typedef void (*hapticCallback)(void* arg, int controllerIndex, float amplitude, float seconds, float frequency);

class IApplication {
public:
  virtual ~IApplication() = default;
  virtual bool initialize(const XrInstance instance, const XrSession session, Extentions* extention) = 0;
  virtual void setHapticCallback(void* arg, hapticCallback hapticCb) = 0;
  virtual void setGazeLocation(XrSpaceLocation& gazeLocation, std::vector<XrView>& views, float ipd, XrResult result = XR_SUCCESS) = 0;
  virtual void setHandJointLocation(XrHandJointLocationEXT* location) = 0;
};

// Wraps platform-specific implementation so the main openxr program can be platform-independent.
struct IPlatformPlugin {
  virtual ~IPlatformPlugin() = default;

  // Provide extension to XrInstanceCreateInfo for xrCreateInstance.
  virtual XrBaseInStructure* GetInstanceCreateExtension() const = 0;

  // OpenXR instance-level extensions required by this platform.
  virtual std::vector<std::string> GetInstanceExtensions() const = 0;
};

typedef enum {
  DeviceTypeNone = 0,
  DeviceTypeNeo3,
  DeviceTypeNeo3Pro,
  DeviceTypeNeo3ProEye,
  DeviceTypePico4,
  DeviceTypePico4Pro,
}DeviceType;

#if !defined(XR_USE_PLATFORM_WIN32)
#define strcpy_s(dest, source) strncpy((dest), (source), sizeof(dest))
#endif

namespace Side {
  const int LEFT = 0;
  const int RIGHT = 1;
  const int COUNT = 2;
}  // namespace Side

// Map color buffer to associated depth buffer. This map is populated on demand.
static std::map<uint32_t, uint32_t> m_colorToDepthMap;
static XrHandJointLocationEXT m_jointLocations[2][XR_HAND_JOINT_COUNT_EXT];
static GLuint m_swapchainFramebuffer{ 0 };


namespace Math {
  namespace Pose {
    XrPosef Identity() {
      XrPosef t{};
      t.orientation.w = 1;
      return t;
    }

    XrPosef Translation(const XrVector3f& translation) {
      XrPosef t = Identity();
      t.position = translation;
      return t;
    }

    XrPosef RotateCCWAboutYAxis(float radians, XrVector3f translation) {
      XrPosef t = Identity();
      t.orientation.x = 0.f;
      t.orientation.y = std::sin(radians * 0.5f);
      t.orientation.z = 0.f;
      t.orientation.w = std::cos(radians * 0.5f);
      t.position = translation;
      return t;
    }
  }  // namespace Pose
}  // namespace Math

static void openGLDebugMessageCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) {
  (void)source;
  (void)type;
  (void)id;
  (void)severity;
  //ASSERT(false);
//Log::Write(Log::Level::Info, "GLES Debug: " + std::string(message, 0, length));
}

struct OpenGLESGraphicsPlugin {
  OpenGLESGraphicsPlugin() {};
  OpenGLESGraphicsPlugin(const OpenGLESGraphicsPlugin&) = delete;
  OpenGLESGraphicsPlugin& operator=(const OpenGLESGraphicsPlugin&) = delete;
  OpenGLESGraphicsPlugin(OpenGLESGraphicsPlugin&&) = delete;
  OpenGLESGraphicsPlugin& operator=(OpenGLESGraphicsPlugin&&) = delete;

  std::vector<std::string> GetInstanceExtensions() const { return { XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME }; }

  ksGpuWindow window{};

  void InitializeDevice(XrInstance instance, XrSystemId systemId) {
    // Extension function must be loaded by name
    PFN_xrGetOpenGLESGraphicsRequirementsKHR pfnGetOpenGLESGraphicsRequirementsKHR = nullptr;
    OXR(xrGetInstanceProcAddr(instance, "xrGetOpenGLESGraphicsRequirementsKHR",
      reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetOpenGLESGraphicsRequirementsKHR)));

    XrGraphicsRequirementsOpenGLESKHR graphicsRequirements{ XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR };
    OXR(pfnGetOpenGLESGraphicsRequirementsKHR(instance, systemId, &graphicsRequirements));

    // Initialize the gl extensions. Note we have to open a window.
    ksDriverInstance driverInstance{};
    ksGpuQueueInfo queueInfo{};
    ksGpuSurfaceColorFormat colorFormat{ KS_GPU_SURFACE_COLOR_FORMAT_B8G8R8A8 };
    ksGpuSurfaceDepthFormat depthFormat{ KS_GPU_SURFACE_DEPTH_FORMAT_D24 };
    ksGpuSampleCount sampleCount{ KS_GPU_SAMPLE_COUNT_1 }; // does not matter for setting msaa later
    if (!ksGpuWindow_Create(&window, &driverInstance, &queueInfo, 0, colorFormat, depthFormat, sampleCount, 640, 480, false)) {
      THROW("Unable to create GL context");
    }

    GLint major = 0;
    GLint minor = 0;
    glGetIntegerv(GL_MAJOR_VERSION, &major);
    glGetIntegerv(GL_MINOR_VERSION, &minor);

    const XrVersion desiredApiVersion = XR_MAKE_VERSION(major, minor, 0);
    if (graphicsRequirements.minApiVersionSupported > desiredApiVersion) {
      THROW("Runtime does not support desired Graphics API and/or version");
    }

#if defined(XR_USE_PLATFORM_ANDROID)
    m_graphicsBinding.display = window.display;
    m_graphicsBinding.config = (EGLConfig)0;
    m_graphicsBinding.context = window.context.context;
#endif

#ifdef SHR3D_GRAPHICS_MSAA
    if (Settings::graphicsMSAA >= 1)
    {
      glEnable(GL_MULTISAMPLE_EXT);
      ASSERT(glIsEnabled(GL_MULTISAMPLE_EXT));
    }
#endif // SHR3D_GRAPHICS_MSAA

    glEnable(GL_DEBUG_OUTPUT);
    glDebugMessageCallback(openGLDebugMessageCallback, nullptr);

    glGenFramebuffers(1, &m_swapchainFramebuffer);
  }

  int64_t SelectColorSwapchainFormat(const std::vector<int64_t>& runtimeFormats) const {
    // List of supported color swapchain formats.
    constexpr int64_t SupportedColorSwapchainFormats[] = {
            GL_RGBA8,
            GL_RGBA8_SNORM,
    };

    auto swapchainFormatIt = std::find_first_of(runtimeFormats.begin(), runtimeFormats.end(), std::begin(SupportedColorSwapchainFormats),
      std::end(SupportedColorSwapchainFormats));
    if (swapchainFormatIt == runtimeFormats.end()) {
      THROW("No runtime swapchain format supported for color swapchain");
    }

    return *swapchainFormatIt;
  }

  const XrBaseInStructure* GetGraphicsBinding() const {
    return reinterpret_cast<const XrBaseInStructure*>(&m_graphicsBinding);
  }

  std::vector<XrSwapchainImageBaseHeader*> AllocateSwapchainImageStructs(uint32_t capacity, const XrSwapchainCreateInfo& /*swapchainCreateInfo*/) {
    // Allocate and initialize the buffer of image structs (must be sequential in memory for xrEnumerateSwapchainImages).
    // Return back an array of pointers to each swapchain image struct so the consumer doesn't need to know the type/size.
    std::vector<XrSwapchainImageOpenGLESKHR> swapchainImageBuffer(capacity);
    std::vector<XrSwapchainImageBaseHeader*> swapchainImageBase;
    for (XrSwapchainImageOpenGLESKHR& image : swapchainImageBuffer) {
      image.type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
      swapchainImageBase.push_back(reinterpret_cast<XrSwapchainImageBaseHeader*>(&image));
    }
    // Keep the buffer alive by moving it into the list of buffers.
    m_swapchainImageBuffers.push_back(std::move(swapchainImageBuffer));
    return swapchainImageBase;
  }



private:
#ifdef XR_USE_PLATFORM_ANDROID
  XrGraphicsBindingOpenGLESAndroidKHR m_graphicsBinding{ XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR };
#endif
  std::list<std::vector<XrSwapchainImageOpenGLESKHR>> m_swapchainImageBuffers;
};

class Application : public IApplication {
public:
  Application(OpenGLESGraphicsPlugin* graphicsPlugin) {
    mGraphicsPlugin = graphicsPlugin;
    mHapticCallback = nullptr;
  }
  virtual ~Application() override = default;
  bool initialize(const XrInstance instance, const XrSession session, Extentions* extentions) override {
    m_instance = instance;
    m_session = session;
    m_extentions = extentions;

    const XrGraphicsBindingOpenGLESAndroidKHR* binding = reinterpret_cast<const XrGraphicsBindingOpenGLESAndroidKHR*>(mGraphicsPlugin->GetGraphicsBinding());

    return true;
  }
  void setHapticCallback(void* arg, hapticCallback hapticCb) override {
    mHapticCallbackArg = arg;
    mHapticCallback = hapticCb;
  }


  void setGazeLocation(XrSpaceLocation& gazeLocation, std::vector<XrView>& views, float ipd, XrResult result) override {
    mIpd = ipd;
    memcpy(&m_gazeLocation, &gazeLocation, sizeof(gazeLocation));
    m_views = views;
  }
  void setHandJointLocation(XrHandJointLocationEXT* location) override {
    memcpy(&m_jointLocations, location, sizeof(m_jointLocations));
  }
private:
  void haptic(int leftright, float amplitude, float frequency/*not used now*/, float duration/*seconds*/) {
    if (mHapticCallback) {
      mHapticCallback(mHapticCallbackArg, leftright, amplitude, frequency, duration);
    }
  }

  hapticCallback mHapticCallback;
  void* mHapticCallbackArg;

public:
  OpenGLESGraphicsPlugin* mGraphicsPlugin;

  //openxr
  XrInstance m_instance;          //Keep the same naming as openxr_program.cpp
  XrSession m_session;
  Extentions* m_extentions;
  XrSpaceLocation m_gazeLocation;
  std::vector<XrView> m_views;
  float mIpd;
  int32_t mCount = 0;
};




static XrPath handSubactionPath[] = {XR_NULL_PATH, XR_NULL_PATH};
static XrSpace gripSpace[] = {XR_NULL_PATH, XR_NULL_PATH};
static XrSpace aimSpace[] = {XR_NULL_PATH, XR_NULL_PATH};

static XrActionSet actionSet{ XR_NULL_HANDLE };
static XrAction gripPoseAction{ XR_NULL_HANDLE };
static XrAction aimPoseAction{ XR_NULL_HANDLE };
static XrAction hapticAction{ XR_NULL_HANDLE };

static XrAction thumbstickValueAction{ XR_NULL_HANDLE };
static XrAction thumbstickClickAction{ XR_NULL_HANDLE };
static XrAction thumbstickTouchAction{ XR_NULL_HANDLE };
static XrAction triggerValueAction{ XR_NULL_HANDLE };
static XrAction triggerClickAction{ XR_NULL_HANDLE };
static XrAction triggerTouchAction{ XR_NULL_HANDLE };
static XrAction squeezeValueAction{ XR_NULL_HANDLE };
static XrAction squeezeClickAction{ XR_NULL_HANDLE };

static XrAction AAction{ XR_NULL_HANDLE };
static XrAction BAction{ XR_NULL_HANDLE };
static XrAction XAction{ XR_NULL_HANDLE };
static XrAction YAction{ XR_NULL_HANDLE };
static XrAction ATouchAction{ XR_NULL_HANDLE };
static XrAction BTouchAction{ XR_NULL_HANDLE };
static XrAction XTouchAction{ XR_NULL_HANDLE };
static XrAction YTouchAction{ XR_NULL_HANDLE };
static XrAction menuAction{ XR_NULL_HANDLE };

//eye tracking
static XrAction gazeAction{ XR_NULL_HANDLE };
static XrSpace  gazeActionSpace;
static XrBool32 gazeActive;




struct OpenXrProgram {
  OpenXrProgram(IPlatformPlugin* platformPlugin,
    OpenGLESGraphicsPlugin* graphicsPlugin)
    : m_platformPlugin(platformPlugin), m_graphicsPlugin(graphicsPlugin), m_application(graphicsPlugin) {
  }

  ~OpenXrProgram() {
    if (actionSet != XR_NULL_HANDLE) {
      for (auto hand : { Side::LEFT, Side::RIGHT }) {
        xrDestroySpace(gripSpace[hand]);
        xrDestroySpace(aimSpace[hand]);
      }
      xrDestroyActionSet(actionSet);
    }

    for (Swapchain swapchain : m_swapchains) {
      xrDestroySwapchain(swapchain.handle);
    }

    if (m_appSpace != XR_NULL_HANDLE) {
      xrDestroySpace(m_appSpace);
    }

    if (m_session != XR_NULL_HANDLE) {
      xrDestroySession(m_session);
    }

    if (m_instance != XR_NULL_HANDLE) {
      xrDestroyInstance(m_instance);
    }
  }

  void CheckDeviceSupportExtentions() {
    //    char buffer[64] = { 0 };
    //    __system_property_get("sys.pxr.product.name", buffer);
    //    //Log::Write(Log::Level::Info, Fmt("device is: %s", buffer));
    //    if (std::string(buffer) == "Pico Neo 3 Pro Eye") {
    //      m_deviceType = DeviceTypeNeo3ProEye;
    //    }
    //    else if (std::string(buffer) == "Pico 4") {
    //      m_deviceType = DeviceTypePico4;
    //    }
    //    else if (std::string(buffer) == "PICO 4 Pro") {
    //      m_deviceType = DeviceTypePico4Pro;
    //    }
    //
    //    __system_property_get("ro.build.id", buffer);
    //    int a, b, c;
    //    sscanf(buffer, "%d.%d.%d", &a, &b, &c);
    //    m_deviceROM = (a << 8) + (b << 4) + c;
    //    //Log::Write(Log::Level::Info, Fmt("device ROM: %x", m_deviceROM));
    //    if (m_deviceROM < 0x540) {
    //      CHECK_XRRESULT(XR_ERROR_VALIDATION_FAILURE, "This demo can only run on devices with ROM version greater than 540");
    //    }

    XrSystemEyeGazeInteractionPropertiesEXT eyeTrackingSystemProperties{ XR_TYPE_SYSTEM_EYE_GAZE_INTERACTION_PROPERTIES_EXT };
    XrSystemProperties systemProperties{ XR_TYPE_SYSTEM_PROPERTIES, &eyeTrackingSystemProperties };
    OXR(xrGetSystemProperties(m_instance, m_systemId, &systemProperties));

    if (eyeTrackingSystemProperties.supportsEyeGazeInteraction) {
      m_extentions.isSupportEyeTracking = true;
      //Log::Write(Log::Level::Info, Fmt("support eye tracking"));
    }
    else {
      m_extentions.isSupportEyeTracking = false;
    }

    // Log system properties.
    //Log::Write(Log::Level::Info, Fmt("System Properties: Name=%s VendorId=%d", systemProperties.systemName, systemProperties.vendorId));
    //Log::Write(Log::Level::Info, Fmt("System Graphics Properties: MaxWidth=%d MaxHeight=%d MaxLayers=%d", systemProperties.graphicsProperties.maxSwapchainImageWidth, systemProperties.graphicsProperties.maxSwapchainImageHeight, systemProperties.graphicsProperties.maxLayerCount));
    //Log::Write(Log::Level::Info, Fmt("System Tracking Properties: OrientationTracking=%s PositionTracking=%s", systemProperties.trackingProperties.orientationTracking == XR_TRUE ? "True" : "False", systemProperties.trackingProperties.positionTracking == XR_TRUE ? "True" : "False"));
  }

  static void LogLayersAndExtensions() {
    // Write out extension properties for a given layer.
    const auto logExtensions = [](const char* layerName, int indent = 0) {
      uint32_t instanceExtensionCount;
      OXR(xrEnumerateInstanceExtensionProperties(layerName, 0, &instanceExtensionCount, nullptr));

      std::vector<XrExtensionProperties> extensions(instanceExtensionCount);
      for (XrExtensionProperties& extension : extensions) {
        extension.type = XR_TYPE_EXTENSION_PROPERTIES;
      }

      OXR(xrEnumerateInstanceExtensionProperties(layerName, (uint32_t)extensions.size(), &instanceExtensionCount, extensions.data()));

      const std::string indentStr(indent, ' ');
      //Log::Write(Log::Level::Verbose, Fmt("%sAvailable Extensions: (%d)", indentStr.c_str(), instanceExtensionCount));
//            for (const XrExtensionProperties& extension : extensions) {
//                Log::Write(Log::Level::Verbose, Fmt("%sAvailable Extensions:  Name=%s", indentStr.c_str(), extension.extensionName,
//                                                    extension.extensionVersion));
//            }
      };

    // Log non-layer extensions (layerName==nullptr).
    logExtensions(nullptr);

    // Log layers and any of their extensions.
    {
      uint32_t layerCount;
      OXR(xrEnumerateApiLayerProperties(0, &layerCount, nullptr));

      std::vector<XrApiLayerProperties> layers(layerCount);
      for (XrApiLayerProperties& layer : layers) {
        layer.type = XR_TYPE_API_LAYER_PROPERTIES;
      }

      OXR(xrEnumerateApiLayerProperties((uint32_t)layers.size(), &layerCount, layers.data()));

      //            Log::Write(Log::Level::Info, Fmt("Available Layers: (%d)", layerCount));
      //            for (const XrApiLayerProperties& layer : layers) {
      //                Log::Write(Log::Level::Info, Fmt("  Name=%s SpecVersion=%s LayerVersion=%d Description=%s", layer.layerName,
      //                                                        GetXrVersionString(layer.specVersion).c_str(), layer.layerVersion, layer.description));
      //                logExtensions(layer.layerName, 4);
      //            }
    }
  }

  void LogInstanceInfo() {
    CHECK(m_instance != XR_NULL_HANDLE);

    XrInstanceProperties instanceProperties{ XR_TYPE_INSTANCE_PROPERTIES };
    OXR(xrGetInstanceProperties(m_instance, &instanceProperties));

    //Log::Write(Log::Level::Info, Fmt("Instance RuntimeName=%s RuntimeVersion=%s", instanceProperties.runtimeName, GetXrVersionString(instanceProperties.runtimeVersion).c_str()));
  }

  void CreateInstanceInternal() {
    CHECK(m_instance == XR_NULL_HANDLE);

    // Create union of extensions required by platform and graphics plugins.
    std::vector<const char*> extensions;
    // Transform platform and graphics extension std::strings to C strings.
    const std::vector<std::string> platformExtensions = m_platformPlugin->GetInstanceExtensions();
    std::transform(platformExtensions.begin(), platformExtensions.end(), std::back_inserter(extensions),
      [](const std::string& ext) { return ext.c_str(); });
    const std::vector<std::string> graphicsExtensions = m_graphicsPlugin->GetInstanceExtensions();
    std::transform(graphicsExtensions.begin(), graphicsExtensions.end(), std::back_inserter(extensions),
      [](const std::string& ext) { return ext.c_str(); });

    extensions.push_back(XR_EPIC_VIEW_CONFIGURATION_FOV_EXTENSION_NAME);
    //extensions.push_back(XR_KHR_CONVERT_TIMESPEC_TIME_EXTENSION_NAME);

    //XR_FB_passthrough
    extensions.push_back(XR_FB_PASSTHROUGH_EXTENSION_NAME);
    extensions.push_back(XR_FB_TRIANGLE_MESH_EXTENSION_NAME);

    //refresh rate
    extensions.push_back(XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);

    //eye tracking
    extensions.push_back(XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME);

    //hand tracking
    extensions.push_back(XR_EXT_HAND_TRACKING_EXTENSION_NAME);

    XrInstanceCreateInfo createInfo{ XR_TYPE_INSTANCE_CREATE_INFO };
    createInfo.next = m_platformPlugin->GetInstanceCreateExtension();
    createInfo.enabledExtensionCount = (uint32_t)extensions.size();
    createInfo.enabledExtensionNames = extensions.data();

    strcpy(createInfo.applicationInfo.applicationName, "demos");
    createInfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION;

    OXR(xrCreateInstance(&createInfo, &m_instance));
  }

  void CreateInstance() {
    LogLayersAndExtensions();
    CreateInstanceInternal();
    LogInstanceInfo();

    //XR_FB_passthrough
    PFN_INITIALIZE(xrCreatePassthroughFB);
    PFN_INITIALIZE(xrDestroyPassthroughFB);
    PFN_INITIALIZE(xrPassthroughStartFB);
    PFN_INITIALIZE(xrPassthroughPauseFB);
    PFN_INITIALIZE(xrCreatePassthroughLayerFB);
    PFN_INITIALIZE(xrDestroyPassthroughLayerFB);
    PFN_INITIALIZE(xrPassthroughLayerSetStyleFB);
    PFN_INITIALIZE(xrPassthroughLayerPauseFB);
    PFN_INITIALIZE(xrPassthroughLayerResumeFB);
    PFN_INITIALIZE(xrCreateGeometryInstanceFB);
    PFN_INITIALIZE(xrDestroyGeometryInstanceFB);
    PFN_INITIALIZE(xrGeometryInstanceSetTransformFB);
    // FB_triangle_mesh
    PFN_INITIALIZE(xrCreateTriangleMeshFB);
    PFN_INITIALIZE(xrDestroyTriangleMeshFB);
    PFN_INITIALIZE(xrTriangleMeshGetVertexBufferFB);
    PFN_INITIALIZE(xrTriangleMeshGetIndexBufferFB);
    PFN_INITIALIZE(xrTriangleMeshBeginUpdateFB);
    PFN_INITIALIZE(xrTriangleMeshEndUpdateFB);
    PFN_INITIALIZE(xrTriangleMeshBeginVertexBufferUpdateFB);
    PFN_INITIALIZE(xrTriangleMeshEndVertexBufferUpdateFB);

    //hand tracking
    PFN_INITIALIZE(xrCreateHandTrackerEXT);
    PFN_INITIALIZE(xrDestroyHandTrackerEXT);
    PFN_INITIALIZE(xrLocateHandJointsEXT);

    m_extentions.initialize(m_instance);
  }

  void LogViewConfigurations() {
    CHECK(m_instance != XR_NULL_HANDLE);
    CHECK(m_systemId != XR_NULL_SYSTEM_ID);

    uint32_t viewConfigTypeCount;
    OXR(xrEnumerateViewConfigurations(m_instance, m_systemId, 0, &viewConfigTypeCount, nullptr));
    std::vector<XrViewConfigurationType> viewConfigTypes(viewConfigTypeCount);
    OXR(xrEnumerateViewConfigurations(m_instance, m_systemId, viewConfigTypeCount, &viewConfigTypeCount, viewConfigTypes.data()));
    CHECK((uint32_t)viewConfigTypes.size() == viewConfigTypeCount);

    //Log::Write(Log::Level::Info, Fmt("Available View Configuration Types: (%d)", viewConfigTypeCount));
    for (XrViewConfigurationType viewConfigType : viewConfigTypes) {
      //Log::Write(Log::Level::Verbose, Fmt("  View Configuration Type: %s %s", to_string(viewConfigType), viewConfigType == m_options.Parsed.ViewConfigType ? "(Selected)" : ""));

      XrViewConfigurationProperties viewConfigProperties{ XR_TYPE_VIEW_CONFIGURATION_PROPERTIES };
      OXR(xrGetViewConfigurationProperties(m_instance, m_systemId, viewConfigType, &viewConfigProperties));

      //Log::Write(Log::Level::Verbose, Fmt("  View configuration FovMutable=%s", viewConfigProperties.fovMutable == XR_TRUE ? "True" : "False"));

      uint32_t viewCount;
      OXR(xrEnumerateViewConfigurationViews(m_instance, m_systemId, viewConfigType, 0, &viewCount, nullptr));
      if (viewCount > 0) {
        std::vector<XrViewConfigurationView> views(viewCount, { XR_TYPE_VIEW_CONFIGURATION_VIEW });
        OXR(xrEnumerateViewConfigurationViews(m_instance, m_systemId, viewConfigType, viewCount, &viewCount, views.data()));

        for (uint32_t i = 0; i < views.size(); i++) {
          const XrViewConfigurationView& view = views[i];

          //Log::Write(Log::Level::Verbose, Fmt(" View [%d]: Recommended Width=%d Height=%d SampleCount=%d", i, view.recommendedImageRectWidth, view.recommendedImageRectHeight, view.recommendedSwapchainSampleCount));
          //Log::Write(Log::Level::Verbose, Fmt(" View [%d]:     Maximum Width=%d Height=%d SampleCount=%d", i, view.maxImageRectWidth, view.maxImageRectHeight, view.maxSwapchainSampleCount));
        }
      }
      else {
        //Log::Write(Log::Level::Error, Fmt("Empty view configuration type"));
      }

      LogEnvironmentBlendMode(viewConfigType);
    }
  }

  void LogEnvironmentBlendMode(XrViewConfigurationType type) {
    CHECK(m_instance != XR_NULL_HANDLE);
    CHECK(m_systemId != 0);

    uint32_t count;
    OXR(xrEnumerateEnvironmentBlendModes(m_instance, m_systemId, type, 0, &count, nullptr));
    CHECK(count > 0);

    //Log::Write(Log::Level::Info, Fmt("Available Environment Blend Mode count : (%d)", count));

    std::vector<XrEnvironmentBlendMode> blendModes(count);
    OXR(xrEnumerateEnvironmentBlendModes(m_instance, m_systemId, type, count, &count, blendModes.data()));

    bool blendModeFound = false;
    for (XrEnvironmentBlendMode mode : blendModes) {
      const bool blendModeMatch = (mode == XR_ENVIRONMENT_BLEND_MODE_OPAQUE);
      //Log::Write(Log::Level::Info, Fmt("Environment Blend Mode (%s) : %s", to_string(mode), blendModeMatch ? "(Selected)" : ""));
      blendModeFound |= blendModeMatch;
    }
    CHECK(blendModeFound);
  }

  void InitializeSystem() {
    CHECK(m_instance != XR_NULL_HANDLE);
    CHECK(m_systemId == XR_NULL_SYSTEM_ID);

    XrSystemGetInfo systemInfo{ XR_TYPE_SYSTEM_GET_INFO };
    systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
    OXR(xrGetSystem(m_instance, &systemInfo, &m_systemId));

    //Log::Write(Log::Level::Verbose, Fmt("Using system %d for form factor %s", m_systemId, to_string(m_options.Parsed.FormFactor)));
    CHECK(m_instance != XR_NULL_HANDLE);
    CHECK(m_systemId != XR_NULL_SYSTEM_ID);

    LogViewConfigurations();

    // The graphics API can initialize the graphics device now that the systemId and instance
    // handle are available.
    m_graphicsPlugin->InitializeDevice(m_instance, m_systemId);
  }

  void LogReferenceSpaces() {
    CHECK(m_session != XR_NULL_HANDLE);

    uint32_t spaceCount;
    OXR(xrEnumerateReferenceSpaces(m_session, 0, &spaceCount, nullptr));
    std::vector<XrReferenceSpaceType> spaces(spaceCount);
    OXR(xrEnumerateReferenceSpaces(m_session, spaceCount, &spaceCount, spaces.data()));

    //Log::Write(Log::Level::Info, Fmt("Available reference spaces: %d", spaceCount));
    for (XrReferenceSpaceType space : spaces) {
      //Log::Write(Log::Level::Verbose, Fmt("  Name: %s", to_string(space)));
    }
  }

  struct InputState {

  };

  void InitializeActions() {
    // Create an action set.
    {
      XrActionSetCreateInfo actionSetInfo{ XR_TYPE_ACTION_SET_CREATE_INFO };
      strcpy_s(actionSetInfo.actionSetName, "gameplay");
      strcpy_s(actionSetInfo.localizedActionSetName, "Gameplay");
      actionSetInfo.priority = 0;
      OXR(xrCreateActionSet(m_instance, &actionSetInfo, &actionSet));
    }

    // Get the XrPath for the left and right hands - we will use them as subaction paths.
    OXR(xrStringToPath(m_instance, "/user/hand/left", &handSubactionPath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right", &handSubactionPath[Side::RIGHT]));

    // Create actions.
    {
      // Create an input action getting the left and right hand poses.
      XrActionCreateInfo actionInfo{ XR_TYPE_ACTION_CREATE_INFO };
      actionInfo.actionType = XR_ACTION_TYPE_POSE_INPUT;
      strcpy_s(actionInfo.actionName, "grip_pose");
      strcpy_s(actionInfo.localizedActionName, "Grip_pose");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &gripPoseAction));

      actionInfo.actionType = XR_ACTION_TYPE_POSE_INPUT;
      strcpy_s(actionInfo.actionName, "aim_pose");
      strcpy_s(actionInfo.localizedActionName, "Aim_pose");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &aimPoseAction));

      // Create output actions for vibrating the left and right controller.
      actionInfo.actionType = XR_ACTION_TYPE_VIBRATION_OUTPUT;
      strcpy_s(actionInfo.actionName, "haptic");
      strcpy_s(actionInfo.localizedActionName, "Haptic");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &hapticAction));

      actionInfo.actionType = XR_ACTION_TYPE_VECTOR2F_INPUT;
      strcpy_s(actionInfo.actionName, "thumbstick_value");
      strcpy_s(actionInfo.localizedActionName, "Thumbstick_value");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &thumbstickValueAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "thumbstick_click");
      strcpy_s(actionInfo.localizedActionName, "Thumbstick_click");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &thumbstickClickAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "thumbstick_touch");
      strcpy_s(actionInfo.localizedActionName, "Thumbstick_touch");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &thumbstickTouchAction));

      actionInfo.actionType = XR_ACTION_TYPE_FLOAT_INPUT;
      strcpy_s(actionInfo.actionName, "trigger_value");
      strcpy_s(actionInfo.localizedActionName, "Trigger_value");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &triggerValueAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "trigger_click");
      strcpy_s(actionInfo.localizedActionName, "Trigger_click");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &triggerClickAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "trigger_touch");
      strcpy_s(actionInfo.localizedActionName, "Trigger_touch");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &triggerTouchAction));

      actionInfo.actionType = XR_ACTION_TYPE_FLOAT_INPUT;
      strcpy_s(actionInfo.actionName, "squeeze_value");
      strcpy_s(actionInfo.localizedActionName, "Squeeze_value");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &squeezeValueAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "squeeze_click");
      strcpy_s(actionInfo.localizedActionName, "Squeeze_click");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &squeezeClickAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "akey");
      strcpy_s(actionInfo.localizedActionName, "Akey");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &AAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "bkey");
      strcpy_s(actionInfo.localizedActionName, "Bkey");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &BAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "xkey");
      strcpy_s(actionInfo.localizedActionName, "Xkey");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &XAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "ykey");
      strcpy_s(actionInfo.localizedActionName, "Ykey");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &YAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "akey_touch");
      strcpy_s(actionInfo.localizedActionName, "Akey_touch");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &ATouchAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "bkey_touch");
      strcpy_s(actionInfo.localizedActionName, "Bkey_touch");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &BTouchAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "xkey_touch");
      strcpy_s(actionInfo.localizedActionName, "Xkey_touch");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &XTouchAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "ykey_touch");
      strcpy_s(actionInfo.localizedActionName, "Ykey_touch");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &YTouchAction));

      actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
      strcpy_s(actionInfo.actionName, "menukey");
      strcpy_s(actionInfo.localizedActionName, "Menukey");
      actionInfo.countSubactionPaths = ARRAY_SIZE(handSubactionPath);
      actionInfo.subactionPaths = handSubactionPath;
      OXR(xrCreateAction(actionSet, &actionInfo, &menuAction));
    }

    XrPath gripPosePath[2];
    XrPath aimPosePath[2];
    XrPath hapticPath[2];
    XrPath thumbstickValuePath[2];
    XrPath thumbstickClickPath[2];
    XrPath thumbstickTouchPath[2];
    XrPath squeezeValuePath[2];
    XrPath squeezeClickPath[2];
    XrPath triggerClickPath[2];
    XrPath triggerValuePath[2];
    XrPath triggerTouchPath[2];
    XrPath AClickPath[2];
    XrPath BClickPath[2];
    XrPath XClickPath[2];
    XrPath YClickPath[2];
    XrPath ATouchPath[2];
    XrPath BTouchPath[2];
    XrPath XTouchPath[2];
    XrPath YTouchPath[2];
    //std::array<XrPath, Side::COUNT> menuPath;

    // //see https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XR_BD_controller_interaction
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/grip/pose", &gripPosePath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/grip/pose", &gripPosePath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/aim/pose", &aimPosePath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/aim/pose", &aimPosePath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/output/haptic", &hapticPath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/output/haptic", &hapticPath[Side::RIGHT]));

    OXR(xrStringToPath(m_instance, "/user/hand/left/input/thumbstick", &thumbstickValuePath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/thumbstick", &thumbstickValuePath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/thumbstick/click", &thumbstickClickPath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/thumbstick/click", &thumbstickClickPath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/thumbstick/touch", &thumbstickTouchPath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/thumbstick/touch", &thumbstickTouchPath[Side::RIGHT]));

    OXR(xrStringToPath(m_instance, "/user/hand/left/input/trigger/value", &triggerValuePath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/trigger/value", &triggerValuePath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/trigger/click", &triggerClickPath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/trigger/click", &triggerClickPath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/trigger/touch", &triggerTouchPath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/trigger/touch", &triggerTouchPath[Side::RIGHT]));

    OXR(xrStringToPath(m_instance, "/user/hand/left/input/squeeze/value", &squeezeValuePath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/squeeze/value", &squeezeValuePath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/squeeze/click", &squeezeClickPath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/squeeze/click", &squeezeClickPath[Side::RIGHT]));

    OXR(xrStringToPath(m_instance, "/user/hand/right/input/a/click", &AClickPath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/b/click", &BClickPath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/x/click", &XClickPath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/y/click", &YClickPath[Side::LEFT]));

    OXR(xrStringToPath(m_instance, "/user/hand/right/input/a/touch", &ATouchPath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/right/input/b/touch", &BTouchPath[Side::RIGHT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/x/touch", &XTouchPath[Side::LEFT]));
    OXR(xrStringToPath(m_instance, "/user/hand/left/input/y/touch", &YTouchPath[Side::LEFT]));

    //    OXR(xrStringToPath(m_instance, "/user/hand/left/input/menu/click", &menuPath[Side::LEFT]));
    //    OXR(xrStringToPath(m_instance, "/user/hand/right/input/menu/click", &menuPath[Side::RIGHT]));

    { // Suggest bindings for the PICO Controller.
      //see https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XR_BD_controller_interaction
      XrPath picoMixedRealityInteractionProfilePath;
      OXR(xrStringToPath(m_instance, "/interaction_profiles/pico/neo3_controller", &picoMixedRealityInteractionProfilePath));
      std::vector<XrActionSuggestedBinding> bindings{ {{gripPoseAction, gripPosePath[Side::LEFT]},
                                                      {gripPoseAction, gripPosePath[Side::RIGHT]},
                                                      {aimPoseAction, aimPosePath[Side::LEFT]},
                                                      {aimPoseAction, aimPosePath[Side::RIGHT]},
                                                      {hapticAction, hapticPath[Side::LEFT]},
                                                      {hapticAction, hapticPath[Side::RIGHT]},

                                                      {thumbstickValueAction, thumbstickValuePath[Side::LEFT]},
                                                      {thumbstickValueAction, thumbstickValuePath[Side::RIGHT]},
                                                      {thumbstickClickAction, thumbstickClickPath[Side::LEFT]},
                                                      {thumbstickClickAction, thumbstickClickPath[Side::RIGHT]},
                                                      {thumbstickTouchAction, thumbstickTouchPath[Side::LEFT]},
                                                      {thumbstickTouchAction, thumbstickTouchPath[Side::RIGHT]},

                                                      {triggerValueAction, triggerValuePath[Side::LEFT]},
                                                      {triggerValueAction, triggerValuePath[Side::RIGHT]},
                                                      {triggerClickAction, triggerClickPath[Side::LEFT]},
                                                      {triggerClickAction, triggerClickPath[Side::RIGHT]},
                                                      {triggerTouchAction, triggerTouchPath[Side::LEFT]},
                                                      {triggerTouchAction, triggerTouchPath[Side::RIGHT]},

                                                      {squeezeClickAction, squeezeClickPath[Side::LEFT]},
                                                      {squeezeClickAction, squeezeClickPath[Side::RIGHT]},
                                                      {squeezeValueAction, squeezeValuePath[Side::LEFT]},
                                                      {squeezeValueAction, squeezeValuePath[Side::RIGHT]},

                                                      {AAction, AClickPath[Side::RIGHT]},
                                                      {BAction, BClickPath[Side::RIGHT]},
                                                      {XAction, XClickPath[Side::LEFT]},
                                                      {YAction, YClickPath[Side::LEFT]},

                                                      {ATouchAction, ATouchPath[Side::RIGHT]},
                                                      {BTouchAction, BTouchPath[Side::RIGHT]},
                                                      {XTouchAction, XTouchPath[Side::LEFT]},
                                                      {YTouchAction, YTouchPath[Side::LEFT]},

        //                                                            {menuAction, menuPath[Side::LEFT]}
                                                             } };

      XrInteractionProfileSuggestedBinding suggestedBindings{ XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
      suggestedBindings.interactionProfile = picoMixedRealityInteractionProfilePath;
      suggestedBindings.suggestedBindings = bindings.data();
      suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size();
      OXR(xrSuggestInteractionProfileBindings(m_instance, &suggestedBindings));
    }

    // eye tracking
    if (m_extentions.isSupportEyeTracking) {
      XrActionCreateInfo actionInfo{ XR_TYPE_ACTION_CREATE_INFO };
      strcpy_s(actionInfo.actionName, "user_intent");
      actionInfo.actionType = XR_ACTION_TYPE_POSE_INPUT;
      strcpy_s(actionInfo.localizedActionName, "User intent");
      OXR(xrCreateAction(actionSet, &actionInfo, &gazeAction));

      XrPath eyeGazeInteractionProfilePath;
      XrPath gazePosePath;
      OXR(xrStringToPath(m_instance, "/interaction_profiles/ext/eye_gaze_interaction", &eyeGazeInteractionProfilePath));
      OXR(xrStringToPath(m_instance, "/user/eyes_ext/input/gaze_ext/pose", &gazePosePath));

      XrActionSuggestedBinding bindings;
      bindings.action = gazeAction;
      bindings.binding = gazePosePath;

      XrInteractionProfileSuggestedBinding suggestedBindings{ XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
      suggestedBindings.interactionProfile = eyeGazeInteractionProfilePath;
      suggestedBindings.suggestedBindings = &bindings;
      suggestedBindings.countSuggestedBindings = 1;
      OXR(xrSuggestInteractionProfileBindings(m_instance, &suggestedBindings));

      XrActionSpaceCreateInfo createActionSpaceInfo{ XR_TYPE_ACTION_SPACE_CREATE_INFO };
      createActionSpaceInfo.action = gazeAction;
      createActionSpaceInfo.poseInActionSpace = Math::Pose::Identity();
      OXR(xrCreateActionSpace(m_session, &createActionSpaceInfo, &gazeActionSpace));
    }

    XrActionSpaceCreateInfo actionSpaceInfo{ XR_TYPE_ACTION_SPACE_CREATE_INFO };
    actionSpaceInfo.action = gripPoseAction;
    actionSpaceInfo.poseInActionSpace.orientation.w = 1.0f;
    actionSpaceInfo.subactionPath = handSubactionPath[Side::LEFT];
    OXR(xrCreateActionSpace(m_session, &actionSpaceInfo, &gripSpace[Side::LEFT]));
    actionSpaceInfo.subactionPath = handSubactionPath[Side::RIGHT];
    OXR(xrCreateActionSpace(m_session, &actionSpaceInfo, &gripSpace[Side::RIGHT]));

    actionSpaceInfo.action = aimPoseAction;
    actionSpaceInfo.poseInActionSpace.orientation.w = 1.0f;
    actionSpaceInfo.subactionPath = handSubactionPath[Side::LEFT];
    OXR(xrCreateActionSpace(m_session, &actionSpaceInfo, &aimSpace[Side::LEFT]));
    actionSpaceInfo.subactionPath = handSubactionPath[Side::RIGHT];
    OXR(xrCreateActionSpace(m_session, &actionSpaceInfo, &aimSpace[Side::RIGHT]));

    XrSessionActionSetsAttachInfo attachInfo{ XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO };
    attachInfo.countActionSets = 1;
    attachInfo.actionSets = &actionSet;
    OXR(xrAttachSessionActionSets(m_session, &attachInfo));
  }

  void CreateVisualizedSpaces() {
    CHECK(m_session != XR_NULL_HANDLE);
    XrReferenceSpaceCreateInfo referenceSpaceCreateInfo{ XR_TYPE_REFERENCE_SPACE_CREATE_INFO };
    referenceSpaceCreateInfo.poseInReferenceSpace = Math::Pose::Identity();
    referenceSpaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
    OXR(xrCreateReferenceSpace(m_session, &referenceSpaceCreateInfo, &m_ViewSpace));
  }

  void InitializeSession() {
    CHECK(m_instance != XR_NULL_HANDLE);
    CHECK(m_session == XR_NULL_HANDLE);

    {
      //Log::Write(Log::Level::Verbose, Fmt("Creating session..."));
      XrSessionCreateInfo createInfo{ XR_TYPE_SESSION_CREATE_INFO };
      createInfo.next = m_graphicsPlugin->GetGraphicsBinding();
      createInfo.systemId = m_systemId;
      OXR(xrCreateSession(m_instance, &createInfo, &m_session));
    }

    CheckDeviceSupportExtentions();
    LogReferenceSpaces();
    InitializeActions();
    CreateVisualizedSpaces();

    {
      XrReferenceSpaceCreateInfo referenceSpaceCreateInfo{ XR_TYPE_REFERENCE_SPACE_CREATE_INFO };
      referenceSpaceCreateInfo.poseInReferenceSpace.orientation.w = 1.0f;
      referenceSpaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
      OXR(xrCreateReferenceSpace(m_session, &referenceSpaceCreateInfo, &m_appSpace));
    }
    {
      XrReferenceSpaceCreateInfo referenceSpaceCreateInfo{ XR_TYPE_REFERENCE_SPACE_CREATE_INFO };
      referenceSpaceCreateInfo.poseInReferenceSpace.orientation.w = 1.0f;
      referenceSpaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE;
      OXR(xrCreateReferenceSpace(m_session, &referenceSpaceCreateInfo, &m_stageSpace));
    }

    //XR_FB_passthrough
    {
      XrPassthroughCreateInfoFB passthroughCreateInfo = { XR_TYPE_PASSTHROUGH_CREATE_INFO_FB };
      passthroughCreateInfo.flags = XR_PASSTHROUGH_IS_RUNNING_AT_CREATION_BIT_FB;
      OXR(xrCreatePassthroughFB(m_session, &passthroughCreateInfo, &m_passthrough));

      XrPassthroughLayerCreateInfoFB passthroughLayerCreateInfo = { XR_TYPE_PASSTHROUGH_LAYER_CREATE_INFO_FB };
      passthroughLayerCreateInfo.flags = XR_PASSTHROUGH_IS_RUNNING_AT_CREATION_BIT_FB;
      passthroughLayerCreateInfo.passthrough = m_passthrough;
      passthroughLayerCreateInfo.purpose = XR_PASSTHROUGH_LAYER_PURPOSE_RECONSTRUCTION_FB;
      OXR(xrCreatePassthroughLayerFB(m_session, &passthroughLayerCreateInfo, &m_passthroughLayerReconstruction));

      passthroughLayerCreateInfo.purpose = XR_PASSTHROUGH_LAYER_PURPOSE_PROJECTED_FB;
      OXR(xrCreatePassthroughLayerFB(m_session, &passthroughLayerCreateInfo, &m_passthroughLayerProject));

      OXR(xrPassthroughStartFB(m_passthrough));
      OXR(xrPassthroughLayerResumeFB(m_passthroughLayerReconstruction));

      const XrVector3f verts[] = { {0, 0, 0}, {1, 0, 0}, {0.5, 1, 0}, {-1, 0, 0}, {-0.5, -1, 0} };
      const uint32_t indexes[] = { 0, 1, 2, 0, 3, 4 };
      XrTriangleMeshCreateInfoFB triangleMeshCreateInfo = { XR_TYPE_TRIANGLE_MESH_CREATE_INFO_FB };
      triangleMeshCreateInfo.vertexCount = 5;
      triangleMeshCreateInfo.vertexBuffer = &verts[0];
      triangleMeshCreateInfo.triangleCount = 2;
      triangleMeshCreateInfo.indexBuffer = &indexes[0];

      OXR(xrCreateTriangleMeshFB(m_session, &triangleMeshCreateInfo, &m_triangleMesh));
      //Log::Write(Log::Level::Info, Fmt("Passthrough-Create Triangle_Mesh:%d", m_triangleMesh));

      XrGeometryInstanceCreateInfoFB geometryInstanceCreateInfo = { XR_TYPE_GEOMETRY_INSTANCE_CREATE_INFO_FB };
      geometryInstanceCreateInfo.layer = m_passthroughLayerProject;
      geometryInstanceCreateInfo.mesh = m_triangleMesh;
      geometryInstanceCreateInfo.baseSpace = m_appSpace;
      geometryInstanceCreateInfo.pose.orientation.w = 1.0f;
      geometryInstanceCreateInfo.scale = { 1.0f, 1.0f, 1.0f };
      OXR(xrCreateGeometryInstanceFB(m_session, &geometryInstanceCreateInfo, &m_geometryInstance));
      //Log::Write(Log::Level::Info, Fmt("Passthrough-Create Geometry Instance:%d", m_geometryInstance));
    }


    //hand tracking
    if (xrCreateHandTrackerEXT) {
      for (auto hand : { Side::LEFT, Side::RIGHT }) {
        XrHandTrackerCreateInfoEXT createInfo{ XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT };
        createInfo.handJointSet = XR_HAND_JOINT_SET_DEFAULT_EXT;
        createInfo.hand = (hand == Side::LEFT ? XR_HAND_LEFT_EXT : XR_HAND_RIGHT_EXT);
        OXR(xrCreateHandTrackerEXT(m_session, &createInfo, &m_handTracker[hand]));
      }
    }
  }

  void InitializeApplication() {
    m_application.initialize(m_instance, m_session, &m_extentions);
    m_application.setHapticCallback((void*)this, [](void* arg, int controllerIndex, float amplitude, float seconds, float frequency) {
      OpenXrProgram* thiz = (OpenXrProgram*)arg;
      XrHapticVibration vibration{ XR_TYPE_HAPTIC_VIBRATION };
      vibration.amplitude = amplitude;
      vibration.duration = seconds * 1000 * 10000000;  //nanoseconds
      vibration.frequency = frequency;
      XrHapticActionInfo hapticActionInfo{ XR_TYPE_HAPTIC_ACTION_INFO };
      hapticActionInfo.action = hapticAction;
      hapticActionInfo.subactionPath = handSubactionPath[controllerIndex];
      OXR(xrApplyHapticFeedback(thiz->m_session, &hapticActionInfo, (XrHapticBaseHeader*)&vibration)); });
  }

  void CreateSwapchains() {
    CHECK(m_session != XR_NULL_HANDLE);
    CHECK(m_swapchains.empty());
    CHECK(m_configViews.empty());

    // Note: No other view configurations exist at the time this code was written. If this
    // condition is not met, the project will need to be audited to see how support should be
    // added.

    // Query and cache view configuration views.
    uint32_t viewCount;
    OXR(xrEnumerateViewConfigurationViews(m_instance, m_systemId, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 0, &viewCount, nullptr));
    m_configViews.resize(viewCount, { XR_TYPE_VIEW_CONFIGURATION_VIEW });
    OXR(xrEnumerateViewConfigurationViews(m_instance, m_systemId, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, viewCount, &viewCount, m_configViews.data()));

    // Create and cache view buffer for xrLocateViews later.
    m_views.resize(viewCount, { XR_TYPE_VIEW });

    // Create the swapchain and get the images.
    if (viewCount > 0) {
      // Select a swapchain format.
      uint32_t swapchainFormatCount;
      OXR(xrEnumerateSwapchainFormats(m_session, 0, &swapchainFormatCount, nullptr));
      std::vector<int64_t> swapchainFormats(swapchainFormatCount);
      OXR(xrEnumerateSwapchainFormats(m_session, (uint32_t)swapchainFormats.size(), &swapchainFormatCount, swapchainFormats.data()));
      CHECK(swapchainFormatCount == swapchainFormats.size());
      m_colorSwapchainFormat = m_graphicsPlugin->SelectColorSwapchainFormat(swapchainFormats);

      // Print swapchain formats and the selected one.
      {
        std::string swapchainFormatsString;
        for (int64_t format : swapchainFormats) {
          const bool selected = format == m_colorSwapchainFormat;
          swapchainFormatsString += " ";
          if (selected) {
            swapchainFormatsString += "[";
          }
          swapchainFormatsString += std::to_string(format);
          if (selected) {
            swapchainFormatsString += "]";
          }
        }
        //Log::Write(Log::Level::Verbose, Fmt("Swapchain Formats: %s", swapchainFormatsString.c_str()));
      }

      // Create a swapchain for each view.
      for (uint32_t i = 0; i < viewCount; i++) {
        const XrViewConfigurationView& vp = m_configViews[i];
        //Log::Write(Log::Level::Info, Fmt("Creating swapchain for view %d with dimensions Width=%d Height=%d SampleCount=%d, maxSampleCount=%d", i, vp.recommendedImageRectWidth, vp.recommendedImageRectHeight, vp.recommendedSwapchainSampleCount, vp.maxSwapchainSampleCount));

        // Create the swapchain.
        XrSwapchainCreateInfo swapchainCreateInfo{ XR_TYPE_SWAPCHAIN_CREATE_INFO };
        swapchainCreateInfo.arraySize = 1;
        swapchainCreateInfo.format = m_colorSwapchainFormat;
        swapchainCreateInfo.width = vp.recommendedImageRectWidth;
        swapchainCreateInfo.height = vp.recommendedImageRectHeight;
        swapchainCreateInfo.mipCount = 1;
        swapchainCreateInfo.faceCount = 1;
#ifdef SHR3D_GRAPHICS_MSAA
        if (const GLsizei msaa = 1 << Settings::graphicsMSAA; msaa >= 2)
        {
          swapchainCreateInfo.sampleCount = msaa;
        }
        else
#endif // SHR3D_GRAPHICS_MSAA
        {
          swapchainCreateInfo.sampleCount = vp.recommendedSwapchainSampleCount;
          ASSERT(swapchainCreateInfo.sampleCount == 1);
        }
        swapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
        Swapchain swapchain;
        swapchain.width = swapchainCreateInfo.width;
        swapchain.height = swapchainCreateInfo.height;
        OXR(xrCreateSwapchain(m_session, &swapchainCreateInfo, &swapchain.handle));

        m_swapchains.push_back(swapchain);

        uint32_t imageCount;
        OXR(xrEnumerateSwapchainImages(swapchain.handle, 0, &imageCount, nullptr));
        // XXX This should really just return XrSwapchainImageBaseHeader*
        std::vector<XrSwapchainImageBaseHeader*> swapchainImages = m_graphicsPlugin->AllocateSwapchainImageStructs(imageCount, swapchainCreateInfo);
        OXR(xrEnumerateSwapchainImages(swapchain.handle, imageCount, &imageCount, swapchainImages[0]));

        m_swapchainImages.insert(std::make_pair(swapchain.handle, std::move(swapchainImages)));
      }
    }
  }

  // Return event if one is available, otherwise return null.
  const XrEventDataBaseHeader* TryReadNextEvent() {
    // It is sufficient to clear the just the XrEventDataBuffer header to
    // XR_TYPE_EVENT_DATA_BUFFER
    XrEventDataBaseHeader* baseHeader = reinterpret_cast<XrEventDataBaseHeader*>(&m_eventDataBuffer);
    *baseHeader = { XR_TYPE_EVENT_DATA_BUFFER };
    const XrResult xr = xrPollEvent(m_instance, &m_eventDataBuffer);
    if (xr == XR_SUCCESS) {
      if (baseHeader->type == XR_TYPE_EVENT_DATA_EVENTS_LOST) {
        const XrEventDataEventsLost* const eventsLost = reinterpret_cast<const XrEventDataEventsLost*>(baseHeader);
        //Log::Write(Log::Level::Warning, Fmt("%d events lost", eventsLost));
      }
      return baseHeader;
    }
    if (xr == XR_EVENT_UNAVAILABLE) {
      return nullptr;
    }
    THROW_XR(xr, "xrPollEvent");
  }

  void PollEvents(bool& exitRenderLoop, bool& requestRestart) {
    //exitRenderLoop = false;
    requestRestart = false;

    // Process all pending messages.
    while (const XrEventDataBaseHeader* event = TryReadNextEvent()) {
      switch (event->type) {
      case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: {
        const auto& instanceLossPending = *reinterpret_cast<const XrEventDataInstanceLossPending*>(event);
        //Log::Write(Log::Level::Warning, Fmt("XrEventDataInstanceLossPending by %lld", instanceLossPending.lossTime));
        exitRenderLoop = true;
        requestRestart = true;
        return;
      }
      case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: {
        auto sessionStateChangedEvent = *reinterpret_cast<const XrEventDataSessionStateChanged*>(event);
        HandleSessionStateChangedEvent(sessionStateChangedEvent, &exitRenderLoop, &requestRestart);
        break;
      }
      case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED:
        //LogActionSourceName(gripPoseAction, "Pose");
        //LogActionSourceName(hapticAction, "haptic");
        break;
      case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING:
      default:
        break;
      }
    }
  }

  void HandleSessionStateChangedEvent(const XrEventDataSessionStateChanged& stateChangedEvent, bool* exitRenderLoop, bool* requestRestart) {
    const XrSessionState oldState = m_sessionState;
    m_sessionState = stateChangedEvent.state;

    //Log::Write(Log::Level::Info, Fmt("XrEventDataSessionStateChanged: state %s->%s session=%lld time=%lld", to_string(oldState), to_string(m_sessionState), stateChangedEvent.session, stateChangedEvent.time));

    if ((stateChangedEvent.session != XR_NULL_HANDLE) && (stateChangedEvent.session != m_session)) {
      //Log::Write(Log::Level::Error, "XrEventDataSessionStateChanged for unknown session");
      return;
    }

    switch (m_sessionState) {
    case XR_SESSION_STATE_READY: {
      CHECK(m_session != XR_NULL_HANDLE);
      XrSessionBeginInfo sessionBeginInfo{ XR_TYPE_SESSION_BEGIN_INFO };
      sessionBeginInfo.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
      OXR(xrBeginSession(m_session, &sessionBeginInfo));
      m_sessionRunning = true;
      break;
    }
    case XR_SESSION_STATE_STOPPING: {
      CHECK(m_session != XR_NULL_HANDLE);
      m_sessionRunning = false;
      OXR(xrEndSession(m_session))
        break;
    }
    case XR_SESSION_STATE_EXITING: {
      *exitRenderLoop = true;
      // Do not attempt to restart because user closed this session.
      *requestRestart = false;
      break;
    }
    case XR_SESSION_STATE_LOSS_PENDING: {
      *exitRenderLoop = true;
      // Poll for a new instance.
      *requestRestart = true;
      break;
    }
    default:
      break;
    }
  }

  //  void LogActionSourceName(XrAction action, const std::string& actionName) const {
  //    XrBoundSourcesForActionEnumerateInfo getInfo = { XR_TYPE_BOUND_SOURCES_FOR_ACTION_ENUMERATE_INFO };
  //    getInfo.action = action;
  //    uint32_t pathCount = 0;
  //    OXR(xrEnumerateBoundSourcesForAction(m_session, &getInfo, 0, &pathCount, nullptr));
  //    std::vector<XrPath> paths(pathCount);
  //    OXR(xrEnumerateBoundSourcesForAction(m_session, &getInfo, uint32_t(paths.size()), &pathCount, paths.data()));
  //
  //    std::string sourceName;
  //    for (uint32_t i = 0; i < pathCount; ++i) {
  //      constexpr XrInputSourceLocalizedNameFlags all = XR_INPUT_SOURCE_LOCALIZED_NAME_USER_PATH_BIT |
  //        XR_INPUT_SOURCE_LOCALIZED_NAME_INTERACTION_PROFILE_BIT |
  //        XR_INPUT_SOURCE_LOCALIZED_NAME_COMPONENT_BIT;
  //
  //      XrInputSourceLocalizedNameGetInfo nameInfo = { XR_TYPE_INPUT_SOURCE_LOCALIZED_NAME_GET_INFO };
  //      nameInfo.sourcePath = paths[i];
  //      nameInfo.whichComponents = all;
  //
  //      uint32_t size = 0;
  //      OXR(xrGetInputSourceLocalizedName(m_session, &nameInfo, 0, &size, nullptr));
  //      if (size < 1) {
  //        continue;
  //      }
  //      std::vector<char> grabSource(size);
  //      OXR(xrGetInputSourceLocalizedName(m_session, &nameInfo, uint32_t(grabSource.size()), &size, grabSource.data()));
  //      if (!sourceName.empty()) {
  //        sourceName += " and ";
  //      }
  //      sourceName += "'";
  //      sourceName += std::string(grabSource.data(), size - 1);
  //      sourceName += "'";
  //    }
  //
  //    //Log::Write(Log::Level::Info, Fmt("%s action is bound to %s", actionName.c_str(), ((!sourceName.empty()) ? sourceName.c_str() : "nothing")));
  //  }

  bool IsSessionRunning() const { return m_sessionRunning; }

  bool IsSessionFocused() const { return m_sessionState == XR_SESSION_STATE_FOCUSED; }

  void PollActions() {
    // Sync actions
    const XrActiveActionSet activeActionSet{ actionSet, XR_NULL_PATH };
    XrActionsSyncInfo syncInfo{ XR_TYPE_ACTIONS_SYNC_INFO };
    syncInfo.countActiveActionSets = 1;
    syncInfo.activeActionSets = &activeActionSet;
    OXR(xrSyncActions(m_session, &syncInfo));

    static float joystick_x[Side::COUNT] = { 0 };
    static float joystick_y[Side::COUNT] = { 0 };
    static float trigger[Side::COUNT] = { 0 };
    static float squeeze[Side::COUNT] = { 0 };

    // Get pose and grab action state and start haptic vibrate when hand is 90% squeezed.
    for (auto hand : { Side::LEFT, Side::RIGHT })
    {
      XrControllerEvent& applicationEvent = Global::inputXRControllerEvent[hand];
      applicationEvent.controllerEventBit = ControllerEventBit::none;

      XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
      getInfo.subactionPath = handSubactionPath[hand];
      //menu click
      getInfo.action = menuAction;
      XrActionStateBoolean homeValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &homeValue));
      if (homeValue.isActive == XR_TRUE) {
        if (homeValue.changedSinceLastSync == XR_TRUE) {
          applicationEvent.controllerEventBit |= ControllerEventBit::click_menu;
          if (homeValue.currentState == XR_TRUE) {
            applicationEvent.click_menu = true;
          }
          else {
            applicationEvent.click_menu = false;
          }
        }
      }

      //thumbstick value x/y
      getInfo.action = thumbstickValueAction;
      XrActionStateVector2f thumbstickValue{ XR_TYPE_ACTION_STATE_VECTOR2F };
      OXR(xrGetActionStateVector2f(m_session, &getInfo, &thumbstickValue));
      if (thumbstickValue.isActive == XR_TRUE) {
        if (thumbstickValue.currentState.x == 0 && thumbstickValue.currentState.y == 0 && joystick_x[hand] == 0 && joystick_y[hand] == 0) {
        }
        else {
          applicationEvent.controllerEventBit |= ControllerEventBit::value_thumbstick;
          applicationEvent.thumbstick_x = thumbstickValue.currentState.x;
          applicationEvent.thumbstick_y = thumbstickValue.currentState.y;
        }
        joystick_x[hand] = thumbstickValue.currentState.x;
        joystick_y[hand] = thumbstickValue.currentState.y;
      }
      // thumbstick click
      getInfo.action = thumbstickClickAction;
      XrActionStateBoolean thumbstickClick{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &thumbstickClick));
      if ((thumbstickClick.isActive == XR_TRUE) && (thumbstickClick.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::click_thumbstick;
        if (thumbstickClick.currentState == XR_TRUE) {
          applicationEvent.click_thumbstick = true;
        }
        else {
          applicationEvent.click_thumbstick = false;
        }
      }
      // thumbstick touch
      getInfo.action = thumbstickTouchAction;
      XrActionStateBoolean thumbstickTouch{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &thumbstickTouch));
      if (thumbstickTouch.isActive == XR_TRUE) {
        if (thumbstickTouch.changedSinceLastSync == XR_TRUE) {
          applicationEvent.controllerEventBit |= ControllerEventBit::touch_thumbstick;
          if (thumbstickTouch.currentState == XR_TRUE) {
            applicationEvent.touch_thumbstick = true;
          }
          else {
            applicationEvent.touch_thumbstick = false;
          }
        }
      }

      //trigger value
      getInfo.action = triggerValueAction;
      XrActionStateFloat triggerValue{ XR_TYPE_ACTION_STATE_FLOAT };
      OXR(xrGetActionStateFloat(m_session, &getInfo, &triggerValue));
      if (triggerValue.isActive == XR_TRUE) {
        if (triggerValue.currentState == 0 && trigger[hand] == 0) {
        }
        else {
          applicationEvent.controllerEventBit |= ControllerEventBit::value_trigger;
          applicationEvent.trigger = triggerValue.currentState;
        }
        trigger[hand] = triggerValue.currentState;
      }
      // trigger click
      getInfo.action = triggerClickAction;
      XrActionStateBoolean TriggerClick{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &TriggerClick));
      if (TriggerClick.isActive == XR_TRUE) {
        if (TriggerClick.changedSinceLastSync == XR_TRUE) {
          applicationEvent.controllerEventBit |= ControllerEventBit::click_trigger;
          if (TriggerClick.currentState == XR_TRUE) {
            applicationEvent.click_trigger = true;
          }
          else {
            applicationEvent.click_trigger = false;
          }
        }
      }
      // trigger touch
      getInfo.action = triggerTouchAction;
      XrActionStateBoolean TriggerTouch{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &TriggerTouch));
      if (TriggerTouch.isActive == XR_TRUE) {
        if (TriggerTouch.changedSinceLastSync == XR_TRUE) {
          applicationEvent.controllerEventBit |= ControllerEventBit::touch_trigger;
          if (TriggerTouch.currentState == XR_TRUE) {
            applicationEvent.touch_trigger = true;
          }
          else {
            applicationEvent.touch_trigger = false;
          }
        }
      }

      // squeeze value
      getInfo.action = squeezeValueAction;
      XrActionStateFloat squeezeValue{ XR_TYPE_ACTION_STATE_FLOAT };
      OXR(xrGetActionStateFloat(m_session, &getInfo, &squeezeValue));
      if (squeezeValue.isActive == XR_TRUE) {
        if (squeezeValue.currentState == 0 && squeeze[hand] == 0) {
        }
        else {
          applicationEvent.controllerEventBit |= ControllerEventBit::value_squeeze;
          applicationEvent.squeeze = squeezeValue.currentState;
        }
        squeeze[hand] = squeezeValue.currentState;
      }
      // squeeze click
      getInfo.action = squeezeClickAction;
      XrActionStateBoolean squeezeClick{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &squeezeClick));
      if ((squeezeClick.isActive == XR_TRUE) && (squeezeClick.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::click_squeeze;
        if (squeezeClick.currentState == XR_TRUE) {
          applicationEvent.click_squeeze = true;
        }
        else {
          applicationEvent.click_squeeze = false;
        }
      }

      // A button
      getInfo.action = AAction;
      XrActionStateBoolean AValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &AValue));
      if ((AValue.isActive == XR_TRUE) && (AValue.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::click_a;
        if (AValue.currentState == XR_TRUE) {
          applicationEvent.click_a = true;
        }
        else {
          applicationEvent.click_a = false;
        }
      }
      // B button
      getInfo.action = BAction;
      XrActionStateBoolean BValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &BValue));
      if ((BValue.isActive == XR_TRUE) && (BValue.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::click_b;
        if (BValue.currentState == XR_TRUE) {
          applicationEvent.click_b = true;
        }
        else {
          applicationEvent.click_b = false;
        }
      }
      // X button
      getInfo.action = XAction;
      XrActionStateBoolean XValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &XValue));
      if ((XValue.isActive == XR_TRUE) && (XValue.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::click_x;
        if (XValue.currentState == XR_TRUE) {
          applicationEvent.click_x = true;
        }
        else {
          applicationEvent.click_x = false;
        }
      }
      // Y button
      getInfo.action = YAction;
      XrActionStateBoolean YValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &YValue));
      if ((YValue.isActive == XR_TRUE) && (YValue.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::click_y;
        if (YValue.currentState == XR_TRUE) {
          applicationEvent.click_y = true;
        }
        else {
          applicationEvent.click_y = false;
        }
      }

      // A button touch
      getInfo.action = ATouchAction;
      XrActionStateBoolean ATouchValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &ATouchValue));
      if ((ATouchValue.isActive == XR_TRUE) && (ATouchValue.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::touch_a;
        if (ATouchValue.currentState == XR_TRUE) {
          applicationEvent.touch_a = true;
        }
        else {
          applicationEvent.touch_a = false;
        }
      }
      // B button touch
      getInfo.action = BTouchAction;
      XrActionStateBoolean BTouchValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &BTouchValue));
      if ((BTouchValue.isActive == XR_TRUE) && (BTouchValue.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::touch_b;
        if (BTouchValue.currentState == XR_TRUE) {
          applicationEvent.touch_b = true;
        }
        else {
          applicationEvent.touch_b = false;
        }
      }
      // X button touch
      getInfo.action = XTouchAction;
      XrActionStateBoolean XTouchValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &XTouchValue));
      if ((XTouchValue.isActive == XR_TRUE) && (XTouchValue.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::touch_x;
        if (XTouchValue.currentState == XR_TRUE) {
          applicationEvent.touch_x = true;
        }
        else {
          applicationEvent.touch_x = false;
        }
      }
      // Y button touch
      getInfo.action = YTouchAction;
      XrActionStateBoolean YTouchValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
      OXR(xrGetActionStateBoolean(m_session, &getInfo, &YTouchValue));
      if ((YTouchValue.isActive == XR_TRUE) && (YTouchValue.changedSinceLastSync == XR_TRUE)) {
        applicationEvent.controllerEventBit |= ControllerEventBit::touch_y;
        if (YTouchValue.currentState == XR_TRUE) {
          applicationEvent.touch_y = true;
        }
        else {
          applicationEvent.touch_y = false;
        }
      }
    }

    if (m_extentions.isSupportEyeTracking && m_extentions.activeEyeTracking) {
      XrActionStatePose actionStatePose{ XR_TYPE_ACTION_STATE_POSE };
      XrActionStateGetInfo getActionStateInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
      getActionStateInfo.action = gazeAction;
      OXR(xrGetActionStatePose(m_session, &getActionStateInfo, &actionStatePose));
      gazeActive = actionStatePose.isActive;
    }
  }

  void RenderFrame() {
    CHECK(m_session != XR_NULL_HANDLE);
    XrFrameWaitInfo frameWaitInfo{ XR_TYPE_FRAME_WAIT_INFO };
    XrFrameState frameState{ XR_TYPE_FRAME_STATE };
    OXR(xrWaitFrame(m_session, &frameWaitInfo, &frameState));

    XrFrameBeginInfo frameBeginInfo{ XR_TYPE_FRAME_BEGIN_INFO };
    OXR(xrBeginFrame(m_session, &frameBeginInfo));

    std::vector<XrCompositionLayerBaseHeader*> layers;
    XrCompositionLayerProjection layer{ XR_TYPE_COMPOSITION_LAYER_PROJECTION };
    std::vector<XrCompositionLayerProjectionView> projectionLayerViews;
    if (frameState.shouldRender == XR_TRUE) {
      if (RenderLayer(frameState.predictedDisplayTime, projectionLayerViews, layer)) {
        layers.push_back(reinterpret_cast<XrCompositionLayerBaseHeader*>(&layer));
      }
    }

    if (m_extentions.activePassthrough) {
      XrCompositionLayerPassthroughFB compositionLayerPassthrough = { XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB };
      compositionLayerPassthrough.layerHandle = m_passthroughLayerReconstruction;
      //passthrough_layer.layerHandle = m_passthroughLayer_project;
      compositionLayerPassthrough.flags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
      compositionLayerPassthrough.space = XR_NULL_HANDLE;
      layers.push_back(reinterpret_cast<XrCompositionLayerBaseHeader*>(&compositionLayerPassthrough));
    }

    XrFrameEndInfo frameEndInfo{ XR_TYPE_FRAME_END_INFO };
    frameEndInfo.displayTime = frameState.predictedDisplayTime;
    frameEndInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
    frameEndInfo.layerCount = (uint32_t)layers.size();
    frameEndInfo.layers = layers.data();
    OXR(xrEndFrame(m_session, &frameEndInfo));
  }

  bool RenderLayer(XrTime predictedDisplayTime, std::vector<XrCompositionLayerProjectionView>& projectionLayerViews, XrCompositionLayerProjection& layer) {
    XrResult res;
    XrViewState viewState{ XR_TYPE_VIEW_STATE };
    uint32_t viewCapacityInput = (uint32_t)m_views.size();
    uint32_t viewCountOutput;
    XrViewLocateInfo viewLocateInfo{ XR_TYPE_VIEW_LOCATE_INFO };
    viewLocateInfo.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
    viewLocateInfo.displayTime = predictedDisplayTime;
    viewLocateInfo.space = m_appSpace;

    res = xrLocateViews(m_session, &viewLocateInfo, &viewState, viewCapacityInput, &viewCountOutput, m_views.data());
    CHECK_XRRESULT(res, "xrLocateViews");
    if ((viewState.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) == 0 || (viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) == 0) {
      return false;  // There is no valid tracking poses for the views.
    }

    // get ipd
    float ipd = sqrt(pow(abs(m_views[1].pose.position.x - m_views[0].pose.position.x), 2) + pow(abs(m_views[1].pose.position.y - m_views[0].pose.position.y), 2) + pow(abs(m_views[1].pose.position.z - m_views[0].pose.position.z), 2));

    CHECK(viewCountOutput == viewCapacityInput);
    CHECK(viewCountOutput == m_configViews.size());
    CHECK(viewCountOutput == m_swapchains.size());

    projectionLayerViews.resize(viewCountOutput);

    XrActionStateGetInfo actionStateGetInfo{ XR_TYPE_ACTION_STATE_GET_INFO };

    actionStateGetInfo.action = gripPoseAction;

    for (u8 i = 0; i < 2; ++i)
    {
      actionStateGetInfo.subactionPath = handSubactionPath[i];
      res = xrGetActionStatePose(m_session, &actionStateGetInfo, &Global::mControllerPoseState[i]);
      CHECK_XRRESULT(res, "xrGetActionStatePose");

      XrSpaceLocation spaceLocation{ XR_TYPE_SPACE_LOCATION };
      res = xrLocateSpace(gripSpace[i], m_appSpace, predictedDisplayTime, &spaceLocation);
      CHECK_XRRESULT(res, "xrLocateSpace");
      if (XR_UNQUALIFIED_SUCCESS(res) &&
          (spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0 &&
          (spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0)
      {
        Global::mControllerModel[i] = Xr::mat4_CreateTranslationRotation(spaceLocation.pose.position, spaceLocation.pose.orientation);
        //mControllerPose[i] = spaceLocation.pose;
      }
      else
      {
        Global::mControllerPoseState[i].isActive = false;
      }
    }

    for (u8 i = 0; i < 2; ++i)
    {
      XrSpaceLocation spaceLocation{ XR_TYPE_SPACE_LOCATION };
      res = xrLocateSpace(aimSpace[i], m_appSpace, predictedDisplayTime, &spaceLocation);
      CHECK_XRRESULT(res, "xrLocateSpace");
      if (XR_UNQUALIFIED_SUCCESS(res)) {
        if ((spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0 &&
            (spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0) {
          Global::mAimModel[i] = Xr::mat4_CreateTranslationRotation(spaceLocation.pose.position, spaceLocation.pose.orientation);
          Global::mAimPose[i] = spaceLocation.pose;
        }
      }
    }

    //stage
    {
      XrSpaceLocation spaceLocation{ XR_TYPE_SPACE_LOCATION };
      res = xrLocateSpace(m_stageSpace, m_appSpace, predictedDisplayTime, &spaceLocation);
      CHECK_XRRESULT(res, "xrLocateSpace");
      if (XR_UNQUALIFIED_SUCCESS(res)) {
        if ((spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0 &&
            (spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0) {
          Global::mStageModel = Xr::mat4_CreateTranslationRotation(spaceLocation.pose.position, spaceLocation.pose.orientation);
          Global::mStagePose = spaceLocation.pose;
        }
      }
    }

    //eye tracking
    if (m_extentions.isSupportEyeTracking && m_extentions.activeEyeTracking) {
      if (gazeActive) {
        XrEyeGazeSampleTimeEXT eyeGazeSampleTime{ XR_TYPE_EYE_GAZE_SAMPLE_TIME_EXT };
        XrSpaceLocation gazeLocation{ XR_TYPE_SPACE_LOCATION, &eyeGazeSampleTime };
        res = xrLocateSpace(gazeActionSpace, m_appSpace, predictedDisplayTime, &gazeLocation);
        //Log::Write(Log::Level::Info, Fmt("gazeActionSpace pose(%f %f %f)  orientation(%f %f %f %f)",
        //                                gazeLocation.pose.position.x, gazeLocation.pose.position.y, gazeLocation.pose.position.z,
        //                                gazeLocation.pose.orientation.x, gazeLocation.pose.orientation.y, gazeLocation.pose.orientation.z, gazeLocation.pose.orientation.w));
        m_application.setGazeLocation(gazeLocation, m_views, ipd, res);
      }
    }

    //hand tracking
    XrHandJointLocationEXT jointLocations[Side::COUNT][XR_HAND_JOINT_COUNT_EXT];
    for (auto hand : { Side::LEFT, Side::RIGHT }) {
      XrHandJointLocationsEXT locations{ XR_TYPE_HAND_JOINT_LOCATIONS_EXT };
      locations.jointCount = XR_HAND_JOINT_COUNT_EXT;
      locations.jointLocations = jointLocations[hand];

      XrHandJointsLocateInfoEXT locateInfo{ XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT };
      locateInfo.baseSpace = m_appSpace;
      locateInfo.time = predictedDisplayTime;
      XrResult res = xrLocateHandJointsEXT(m_handTracker[hand], &locateInfo, &locations);
      if (res != XR_SUCCESS) {
        //Log::Write(Log::Level::Error, Fmt("m_pfnXrLocateHandJointsEXT res %d", res));
      }
    }
    XrHandJointLocationEXT& leftIndexTip = jointLocations[Side::LEFT][XR_HAND_JOINT_INDEX_TIP_EXT];
    XrHandJointLocationEXT& rightIndexTip = jointLocations[Side::RIGHT][XR_HAND_JOINT_INDEX_TIP_EXT];
    //Log::Write(Log::Level::Error, Fmt("leftIndexTip.locationFlags:%d, rightIndexTip.locationFlags %d", leftIndexTip.locationFlags, rightIndexTip.locationFlags));
    if ((leftIndexTip.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0 && (rightIndexTip.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0) {
      XrVector3f distance;
      XrVector3f_Sub(&distance, &leftIndexTip.pose.position, &rightIndexTip.pose.position);
      float len = XrVector3f_Length(&distance);
      // bring center of index fingers to within 1cm. Probably fine for most humans, unless
      // they have huge fingers.
      if (len < 0.01f) {
        //Log::Write(Log::Level::Error, Fmt("len %f", len));
      }
    }
    for (auto hand : { Side::LEFT, Side::RIGHT }) {
      for (int i = 0; i < XR_HAND_JOINT_COUNT_EXT; i++) {
        XrHandJointLocationEXT& jointLocation = jointLocations[hand][i];
        if ((jointLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0) {
          //-------------------------------------------------------------
          XrHandJointLocationEXT reference_position = jointLocations[hand][1];
          XrHandJointLocationEXT reference;
          if (i == 2 || i == 7 || i == 12 || i == 17 || i == 21) {
            reference = jointLocations[hand][1];
          }
          else {
            reference = jointLocations[hand][i - 1];
          }
          if (i > 1) {
            XrQuaternionf orientation;
            XrQuaternionf_Multiply(&orientation, &jointLocation.pose.orientation, &reference.pose.orientation);
            jointLocation.pose.orientation = orientation;

            //jointLocation.pose.position.x = jointLocation.pose.position.x - reference_position.pose.position.x;
            //jointLocation.pose.position.y = jointLocation.pose.position.y - reference_position.pose.position.y;
            //jointLocation.pose.position.z = jointLocation.pose.position.z - reference_position.pose.position.z;
          }
          //--------------------------------------------------------------
        }
      }
    }
    m_application.setHandJointLocation((XrHandJointLocationEXT*)jointLocations);
    //end hand tracking

    XrSpaceVelocity velocity{ XR_TYPE_SPACE_VELOCITY };
    XrSpaceLocation spaceLocation{ XR_TYPE_SPACE_LOCATION, &velocity };
    res = xrLocateSpace(m_ViewSpace, m_appSpace, predictedDisplayTime, &spaceLocation);
    CHECK_XRRESULT(res, "xrLocateSpace");


    XrPosef pose[Side::COUNT];
    for (uint32_t i = 0; i < viewCountOutput; i++) {
      pose[i] = m_views[i].pose;
    }

    Ui::tick(Global::xrResolutionWidth, Global::xrResolutionHeight);

    // Render view to the appropriate part of the swapchain image.
    for (uint32_t i = 0; i < viewCountOutput; i++) {
      // Each view has a separate swapchain which is acquired, rendered to, and released.
      const Swapchain viewSwapchain = m_swapchains[i];
      XrSwapchainImageAcquireInfo acquireInfo{ XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
      uint32_t swapchainImageIndex;
      OXR(xrAcquireSwapchainImage(viewSwapchain.handle, &acquireInfo, &swapchainImageIndex));

      XrSwapchainImageWaitInfo waitInfo{ XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO };
      waitInfo.timeout = XR_INFINITE_DURATION;
      OXR(xrWaitSwapchainImage(viewSwapchain.handle, &waitInfo));

      projectionLayerViews[i] = { XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW };
      projectionLayerViews[i].pose = pose[i];
      projectionLayerViews[i].fov = m_views[i].fov;
      projectionLayerViews[i].subImage.swapchain = viewSwapchain.handle;
      projectionLayerViews[i].subImage.imageRect.offset = { 0, 0 };
      projectionLayerViews[i].subImage.imageRect.extent = { viewSwapchain.width, viewSwapchain.height };

      ASSERT(projectionLayerViews[i].subImage.imageArrayIndex == 0);  // Texture arrays not supported.
      glBindFramebuffer(GL_FRAMEBUFFER, m_swapchainFramebuffer);
      {
        const XrSwapchainImageBaseHeader* const swapchainImage = m_swapchainImages[viewSwapchain.handle][swapchainImageIndex];
        const GLuint colorTexture = reinterpret_cast<const XrSwapchainImageOpenGLESKHR*>(swapchainImage)->image;
        const GLuint depthTexture = Xr::GetDepthTexture(colorTexture);
        Xr::RenderView(projectionLayerViews[i], colorTexture, depthTexture, Global::mControllerPoseState, Global::mAimPose, Global::mControllerModel, Global::mAimModel, Global::mStageModel, m_jointLocations);
      }

      XrSwapchainImageReleaseInfo releaseInfo{ XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
      OXR(xrReleaseSwapchainImage(viewSwapchain.handle, &releaseInfo));
    }

    Ui::renderClear();

    layer.space = m_appSpace;
    if (m_extentions.activePassthrough) {
      layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
    }
    layer.viewCount = (uint32_t)projectionLayerViews.size();
    layer.views = projectionLayerViews.data();
    return true;
  }


private:
  IPlatformPlugin* m_platformPlugin;
  OpenGLESGraphicsPlugin* m_graphicsPlugin;
  XrInstance m_instance{ XR_NULL_HANDLE };
  XrSession m_session{ XR_NULL_HANDLE };
  XrSpace m_appSpace{ XR_NULL_HANDLE };
  XrSpace m_stageSpace{ XR_NULL_HANDLE };
  XrSystemId m_systemId{ XR_NULL_SYSTEM_ID };

  std::vector<XrViewConfigurationView> m_configViews;
  std::vector<Swapchain> m_swapchains;
  std::map<XrSwapchain, std::vector<XrSwapchainImageBaseHeader*>> m_swapchainImages;
  std::vector<XrView> m_views;
  int64_t m_colorSwapchainFormat{ -1 };

  // Application's current lifecycle state according to the runtime
  XrSessionState m_sessionState{ XR_SESSION_STATE_UNKNOWN };
  bool m_sessionRunning{ false };

  XrEventDataBuffer m_eventDataBuffer;
  InputState m_input;

  XrSpace m_ViewSpace{ XR_NULL_HANDLE };

  Application m_application;

  //for XR_FB_passthrough
  XrPassthroughFB m_passthrough{ XR_NULL_HANDLE };
  XrPassthroughLayerFB m_passthroughLayerReconstruction{ XR_NULL_HANDLE };
  XrPassthroughLayerFB m_passthroughLayerProject{ XR_NULL_HANDLE };
  XrTriangleMeshFB m_triangleMesh{ XR_NULL_HANDLE };
  XrGeometryInstanceFB m_geometryInstance{ XR_NULL_HANDLE };
  //XR_FB_passthrough PFN
  PFN_DECLARE(xrCreatePassthroughFB);
  PFN_DECLARE(xrDestroyPassthroughFB);
  PFN_DECLARE(xrPassthroughStartFB);
  PFN_DECLARE(xrPassthroughPauseFB);
  PFN_DECLARE(xrCreatePassthroughLayerFB);
  PFN_DECLARE(xrDestroyPassthroughLayerFB);
  PFN_DECLARE(xrPassthroughLayerSetStyleFB);
  PFN_DECLARE(xrPassthroughLayerPauseFB);
  PFN_DECLARE(xrPassthroughLayerResumeFB);
  PFN_DECLARE(xrCreateGeometryInstanceFB);
  PFN_DECLARE(xrDestroyGeometryInstanceFB);
  PFN_DECLARE(xrGeometryInstanceSetTransformFB);
  // FB_triangle_mesh PFN
  PFN_DECLARE(xrCreateTriangleMeshFB);
  PFN_DECLARE(xrDestroyTriangleMeshFB);
  PFN_DECLARE(xrTriangleMeshGetVertexBufferFB);
  PFN_DECLARE(xrTriangleMeshGetIndexBufferFB);
  PFN_DECLARE(xrTriangleMeshBeginUpdateFB);
  PFN_DECLARE(xrTriangleMeshEndUpdateFB);
  PFN_DECLARE(xrTriangleMeshBeginVertexBufferUpdateFB);
  PFN_DECLARE(xrTriangleMeshEndVertexBufferUpdateFB);


  //hand tracking
  PFN_DECLARE(xrCreateHandTrackerEXT);
  PFN_DECLARE(xrDestroyHandTrackerEXT);
  PFN_DECLARE(xrLocateHandJointsEXT);
  XrHandTrackerEXT m_handTracker[Side::COUNT] = { 0 };

  Extentions m_extentions;

  //XrControllerEvent m_applicationEvent[Side::COUNT] = {0};
  uint32_t m_deviceROM;

};












static int32_t onInputEvent(struct android_app* app, AInputEvent* event) {
  int type = AInputEvent_getType(event);
  if (type == AINPUT_EVENT_TYPE_KEY) {
    int32_t action = AKeyEvent_getAction(event);
    int32_t code = AKeyEvent_getKeyCode(event);
    //Log::Write(Log::Level::Info, __FILE__, __LINE__, Fmt("onInputEvent:%d %d\n", code, action));
  }
  return 0;
}


namespace {
  struct AndroidPlatformPlugin : public IPlatformPlugin {
    AndroidPlatformPlugin() {
      instanceCreateInfoAndroid = { XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR };
      instanceCreateInfoAndroid.applicationVM = Global::g_JVM;
      instanceCreateInfoAndroid.applicationActivity = Global::androidActivity;

      PFN_xrInitializeLoaderKHR xrInitializeLoaderKHR;
      xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR", (PFN_xrVoidFunction*)&xrInitializeLoaderKHR);
      if (xrInitializeLoaderKHR != NULL) {
        XrLoaderInitInfoAndroidKHR loaderInitializeInfoAndroid;
        memset(&loaderInitializeInfoAndroid, 0, sizeof(loaderInitializeInfoAndroid));
        loaderInitializeInfoAndroid.type = XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR;
        loaderInitializeInfoAndroid.next = nullptr;
        loaderInitializeInfoAndroid.applicationVM = Global::g_JVM;
        loaderInitializeInfoAndroid.applicationContext = Global::androidActivity;
        xrInitializeLoaderKHR((XrLoaderInitInfoBaseHeaderKHR*)&loaderInitializeInfoAndroid);
      }
    }

    std::vector<std::string> GetInstanceExtensions() const override { return { XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME }; }

    XrBaseInStructure* GetInstanceCreateExtension() const override { return (XrBaseInStructure*)&instanceCreateInfoAndroid; }

    XrInstanceCreateInfoAndroidKHR instanceCreateInfoAndroid;
  };
}  // namespace

void android_main(struct android_app* app) {
  JNIEnv* Env;
  app->activity->vm->AttachCurrentThread(&Env, nullptr);

  AndroidAppState appState = {};

  app->userData = &appState;
  app->onAppCmd = app_handle_cmd;
  app->onInputEvent = onInputEvent;

  Global::g_JVM = app->activity->vm;
  Global::androidActivity = app->activity->clazz;

  bool requestRestart = false;

  // Create platform-specific implementation.
  AndroidPlatformPlugin platformPlugin;
  // Create graphics API implementation.
  OpenGLESGraphicsPlugin graphicsPlugin;

  // Initialize the OpenXR program.
  OpenXrProgram program(&platformPlugin, &graphicsPlugin);

  // Initialize the loader for this platform
  PFN_xrInitializeLoaderKHR initializeLoader = nullptr;
  if (XR_SUCCEEDED(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR", (PFN_xrVoidFunction*)(&initializeLoader)))) {
    XrLoaderInitInfoAndroidKHR loaderInitInfoAndroid;
    memset(&loaderInitInfoAndroid, 0, sizeof(loaderInitInfoAndroid));
    loaderInitInfoAndroid.type = XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR;
    loaderInitInfoAndroid.next = NULL;
    loaderInitInfoAndroid.applicationVM = app->activity->vm;
    loaderInitInfoAndroid.applicationContext = app->activity->clazz;
    initializeLoader((const XrLoaderInitInfoBaseHeaderKHR*)&loaderInitInfoAndroid);
  }

  program.CreateInstance();
  program.InitializeSystem();
  program.InitializeSession();
  program.CreateSwapchains();
  program.InitializeApplication();

  Settings::init();
  init();

  while (app->destroyRequested == 0) {
    // Read all pending events.
    for (;;) {
      int events;
      struct android_poll_source* source;
      // If the timeout is zero, returns immediately without blocking.
      // If the timeout is negative, waits indefinitely until an event appears.
      const int timeoutMilliseconds = (!appState.Resumed && !program.IsSessionRunning() && app->destroyRequested == 0) ? -1 : 0;
      if (ALooper_pollOnce(timeoutMilliseconds, nullptr, &events, (void**)&source) < 0) {
        break;
      }

      // Process this event.
      if (source != nullptr) {
        source->process(app, source);
      }
    }

    program.PollEvents(Global::appQuit, requestRestart);

    if (Global::appQuit && !requestRestart) {
      ANativeActivity_finish(app->activity);
    }

    if (!program.IsSessionRunning()) {
      // Throttle loop since xrWaitFrame won't be called.
      std::this_thread::sleep_for(std::chrono::milliseconds(250));
      continue;
    }

    program.PollActions();

    {
      if (Global::inputXRControllerEvent[0].click_trigger)
        Global::inputXRActiveController = XrActiveController::left;
      else if (Global::inputXRControllerEvent[1].click_trigger)
        Global::inputXRActiveController = XrActiveController::right;
      {
        Input::handleInputBegin();
        Ui::handleInputBegin();
        Xr::handleXrControllerInput();
        Ui::handleInputXr();
        Ui::handleInputEnd();
        Input::handleInputEnd();
        Input::proccessInputEvents();
      }
    }
    tick();

    program.RenderFrame();
  }
  app->activity->vm->DetachCurrentThread();
  fini();
  unreachable();
}

#endif // PLATFORM_PICO_4
