From 18fe809d4af6e177024d27fe75e031abf6c8bea4 Mon Sep 17 00:00:00 2001 From: DefiantWanderer Date: Tue, 11 Aug 2026 19:58:07 +0800 Subject: [PATCH] Debugging linkage stuff --- .idea/vcs.xml | 6 + app/src/main/cpp/CMakeLists.txt | 2 +- app/src/main/cpp/cpp/CMakeLists.txt | 50 ++ app/src/main/cpp/cpp/NativeTemplate.cpp | 105 ++++ app/src/main/cpp/my_gl_app/GLRenderer.cpp | 347 +++++++++++++ app/src/main/cpp/my_gl_app/GLRenderer.h | 71 +++ app/src/main/cpp/my_gl_app/GameState.h | 31 ++ .../main/cpp/my_gl_app/GameStateManager.cpp | 42 ++ app/src/main/cpp/my_gl_app/GameStateManager.h | 27 + app/src/main/cpp/my_gl_app/Platform.h | 32 ++ app/src/main/cpp/my_gl_app/PlatformBridge.cpp | 77 +++ app/src/main/cpp/my_gl_app/PlatformBridge.h | 35 ++ app/src/main/cpp/my_gl_app/StateFactories.h | 16 + app/src/main/cpp/my_gl_app/UIRenderer.cpp | 473 ++++++++++++++++++ app/src/main/cpp/my_gl_app/UIRenderer.h | 76 +++ app/src/main/cpp/my_gl_app/main.cpp | 40 ++ .../cpp/my_gl_app/states/MainMenuState.cpp | 35 ++ .../main/cpp/my_gl_app/states/Menu1State.cpp | 25 + .../main/cpp/my_gl_app/states/Menu2State.cpp | 25 + .../main/cpp/my_gl_app/states/Menu3State.cpp | 44 ++ .../cpp/my_gl_app/states/SettingsState.cpp | 26 + 21 files changed, 1584 insertions(+), 1 deletion(-) create mode 100644 .idea/vcs.xml create mode 100644 app/src/main/cpp/cpp/CMakeLists.txt create mode 100644 app/src/main/cpp/cpp/NativeTemplate.cpp create mode 100644 app/src/main/cpp/my_gl_app/GLRenderer.cpp create mode 100644 app/src/main/cpp/my_gl_app/GLRenderer.h create mode 100644 app/src/main/cpp/my_gl_app/GameState.h create mode 100644 app/src/main/cpp/my_gl_app/GameStateManager.cpp create mode 100644 app/src/main/cpp/my_gl_app/GameStateManager.h create mode 100644 app/src/main/cpp/my_gl_app/Platform.h create mode 100644 app/src/main/cpp/my_gl_app/PlatformBridge.cpp create mode 100644 app/src/main/cpp/my_gl_app/PlatformBridge.h create mode 100644 app/src/main/cpp/my_gl_app/StateFactories.h create mode 100644 app/src/main/cpp/my_gl_app/UIRenderer.cpp create mode 100644 app/src/main/cpp/my_gl_app/UIRenderer.h create mode 100644 app/src/main/cpp/my_gl_app/main.cpp create mode 100644 app/src/main/cpp/my_gl_app/states/MainMenuState.cpp create mode 100644 app/src/main/cpp/my_gl_app/states/Menu1State.cpp create mode 100644 app/src/main/cpp/my_gl_app/states/Menu2State.cpp create mode 100644 app/src/main/cpp/my_gl_app/states/Menu3State.cpp create mode 100644 app/src/main/cpp/my_gl_app/states/SettingsState.cpp 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(); +}