Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
240 changes: 240 additions & 0 deletions Engine/system/api/androidapi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#include "androidapi.h"

#ifdef __ANDROID__

#include <android_native_app_glue.h>

#include <Tempest/Event>
#include <Tempest/Log>
#include <Tempest/Window>

#include <android/native_activity.h>
#include <android/native_window.h>

#include <atomic>
#include <cstdlib>
#include <dlfcn.h>
#include <exception>
#include <thread>

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<void**>(&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;
Comment thread
Try marked this conversation as resolved.
return reinterpret_cast<SystemApi::Window*>(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<ANativeWindow*>(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.
}
Comment thread
Try marked this conversation as resolved.

static int runMain() {
Dl_info module = {};
if(dladdr(reinterpret_cast<void*>(&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<Main>(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
37 changes: 37 additions & 0 deletions Engine/system/api/androidapi.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once

#include <Tempest/SystemApi>

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;
};

}
3 changes: 3 additions & 0 deletions Engine/system/systemapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "api/x11api.h"
#include "api/macosapi.h"
#include "api/iosapi.h"
#include "api/androidapi.h"
#include "eventdispatcher.h"

#include <Tempest/Event>
Expand Down Expand Up @@ -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__)
Expand Down
11 changes: 7 additions & 4 deletions Examples/Android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
Try marked this conversation as resolved.
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
Expand Down
6 changes: 4 additions & 2 deletions Examples/Android/README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
43 changes: 19 additions & 24 deletions Examples/Android/main.cpp
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
#include <android/log.h>
#include <android/native_activity.h>
#include <android/native_window.h>
#include <cstdint>
#include <Tempest/Application>
#include <Tempest/Event>
#include <Tempest/Log>
#include <Tempest/Window>

// 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<uint32_t*>(buffer.bits);
for(int y=0; y<buffer.height; ++y)
for(int x=0; x<buffer.width; ++x) {
const bool center = x>buffer.width/3 && x<buffer.width*2/3 &&
y>buffer.height/3 && y<buffer.height*2/3;
pixels[y*buffer.stride+x] = center ? 0xffa1c2d7u : 0xff593314u;
class Example final : public Tempest::Window {
private:
void resizeEvent(Tempest::SizeEvent& event) override {
Tempest::Log::i("Window resized: ",event.w,"x",event.h);
Tempest::Window::resizeEvent(event);
}
ANativeWindow_unlockAndPost(window);
}

extern "C" void ANativeActivity_onCreate(ANativeActivity* activity, void*, size_t) {
activity->callbacks->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();
}
Loading