diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
index 8fd828f..043ac04 100644
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -25,7 +25,7 @@ project("jnicpp")
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
-set(SRC_DIR ../../../../../my_gl_app)
+set(SRC_DIR my_gl_app)
# Get source files from my_gl_app directory
file(GLOB_RECURSE MY_GL_APP_SRC CONFIGURE_DEPENDS
"${SRC_DIR}/*.cpp"
diff --git a/app/src/main/cpp/cpp/CMakeLists.txt b/app/src/main/cpp/cpp/CMakeLists.txt
new file mode 100644
index 0000000..8fd828f
--- /dev/null
+++ b/app/src/main/cpp/cpp/CMakeLists.txt
@@ -0,0 +1,50 @@
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html.
+# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
+# Sets the minimum CMake version required for this project.
+cmake_minimum_required(VERSION 3.22.1)
+# Set C++ standard
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
+# Since this is the top level CMakeLists.txt, the project name is also accessible
+# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
+# build script scope).
+
+project("jnicpp")
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+#
+# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
+# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
+# is preferred for the same purpose.
+#
+# In order to load a library into your app from Java/Kotlin, you must call
+# System.loadLibrary() and pass the name of the library defined here;
+# for GameActivity/NativeActivity derived applications, the same library name must be
+# used in the AndroidManifest.xml file.
+set(SRC_DIR ../../../../../my_gl_app)
+# Get source files from my_gl_app directory
+file(GLOB_RECURSE MY_GL_APP_SRC CONFIGURE_DEPENDS
+"${SRC_DIR}/*.cpp"
+"${SRC_DIR}/*.h"
+"${SRC_DIR}/*.hpp"
+)
+# Include directories
+include_directories(${SRC_DIR})
+include_directories(${CMAKE_CURRENT_SOURCE_DIR})
+add_library(${CMAKE_PROJECT_NAME} SHARED
+# List C/C++ source files with relative paths to this CMakeLists.txt.
+NativeTemplate.cpp
+${MY_GL_APP_SRC})
+# Specifies libraries CMake should link to your target library. You
+# can link libraries from various origins, such as libraries defined in this
+# build script, prebuilt third-party libraries, or Android system libraries.
+target_link_libraries(${CMAKE_PROJECT_NAME}
+# List libraries link to the target library
+android
+log
+GLESv3
+EGL)
diff --git a/app/src/main/cpp/cpp/NativeTemplate.cpp b/app/src/main/cpp/cpp/NativeTemplate.cpp
new file mode 100644
index 0000000..c698a87
--- /dev/null
+++ b/app/src/main/cpp/cpp/NativeTemplate.cpp
@@ -0,0 +1,105 @@
+#include
+#include
+#include
+// Ensure JNI macros are available
+#ifndef JNIEXPORT
+
+#define JNIEXPORT
+#endif
+#ifndef JNICALL
+#define JNICALL
+#endif
+#include "GLRenderer.h"
+#include "UIRenderer.h"
+#include "PlatformBridge.h"
+// Define logging macros for this file
+#ifndef LOG_TAG
+#define LOG_TAG "NativeTemplate"
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
+#endif
+// Global renderer instance
+static GLRenderer* g_renderer = nullptr;
+extern "C" JNIEXPORT jstring JNICALL
+Java_com_example_jnicpp_MainActivity_stringFromJNI(
+ JNIEnv* env,
+ jobject /* this */) {
+ LOGI("stringFromJNI called");
+ std::string hello = "OpenGL ES 3.0 Triangle Demo";
+ return env->NewStringUTF(hello.c_str());
+}
+extern "C" JNIEXPORT jboolean JNICALL
+Java_com_example_jnicpp_MainActivity_initGL(
+ JNIEnv* env,
+ jobject thiz) {
+ LOGI("initGL called");
+// Cache a global ref to the activity so native code (e.g. Menu3State,
+// running on this same GL thread) can call back into Java later, e.g. to
+// launch BowlingCameraActivity. See PlatformBridge.h.
+ PlatformBridge::SetAndroidActivity(env, thiz);
+// Create renderer if not exists
+ if (g_renderer == nullptr) {
+ LOGI("Creating new GLRenderer instance");
+ g_renderer = new GLRenderer();
+ if (g_renderer == nullptr) {
+ LOGE("Failed to create GLRenderer instance");
+ return JNI_FALSE;
+ }
+ }
+// Initialize OpenGL renderer (no longer needs surface parameter)
+ bool success = g_renderer->initialize();
+ if (success) {
+ LOGI("OpenGL initialization successful");
+ } else {
+ LOGE("OpenGL initialization failed");
+ }
+
+ return success ? JNI_TRUE : JNI_FALSE;
+}
+extern "C" JNIEXPORT void JNICALL
+Java_com_example_jnicpp_MainActivity_renderFrame(
+ JNIEnv* env,
+ jobject /* this */) {
+ if (g_renderer != nullptr) {
+ g_renderer->render();
+ } else {
+ LOGE("Renderer is null in renderFrame");
+ }
+}
+extern "C" JNIEXPORT void JNICALL
+Java_com_example_jnicpp_MainActivity_onSurfaceResized(
+ JNIEnv* env,
+ jobject /* this */,
+ jint width,
+ jint height) {
+ LOGI("onSurfaceResized called: %dx%d", width, height);
+ glViewport(0, 0, width, height);
+ UI::Init(width, height); // no-op besides updating cached size if already initialized
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_example_jnicpp_MainActivity_nativeOnTouch(
+ JNIEnv* env,
+ jobject /* this */,
+ jfloat x,
+ jfloat y,
+ jboolean isDown) {
+ UI::SetPointerPosition(x, y);
+ UI::SetPointerDown(isDown == JNI_TRUE);
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_example_jnicpp_MainActivity_cleanupGL(
+ JNIEnv* env,
+ jobject /* this */) {
+ LOGI("cleanupGL called");
+ if (g_renderer != nullptr) {
+ g_renderer->cleanup();
+ delete g_renderer;
+ g_renderer = nullptr;
+ LOGI("GLRenderer cleaned up and deleted");
+ } else {
+ LOGI("GLRenderer was already null");
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/cpp/my_gl_app/GLRenderer.cpp b/app/src/main/cpp/my_gl_app/GLRenderer.cpp
new file mode 100644
index 0000000..ba022c5
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/GLRenderer.cpp
@@ -0,0 +1,347 @@
+#include "GLRenderer.h"
+#include "UIRenderer.h"
+#include "GameStateManager.h"
+#include
+
+// Define LOG_TAG for this file
+#define LOG_TAG "GLRenderer"
+
+GLRenderer::GLRenderer() : program(0), vertexShader(0),
+ fragmentShader(0), vbo(0), vao(0)
+#ifdef PLATFORM_WINDOWS
+ , window(nullptr)
+#endif
+{
+ LOGI("GLRenderer constructor called");
+}
+
+GLRenderer::~GLRenderer() {
+ LOGI("GLRenderer destructor called");
+ cleanup();
+}
+
+bool GLRenderer::initialize() {
+ LOGI("Initializing OpenGL ES renderer");
+
+#ifdef PLATFORM_WINDOWS
+ if (!initializeGLFW()) {
+ LOGE("Failed to initialize GLFW");
+ return false;
+ }
+#endif
+
+ if (!createShaders()) {
+ LOGE("Failed to create shaders");
+ return false;
+ }
+
+ if (!createBuffers()) {
+ LOGE("Failed to create buffers");
+ return false;
+ }
+
+ GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
+
+ LOGI("OpenGL ES renderer initialized successfully");
+ return true;
+}
+
+#ifdef PLATFORM_WINDOWS
+bool GLRenderer::initializeGLFW() {
+ LOGI("Initializing GLFW");
+
+ // Initialize GLFW
+ if (!glfwInit()) {
+ LOGE("Failed to initialize GLFW");
+ return false;
+ }
+
+ // Configure GLFW for OpenGL ES
+ glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+
+ // Create window. Tall enough that the menu layout (which also has to
+ // clear a ~350px safe-top margin on Android, see GameStateManager
+ // usage in render()) fits without buttons running off the bottom.
+ window = glfwCreateWindow(900, 1000, "OpenGL ES 3.0 Triangle Demo", nullptr, nullptr);
+ if (!window) {
+ LOGE("Failed to create GLFW window");
+ glfwTerminate();
+ return false;
+ }
+
+ glfwMakeContextCurrent(window);
+
+ // Initialize GLEW
+ glewExperimental = GL_TRUE;
+ if (glewInit() != GLEW_OK) {
+ LOGE("Failed to initialize GLEW");
+ return false;
+ }
+
+ glViewport(0, 0, 900, 1000);
+ UI::Init(900, 1000);
+
+ // Forward mouse input to the UI system so UI::WasClicked() works.
+ glfwSetCursorPosCallback(window, [](GLFWwindow*, double x, double y) {
+ UI::SetPointerPosition(static_cast(x), static_cast(y));
+ });
+ glfwSetMouseButtonCallback(window, [](GLFWwindow*, int button, int action, int /*mods*/) {
+ if (button == GLFW_MOUSE_BUTTON_LEFT) {
+ UI::SetPointerDown(action == GLFW_PRESS);
+ }
+ });
+ glfwSetFramebufferSizeCallback(window, [](GLFWwindow*, int width, int height) {
+ glViewport(0, 0, width, height);
+ UI::OnScreenResize(width, height);
+ });
+
+ LOGI("GLFW initialized successfully");
+ return true;
+}
+
+void GLRenderer::setWindow(GLFWwindow* win) {
+ window = win;
+}
+#endif
+
+bool GLRenderer::createShaders() {
+ LOGI("Creating shaders");
+
+ // Determine OpenGL version
+ const char* version = (const char*)glGetString(GL_VERSION);
+ LOGI("OpenGL version: %s", version ? version : "unknown");
+
+ bool useOpenGLES3 = false;
+ if (version && strstr(version, "OpenGL ES 3")) {
+ useOpenGLES3 = true;
+ LOGI("Using OpenGL ES 3.0 shaders");
+ } else {
+ LOGI("Using OpenGL ES 2.0 shaders");
+ }
+
+ // Choose appropriate shader sources
+ const char* vsSource = useOpenGLES3 ? vertexShaderSource : vertexShaderSource2;
+ const char* fsSource = useOpenGLES3 ? fragmentShaderSource : fragmentShaderSource2;
+
+ // Create vertex shader
+ vertexShader = glCreateShader(GL_VERTEX_SHADER);
+ if (vertexShader == 0) {
+ LOGE("Failed to create vertex shader");
+ return false;
+ }
+
+ glShaderSource(vertexShader, 1, &vsSource, nullptr);
+ glCompileShader(vertexShader);
+
+ // Check vertex shader compilation
+ GLint success;
+ glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
+ if (!success) {
+ GLchar infoLog[512];
+ glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
+ LOGE("Vertex shader compilation failed: %s", infoLog);
+ return false;
+ }
+ LOGI("Vertex shader compiled successfully");
+
+ // Create fragment shader
+ fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
+ if (fragmentShader == 0) {
+ LOGE("Failed to create fragment shader");
+ return false;
+ }
+
+ glShaderSource(fragmentShader, 1, &fsSource, nullptr);
+ glCompileShader(fragmentShader);
+
+ // Check fragment shader compilation
+ glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
+ if (!success) {
+ GLchar infoLog[512];
+ glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
+ LOGE("Fragment shader compilation failed: %s", infoLog);
+ return false;
+ }
+ LOGI("Fragment shader compiled successfully");
+
+ // Create shader program
+ program = glCreateProgram();
+ if (program == 0) {
+ LOGE("Failed to create shader program");
+ return false;
+ }
+
+ glAttachShader(program, vertexShader);
+ glAttachShader(program, fragmentShader);
+ glLinkProgram(program);
+
+ // Check program linking
+ glGetProgramiv(program, GL_LINK_STATUS, &success);
+ if (!success) {
+ GLchar infoLog[512];
+ glGetProgramInfoLog(program, 512, nullptr, infoLog);
+ LOGE("Shader program linking failed: %s", infoLog);
+ return false;
+ }
+ LOGI("Shader program linked successfully");
+
+ return true;
+}
+
+bool GLRenderer::createBuffers() {
+ LOGI("Creating buffers");
+
+ // Triangle vertices (position + color)
+ float vertices[] = {
+ // Position (x, y, z) // Color (r, g, b)
+ 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // Top vertex (red)
+ -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // Bottom left (green)
+ 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // Bottom right (blue)
+ };
+
+ // Check if VAOs are supported (OpenGL ES 3.0+)
+ const char* version = (const char*)glGetString(GL_VERSION);
+ bool useVAO = (version && strstr(version, "OpenGL ES 3"));
+
+ if (useVAO) {
+ // Create VAO (OpenGL ES 3.0+)
+ glGenVertexArrays(1, &vao);
+ if (vao == 0) {
+ LOGE("Failed to create VAO");
+ return false;
+ }
+ glBindVertexArray(vao);
+ LOGI("Using VAO for vertex attributes");
+ } else {
+ LOGI("VAO not supported, using direct attribute binding");
+ }
+
+ // Create VBO
+ glGenBuffers(1, &vbo);
+ if (vbo == 0) {
+ LOGE("Failed to create VBO");
+ return false;
+ }
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+
+ if (useVAO) {
+ // Position attribute
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), nullptr);
+ glEnableVertexAttribArray(0);
+
+ // Color attribute
+ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
+ glEnableVertexAttribArray(1);
+
+ // Unbind
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+ } else {
+ // For OpenGL ES 2.0, we'll bind attributes in render function
+ LOGI("Will bind attributes in render function for OpenGL ES 2.0");
+ }
+
+ LOGI("Buffers created successfully");
+ return true;
+}
+
+void GLRenderer::render() const {
+#ifdef PLATFORM_WINDOWS
+ // Poll input first so this frame's Update()/Render() see clicks that
+ // just came in, rather than acting on them a frame late (previously
+ // this ran after UI::EndFrame() below, so every click was consumed one
+ // frame after it happened).
+ if (window) {
+ glfwPollEvents();
+ }
+#endif
+
+ // Clear the screen
+ glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Use shader program
+ glUseProgram(program);
+
+ // Check if VAOs are supported
+ const char* version = (const char*)glGetString(GL_VERSION);
+ bool useVAO = (version && strstr(version, "OpenGL ES 3"));
+
+ if (useVAO) {
+ // Bind VAO and draw (OpenGL ES 3.0+)
+ glBindVertexArray(vao);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ } else {
+ // For OpenGL ES 2.0, bind attributes manually
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+
+ // Position attribute
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), nullptr);
+ glEnableVertexAttribArray(0);
+
+ // Color attribute
+ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
+ glEnableVertexAttribArray(1);
+
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ }
+
+ // Drive and draw the active game state on top of the scene. This runs
+ // every frame on both Windows and Android, since both call into this
+ // same render() function. States themselves are responsible for
+ // keeping top-anchored UI below ~350px - on Android this app is
+ // edge-to-edge (targetSdk 36), and the ActionBar is drawn as an overlay
+ // on top of the GLSurfaceView rather than pushing it down, so anything
+ // anchored near y=0 renders underneath it and is invisible.
+ GameStateManager::Instance().Update();
+ GameStateManager::Instance().Render();
+
+ // Marks the click event (if any) as consumed for this frame - call
+ // after all UI draw/hit-test calls above.
+ UI::EndFrame();
+
+#ifdef PLATFORM_WINDOWS
+ // Swap buffers for Windows (input already polled at the top of this
+ // function).
+ if (window) {
+ glfwSwapBuffers(window);
+ }
+#endif
+}
+
+void GLRenderer::cleanup() {
+ LOGI("Cleaning up OpenGL resources");
+
+ UI::Shutdown();
+
+ // Clean up OpenGL resources
+ if (program != 0) {
+ glDeleteProgram(program);
+ }
+ if (vertexShader != 0) {
+ glDeleteShader(vertexShader);
+ }
+ if (fragmentShader != 0) {
+ glDeleteShader(fragmentShader);
+ }
+ if (vbo != 0) {
+ glDeleteBuffers(1, &vbo);
+ }
+ if (vao != 0) {
+ glDeleteVertexArrays(1, &vao);
+ }
+
+#ifdef PLATFORM_WINDOWS
+ if (window) {
+ glfwDestroyWindow(window);
+ window = nullptr;
+ }
+ glfwTerminate();
+#endif
+
+ LOGI("OpenGL resources cleaned up");
+}
\ No newline at end of file
diff --git a/app/src/main/cpp/my_gl_app/GLRenderer.h b/app/src/main/cpp/my_gl_app/GLRenderer.h
new file mode 100644
index 0000000..966308d
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/GLRenderer.h
@@ -0,0 +1,71 @@
+#ifndef GLRENDERER_H
+#define GLRENDERER_H
+
+#include "Platform.h"
+
+class GLRenderer {
+public:
+ GLRenderer();
+ ~GLRenderer();
+
+ bool initialize();
+ void render() const;
+ void cleanup();
+
+#ifdef PLATFORM_WINDOWS
+ // Windows-specific methods
+ bool initializeGLFW();
+ void setWindow(GLFWwindow* window);
+ GLFWwindow* getWindow() const { return window; }
+#endif
+
+private:
+ bool createShaders();
+ bool createBuffers();
+
+ // OpenGL variables
+ GLuint program;
+ GLuint vertexShader;
+ GLuint fragmentShader;
+ GLuint vbo;
+ GLuint vao;
+
+#ifdef PLATFORM_WINDOWS
+ GLFWwindow* window;
+#endif
+
+ // Shader source code - compatible with both OpenGL ES 2.0 and 3.0
+ const char* vertexShaderSource = R"(#version 300 es
+layout(location = 0) in vec3 position;
+layout(location = 1) in vec3 color;
+out vec3 fragColor;
+void main() {
+ gl_Position = vec4(position, 1.0);
+ fragColor = color;
+})";
+
+ const char* fragmentShaderSource = R"(#version 300 es
+precision mediump float;
+in vec3 fragColor;
+out vec4 outColor;
+void main() {
+ outColor = vec4(fragColor, 1.0);
+})";
+
+ // Fallback shaders for OpenGL ES 2.0
+ const char* vertexShaderSource2 = R"(attribute vec3 position;
+attribute vec3 color;
+varying vec3 fragColor;
+void main() {
+ gl_Position = vec4(position, 1.0);
+ fragColor = color;
+})";
+
+ const char* fragmentShaderSource2 = R"(precision mediump float;
+varying vec3 fragColor;
+void main() {
+ gl_FragColor = vec4(fragColor, 1.0);
+})";
+};
+
+#endif // GLRENDERER_H
\ No newline at end of file
diff --git a/app/src/main/cpp/my_gl_app/GameState.h b/app/src/main/cpp/my_gl_app/GameState.h
new file mode 100644
index 0000000..65006f0
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/GameState.h
@@ -0,0 +1,31 @@
+#ifndef GAME_STATE_H
+#define GAME_STATE_H
+
+enum class StateID {
+ MainMenu,
+ Settings,
+ Menu1,
+ Menu2,
+ Menu3
+};
+
+// Base interface every game state implements. GameStateManager owns exactly
+// one active GameState at a time and drives it each frame.
+class GameState {
+public:
+ virtual ~GameState() = default;
+
+ // Called once when the manager switches to this state.
+ virtual void Enter() {}
+
+ // Called once when the manager switches away from this state.
+ virtual void Exit() {}
+
+ // Called once per frame, before Render().
+ virtual void Update() {}
+
+ // Called once per frame to draw this state's menu/UI.
+ virtual void Render() = 0;
+};
+
+#endif // GAME_STATE_H
diff --git a/app/src/main/cpp/my_gl_app/GameStateManager.cpp b/app/src/main/cpp/my_gl_app/GameStateManager.cpp
new file mode 100644
index 0000000..3dd390a
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/GameStateManager.cpp
@@ -0,0 +1,42 @@
+#include "GameStateManager.h"
+#include "StateFactories.h"
+
+GameStateManager& GameStateManager::Instance() {
+ static GameStateManager instance;
+ return instance;
+}
+
+void GameStateManager::RequestStateChange(StateID id) {
+ hasPendingChange = true;
+ pendingState = id;
+}
+
+void GameStateManager::Update() {
+ if (hasPendingChange) {
+ hasPendingChange = false;
+
+ if (currentState) {
+ currentState->Exit();
+ }
+
+ switch (pendingState) {
+ case StateID::MainMenu: currentState = CreateMainMenuState(); break;
+ case StateID::Settings: currentState = CreateSettingsState(); break;
+ case StateID::Menu1: currentState = CreateMenu1State(); break;
+ case StateID::Menu2: currentState = CreateMenu2State(); break;
+ case StateID::Menu3: currentState = CreateMenu3State(); break;
+ }
+
+ currentState->Enter();
+ }
+
+ if (currentState) {
+ currentState->Update();
+ }
+}
+
+void GameStateManager::Render() {
+ if (currentState) {
+ currentState->Render();
+ }
+}
diff --git a/app/src/main/cpp/my_gl_app/GameStateManager.h b/app/src/main/cpp/my_gl_app/GameStateManager.h
new file mode 100644
index 0000000..96dff12
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/GameStateManager.h
@@ -0,0 +1,27 @@
+#ifndef GAME_STATE_MANAGER_H
+#define GAME_STATE_MANAGER_H
+
+#include
+#include "GameState.h"
+
+class GameStateManager {
+public:
+ static GameStateManager& Instance();
+
+ // Queues a switch to `id`, applied at the start of the next Update().
+ // Deferred rather than immediate so a state can safely request its own
+ // replacement from inside a button click handled during Render().
+ void RequestStateChange(StateID id);
+
+ void Update();
+ void Render();
+
+private:
+ GameStateManager() = default;
+
+ std::unique_ptr currentState;
+ bool hasPendingChange = false;
+ StateID pendingState = StateID::MainMenu;
+};
+
+#endif // GAME_STATE_MANAGER_H
diff --git a/app/src/main/cpp/my_gl_app/Platform.h b/app/src/main/cpp/my_gl_app/Platform.h
new file mode 100644
index 0000000..445ae02
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/Platform.h
@@ -0,0 +1,32 @@
+#ifndef PLATFORM_H
+#define PLATFORM_H
+// Platform detection
+#ifdef _WIN32
+#define PLATFORM_WINDOWS
+#elif defined(__ANDROID__)
+#define PLATFORM_ANDROID
+#endif
+// Platform-specific includes
+#ifdef PLATFORM_WINDOWS
+#include
+#include
+#include
+#include
+// Windows logging macros
+#include
+#define LOGI(...) printf("[INFO] " __VA_ARGS__); printf("\n")
+#define LOGE(...) printf("[ERROR] " __VA_ARGS__); printf("\n")
+#define LOGD(...) printf("[DEBUG] " __VA_ARGS__); printf("\n")
+#elif defined(PLATFORM_ANDROID)
+#include
+#include
+#include
+
+// Android logging macros (LOG_TAG should be defined in each source file)
+#ifndef LOGI
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
+#endif
+#endif
+#endif // PLATFORM_H
\ No newline at end of file
diff --git a/app/src/main/cpp/my_gl_app/PlatformBridge.cpp b/app/src/main/cpp/my_gl_app/PlatformBridge.cpp
new file mode 100644
index 0000000..e95cbd9
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/PlatformBridge.cpp
@@ -0,0 +1,77 @@
+#include "PlatformBridge.h"
+
+#define LOG_TAG "PlatformBridge"
+
+#ifdef PLATFORM_ANDROID
+
+namespace {
+ JavaVM* g_jvm = nullptr;
+ // Global ref so the MainActivity instance stays valid for as long as
+ // this bridge might need to call back into it - local/weak refs from
+ // the original initGL() call wouldn't survive past that call.
+ jobject g_activityRef = nullptr;
+}
+
+namespace PlatformBridge {
+
+void SetAndroidActivity(JNIEnv* env, jobject activity) {
+ if (env->GetJavaVM(&g_jvm) != JNI_OK) {
+ LOGE("SetAndroidActivity: failed to cache JavaVM");
+ g_jvm = nullptr;
+ return;
+ }
+ if (g_activityRef != nullptr) {
+ env->DeleteGlobalRef(g_activityRef);
+ }
+ g_activityRef = env->NewGlobalRef(activity);
+ LOGI("SetAndroidActivity: MainActivity reference cached");
+}
+
+void LaunchBowlingCamera() {
+ if (g_jvm == nullptr || g_activityRef == nullptr) {
+ LOGE("LaunchBowlingCamera: Android activity not set up yet (SetAndroidActivity never called?)");
+ return;
+ }
+
+ JNIEnv* env = nullptr;
+ // This is only ever called from a thread Java originally called into
+ // native from (the GL render thread, via
+ // GameStateManager::Update() -> Menu3State::Enter()), so it's already
+ // attached to the JVM and GetEnv should always succeed without needing
+ // AttachCurrentThread.
+ if (g_jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK || env == nullptr) {
+ LOGE("LaunchBowlingCamera: current thread is not attached to the JVM");
+ return;
+ }
+
+ jclass activityClass = env->GetObjectClass(g_activityRef);
+ jmethodID launchMethod = env->GetMethodID(activityClass, "launchBowlingCamera", "()V");
+ env->DeleteLocalRef(activityClass);
+
+ if (launchMethod == nullptr) {
+ LOGE("LaunchBowlingCamera: MainActivity.launchBowlingCamera() not found");
+ env->ExceptionClear();
+ return;
+ }
+
+ env->CallVoidMethod(g_activityRef, launchMethod);
+ if (env->ExceptionCheck()) {
+ LOGE("LaunchBowlingCamera: exception thrown while calling into Java");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+}
+
+} // namespace PlatformBridge
+
+#else // Desktop (Windows/GLFW) build - no camera feature there yet.
+
+namespace PlatformBridge {
+
+void LaunchBowlingCamera() {
+ LOGI("LaunchBowlingCamera: requested, but this platform has no camera feature");
+}
+
+} // namespace PlatformBridge
+
+#endif
diff --git a/app/src/main/cpp/my_gl_app/PlatformBridge.h b/app/src/main/cpp/my_gl_app/PlatformBridge.h
new file mode 100644
index 0000000..2d31703
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/PlatformBridge.h
@@ -0,0 +1,35 @@
+#ifndef PLATFORM_BRIDGE_H
+#define PLATFORM_BRIDGE_H
+
+#include "Platform.h"
+
+#ifdef PLATFORM_ANDROID
+#include
+#endif
+
+// Thin seam between shared game-state code and platform-specific "launch a
+// native platform feature" hooks, so states like Menu3State don't need to
+// know or care whether they're running on Android or the Windows/GLFW
+// build.
+namespace PlatformBridge {
+
+#ifdef PLATFORM_ANDROID
+ // Called once from NativeTemplate.cpp's initGL() JNI entry. Caches a
+ // JavaVM + a global ref to the MainActivity instance so LaunchBowlingCamera()
+ // can call back into Java later from whatever thread it's invoked on
+ // (in practice, the GL render thread that drives GameStateManager -
+ // see GLRenderer::render()).
+ void SetAndroidActivity(JNIEnv* env, jobject activity);
+#endif
+
+ // Requests that the host platform open the bowling camera + pose
+ // detection screen. On Android this launches BowlingCameraActivity via
+ // a JNI callback into MainActivity; front/back camera switching happens
+ // on that screen itself, not here. On Windows there's no camera feature
+ // (the desktop build has no CameraX/ML Kit equivalent), so this just
+ // logs the request.
+ void LaunchBowlingCamera();
+
+} // namespace PlatformBridge
+
+#endif // PLATFORM_BRIDGE_H
diff --git a/app/src/main/cpp/my_gl_app/StateFactories.h b/app/src/main/cpp/my_gl_app/StateFactories.h
new file mode 100644
index 0000000..9efeaf3
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/StateFactories.h
@@ -0,0 +1,16 @@
+#ifndef STATE_FACTORIES_H
+#define STATE_FACTORIES_H
+
+#include
+#include "GameState.h"
+
+// Each state's .cpp defines its class privately (in an anonymous namespace)
+// and exposes only its factory function here, so GameStateManager doesn't
+// need every state's full class declaration - just these five functions.
+std::unique_ptr CreateMainMenuState();
+std::unique_ptr CreateSettingsState();
+std::unique_ptr CreateMenu1State();
+std::unique_ptr CreateMenu2State();
+std::unique_ptr CreateMenu3State();
+
+#endif // STATE_FACTORIES_H
diff --git a/app/src/main/cpp/my_gl_app/UIRenderer.cpp b/app/src/main/cpp/my_gl_app/UIRenderer.cpp
new file mode 100644
index 0000000..fcc5c07
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/UIRenderer.cpp
@@ -0,0 +1,473 @@
+#include "UIRenderer.h"
+#define LOG_TAG "UIRenderer"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace UI {
+
+namespace {
+
+ int s_screenWidth = 1;
+ int s_screenHeight = 1;
+ bool s_initialized = false;
+
+ GLuint s_lineProgram = 0;
+ GLuint s_lineVBO = 0;
+ GLuint s_lineVAO = 0;
+ GLint s_lineScreenSizeLoc = -1;
+ GLint s_lineColorLoc = -1;
+
+ GLuint s_imageProgram = 0;
+ GLuint s_imageVBO = 0;
+ GLuint s_imageVAO = 0;
+ GLint s_imageScreenSizeLoc = -1;
+
+ // Guards the pointer/click state below. On Android, touch events arrive
+ // on the app's UI thread (MainActivity.onTouchEvent -> nativeOnTouch),
+ // while Update()/Render()/EndFrame() run on GLSurfaceView's own
+ // dedicated GL rendering thread - without this lock, those two threads
+ // read/write this state unsynchronized, which can drop a click
+ // (in-flight write raced by EndFrame's clear) or leave s_clickPos
+ // holding a torn mix of an old/new touch position. Windows doesn't hit
+ // this: GLFW callbacks fire from glfwPollEvents() on the same thread
+ // that runs Update()/Render(), so there's nothing to race there.
+ std::mutex s_inputMutex;
+ Vec2 s_pointerPos{};
+ bool s_pointerIsDown = false;
+ bool s_pointerWasDown = false;
+ bool s_clickPending = false;
+ Vec2 s_clickPos{};
+
+ const char* kLineVS = R"(#version 300 es
+layout(location = 0) in vec2 aPos;
+uniform vec2 uScreenSize;
+void main() {
+ vec2 ndc = vec2((aPos.x / uScreenSize.x) * 2.0 - 1.0,
+ 1.0 - (aPos.y / uScreenSize.y) * 2.0);
+ gl_Position = vec4(ndc, 0.0, 1.0);
+})";
+
+ const char* kLineFS = R"(#version 300 es
+precision mediump float;
+out vec4 outColor;
+uniform vec4 uColor;
+void main() { outColor = uColor; })";
+
+ const char* kImageVS = R"(#version 300 es
+layout(location = 0) in vec2 aPos;
+layout(location = 1) in vec2 aUV;
+out vec2 vUV;
+uniform vec2 uScreenSize;
+void main() {
+ vec2 ndc = vec2((aPos.x / uScreenSize.x) * 2.0 - 1.0,
+ 1.0 - (aPos.y / uScreenSize.y) * 2.0);
+ gl_Position = vec4(ndc, 0.0, 1.0);
+ vUV = aUV;
+})";
+
+ const char* kImageFS = R"(#version 300 es
+precision mediump float;
+in vec2 vUV;
+out vec4 outColor;
+uniform sampler2D uTexture;
+void main() { outColor = texture(uTexture, vUV); })";
+
+ GLuint CompileShader(GLenum type, const char* source) {
+ GLuint shader = glCreateShader(type);
+ glShaderSource(shader, 1, &source, nullptr);
+ glCompileShader(shader);
+ GLint success = 0;
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
+ if (!success) {
+ GLchar infoLog[512];
+ glGetShaderInfoLog(shader, 512, nullptr, infoLog);
+ LOGE("UI shader compilation failed: %s", infoLog);
+ glDeleteShader(shader);
+ return 0;
+ }
+ return shader;
+ }
+
+ GLuint LinkProgram(const char* vsSrc, const char* fsSrc) {
+ GLuint vs = CompileShader(GL_VERTEX_SHADER, vsSrc);
+ GLuint fs = CompileShader(GL_FRAGMENT_SHADER, fsSrc);
+ if (vs == 0 || fs == 0) {
+ return 0;
+ }
+ GLuint program = glCreateProgram();
+ glAttachShader(program, vs);
+ glAttachShader(program, fs);
+ glLinkProgram(program);
+ GLint success = 0;
+ glGetProgramiv(program, GL_LINK_STATUS, &success);
+ if (!success) {
+ GLchar infoLog[512];
+ glGetProgramInfoLog(program, 512, nullptr, infoLog);
+ LOGE("UI program link failed: %s", infoLog);
+ glDeleteProgram(program);
+ program = 0;
+ }
+ glDeleteShader(vs);
+ glDeleteShader(fs);
+ return program;
+ }
+
+ // Built-in vector font: each glyph is a handful of line segments in a
+ // normalized 0..1 (x) by 0..1 (y) cell, origin bottom-left. Covers
+ // digits, A-Z and common punctuation - no font file required.
+ void AppendGlyphSegments(std::vector& verts, char c, float penX, float topY, float w, float h) {
+ constexpr float L = 0.0f, R = 1.0f, B = 0.0f, T = 1.0f, M = 0.5f;
+
+ // Stroke half-thickness in screen pixels. Kept comfortably wide
+ // (rather than a hairline) since very thin quads were observed to
+ // drop out during rasterization on some GLES driver/emulator
+ // combinations at high framebuffer resolutions.
+ const float strokeHalf = std::max(h * 0.08f, 4.0f);
+
+ auto seg = [&](float x0, float y0, float x1, float y1) {
+ float sx0 = penX + x0 * w;
+ float sy0 = topY + (1.0f - y0) * h;
+ float sx1 = penX + x1 * w;
+ float sy1 = topY + (1.0f - y1) * h;
+
+ float dx = sx1 - sx0;
+ float dy = sy1 - sy0;
+ float len = std::sqrt(dx * dx + dy * dy);
+ float nx = 0.0f, ny = strokeHalf;
+ if (len > 0.0001f) {
+ nx = -dy / len * strokeHalf;
+ ny = dx / len * strokeHalf;
+ }
+
+ float ax = sx0 + nx, ay = sy0 + ny;
+ float bx = sx0 - nx, by = sy0 - ny;
+ float cx = sx1 + nx, cy = sy1 + ny;
+ float dxp = sx1 - nx, dyp = sy1 - ny;
+
+ float quad[] = {
+ ax, ay, bx, by, cx, cy,
+ cx, cy, bx, by, dxp, dyp,
+ };
+ verts.insert(verts.end(), std::begin(quad), std::end(quad));
+ };
+
+ switch (std::toupper(static_cast(c))) {
+ case '0': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); seg(L,B,L,T); seg(L,B,R,T); break;
+ case '1': seg(M,T,M,B); seg(L,B,R,B); break;
+ case '2': seg(L,T,R,T); seg(R,T,R,M); seg(R,M,M,M); seg(M,M,L,B); seg(L,B,R,B); break;
+ case '3': seg(L,T,R,T); seg(R,T,R,B); seg(L,B,R,B); seg(L,M,R,M); break;
+ case '4': seg(L,T,L,M); seg(L,M,R,M); seg(R,T,R,B); break;
+ case '5': seg(R,T,L,T); seg(L,T,L,M); seg(L,M,R,M); seg(R,M,R,B); seg(R,B,L,B); break;
+ case '6': seg(R,T,L,T); seg(L,T,L,B); seg(L,B,R,B); seg(R,B,R,M); seg(R,M,L,M); break;
+ case '7': seg(L,T,R,T); seg(R,T,M,B); break;
+ case '8': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); seg(L,B,L,T); seg(L,M,R,M); break;
+ case '9': seg(L,B,R,B); seg(R,B,R,T); seg(R,T,L,T); seg(L,T,L,M); seg(L,M,R,M); break;
+
+ case 'A': seg(L,B,M,T); seg(M,T,R,B); seg(L,M,R,M); break;
+ case 'B': seg(L,T,L,B); seg(L,T,R,T); seg(R,T,R,M); seg(L,M,R,M); seg(R,M,R,B); seg(L,B,R,B); break;
+ case 'C': seg(R,T,L,T); seg(L,T,L,B); seg(L,B,R,B); break;
+ case 'D': seg(L,T,L,B); seg(L,T,R,T); seg(R,T,R,B); seg(L,B,R,B); break;
+ case 'E': seg(R,T,L,T); seg(L,T,L,B); seg(L,B,R,B); seg(L,M,R,M); break;
+ case 'F': seg(R,T,L,T); seg(L,T,L,B); seg(L,M,R,M); break;
+ case 'G': seg(R,T,L,T); seg(L,T,L,B); seg(L,B,R,B); seg(R,B,R,M); seg(R,M,M,M); break;
+ case 'H': seg(L,T,L,B); seg(R,T,R,B); seg(L,M,R,M); break;
+ case 'I': seg(L,T,R,T); seg(M,T,M,B); seg(L,B,R,B); break;
+ case 'J': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); break;
+ case 'K': seg(L,T,L,B); seg(R,T,L,M); seg(L,M,R,B); break;
+ case 'L': seg(L,T,L,B); seg(L,B,R,B); break;
+ case 'M': seg(L,B,L,T); seg(L,T,M,M); seg(M,M,R,T); seg(R,T,R,B); break;
+ case 'N': seg(L,B,L,T); seg(L,T,R,B); seg(R,B,R,T); break;
+ case 'O': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); seg(L,B,L,T); break;
+ case 'P': seg(L,T,L,B); seg(L,T,R,T); seg(R,T,R,M); seg(R,M,L,M); break;
+ case 'Q': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); seg(L,B,L,T); seg(M,M,R,B); break;
+ case 'R': seg(L,T,L,B); seg(L,T,R,T); seg(R,T,R,M); seg(R,M,L,M); seg(L,M,R,B); break;
+ case 'S': seg(R,T,L,T); seg(L,T,L,M); seg(L,M,R,M); seg(R,M,R,B); seg(R,B,L,B); break;
+ case 'T': seg(L,T,R,T); seg(M,T,M,B); break;
+ case 'U': seg(L,T,L,B); seg(L,B,R,B); seg(R,B,R,T); break;
+ case 'V': seg(L,T,M,B); seg(M,B,R,T); break;
+ case 'W': seg(L,T,L,B); seg(L,B,M,M); seg(M,M,R,B); seg(R,B,R,T); break;
+ case 'X': seg(L,T,R,B); seg(R,T,L,B); break;
+ case 'Y': seg(L,T,M,M); seg(R,T,M,M); seg(M,M,M,B); break;
+ case 'Z': seg(L,T,R,T); seg(R,T,L,B); seg(L,B,R,B); break;
+
+ case '.': seg(M-0.05f,B,M+0.05f,B); break;
+ case ',': seg(M,B,M-0.15f,B-0.25f); break;
+ case '!': seg(M,T,M,M+0.1f); seg(M,0.12f,M,B); break;
+ case '?': seg(L+0.1f,T,R-0.1f,T); seg(R-0.1f,T,R-0.1f,M); seg(R-0.1f,M,M,M); seg(M,M,M,M-0.15f); seg(M,0.12f,M,B); break;
+ case ':': seg(M,0.65f,M,0.6f); seg(M,0.35f,M,0.3f); break;
+ case '-': seg(L,M,R,M); break;
+ case ' ': default: break; // blank / unsupported characters just advance the pen
+ }
+ }
+
+ // Shared by DrawText and Button so hit-testing always matches what's
+ // actually drawn.
+ float TextTotalWidth(const std::string& text, float textSize) {
+ const float glyphW = textSize * 0.6f;
+ const float advance = glyphW * 1.3f;
+ return advance * static_cast(text.size());
+ }
+
+ float TextStartX(const std::string& text, float textSize, float locationX, TextAlign alignment) {
+ const float totalWidth = TextTotalWidth(text, textSize);
+ if (alignment == TextAlign::Center) return locationX - totalWidth * 0.5f;
+ if (alignment == TextAlign::Right) return locationX - totalWidth;
+ return locationX;
+ }
+
+ // Uploads `verts` (x,y pairs) and draws them as opaque white triangles
+ // via the shared line/shape program. Used by both DrawText and
+ // DrawRectOutline.
+ void DrawWhiteTriangles(const std::vector& verts) {
+ if (verts.empty()) return;
+
+ glUseProgram(s_lineProgram);
+ glUniform2f(s_lineScreenSizeLoc, static_cast(s_screenWidth), static_cast(s_screenHeight));
+ glUniform4f(s_lineColorLoc, 1.0f, 1.0f, 1.0f, 1.0f);
+
+ // Quads here aren't wound consistently (mixed by design, since
+ // strokes/edges run in arbitrary directions), so back-face culling
+ // must be off regardless of whatever state the rest of the app
+ // leaves enabled.
+ glDisable(GL_CULL_FACE);
+
+ glBindVertexArray(s_lineVAO);
+ glBindBuffer(GL_ARRAY_BUFFER, s_lineVBO);
+ glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_DYNAMIC_DRAW);
+ glDrawArrays(GL_TRIANGLES, 0, static_cast(verts.size() / 2));
+ glBindVertexArray(0);
+ }
+
+} // namespace
+
+bool Init(int screenWidth, int screenHeight) {
+ s_screenWidth = screenWidth > 0 ? screenWidth : 1;
+ s_screenHeight = screenHeight > 0 ? screenHeight : 1;
+
+ if (s_initialized) {
+ return true; // already set up - this call was just a resize
+ }
+
+ s_lineProgram = LinkProgram(kLineVS, kLineFS);
+ s_imageProgram = LinkProgram(kImageVS, kImageFS);
+ if (s_lineProgram == 0 || s_imageProgram == 0) {
+ LOGE("UI::Init failed to build shader programs");
+ return false;
+ }
+
+ s_lineScreenSizeLoc = glGetUniformLocation(s_lineProgram, "uScreenSize");
+ s_lineColorLoc = glGetUniformLocation(s_lineProgram, "uColor");
+ s_imageScreenSizeLoc = glGetUniformLocation(s_imageProgram, "uScreenSize");
+
+ glGenVertexArrays(1, &s_lineVAO);
+ glBindVertexArray(s_lineVAO);
+ glGenBuffers(1, &s_lineVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, s_lineVBO);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
+ glEnableVertexAttribArray(0);
+ glBindVertexArray(0);
+
+ glGenVertexArrays(1, &s_imageVAO);
+ glBindVertexArray(s_imageVAO);
+ glGenBuffers(1, &s_imageVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, s_imageVBO);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
+ glEnableVertexAttribArray(1);
+ glBindVertexArray(0);
+
+ s_initialized = true;
+ LOGI("UI renderer initialized (%dx%d)", s_screenWidth, s_screenHeight);
+ return true;
+}
+
+void OnScreenResize(int screenWidth, int screenHeight) {
+ s_screenWidth = screenWidth > 0 ? screenWidth : 1;
+ s_screenHeight = screenHeight > 0 ? screenHeight : 1;
+}
+
+void Shutdown() {
+ if (!s_initialized) return;
+ glDeleteProgram(s_lineProgram);
+ glDeleteProgram(s_imageProgram);
+ glDeleteBuffers(1, &s_lineVBO);
+ glDeleteBuffers(1, &s_imageVBO);
+ glDeleteVertexArrays(1, &s_lineVAO);
+ glDeleteVertexArrays(1, &s_imageVAO);
+ s_lineProgram = s_imageProgram = s_lineVBO = s_imageVBO = s_lineVAO = s_imageVAO = 0;
+ s_initialized = false;
+ LOGI("UI renderer shut down");
+}
+
+void EndFrame() {
+ std::lock_guard lock(s_inputMutex);
+ s_pointerWasDown = s_pointerIsDown;
+ s_clickPending = false;
+}
+
+// ---- Function 1: text -------------------------------------------------
+
+void DrawText(const std::string& text, float textSize, Vec2 textLocation, TextAlign alignment) {
+ if (!s_initialized || text.empty() || textSize <= 0.0f) return;
+
+ const float glyphW = textSize * 0.6f;
+ const float glyphH = textSize;
+ const float advance = glyphW * 1.3f;
+ const float startX = TextStartX(text, textSize, textLocation.x, alignment);
+
+ std::vector verts;
+ verts.reserve(text.size() * 12);
+ float penX = startX;
+ for (char c : text) {
+ AppendGlyphSegments(verts, c, penX, textLocation.y, glyphW, glyphH);
+ penX += advance;
+ }
+ DrawWhiteTriangles(verts);
+}
+
+// ---- Function 2: images -------------------------------------------------
+
+Texture CreateTexture(const unsigned char* pixelsRGBA, int width, int height) {
+ Texture tex;
+ if (pixelsRGBA == nullptr || width <= 0 || height <= 0) {
+ LOGE("UI::CreateTexture called with invalid pixel data");
+ return tex;
+ }
+ glGenTextures(1, &tex.id);
+ glBindTexture(GL_TEXTURE_2D, tex.id);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixelsRGBA);
+ glBindTexture(GL_TEXTURE_2D, 0);
+ tex.width = width;
+ tex.height = height;
+ return tex;
+}
+
+void DestroyTexture(Texture& texture) {
+ if (texture.id != 0) {
+ glDeleteTextures(1, &texture.id);
+ texture = Texture{};
+ }
+}
+
+void DrawImage(const Texture& image, Vec2 locationOfImage, Vec2 sizeOfImage) {
+ if (!s_initialized || image.id == 0) return;
+
+ const float x0 = locationOfImage.x;
+ const float y0 = locationOfImage.y;
+ const float x1 = locationOfImage.x + sizeOfImage.x;
+ const float y1 = locationOfImage.y + sizeOfImage.y;
+
+ // pos.x, pos.y, uv.x, uv.y - two triangles
+ float verts[] = {
+ x0, y0, 0.0f, 0.0f,
+ x0, y1, 0.0f, 1.0f,
+ x1, y0, 1.0f, 0.0f,
+
+ x1, y0, 1.0f, 0.0f,
+ x0, y1, 0.0f, 1.0f,
+ x1, y1, 1.0f, 1.0f,
+ };
+
+ glUseProgram(s_imageProgram);
+ glUniform2f(s_imageScreenSizeLoc, static_cast(s_screenWidth), static_cast(s_screenHeight));
+ glDisable(GL_CULL_FACE);
+
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, image.id);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ glBindVertexArray(s_imageVAO);
+ glBindBuffer(GL_ARRAY_BUFFER, s_imageVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_DYNAMIC_DRAW);
+ glDrawArrays(GL_TRIANGLES, 0, 6);
+ glBindVertexArray(0);
+
+ glDisable(GL_BLEND);
+}
+
+// ---- Function 3: click detection ---------------------------------------
+
+void SetPointerPosition(float x, float y) {
+ std::lock_guard lock(s_inputMutex);
+ s_pointerPos.x = x;
+ s_pointerPos.y = y;
+}
+
+void SetPointerDown(bool isDown) {
+ std::lock_guard lock(s_inputMutex);
+ if (isDown && !s_pointerWasDown) {
+ s_clickPending = true;
+ s_clickPos = s_pointerPos;
+ }
+ s_pointerIsDown = isDown;
+}
+
+bool WasClicked(Vec2 location, Vec2 size) {
+ std::lock_guard lock(s_inputMutex);
+ if (!s_clickPending) return false;
+ return s_clickPos.x >= location.x && s_clickPos.x <= location.x + size.x &&
+ s_clickPos.y >= location.y && s_clickPos.y <= location.y + size.y;
+}
+
+void DrawRectOutline(Vec2 location, Vec2 size, float thickness) {
+ if (!s_initialized) return;
+
+ const float x0 = location.x, y0 = location.y;
+ const float x1 = location.x + size.x, y1 = location.y + size.y;
+ const float t = thickness;
+
+ std::vector verts;
+ verts.reserve(24 * 4);
+ auto addRect = [&](float rx0, float ry0, float rx1, float ry1) {
+ float quad[] = {
+ rx0, ry0, rx0, ry1, rx1, ry0,
+ rx1, ry0, rx0, ry1, rx1, ry1,
+ };
+ verts.insert(verts.end(), std::begin(quad), std::end(quad));
+ };
+
+ addRect(x0, y0, x1, y0 + t); // top edge
+ addRect(x0, y1 - t, x1, y1); // bottom edge
+ addRect(x0, y0, x0 + t, y1); // left edge
+ addRect(x1 - t, y0, x1, y1); // right edge
+
+ DrawWhiteTriangles(verts);
+}
+
+bool Button(const std::string& text, float textSize, Vec2 location, TextAlign alignment) {
+ // Pad the outline out from the raw glyph bounds so the button reads as
+ // an actual rectangular button, not a box hugging the text - and give
+ // it a minimum width so short labels (e.g. "BACK") still get a
+ // comfortably large, easy-to-hit target instead of a narrow sliver.
+ const float padding = std::max(textSize * 0.35f, 20.0f);
+ const float minWidth = textSize * 4.5f;
+ const float rectWidth = std::max(TextTotalWidth(text, textSize) + padding * 2.0f, minWidth);
+ Vec2 rectLocation{
+ TextStartX(text, textSize, location.x, alignment) - padding,
+ location.y - padding
+ };
+ Vec2 rectSize{rectWidth, textSize + padding * 2.0f};
+
+ // Outline thickness scales with text size but never drops below a
+ // pixel width that reliably rasterizes (thin geometry has been
+ // observed to drop out entirely on some Android GPU/driver combos).
+ const float thickness = std::max(textSize * 0.06f, 4.0f);
+
+ DrawRectOutline(rectLocation, rectSize, thickness);
+ DrawText(text, textSize, location, alignment);
+
+ return WasClicked(rectLocation, rectSize);
+}
+
+} // namespace UI
diff --git a/app/src/main/cpp/my_gl_app/UIRenderer.h b/app/src/main/cpp/my_gl_app/UIRenderer.h
new file mode 100644
index 0000000..cfaac29
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/UIRenderer.h
@@ -0,0 +1,76 @@
+#ifndef UI_RENDERER_H
+#define UI_RENDERER_H
+
+#include "Platform.h"
+#include
+
+// Minimal immediate-mode UI helpers built directly on the app's existing
+// OpenGL ES context. Text is drawn with a built-in vector font (no font
+// files needed); images are plain textured quads from RGBA pixel data.
+namespace UI {
+
+ enum class TextAlign { Left, Center, Right };
+
+ struct Vec2 {
+ float x = 0.0f;
+ float y = 0.0f;
+ };
+
+ struct Texture {
+ GLuint id = 0;
+ int width = 0;
+ int height = 0;
+ };
+
+ // Call once the GL context exists (e.g. right after GLEW/EGL init),
+ // with the current screen/framebuffer size in pixels. Safe to call
+ // again on resize - later calls just update the cached screen size.
+ bool Init(int screenWidth, int screenHeight);
+ void OnScreenResize(int screenWidth, int screenHeight);
+ void Shutdown();
+
+ // Must be called once per rendered frame, after all DrawText/DrawImage/
+ // WasClicked calls for that frame, so click events don't leak into the
+ // next frame.
+ void EndFrame();
+
+ // ---- Function 1: text -------------------------------------------------
+ // textLocation is the anchor point in screen pixels (origin top-left).
+ // alignment controls how the text is positioned relative to that anchor.
+ void DrawText(const std::string& text, float textSize, Vec2 textLocation, TextAlign alignment);
+
+ // ---- Function 2: images ------------------------------------------------
+ // Uploads raw RGBA8 pixels (width * height * 4 bytes) as a GL texture.
+ Texture CreateTexture(const unsigned char* pixelsRGBA, int width, int height);
+ void DestroyTexture(Texture& texture);
+
+ // locationOfImage is the top-left corner in screen pixels, sizeOfImage
+ // is the width/height to draw it at (also in screen pixels).
+ void DrawImage(const Texture& image, Vec2 locationOfImage, Vec2 sizeOfImage);
+
+ // ---- Function 3: click detection ---------------------------------------
+ // Fed by platform input glue (GLFW mouse callbacks on Windows, forwarded
+ // touch events on Android). x/y are in screen pixels, origin top-left -
+ // the same space as textLocation/locationOfImage above.
+ void SetPointerPosition(float x, float y);
+ void SetPointerDown(bool isDown);
+
+ // Returns true on the frame the pointer went down (mouse click / touch
+ // tap) inside the rectangle [location, location + size]. Call once per
+ // button per frame, followed by UI::EndFrame().
+ bool WasClicked(Vec2 location, Vec2 size);
+
+ // Draws an unfilled rectangle border (not solid-filled) `thickness`
+ // pixels wide, at `location` with the given `size`.
+ void DrawRectOutline(Vec2 location, Vec2 size, float thickness);
+
+ // Convenience wrapper: draws `text` as a label inside a rectangular
+ // outline and reports whether it was clicked/tapped this frame. The
+ // drawn outline always exactly matches the clickable hit area, padded
+ // out a bit from the raw glyph bounds so the button doesn't hug the
+ // text too tightly.
+ bool Button(const std::string& text, float textSize, Vec2 location, TextAlign alignment);
+
+} // namespace UI
+
+#endif // UI_RENDERER_H
diff --git a/app/src/main/cpp/my_gl_app/main.cpp b/app/src/main/cpp/my_gl_app/main.cpp
new file mode 100644
index 0000000..1217951
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/main.cpp
@@ -0,0 +1,40 @@
+#include "Platform.h"
+// [IMPORTANT] This main entry file is only for Desktop Based applications support Window, Ubuntu, Mac etc
+#ifdef PLATFORM_WINDOWS
+
+#include "GLRenderer.h"
+#include
+
+int main() {
+ std::cout << "Starting OpenGL ES 3.0 Triangle Demo on Windows" << std::endl;
+
+ // Create renderer
+ GLRenderer renderer;
+
+ // Initialize
+ if (!renderer.initialize()) {
+ std::cerr << "Failed to initialize renderer" << std::endl;
+ return -1;
+ }
+
+ std::cout << "Renderer initialized successfully" << std::endl;
+ std::cout << "Press ESC to exit" << std::endl;
+
+ // Main render loop
+ while (!glfwWindowShouldClose(renderer.getWindow())) {
+ // Render frame
+ renderer.render();
+
+ // Check for ESC key to exit
+ if (glfwGetKey(renderer.getWindow(), GLFW_KEY_ESCAPE) == GLFW_PRESS) {
+ glfwSetWindowShouldClose(renderer.getWindow(), GLFW_TRUE);
+ }
+ }
+
+ // Cleanup
+ renderer.cleanup();
+
+ std::cout << "Application closed successfully" << std::endl;
+ return 0;
+}
+#endif
diff --git a/app/src/main/cpp/my_gl_app/states/MainMenuState.cpp b/app/src/main/cpp/my_gl_app/states/MainMenuState.cpp
new file mode 100644
index 0000000..dfbab6e
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/states/MainMenuState.cpp
@@ -0,0 +1,35 @@
+#include "../GameState.h"
+#include "../GameStateManager.h"
+#include "../UIRenderer.h"
+
+namespace {
+
+// Text anchored below this line clears the Android ActionBar overlay - see
+// the note in GLRenderer.cpp.
+constexpr float kSafeTop = 350.0f;
+
+class MainMenuState : public GameState {
+public:
+ void Render() override {
+ UI::DrawText("MAIN MENU", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
+
+ if (UI::Button("SETTINGS", 56.0f, {20.0f, kSafeTop + 110.0f}, UI::TextAlign::Left)) {
+ GameStateManager::Instance().RequestStateChange(StateID::Settings);
+ }
+ if (UI::Button("MENU 1", 56.0f, {20.0f, kSafeTop + 230.0f}, UI::TextAlign::Left)) {
+ GameStateManager::Instance().RequestStateChange(StateID::Menu1);
+ }
+ if (UI::Button("MENU 2", 56.0f, {20.0f, kSafeTop + 350.0f}, UI::TextAlign::Left)) {
+ GameStateManager::Instance().RequestStateChange(StateID::Menu2);
+ }
+ if (UI::Button("MENU 3", 56.0f, {20.0f, kSafeTop + 470.0f}, UI::TextAlign::Left)) {
+ GameStateManager::Instance().RequestStateChange(StateID::Menu3);
+ }
+ }
+};
+
+} // namespace
+
+std::unique_ptr CreateMainMenuState() {
+ return std::make_unique();
+}
diff --git a/app/src/main/cpp/my_gl_app/states/Menu1State.cpp b/app/src/main/cpp/my_gl_app/states/Menu1State.cpp
new file mode 100644
index 0000000..a3c1fe0
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/states/Menu1State.cpp
@@ -0,0 +1,25 @@
+#include "../GameState.h"
+#include "../GameStateManager.h"
+#include "../UIRenderer.h"
+
+namespace {
+
+constexpr float kSafeTop = 350.0f;
+
+class Menu1State : public GameState {
+public:
+ void Render() override {
+ UI::DrawText("MENU 1", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
+ UI::DrawText("THIS IS MENU ONE", 36.0f, {20.0f, kSafeTop + 90.0f}, UI::TextAlign::Left);
+
+ if (UI::Button("BACK", 56.0f, {20.0f, kSafeTop + 220.0f}, UI::TextAlign::Left)) {
+ GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
+ }
+ }
+};
+
+} // namespace
+
+std::unique_ptr CreateMenu1State() {
+ return std::make_unique();
+}
diff --git a/app/src/main/cpp/my_gl_app/states/Menu2State.cpp b/app/src/main/cpp/my_gl_app/states/Menu2State.cpp
new file mode 100644
index 0000000..3cf8ffa
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/states/Menu2State.cpp
@@ -0,0 +1,25 @@
+#include "../GameState.h"
+#include "../GameStateManager.h"
+#include "../UIRenderer.h"
+
+namespace {
+
+constexpr float kSafeTop = 350.0f;
+
+class Menu2State : public GameState {
+public:
+ void Render() override {
+ UI::DrawText("MENU 2", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
+ UI::DrawText("THIS IS MENU TWO", 36.0f, {20.0f, kSafeTop + 90.0f}, UI::TextAlign::Left);
+
+ if (UI::Button("BACK", 56.0f, {20.0f, kSafeTop + 220.0f}, UI::TextAlign::Left)) {
+ GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
+ }
+ }
+};
+
+} // namespace
+
+std::unique_ptr CreateMenu2State() {
+ return std::make_unique();
+}
diff --git a/app/src/main/cpp/my_gl_app/states/Menu3State.cpp b/app/src/main/cpp/my_gl_app/states/Menu3State.cpp
new file mode 100644
index 0000000..ae34657
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/states/Menu3State.cpp
@@ -0,0 +1,44 @@
+#include "../GameState.h"
+#include "../GameStateManager.h"
+#include "../UIRenderer.h"
+#include "../PlatformBridge.h"
+
+namespace {
+
+constexpr float kSafeTop = 350.0f;
+
+class Menu3State : public GameState {
+public:
+ // Fires once, the moment GameStateManager switches into this state -
+ // i.e. as soon as the "MENU 3" button is pressed on the main menu. On
+ // Android this opens BowlingCameraActivity (permission request, camera
+ // preview, pose overlay); on the desktop/GLFW build it's a no-op log,
+ // since there's no camera feature there.
+ void Enter() override {
+ PlatformBridge::LaunchBowlingCamera();
+ }
+
+ void Render() override {
+ UI::DrawText("MENU 3", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
+ UI::DrawText("BOWLING FORM CAMERA", 36.0f, {20.0f, kSafeTop + 90.0f}, UI::TextAlign::Left);
+
+ // The camera screen also opens automatically via Enter() above,
+ // but if the user backs out of it without recording anything,
+ // this lets them reopen it without leaving and re-entering Menu 3.
+ // Front/back camera switching and the way back out live on the
+ // camera screen itself (see BowlingCameraActivity's SWITCH CAMERA
+ // and BACK buttons), not here.
+ if (UI::Button("OPEN CAMERA", 56.0f, {20.0f, kSafeTop + 220.0f}, UI::TextAlign::Left)) {
+ PlatformBridge::LaunchBowlingCamera();
+ }
+ if (UI::Button("BACK", 56.0f, {20.0f, kSafeTop + 340.0f}, UI::TextAlign::Left)) {
+ GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
+ }
+ }
+};
+
+} // namespace
+
+std::unique_ptr CreateMenu3State() {
+ return std::make_unique();
+}
diff --git a/app/src/main/cpp/my_gl_app/states/SettingsState.cpp b/app/src/main/cpp/my_gl_app/states/SettingsState.cpp
new file mode 100644
index 0000000..955c9aa
--- /dev/null
+++ b/app/src/main/cpp/my_gl_app/states/SettingsState.cpp
@@ -0,0 +1,26 @@
+#include "../GameState.h"
+#include "../GameStateManager.h"
+#include "../UIRenderer.h"
+
+namespace {
+
+constexpr float kSafeTop = 350.0f;
+
+class SettingsState : public GameState {
+public:
+ void Render() override {
+ UI::DrawText("SETTINGS", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
+ UI::DrawText("SOUND: ON", 36.0f, {20.0f, kSafeTop + 90.0f}, UI::TextAlign::Left);
+ UI::DrawText("MUSIC: ON", 36.0f, {20.0f, kSafeTop + 140.0f}, UI::TextAlign::Left);
+
+ if (UI::Button("BACK", 56.0f, {20.0f, kSafeTop + 250.0f}, UI::TextAlign::Left)) {
+ GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
+ }
+ }
+};
+
+} // namespace
+
+std::unique_ptr CreateSettingsState() {
+ return std::make_unique();
+}