diff --git a/examples/example_android_vulkan/.gitignore b/examples/example_android_vulkan/.gitignore new file mode 100644 index 000000000000..aa724b77071a --- /dev/null +++ b/examples/example_android_vulkan/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/examples/example_android_vulkan/README.md b/examples/example_android_vulkan/README.md new file mode 100644 index 000000000000..ee4dfa396c7b --- /dev/null +++ b/examples/example_android_vulkan/README.md @@ -0,0 +1,102 @@ +# Android Vulkan + Dear ImGui Example + +A standalone Android application that renders [Dear ImGui](https://github.com/ocornut/imgui) using the Vulkan graphics API. It is built natively in C++ via `NativeActivity`, alongside a minimal Java subclass to handle OS-level features like the virtual keyboard and clipboard. + +![ImGui Demo](https://img.shields.io/badge/ImGui-1.92.6-blue) ![Vulkan](https://img.shields.io/badge/Vulkan-1.0-red) ![Android](https://img.shields.io/badge/Android-API%2026+-green) + +## Features + +- **Native focus** — `NativeActivity` with `android_native_app_glue` to handle the main loop purely in C++. +- **Keyboard & Clipboard** — Includes a minimal `MainActivity.java` subclass to provide JNI hooks for the Android soft keyboard and native `ClipboardManager`. +- **Vulkan rendering** — Instance, Device, Swapchain, RenderPass all managed via ImGui helpers +- **Touch input** — `imgui_impl_android.h` handles multi-touch natively +- **Orientation handling** — Swapchain auto-rebuilds on rotation +- **Modular code** — Clean separation into `vulkan_helper.h`, `menu.h`, and `main.cpp` + +## Requirements + +| Requirement | Version | +| -------------- | ---------------- | +| Android Studio | 2024.x+ | +| Android NDK | 29.0.x | +| CMake | 3.22.1 | +| Min SDK | 26 (Android 8.0) | +| Target SDK | 36 | +| ABI | `arm64-v8a` | + +## Project Structure + +``` +app/src/main/ +├── AndroidManifest.xml ← Configured to use MainActivity +├── java/imgui/example/android/ +│ └── MainActivity.java ← Minimal NativeActivity subclass for Keyboard & Clipboard JNI +└── cpp/ + ├── CMakeLists.txt ← Build config (links imgui + vulkan) + ├── main.cpp ← Entry point: lifecycle (Init, MainLoopStep, Shutdown) + ├── vulkan_helper.h ← vkh:: namespace — Vulkan setup, rendering, cleanup + └── menu.h ← menu:: namespace — Your ImGui UI (edit this!) +``` + +ImGui sources are referenced from the parent imgui directory (`../../../../../../`). + +## Build & Run + +```bash +# Build +./gradlew assembleDebug + +# Build + install on connected device +./gradlew installDebug + +# View logs +adb logcat -s ImGuiVulkan +``` + +## Customizing the UI + +Edit **`menu.h`** — the `menu::Draw()` function is called every frame: + +```cpp +namespace menu +{ + inline void Draw() + { + ImGui::Begin("My App"); + ImGui::Text("Hello!"); + // Add your widgets here... + ImGui::End(); + } +} +``` + +All ImGui widgets work out of the box: windows, buttons, sliders, checkboxes, color pickers, popups, combo boxes, etc. + +## How It Works + +1. **`android_main()`** — NativeActivity entry point, runs the event + render loop +2. **`Init()`** — Called on `APP_CMD_INIT_WINDOW`: + - Creates `VkInstance`, `VkDevice`, `VkQueue` + - Creates `VkSurfaceKHR` from `ANativeWindow` + - Sets up swapchain, render pass, framebuffers via `ImGui_ImplVulkanH_Window` + - Initializes Dear ImGui with `imgui_impl_android` + `imgui_impl_vulkan` +3. **`MainLoopStep()`** — Called every frame: + - Checks for orientation/size changes → rebuilds swapchain if needed + - Calls `ImGui_ImplVulkan_NewFrame()` + `ImGui_ImplAndroid_NewFrame()` + - Calls `menu::Draw()` for your UI + - Records Vulkan command buffer + presents +4. **`Shutdown()`** — Called on `APP_CMD_TERM_WINDOW`, cleans up everything + +## Key Files Reference + +| File | What to modify | +| --------------------- | ----------------------------------------------- | +| `menu.h` | UI widgets — add buttons, sliders, windows | +| `vulkan_helper.h` | Vulkan settings (present mode, min image count) | +| `main.cpp` | App lifecycle, ImGui style/scaling, JNI calls | +| `MainActivity.java` | Android soft keyboard and clipboard handling | +| `AndroidManifest.xml` | App name, permissions, orientation | + +## License + +This example uses [Dear ImGui](https://github.com/ocornut/imgui) which is licensed under the MIT License. diff --git a/examples/example_android_vulkan/app/.gitignore b/examples/example_android_vulkan/app/.gitignore new file mode 100644 index 000000000000..42afabfd2abe --- /dev/null +++ b/examples/example_android_vulkan/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/examples/example_android_vulkan/app/build.gradle.kts b/examples/example_android_vulkan/app/build.gradle.kts new file mode 100644 index 000000000000..ab13af27a4f6 --- /dev/null +++ b/examples/example_android_vulkan/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "imgui.example.android" + compileSdk = 36 + + defaultConfig { + applicationId = "imgui.example.android" + minSdk = 26 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + ndk{ + abiFilters += "arm64-v8a" + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + ndkVersion = "29.0.14206865" +} + +dependencies { +} diff --git a/examples/example_android_vulkan/app/proguard-rules.pro b/examples/example_android_vulkan/app/proguard-rules.pro new file mode 100644 index 000000000000..481bb4348141 --- /dev/null +++ b/examples/example_android_vulkan/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/examples/example_android_vulkan/app/src/main/AndroidManifest.xml b/examples/example_android_vulkan/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..ea72bdb0d2f2 --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/examples/example_android_vulkan/app/src/main/cpp/CMakeLists.txt b/examples/example_android_vulkan/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000000..748508b32532 --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.22.1) + +project(androidvulkanimgui) + +set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../) + +include_directories( + ${IMGUI_DIR} + ${IMGUI_DIR}/backends + ${ANDROID_NDK}/sources/android/native_app_glue +) + +set(IMGUI_SOURCES + ${IMGUI_DIR}/imgui.cpp + ${IMGUI_DIR}/imgui_draw.cpp + ${IMGUI_DIR}/imgui_tables.cpp + ${IMGUI_DIR}/imgui_widgets.cpp + ${IMGUI_DIR}/imgui_demo.cpp + ${IMGUI_DIR}/backends/imgui_impl_android.cpp + ${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp +) + +add_library(${CMAKE_PROJECT_NAME} SHARED + main.cpp + ${IMGUI_SOURCES} + ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c +) + +target_link_libraries(${CMAKE_PROJECT_NAME} + android + log + vulkan) diff --git a/examples/example_android_vulkan/app/src/main/cpp/main.cpp b/examples/example_android_vulkan/app/src/main/cpp/main.cpp new file mode 100644 index 000000000000..4062397c546e --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/cpp/main.cpp @@ -0,0 +1,294 @@ +// ═════════════════════════════════════════════════════════════════════════════ +// main.cpp — Android NativeActivity entry point +// +// Lifecycle: android_main → Init (on INIT_WINDOW) → MainLoopStep → Shutdown +// Vulkan setup is in vulkan_helper.h, UI drawing is in menu.h. +// ═════════════════════════════════════════════════════════════════════════════ + +#include "vulkan_helper.h" +#include "menu.h" +#include "imgui_impl_android.h" +#include +#include + +static struct android_app* g_App = nullptr; +static bool g_Initialized = false; +static bool g_SoftKeyboardVisible = false; +static std::string g_ClipboardText; + +// ───────────────────────────────────────────────────────────────────────────── +// Forward declarations +// ───────────────────────────────────────────────────────────────────────────── +static void Init(struct android_app* app); +static void Shutdown(); +static void MainLoopStep(); + +// ───────────────────────────────────────────────────────────────────────────── +// Keyboard JNI Helpers +// ───────────────────────────────────────────────────────────────────────────── +static int CallJniVoidMethod(const char* methodName) +{ + JavaVM* java_vm = g_App->activity->vm; + JNIEnv* java_env = nullptr; + + if (java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6) == JNI_ERR) return -1; + if (java_vm->AttachCurrentThread(&java_env, nullptr) != JNI_OK) return -2; + + jclass clazz = java_env->GetObjectClass(g_App->activity->clazz); + if (!clazz) return -3; + + jmethodID method_id = java_env->GetMethodID(clazz, methodName, "()V"); + if (!method_id) return -4; + + java_env->CallVoidMethod(g_App->activity->clazz, method_id); + if (java_vm->DetachCurrentThread() != JNI_OK) return -5; + + return 0; +} + +static void ShowSoftKeyboardInput() { CallJniVoidMethod("showSoftInput"); } +static void HideSoftKeyboardInput() { CallJniVoidMethod("hideSoftInput"); } + +static int PollUnicodeChars() +{ + JavaVM* java_vm = g_App->activity->vm; + JNIEnv* java_env = nullptr; + + if (java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6) == JNI_ERR) return -1; + if (java_vm->AttachCurrentThread(&java_env, nullptr) != JNI_OK) return -2; + + jclass clazz = java_env->GetObjectClass(g_App->activity->clazz); + if (!clazz) return -3; + + jmethodID method_id = java_env->GetMethodID(clazz, "pollUnicodeChar", "()I"); + if (!method_id) return -4; + + ImGuiIO& io = ImGui::GetIO(); + jint unicode_character; + while ((unicode_character = java_env->CallIntMethod(g_App->activity->clazz, method_id)) != 0) + io.AddInputCharacter(unicode_character); + + if (java_vm->DetachCurrentThread() != JNI_OK) return -5; + return 0; +} + +static const char* GetClipboardTextFn(void* user_data) +{ + JavaVM* java_vm = g_App->activity->vm; + JNIEnv* java_env = nullptr; + + if (java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6) == JNI_ERR) return nullptr; + if (java_vm->AttachCurrentThread(&java_env, nullptr) != JNI_OK) return nullptr; + + jclass clazz = java_env->GetObjectClass(g_App->activity->clazz); + if (!clazz) return nullptr; + + jmethodID method_id = java_env->GetMethodID(clazz, "getClipboardText", "()Ljava/lang/String;"); + if (!method_id) return nullptr; + + jstring jstr = (jstring)java_env->CallObjectMethod(g_App->activity->clazz, method_id); + if (jstr) + { + const char* str = java_env->GetStringUTFChars(jstr, nullptr); + g_ClipboardText = str; + java_env->ReleaseStringUTFChars(jstr, str); + java_env->DeleteLocalRef(jstr); + } + else + { + g_ClipboardText = ""; + } + + if (java_vm->DetachCurrentThread() != JNI_OK) return nullptr; + return g_ClipboardText.c_str(); +} + +static void SetClipboardTextFn(void* user_data, const char* text) +{ + JavaVM* java_vm = g_App->activity->vm; + JNIEnv* java_env = nullptr; + + if (java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6) == JNI_ERR) return; + if (java_vm->AttachCurrentThread(&java_env, nullptr) != JNI_OK) return; + + jclass clazz = java_env->GetObjectClass(g_App->activity->clazz); + if (!clazz) return; + + jmethodID method_id = java_env->GetMethodID(clazz, "setClipboardText", "(Ljava/lang/String;)V"); + if (!method_id) return; + + jstring jstr = java_env->NewStringUTF(text); + java_env->CallVoidMethod(g_App->activity->clazz, method_id, jstr); + java_env->DeleteLocalRef(jstr); + + if (java_vm->DetachCurrentThread() != JNI_OK) return; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Android callbacks +// ───────────────────────────────────────────────────────────────────────────── +static void handleAppCmd(struct android_app* app, int32_t cmd) +{ + switch (cmd) + { + case APP_CMD_INIT_WINDOW: Init(app); break; + case APP_CMD_TERM_WINDOW: Shutdown(); break; + case APP_CMD_CONFIG_CHANGED: + case APP_CMD_WINDOW_RESIZED: vkh::g_SwapChainRebuild = true; break; + default: break; + } +} + +static int32_t handleInputEvent(struct android_app*, AInputEvent* event) +{ + return ImGui_ImplAndroid_HandleInputEvent(event); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Entry point +// ───────────────────────────────────────────────────────────────────────────── +void android_main(struct android_app* app) +{ + app->onAppCmd = handleAppCmd; + app->onInputEvent = handleInputEvent; + + while (true) + { + int events; + struct android_poll_source* source; + while (ALooper_pollOnce(g_Initialized ? 0 : -1, nullptr, &events, (void**)&source) >= 0) + { + if (source) source->process(app, source); + if (app->destroyRequested) { if (g_Initialized) Shutdown(); return; } + } + MainLoopStep(); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Init +// ───────────────────────────────────────────────────────────────────────────── +void Init(struct android_app* app) +{ + if (g_Initialized) return; + g_App = app; + ANativeWindow_acquire(app->window); + + // Vulkan + vkh::SetupVulkan(); + + VkAndroidSurfaceCreateInfoKHR sci = { VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR }; + sci.window = app->window; + VkSurfaceKHR surface; + vkCreateAndroidSurfaceKHR(vkh::g_Instance, &sci, nullptr, &surface); + + int w = ANativeWindow_getWidth(app->window); + int h = ANativeWindow_getHeight(app->window); + + auto* wd = &vkh::g_MainWindowData; + vkh::SetupWindow(wd, surface, w, h); + + // ImGui + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.IniFilename = nullptr; + io.SetClipboardTextFn = SetClipboardTextFn; + io.GetClipboardTextFn = GetClipboardTextFn; + + ImGui::StyleColorsDark(); + ImGuiStyle& st = ImGui::GetStyle(); + st.ScaleAllSizes(2.0f); + st.FontScaleDpi = 2.0f; + st.WindowRounding = 8.0f; + st.FrameRounding = 4.0f; + st.GrabRounding = 4.0f; + + ImGui_ImplAndroid_Init(app->window); + + ImGui_ImplVulkan_InitInfo ii = {}; + ii.Instance = vkh::g_Instance; + ii.PhysicalDevice = vkh::g_PhysicalDevice; + ii.Device = vkh::g_Device; + ii.QueueFamily = vkh::g_QueueFamily; + ii.Queue = vkh::g_Queue; + ii.PipelineCache = vkh::g_PipelineCache; + ii.DescriptorPool = vkh::g_DescriptorPool; + ii.MinImageCount = vkh::g_MinImageCount; + ii.ImageCount = wd->ImageCount; + ii.CheckVkResultFn = vkh::CheckVkResult; + ii.PipelineInfoMain.RenderPass = wd->RenderPass; + ii.PipelineInfoMain.Subpass = 0; + ii.PipelineInfoMain.MSAASamples = VK_SAMPLE_COUNT_1_BIT; + ImGui_ImplVulkan_Init(&ii); + + g_Initialized = true; + LOGI("[init] Ready! %dx%d", w, h); +} + +// ───────────────────────────────────────────────────────────────────────────── +// MainLoopStep +// ───────────────────────────────────────────────────────────────────────────── +void MainLoopStep() +{ + if (!g_Initialized) return; + auto* wd = &vkh::g_MainWindowData; + + vkh::RebuildIfNeeded(wd, g_App->window); + + ImGuiIO& io = ImGui::GetIO(); + + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplAndroid_NewFrame(); + ImGui::NewFrame(); + + // Poll unicode characters from Java + PollUnicodeChars(); + + // Show/Hide soft keyboard based on ImGui focus + if (io.WantTextInput && !g_SoftKeyboardVisible) + { + ShowSoftKeyboardInput(); + g_SoftKeyboardVisible = true; + } + else if (!io.WantTextInput && g_SoftKeyboardVisible) + { + HideSoftKeyboardInput(); + g_SoftKeyboardVisible = false; + } + + menu::Draw(); + + ImGui::Render(); + ImDrawData* dd = ImGui::GetDrawData(); + if (dd->DisplaySize.x > 0 && dd->DisplaySize.y > 0) + { + auto& cc = menu::clearColor; + wd->ClearValue.color.float32[0] = cc.x * cc.w; + wd->ClearValue.color.float32[1] = cc.y * cc.w; + wd->ClearValue.color.float32[2] = cc.z * cc.w; + wd->ClearValue.color.float32[3] = cc.w; + vkh::FrameRender(wd, dd); + vkh::FramePresent(wd); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Shutdown +// ───────────────────────────────────────────────────────────────────────────── +void Shutdown() +{ + if (!g_Initialized) return; + vkDeviceWaitIdle(vkh::g_Device); + + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplAndroid_Shutdown(); + ImGui::DestroyContext(); + + vkh::CleanupWindow(&vkh::g_MainWindowData); + vkh::CleanupVulkan(); + + ANativeWindow_release(g_App->window); + g_Initialized = false; + LOGI("[shutdown] Done"); +} diff --git a/examples/example_android_vulkan/app/src/main/cpp/menu.h b/examples/example_android_vulkan/app/src/main/cpp/menu.h new file mode 100644 index 000000000000..eb0253e529b7 --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/cpp/menu.h @@ -0,0 +1,37 @@ +#pragma once +// ═════════════════════════════════════════════════════════════════════════════ +// menu.h — ImGui UI layout +// +// Edit this file to change what's drawn. Called once per frame from main.cpp. +// ═════════════════════════════════════════════════════════════════════════════ + +#include "imgui.h" + +namespace menu +{ + static bool showDemo = true; + static ImVec4 clearColor = ImVec4(0.1f, 0.1f, 0.12f, 1.0f); + + inline void Draw() + { + if (showDemo) + ImGui::ShowDemoWindow(&showDemo); + + { + static float f = 0.0f; + static int counter = 0; + + ImGui::Begin("Hello, Vulkan!"); + ImGui::Text("Standalone Vulkan + ImGui app."); + ImGui::Checkbox("Demo Window", &showDemo); + ImGui::SliderFloat("float", &f, 0.0f, 1.0f); + ImGui::ColorEdit3("clear color", (float*)&clearColor); + if (ImGui::Button("Button")) counter++; + ImGui::SameLine(); + ImGui::Text("counter = %d", counter); + ImGui::Text("%.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); + ImGui::End(); + } + } + +} // namespace menu diff --git a/examples/example_android_vulkan/app/src/main/cpp/vulkan_helper.h b/examples/example_android_vulkan/app/src/main/cpp/vulkan_helper.h new file mode 100644 index 000000000000..475357bbdb6f --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/cpp/vulkan_helper.h @@ -0,0 +1,252 @@ +#pragma once +// ═════════════════════════════════════════════════════════════════════════════ +// vulkan_helper.h — Vulkan setup, cleanup, and frame rendering +// +// Uses ImGui_ImplVulkanH helpers for swapchain/renderpass/framebuffer +// management. Exposes globals and functions for main.cpp to use. +// ═════════════════════════════════════════════════════════════════════════════ + +#include +#include +#include +#include +#include +#include +#include "imgui.h" +#include "imgui_impl_vulkan.h" + +#define LOG_TAG "ImGuiVulkan" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +namespace vkh +{ + +// ───────────────────────────────────────────────────────────────────────────── +// Globals +// ───────────────────────────────────────────────────────────────────────────── +static VkAllocationCallbacks* g_Allocator = nullptr; +static VkInstance g_Instance = VK_NULL_HANDLE; +static VkPhysicalDevice g_PhysicalDevice = VK_NULL_HANDLE; +static VkDevice g_Device = VK_NULL_HANDLE; +static uint32_t g_QueueFamily = (uint32_t)-1; +static VkQueue g_Queue = VK_NULL_HANDLE; +static VkDescriptorPool g_DescriptorPool = VK_NULL_HANDLE; +static VkPipelineCache g_PipelineCache = VK_NULL_HANDLE; + +static ImGui_ImplVulkanH_Window g_MainWindowData; +static uint32_t g_MinImageCount = 2; +static bool g_SwapChainRebuild = false; + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── +inline void CheckVkResult(VkResult err) +{ + if (err == VK_SUCCESS) return; + LOGE("[vulkan] VkResult = %d", err); + if (err < 0) abort(); +} + +static bool IsExtensionAvailable(const ImVector& props, const char* ext) +{ + for (const auto& p : props) + if (strcmp(p.extensionName, ext) == 0) return true; + return false; +} + +// ───────────────────────────────────────────────────────────────────────────── +// SetupVulkan — Instance, PhysicalDevice, Device, Queue, DescriptorPool +// ───────────────────────────────────────────────────────────────────────────── +inline void SetupVulkan() +{ + VkResult err; + + // Instance + { + ImVector extensions; + extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME); + extensions.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); + + uint32_t n; + ImVector props; + vkEnumerateInstanceExtensionProperties(nullptr, &n, nullptr); + props.resize(n); + vkEnumerateInstanceExtensionProperties(nullptr, &n, props.Data); + + if (IsExtensionAvailable(props, VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME)) + extensions.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); + + VkInstanceCreateInfo ci = {}; + ci.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + ci.enabledExtensionCount = (uint32_t)extensions.Size; + ci.ppEnabledExtensionNames = extensions.Data; + err = vkCreateInstance(&ci, g_Allocator, &g_Instance); + CheckVkResult(err); + LOGI("[vulkan] Instance created"); + } + + // PhysicalDevice + QueueFamily + g_PhysicalDevice = ImGui_ImplVulkanH_SelectPhysicalDevice(g_Instance); + g_QueueFamily = ImGui_ImplVulkanH_SelectQueueFamilyIndex(g_PhysicalDevice); + + // Logical Device + { + ImVector dev_ext; + dev_ext.push_back("VK_KHR_swapchain"); + + const float prio = 1.0f; + VkDeviceQueueCreateInfo qci = {}; + qci.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + qci.queueFamilyIndex = g_QueueFamily; + qci.queueCount = 1; + qci.pQueuePriorities = &prio; + + VkDeviceCreateInfo dci = {}; + dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + dci.queueCreateInfoCount = 1; + dci.pQueueCreateInfos = &qci; + dci.enabledExtensionCount = (uint32_t)dev_ext.Size; + dci.ppEnabledExtensionNames = dev_ext.Data; + err = vkCreateDevice(g_PhysicalDevice, &dci, g_Allocator, &g_Device); + CheckVkResult(err); + vkGetDeviceQueue(g_Device, g_QueueFamily, 0, &g_Queue); + LOGI("[vulkan] Device + Queue created"); + } + + // DescriptorPool + { + VkDescriptorPoolSize ps = { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, IMGUI_IMPL_VULKAN_MINIMUM_IMAGE_SAMPLER_POOL_SIZE }; + VkDescriptorPoolCreateInfo dpci = {}; + dpci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + dpci.maxSets = ps.descriptorCount; + dpci.poolSizeCount = 1; + dpci.pPoolSizes = &ps; + err = vkCreateDescriptorPool(g_Device, &dpci, g_Allocator, &g_DescriptorPool); + CheckVkResult(err); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// SetupWindow — Surface, SwapChain, RenderPass, Framebuffers +// ───────────────────────────────────────────────────────────────────────────── +inline void SetupWindow(ImGui_ImplVulkanH_Window* wd, VkSurfaceKHR surface, int w, int h) +{ + VkBool32 res; + vkGetPhysicalDeviceSurfaceSupportKHR(g_PhysicalDevice, g_QueueFamily, surface, &res); + if (res != VK_TRUE) { LOGE("No WSI support!"); abort(); } + + const VkFormat fmts[] = { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM }; + wd->Surface = surface; + wd->SurfaceFormat = ImGui_ImplVulkanH_SelectSurfaceFormat(g_PhysicalDevice, surface, fmts, IM_COUNTOF(fmts), VK_COLORSPACE_SRGB_NONLINEAR_KHR); + + VkPresentModeKHR pm[] = { VK_PRESENT_MODE_FIFO_KHR }; + wd->PresentMode = ImGui_ImplVulkanH_SelectPresentMode(g_PhysicalDevice, surface, pm, IM_COUNTOF(pm)); + + ImGui_ImplVulkanH_CreateOrResizeWindow(g_Instance, g_PhysicalDevice, g_Device, wd, g_QueueFamily, g_Allocator, w, h, g_MinImageCount, 0); + LOGI("[vulkan] Window: %dx%d, %d images", wd->Width, wd->Height, wd->ImageCount); +} + +// ───────────────────────────────────────────────────────────────────────────── +// RebuildSwapchain — handles orientation/size changes +// ───────────────────────────────────────────────────────────────────────────── +inline bool RebuildIfNeeded(ImGui_ImplVulkanH_Window* wd, ANativeWindow* window) +{ + int w = ANativeWindow_getWidth(window); + int h = ANativeWindow_getHeight(window); + + // Detect size change + if (w > 0 && h > 0 && (w != wd->Width || h != wd->Height)) + g_SwapChainRebuild = true; + + if (!g_SwapChainRebuild) return false; + if (w <= 0 || h <= 0) return false; + + vkDeviceWaitIdle(g_Device); + ImGui_ImplVulkan_SetMinImageCount(g_MinImageCount); + ImGui_ImplVulkanH_CreateOrResizeWindow(g_Instance, g_PhysicalDevice, g_Device, wd, g_QueueFamily, g_Allocator, w, h, g_MinImageCount, 0); + wd->FrameIndex = 0; + g_SwapChainRebuild = false; + + ImGui::GetIO().DisplaySize = ImVec2((float)w, (float)h); + LOGI("[resize] %dx%d", w, h); + return true; +} + +// ───────────────────────────────────────────────────────────────────────────── +// FrameRender / FramePresent +// ───────────────────────────────────────────────────────────────────────────── +inline void FrameRender(ImGui_ImplVulkanH_Window* wd, ImDrawData* draw_data) +{ + VkSemaphore image_acquired = wd->FrameSemaphores[wd->SemaphoreIndex].ImageAcquiredSemaphore; + VkSemaphore render_complete = wd->FrameSemaphores[wd->SemaphoreIndex].RenderCompleteSemaphore; + + VkResult err = vkAcquireNextImageKHR(g_Device, wd->Swapchain, UINT64_MAX, image_acquired, VK_NULL_HANDLE, &wd->FrameIndex); + if (err == VK_ERROR_OUT_OF_DATE_KHR || err == VK_SUBOPTIMAL_KHR) g_SwapChainRebuild = true; + if (err == VK_ERROR_OUT_OF_DATE_KHR) return; + if (err != VK_SUBOPTIMAL_KHR) CheckVkResult(err); + + ImGui_ImplVulkanH_Frame* fd = &wd->Frames[wd->FrameIndex]; + vkWaitForFences(g_Device, 1, &fd->Fence, VK_TRUE, UINT64_MAX); + vkResetFences(g_Device, 1, &fd->Fence); + vkResetCommandPool(g_Device, fd->CommandPool, 0); + + VkCommandBufferBeginInfo bi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; + bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(fd->CommandBuffer, &bi); + + VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + rp.renderPass = wd->RenderPass; + rp.framebuffer = fd->Framebuffer; + rp.renderArea = {{0,0}, {(uint32_t)wd->Width, (uint32_t)wd->Height}}; + rp.clearValueCount = 1; + rp.pClearValues = &wd->ClearValue; + vkCmdBeginRenderPass(fd->CommandBuffer, &rp, VK_SUBPASS_CONTENTS_INLINE); + + ImGui_ImplVulkan_RenderDrawData(draw_data, fd->CommandBuffer); + vkCmdEndRenderPass(fd->CommandBuffer); + + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO }; + si.waitSemaphoreCount = 1; si.pWaitSemaphores = &image_acquired; + si.pWaitDstStageMask = &wait_stage; + si.commandBufferCount = 1; si.pCommandBuffers = &fd->CommandBuffer; + si.signalSemaphoreCount = 1; si.pSignalSemaphores = &render_complete; + + vkEndCommandBuffer(fd->CommandBuffer); + vkQueueSubmit(g_Queue, 1, &si, fd->Fence); +} + +inline void FramePresent(ImGui_ImplVulkanH_Window* wd) +{ + if (g_SwapChainRebuild) return; + VkSemaphore render_complete = wd->FrameSemaphores[wd->SemaphoreIndex].RenderCompleteSemaphore; + VkPresentInfoKHR pi = { VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; + pi.waitSemaphoreCount = 1; pi.pWaitSemaphores = &render_complete; + pi.swapchainCount = 1; pi.pSwapchains = &wd->Swapchain; + pi.pImageIndices = &wd->FrameIndex; + + VkResult err = vkQueuePresentKHR(g_Queue, &pi); + if (err == VK_ERROR_OUT_OF_DATE_KHR || err == VK_SUBOPTIMAL_KHR) g_SwapChainRebuild = true; + if (err != VK_ERROR_OUT_OF_DATE_KHR && err != VK_SUBOPTIMAL_KHR) CheckVkResult(err); + wd->SemaphoreIndex = (wd->SemaphoreIndex + 1) % wd->SemaphoreCount; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Cleanup +// ───────────────────────────────────────────────────────────────────────────── +inline void CleanupWindow(ImGui_ImplVulkanH_Window* wd) +{ + ImGui_ImplVulkanH_DestroyWindow(g_Instance, g_Device, wd, g_Allocator); + vkDestroySurfaceKHR(g_Instance, wd->Surface, g_Allocator); +} + +inline void CleanupVulkan() +{ + vkDestroyDescriptorPool(g_Device, g_DescriptorPool, g_Allocator); + vkDestroyDevice(g_Device, g_Allocator); + vkDestroyInstance(g_Instance, g_Allocator); +} + +} // namespace vkh diff --git a/examples/example_android_vulkan/app/src/main/java/imgui/example/android/MainActivity.java b/examples/example_android_vulkan/app/src/main/java/imgui/example/android/MainActivity.java new file mode 100644 index 000000000000..d8b73d9c8123 --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/java/imgui/example/android/MainActivity.java @@ -0,0 +1,112 @@ +package imgui.example.android; + +import android.app.NativeActivity; +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.inputmethod.InputMethodManager; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Minimal NativeActivity subclass that adds: + * 1. showSoftInput() — show the on-screen keyboard (called from C++ via JNI) + * 2. hideSoftInput() — hide the on-screen keyboard (called from C++ via JNI) + * 3. pollUnicodeChar() — return queued unicode chars (called from C++ via JNI) + */ +public class MainActivity extends NativeActivity { + + private final LinkedBlockingQueue mUnicodeCharQueue = new LinkedBlockingQueue<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + // Called from native code via JNI + public void showSoftInput() { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) + imm.showSoftInput(getWindow().getDecorView(), 0); + } + + // Called from native code via JNI + public void hideSoftInput() { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) + imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0); + } + + // Intercept key events to get unicode character values + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + int unicodeChar = event.getUnicodeChar(); + if (unicodeChar != 0) + mUnicodeCharQueue.offer(unicodeChar); + } else if (event.getAction() == KeyEvent.ACTION_MULTIPLE) { + String characters = event.getCharacters(); + if (characters != null) { + for (int i = 0; i < characters.length(); i++) { + mUnicodeCharQueue.offer((int) characters.charAt(i)); + } + } + } + return super.dispatchKeyEvent(event); + } + + // Called from native code via JNI — returns 0 when queue is empty + public int pollUnicodeChar() { + Integer val = mUnicodeCharQueue.poll(); + return (val != null) ? val : 0; + } + + // Called from native code via JNI + public String getClipboardText() { + final java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1); + final String[] result = new String[1]; + result[0] = ""; + + runOnUiThread(new Runnable() { + @Override + public void run() { + try { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService( + Context.CLIPBOARD_SERVICE); + if (clipboard != null && clipboard.hasPrimaryClip()) { + android.content.ClipData clip = clipboard.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + CharSequence text = clip.getItemAt(0).getText(); + if (text != null) + result[0] = text.toString(); + } + } + } finally { + latch.countDown(); + } + } + }); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return result[0]; + } + + // Called from native code via JNI + public void setClipboardText(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService( + Context.CLIPBOARD_SERVICE); + if (clipboard != null) { + android.content.ClipData clip = android.content.ClipData.newPlainText("clipboard", text); + clipboard.setPrimaryClip(clip); + } + } + }); + } +} diff --git a/examples/example_android_vulkan/app/src/main/res/drawable/ic_launcher_background.xml b/examples/example_android_vulkan/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000000..07d5da9cbf14 --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/example_android_vulkan/app/src/main/res/drawable/ic_launcher_foreground.xml b/examples/example_android_vulkan/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000000..7706ab9e6d40 --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/examples/example_android_vulkan/app/src/main/res/layout/activity_main.xml b/examples/example_android_vulkan/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000000..ae9361bcaf69 --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/examples/example_android_vulkan/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 000000000000..b3e26b4c60c2 --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/examples/example_android_vulkan/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 000000000000..b3e26b4c60c2 --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000000..c209e78ecd37 Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000000..b2dfe3d1ba5c Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000000..4f0f1d64e58b Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000000..62b611da0816 Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000000..948a3070fe34 Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000000..1b9a6956b3ac Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000000..28d4b77f9f03 Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000000..9287f5083623 Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000000..aa7d6427e6fa Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/example_android_vulkan/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000000..9126ae37cbc3 Binary files /dev/null and b/examples/example_android_vulkan/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/examples/example_android_vulkan/app/src/main/res/values-night/themes.xml b/examples/example_android_vulkan/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000000..0c9c9d5b564c --- /dev/null +++ b/examples/example_android_vulkan/app/src/main/res/values-night/themes.xml @@ -0,0 +1,4 @@ + + +