diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 455fcd4a..35c2be93 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -66,6 +66,12 @@ if(IOS) set(TEMPEST_BUILD_SHARED OFF) endif() +if(ANDROID) + set(NATIVE_APP_GLUE_DIR "${CMAKE_ANDROID_NDK}/sources/android/native_app_glue") + add_library(native_app_glue STATIC "${NATIVE_APP_GLUE_DIR}/android_native_app_glue.c") + target_include_directories(native_app_glue PUBLIC "${NATIVE_APP_GLUE_DIR}") +endif() + ### Compilers if(MSVC) add_definitions(-D_USE_MATH_DEFINES) @@ -336,6 +342,9 @@ elseif(IOS) target_link_libraries(${PROJECT_NAME} PRIVATE "-framework UiKit" "-framework Foundation" "-framework QuartzCore" "-framework Metal") elseif(APPLE) target_link_libraries(${PROJECT_NAME} PRIVATE "-framework AppKit" "-framework QuartzCore" "-framework Metal") +elseif(ANDROID) + target_link_libraries(${PROJECT_NAME} PRIVATE native_app_glue android dl log) + target_link_options(${PROJECT_NAME} INTERFACE "-Wl,-u,main" "-Wl,--export-dynamic-symbol=main") elseif(UNIX) target_link_libraries(${PROJECT_NAME} PRIVATE X11 Xcursor) endif() diff --git a/Engine/system/api/androidapi.cpp b/Engine/system/api/androidapi.cpp new file mode 100644 index 00000000..f9b3996d --- /dev/null +++ b/Engine/system/api/androidapi.cpp @@ -0,0 +1,240 @@ +#include "androidapi.h" + +#ifdef __ANDROID__ + +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace Tempest; + +extern "C" void android_main(android_app* state); + +static android_app* app = nullptr; +static Tempest::Window* mainWindow = nullptr; +static std::atomic_bool isExit = false; +static bool resumed = false; +static bool focused = false; +static bool active = false; +static bool hasWindow = false; + +void AndroidApi::updateFocus() { + const bool next = resumed && focused; + if(active==next) + return; + active = next; + if(mainWindow!=nullptr) { + FocusEvent event(active,Event::FocusReason::UnknownReason); + AndroidApi::dispatchFocus(*mainWindow,event); + } + } + +void AndroidApi::updateWindow() { + if(app==nullptr || app->window==nullptr) + return; + + hasWindow = true; + if(mainWindow==nullptr) + return; + + SizeEvent event(ANativeWindow_getWidth(app->window),ANativeWindow_getHeight(app->window)); + AndroidApi::dispatchResize(*mainWindow,event); + } + +void AndroidApi::onAppCmd(void*, int32_t cmd) { + switch(cmd) { + case APP_CMD_INIT_WINDOW: + if(mainWindow!=nullptr) { + // TODO: handle native surface recreation in the Vulkan swapchain. + Log::e("Android native window recreation is not implemented"); + std::terminate(); + } + updateWindow(); + break; + case APP_CMD_TERM_WINDOW: + hasWindow = false; + break; + case APP_CMD_WINDOW_RESIZED: + case APP_CMD_CONFIG_CHANGED: + updateWindow(); + break; + case APP_CMD_GAINED_FOCUS: + focused = true; + updateFocus(); + break; + case APP_CMD_LOST_FOCUS: + focused = false; + updateFocus(); + break; + case APP_CMD_RESUME: + resumed = true; + updateFocus(); + break; + case APP_CMD_PAUSE: + resumed = false; + updateFocus(); + break; + case APP_CMD_DESTROY: + if(mainWindow!=nullptr) { + CloseEvent event; + AndroidApi::dispatchClose(*mainWindow,event); + } + isExit.store(true); + break; + default: + break; + } + } + +static void pollAndroid(android_app* state, int timeout) { + int pending = 0; + android_poll_source* source = nullptr; + while(ALooper_pollOnce(timeout,nullptr,&pending,reinterpret_cast(&source))>=0) { + if(source!=nullptr) + source->process(state,source); + timeout = 0; + } + } + +SystemApi::Window* AndroidApi::createAndroidWindow(Tempest::Window* owner) { + if(mainWindow!=nullptr) + return nullptr; + app->onAppCmd = [](android_app* state, int32_t cmd) { onAppCmd(state,cmd); }; + while(!hasWindow && !isExit.load() && app->destroyRequested==0) + pollAndroid(app,-1); + if(isExit.load() || app->destroyRequested!=0) + return nullptr; + mainWindow = owner; + return reinterpret_cast(app->window); + } + +SystemApi::Window* AndroidApi::implCreateWindow(Tempest::Window* owner, uint32_t, uint32_t) { + return createAndroidWindow(owner); + } + +SystemApi::Window* AndroidApi::implCreateWindow(Tempest::Window* owner, ShowMode) { + return createAndroidWindow(owner); + } + +void AndroidApi::implDestroyWindow(SystemApi::Window*) { + mainWindow = nullptr; + } + +void AndroidApi::implExit() { + isExit.store(true); + ALooper_wake(app->looper); + } + +Rect AndroidApi::implWindowClientRect(SystemApi::Window* w) { + const auto window = reinterpret_cast(w); + return Rect(0,0,ANativeWindow_getWidth(window),ANativeWindow_getHeight(window)); + } + +bool AndroidApi::implSetAsFullscreen(SystemApi::Window*, bool) { + // TODO: toggle Android immersive mode. + return false; + } + +bool AndroidApi::implIsFullscreen(SystemApi::Window*) { + // TODO: query Android immersive mode. + return true; + } + +void AndroidApi::implSetCursorPosition(SystemApi::Window*, int, int) { + } + +void AndroidApi::implShowCursor(SystemApi::Window*, CursorShape) { + } + +bool AndroidApi::implIsRunning() { + return !isExit.load(); + } + +int AndroidApi::implExec(AppCallBack& cb) { + while(!isExit.load()) { + implProcessEvents(cb); + } + return 0; + } + +void AndroidApi::implProcessEvents(AppCallBack& cb) { + if(isExit.load()) + return; + pollAndroid(app,active && hasWindow ? 0 : -1); + if(isExit.load()) + return; + if(mainWindow!=nullptr && active && hasWindow) + dispatchRender(*mainWindow); + if(active && hasWindow && !isExit.load() && cb.onTimer()==0) + std::this_thread::yield(); + } + +void AndroidApi::implSetWindowTitle(SystemApi::Window*, const char*) { + // TODO: update the activity title through JNI. + } + +static int runMain() { + Dl_info module = {}; + if(dladdr(reinterpret_cast(&android_main),&module)==0) { + Log::e("Unable to locate the application library"); + return EXIT_FAILURE; + } + void* self = dlopen(module.dli_fname,RTLD_NOW); + if(self==nullptr) { + const char* error = dlerror(); + Log::e("Unable to open the application library: ",error==nullptr ? "unknown error" : error); + return EXIT_FAILURE; + } + using Main = int(*)(int,char**); + auto entry = reinterpret_cast
(dlsym(self,"main")); + if(entry==nullptr) { + const char* error = dlerror(); + Log::e("Unable to find the application entry point: ",error==nullptr ? "unknown error" : error); + dlclose(self); + return EXIT_FAILURE; + } + // NativeActivity owns the library for the duration of android_main. + dlclose(self); + char arg0[] = "app"; + char* argv[] = {arg0,nullptr}; + return entry(1,argv); + } + +extern "C" void android_main(android_app* state) { + static std::atomic_flag started = ATOMIC_FLAG_INIT; + if(started.test_and_set()) { + ANativeActivity_finish(state->activity); + while(state->destroyRequested==0) + pollAndroid(state,-1); + return; + } + app = state; + int result = EXIT_FAILURE; + try { + result = runMain(); + } + catch(const std::exception& e) { + Log::e("Unhandled native exception: ",e.what()); + } + catch(...) { + Log::e("Unhandled native exception"); + } + + if(app->destroyRequested==0) + ANativeActivity_finish(app->activity); + // Re-entering main would reuse application statics from the previous run. + std::exit(result); + } + +#endif diff --git a/Engine/system/api/androidapi.h b/Engine/system/api/androidapi.h new file mode 100644 index 00000000..9329a39c --- /dev/null +++ b/Engine/system/api/androidapi.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace Tempest { + +class AndroidApi final : SystemApi { + private: + AndroidApi() = default; + + static Window* createAndroidWindow(Tempest::Window* owner); + static void onAppCmd(void* app, int32_t cmd); + static void updateFocus(); + static void updateWindow(); + + Window* implCreateWindow(Tempest::Window* owner, uint32_t width, uint32_t height) override; + Window* implCreateWindow(Tempest::Window* owner, ShowMode sm) override; + void implDestroyWindow(Window* w) override; + void implExit() override; + + Rect implWindowClientRect(SystemApi::Window* w) override; + bool implSetAsFullscreen(SystemApi::Window* w, bool fullScreen) override; + bool implIsFullscreen(SystemApi::Window* w) override; + + void implSetCursorPosition(SystemApi::Window* w, int x, int y) override; + void implShowCursor(SystemApi::Window* w, CursorShape cursor) override; + + bool implIsRunning() override; + int implExec(AppCallBack& cb) override; + void implProcessEvents(AppCallBack& cb) override; + + void implSetWindowTitle(SystemApi::Window* w, const char* utf8) override; + + friend class SystemApi; + }; + +} diff --git a/Engine/system/systemapi.cpp b/Engine/system/systemapi.cpp index f942ba89..ad24b859 100644 --- a/Engine/system/systemapi.cpp +++ b/Engine/system/systemapi.cpp @@ -4,6 +4,7 @@ #include "api/x11api.h" #include "api/macosapi.h" #include "api/iosapi.h" +#include "api/androidapi.h" #include "eventdispatcher.h" #include @@ -98,6 +99,8 @@ uint16_t SystemApi::translateKey(uint64_t scancode) { SystemApi& SystemApi::inst() { #ifdef __WINDOWS__ static WindowsApi api; +#elif defined(__ANDROID__) + static AndroidApi api; #elif defined(__UNIX__) static X11Api api; #elif defined(__OSX__) diff --git a/Examples/Android/CMakeLists.txt b/Examples/Android/CMakeLists.txt index 7dc73837..8fedd4f3 100644 --- a/Examples/Android/CMakeLists.txt +++ b/Examples/Android/CMakeLists.txt @@ -5,13 +5,16 @@ if(NOT ANDROID) message(FATAL_ERROR "Configure this example with the Android NDK toolchain") endif() -# Tempest applications get this helper through add_subdirectory(Engine). -# This packaging sample does not depend on the Android backend yet. -include(../../Engine/cmake/TempestAndroid.cmake) +set(TEMPEST_BUILD_SHARED OFF CACHE BOOL "" FORCE) +# Graphics and audio backends are outside this window lifecycle example. +set(TEMPEST_BUILD_AUDIO OFF CACHE BOOL "" FORCE) +set(TEMPEST_BUILD_VULKAN OFF CACHE BOOL "" FORCE) +add_subdirectory(../../Engine Engine) add_library(TempestExample SHARED main.cpp) set_target_properties(TempestExample PROPERTIES OUTPUT_NAME tempest-example) -target_link_libraries(TempestExample PRIVATE android log) +target_link_libraries(TempestExample PRIVATE Tempest) +target_include_directories(TempestExample PRIVATE ../../Engine/include) add_android_apk(TempestExample-apk PACKAGE_NAME org.tempest.example diff --git a/Examples/Android/README.md b/Examples/Android/README.md index 6cb013d6..ef4977ae 100644 --- a/Examples/Android/README.md +++ b/Examples/Android/README.md @@ -1,6 +1,8 @@ -# Android packaging +# Android window -A small NativeActivity packaging example. It will move into `Examples/Empty` when the Android backend is available upstream. +A minimal NativeActivity application using Tempest's Android window backend. It forwards window creation, resize, focus, pause, resume and destruction through Tempest's normal application lifecycle. + +Native surface recreation and immersive-mode switching are not implemented yet. The example terminates if Android recreates its drawing surface. With JDK 17, Gradle 8.9, Ninja and the Android SDK configured (`ANDROID_HOME`), install SDK 35, build-tools 35.0.0, NDK 27.0.12077973 and CMake 3.22.1. Replace `/path/to/ndk` below with the NDK installation directory. diff --git a/Examples/Android/main.cpp b/Examples/Android/main.cpp index 30a01449..786fc0ca 100644 --- a/Examples/Android/main.cpp +++ b/Examples/Android/main.cpp @@ -1,28 +1,23 @@ -#include -#include -#include -#include +#include +#include +#include +#include -// A packaging smoke test using the platform activity, independent of Tempest's Android backend. -// Fold this into Examples/Empty once that backend is available upstream. -static void draw(ANativeActivity*, ANativeWindow* window) { - ANativeWindow_setBuffersGeometry(window,0,0,WINDOW_FORMAT_RGBA_8888); - ANativeWindow_Buffer buffer = {}; - if(ANativeWindow_lock(window,&buffer,nullptr)!=0) - return; - auto pixels = static_cast(buffer.bits); - for(int y=0; ybuffer.width/3 && xbuffer.height/3 && ycallbacks->onNativeWindowCreated = draw; - activity->callbacks->onNativeWindowResized = draw; - activity->callbacks->onNativeWindowRedrawNeeded = draw; - __android_log_print(ANDROID_LOG_INFO,"TempestExample","Native packaging example started"); + void focusEvent(Tempest::FocusEvent& event) override { + Tempest::Log::i(event.in ? "Window resumed" : "Window paused"); + Tempest::Window::focusEvent(event); + } + }; + +int main(int, char**) { + Tempest::Application app; + Example window; + return app.exec(); }