Skip to content

Add Android Vulkan swapchain support - #109

Merged
Try merged 8 commits into
Try:masterfrom
Solessfir:android-swapchain
Oct 3, 2026
Merged

Try merged 8 commits into
Try:masterfrom
Solessfir:android-swapchain

Conversation

@Solessfir

@Solessfir Solessfir commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Adds Vulkan rendering to the Android window backend introduced in #108.

  • Use an acquired ANativeWindow* directly as the window handle.
  • Close the application when Android destroys the native window; treat Vulkan surface loss as fatal.
  • Recover internally from out-of-date/suboptimal swapchains during creation and reset.
  • Select a supported composite alpha mode.
  • Render an animated clear in the Android example and link the NDK Vulkan loader.

The example manifest handles screen, input and UI-mode configuration changes without recreating the Activity. Android may still destroy the window when backgrounded or locked, ending the application.

Input and audio remain separate work.

Comment thread Engine/gapi/vulkan/vswapchain.cpp Outdated
Comment thread Engine/system/api/androidapi.cpp Outdated
return nullptr;
mainWindow = owner;
return reinterpret_cast<SystemApi::Window*>(app->window);
return reinterpret_cast<SystemApi::Window*>(&app->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.

is app persistent? Any risk that NDK might destroy it sporadically, like window?

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.

It stays alive until android_main returns. The NDK glue waits for android_app_destroy to signal completion before freeing it; TERM/INIT_WINDOW only replace its window field. The handle now points to our own static nativeWindow instead of app->window.

Comment thread Engine/gapi/vulkan/vswapchain.cpp Outdated
Comment thread Engine/gapi/vulkan/vswapchain.cpp Outdated
Comment thread Engine/gapi/vulkan/vswapchain.cpp Outdated
Comment thread Engine/gapi/vulkan/vswapchain.cpp
Comment thread Engine/gapi/vulkan/vswapchain.cpp Outdated
Comment thread Engine/gapi/vulkan/vswapchain.cpp Outdated
@Try

Try commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Just noting: I have only limited amount of time on this week, but will do my best to get into review by Friday/weekend.

There are still some things that look suspicious in PR.
One is surface create-destory state machinery. On application level exception in present is processed by recreation of swapchain. However, that might mean that event loop wont progress and you will keep working with old ANativeWindow.

There are several places that look-like you assuming nativeWindoow may be changed during rendering. But since swapchain thread must be same as even loop thread, that wont happen, right?

@Solessfir

Copy link
Copy Markdown
Contributor Author

Checked, yes, in the current Android backend they are effectively the same thread. pollAndroid() and dispatchRender() run sequentially on android_main, and nativeWindow is only updated while processing APP_CMD_INIT_WINDOW, so it cannot change in the middle of acquire/present.

So the nativeWindow != *hwnd checks do not protect against a mid-render replacement.

Your VK_ERROR_SURFACE_LOST_KHR point is valid though. If render() immediately resets the swapchain before pending TERM/INIT_WINDOW events are processed, it may recreate using the old ANativeWindow.

I think surface-loss recovery should be deferred until control returns to the event loop, then recreate after pending window events are processed.

But, I'm on vacation until Monday too, so I can't introduce any fixes until then.

@Solessfir

Solessfir commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

The example now performs initial creation and reset in the render callback, after Android has processed window events. Android swapchain creation reports recoverable failures instead of retrying internally, so each retry returns through the event loop. Surface-query failures are handled too, and VK_INCOMPLETE enumeration results are retried.

The acquire check is removed. The present check is retained to detect a window replacement between frames; the pointer does not change during rendering.

@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.

Error-flow still required much work, unfortunately.
Also, relevant: #110 . I'm thinking that it might be better call to replace api-design of swapchain as a whole

Comment thread Engine/gapi/vulkan/vdevice.cpp Outdated
Comment thread Engine/gapi/vulkan/vswapchain.cpp Outdated
Comment thread Engine/gapi/vulkan/vswapchain.cpp
Comment thread Engine/gapi/vulkan/vswapchain.cpp Outdated
Comment thread Engine/gapi/vulkan/vswapchain.cpp Outdated
@Solessfir

Copy link
Copy Markdown
Contributor Author

Understood - changing createSwapchain() to throw SwapchainSuboptimal broke the existing recovery contract.

Keeping recovery internal means Android must process pending window events during the retry. The normal event path can re-enter rendering, and even polling native events directly can dispatch resize and recursively call reset().

Would you prefer to settle #110 first and adapt this PR afterward, or keep the current API and add an internal Android lifecycle-processing path that defers application callbacks during recovery?

@Try

Try commented Oct 1, 2026

Copy link
Copy Markdown
Owner

I've experimented a bit with current state of the path. I don;t think it can work.
Considering surface area of swapchain-api:

Swapchain s(device, hwnd); // constructor
...
// s.acquire(); - not in api but hidden i constructor and present
device.present(s);
s.reset();
...
// getters
uint32_t w() const;
uint32_t h() const;
uint32_t currentImage() const;
uint32_t imageCount() const;

and use-case in frame:

try {
  ...
  device.present(s); // can throw
  }
catch(SwapchainSuboptimal&) {
  s.reset(); // recovery cant throw!
  }

If Android allows asynchronous failure, everywhere, but does not allow recovery until event loop make progress - we stuck.
Options I see:

  1. Pull events on swapchain error, artificially progressing system. We will lose all events in between, what is huge problem
  2. Recognize surface-lost as fatal. AFAIK it tied to activity been destroyed, so maybe fine?
  3. Virtualize swapchain (create on demand and such). Wont work, because getter would either throw or return garbage

Note on any new top-level api.
Also won't work, as it doesn't solve recovery issue. Constructor also not solvable: by design swapchain either created or not.

@Try

Try commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

At this point option 2 (and flags to keep activity alive) is probably best of the worst.
And in the same time threat APP_CMD_TERM_WINDOW as equivalent to WM_CLOSE/WM_DESTROY

@Solessfir

Solessfir commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Implemented option 2. Surface loss is now fatal; creation/reset retry only out-of-date/suboptimal swapchains. Removed deferred recovery and the surface-loss state, and expanded the example's configChanges flags.

Checked this revision on-device: rendering continued after background/resume, rotation and screen off/on. Exit/reopen also worked, with no crashes.

@Try

Try commented Oct 1, 2026

Copy link
Copy Markdown
Owner

@Solessfir
I've pushes some cleanups. Mostly handling error-codes vs exceptions and wrap image-views in RAII - so we need not to worry about those.
Tested only on windows - can you have a look if android part is fine?

Also, ANativeWindow* nativeWindow and double-pointer thing is probably no longer needed?

@Solessfir

Solessfir commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Checked your cleanup on Android. Removed the cached native window and double pointer, and made APP_CMD_TERM_WINDOW close the application as suggested. This also removes the out-of-scope window assignment that broke Android compilation.

Rendering and rotation work. Home and screen locking destroy the surface on this device, so the example now exits and reopens cleanly.

Repeated shutdown testing caught a JIT-thread SIGSEGV during std::exit. Changed it to std::_Exit after application cleanup, avoiding process-wide destructors while Android threads are still running. Eight shutdowns passed afterward without crash logs.

@Try

Try commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Repeated shutdown testing caught a JIT-thread SIGSEGV during std::exit

Pff... Android being android :D
In theory, one option is to develop boot.so and load/unload application with dlopen/dlclose, but probably not worth the trouble

@Try
Try merged commit d83e06f into Try:master Oct 3, 2026
4 checks passed
@Try

Try commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Great work @Solessfir , merged!

@Solessfir
Solessfir deleted the android-swapchain branch October 3, 2026 17:53
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