Skip to content

Add Android window lifecycle support - #108

Open
Solessfir wants to merge 4 commits into
Try:masterfrom
Solessfir:android-window
Open

Solessfir wants to merge 4 commits into
Try:masterfrom
Solessfir:android-window

Conversation

@Solessfir

@Solessfir Solessfir commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Adds the first Android runtime layer on top of the existing APK packaging helper.

Changes:

  • Add a SystemApi backend using NativeActivity, ANativeWindow and android_native_app_glue.
  • Forward window creation, destruction, resize, focus, pause and resume through Tempest events.
  • Stop rendering while the activity is inactive or has no native window.
  • Replace the stored native window handle when Android recreates the surface.
  • Convert the Android example to a normal Tempest Application and Window.

Vulkan surfaces, input, audio and other platform services are intentionally left for separate PRs.

Validated with:

  • ARM64 Android native build.
  • Debug APK assembly and Android lint.
  • APK inspection for lib/arm64-v8a/libtempest-example.so and the NativeActivity manifest.
  • Windows Debug build of Tempest.
  • Cold launch and background/resume on a Galaxy S24 (SM-S921B) running Android 16.

@Try Try left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented general stuff for now. Probably we would also need to refactor window initialization a bit

Comment thread Engine/system/api/androidapi.cpp Outdated
void AndroidApi::implSetWindowTitle(SystemApi::Window*, const char*) {
}

int main(int argc, const char** argv);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You dont need to relay on static linking here.
dlopen(nullptr) + dlsym(..., "main") will get you function pointer to the main. And you allowed to always assume int(int, char** signature, since it's c-call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the static main declaration and switched to dlsym. On Android 16, dlopen(nullptr) cannot see the locally loaded NativeActivity library, so this locates the library containing android_main with dladdr and opens that before resolving main.

Comment thread Engine/system/api/androidapi.cpp
Comment thread Engine/system/api/androidapi.cpp Outdated
Comment thread Engine/system/api/androidapi.cpp Outdated

SystemApi::Window* createAndroidWindow(Tempest::Window* owner) {
if(mainWindow==nullptr)
mainWindow = new AndroidWindow();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SystemApi::Window* is expected to be something usable directly to swapchain. Similar to HWND on windows.
In case if android that would be ANativeWindow*.

@Solessfir Solessfir Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the wrapper. SystemApi::Window* is currently the ANativeWindow* itself. Replacing that pointer on surface recreation conflicts with the persistent Window::id contract, so that part remains unresolved in the later discussion: #108 (comment)

Comment thread Engine/system/api/androidapi.cpp Outdated
int AndroidApi::implExec(AppCallBack& cb) {
running = true;
while(running) {
pollAndroid(active && hasWindow ? 0 : -1);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pollAndroid is already part of implProcessEvents

@Solessfir Solessfir Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the duplicate polling from implExec. implProcessEvents owns normal event-loop polling. Initial window creation and activity shutdown also pump native events while waiting. In Solessfir@82fd004, an overlapping replacement activity pumps its own callbacks while waiting for the previous native session to finish, so Android's UI thread is not blocked.

Comment thread Examples/Android/CMakeLists.txt
Comment thread Engine/system/eventdispatcher.cpp Outdated
Comment thread Engine/system/api/androidapi.cpp Outdated
Comment thread Engine/system/api/androidapi.cpp Outdated
Comment thread Engine/system/api/androidapi.cpp Outdated
Comment thread Engine/system/api/androidapi.h Outdated

class AndroidApi final : SystemApi {
public:
using SystemApi::dispatchClose;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be public.

I've noticed, that you are using it from free-standing function onAppCmd. You can move onAppCmd -> AndroidApi::onAppCmd(void*, int32_t) instead. Similar to how windowProc is implemented, on windows.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved onAppCmd, focus and resize handling into private AndroidApi methods and removed the public dispatch aliases. A typed lambda bridges the native-app-glue callback to onAppCmd(void*, int32_t).

return;

auto window = reinterpret_cast<SystemApi::Window*>(app->window);
AndroidApi::setWindowHandle(*mainWindow,window);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Window::id is expected to be persistent, and hot swapping should not be used.

I know, that this is general android issue, but engine has to have clean workaround and emulate same windowing as on any other platform.

I think, one option can be to spin-loop events in createAndroidWindow, unit APP_CMD_INIT_WINDOW or APP_CMD_DESTROY is met. APP_CMD_INIT_WINDOW = window created, all good. APP_CMD_DESTROY - exiting while creating window - return null (will be propagated to exception).

Correct me, if I'm wrong, but if AndroidManifest.xml (or activity) configured right way, then android wont sporadically kill the window, until the exit?

@Solessfir Solessfir Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the initial wait into createAndroidWindow.

Android can still destroy and recreate the drawing surface when switching apps. Could we keep a stable Tempest window handle and replace only the ANativeWindow stored inside it?

Vulkan would handle the surface change internally, so Window::id stays unchanged.

Comment thread Engine/system/api/androidapi.cpp Outdated
}
}

void pollAndroid(int timeout) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to use namespace. For helper functions static is enough. static will hide your functions from linker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the anonymous namespace and made the file-local functions and state static.

}

SystemApi::Window* createAndroidWindow(Tempest::Window* owner) {
mainWindow = owner;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a general contract: only one window allowed at a time, on android. In this case, it's right to return null, if mainWindow is already initialized.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the guard: attempting to create a second window returns nullptr without changing the existing owner.

Comment thread Engine/system/api/androidapi.cpp Outdated
}

int AndroidApi::implExec(AppCallBack& cb) {
running.store(true);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

int WindowsApi::implExec(AppCallBack& cb) {
  // main message loop
  while (!isExit.load()) {
    implProcessEvents(cb);
    }
  return 0;
  }

Swap running -> isExit would be nice for consistency.
Timers can be handled inside implProcessEvents

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed running to atomic isExit and moved timer handling into implProcessEvents. Exit also wakes the looper, and event processing checks the exit flag before rendering or running timers.

Comment thread Engine/system/api/androidapi.cpp Outdated

Dl_info module = {};
void* self = nullptr;
if(dladdr(reinterpret_cast<void*>(&android_main),&module)!=0)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably this can be refactored away into separated function, and rewritten more clean with fail-fast approach (instead if multiple if-else)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted entry-point lookup and invocation into runMain with early returns. android_main now handles exceptions and activity shutdown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants