Skip to content

Add Android AAR project for embedding Babylon Native#1682

Open
CedricGuillemet wants to merge 3 commits intoBabylonJS:masterfrom
CedricGuillemet:AndroidAARPackage
Open

Add Android AAR project for embedding Babylon Native#1682
CedricGuillemet wants to merge 3 commits intoBabylonJS:masterfrom
CedricGuillemet:AndroidAARPackage

Conversation

@CedricGuillemet
Copy link
Copy Markdown
Collaborator

@CedricGuillemet CedricGuillemet commented Apr 28, 2026

Adds Install/AndroidAAR/ — a self-contained Gradle project producing a publishable Android AAR (BabylonNative-release.aar) intended to embed Babylon Native inside any Android application with the smallest possible footprint.

Footprint optimizations

  • QuickJS engine, libc++ statically linked
  • arm64-v8a + x86_64 only
  • OpenGL ES 3.0 backend only — no Vulkan / D3D / Metal compiled into bgfx
  • Image loading, shader compiler, NativeInput, XMLHttpRequest, NativeXr plugins disabled
  • Release built with MinSizeRel (-Os), function/data sections, hidden visibility, --gc-sections, --strip-all

Public contract

  • Embedding application drives script loading via BabylonView.loadScript() — nothing is auto-loaded by the AAR.
  • Optional shader cache is read from the embedding application's shadercache.bin asset.

Includes

  • README.md documenting the contract and footprint optimizations
  • build-release.ps1 helper that builds + prints AAR / .so sizes
  • New CI job Android_AAR (build-android-aar.yml) that builds the AAR, prints artifact sizes and uploads the AAR as a workflow artifact

Note

Temporarily pins JsRuntimeHost to the CedricGuillemet/JsRuntimeHost fork until the upstream QuickJS support PR is merged.
Naming is hard. I'm using generic/stupid names like 'Android-AAR'. Please provide your ideas.

Adds Install/AndroidAAR/ - a standalone Gradle project that builds a
publishable AAR (libBabylonNativeJNI.so + BabylonView/Wrapper Java
classes) tuned for the smallest possible footprint:

- QuickJS engine, libc++ statically linked
- Only arm64-v8a / x86_64 ABIs
- OpenGL ES 3.0 only (no Vulkan/D3D/Metal compiled into bgfx)
- Image loading, shader compiler, NativeInput, XMLHttpRequest, NativeXr
  plugins disabled
- Release build uses MinSizeRel (-Os, hidden visibility,
  --gc-sections, --strip-all)

Embedding application is expected to drive script loading via
BabylonView.loadScript() - nothing is auto-loaded by the AAR. Optional
shader cache is read from the embedding application's
'shadercache.bin' asset.

Includes:
- README.md documenting the contract and footprint optimizations
- build-release.ps1 helper that builds and prints AAR/.so sizes
- New CI job Android_AAR (build-android-aar.yml) that builds the AAR,
  prints artifact sizes and uploads the AAR as a workflow artifact

Note: temporarily pins JsRuntimeHost to CedricGuillemet/JsRuntimeHost
fork until the upstream QuickJS support PR is merged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 28, 2026 13:36
CedricGuillemet and others added 2 commits April 29, 2026 10:17
…JsRuntimeHost

- Restore JsRuntimeHost FetchContent declaration without EXCLUDE_FROM_ALL
  (per the comment block above, JsRuntimeHost is intentionally an exception
  because it exposes top-level targets like AppRuntime that are part of
  the Babylon Native contract).

- Rename Install/AndroidAAR -> Install/AndroidQuickJSMinimal and align all
  related identifiers:
    * workflow file: build-android-aar.yml -> build-android-quickjs-minimal.yml
    * CI job:       Android_AAR -> Android_QuickJSMinimal
    * gradle root project: BabylonNativeAAR -> BabylonNativeQuickJSMinimal
    * uploaded artifact: BabylonNative-release-aar -> BabylonNative-QuickJSMinimal-release-aar
    * build staging dir: Build/Android-AAR -> Build/Android-QuickJSMinimal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The minimal AAR does not link NativeXr, so xrSurfaceChanged() and
isXRActive() are dead surface area. Drop them from:
  - BabylonNativeJNI.cpp (the two stub JNI exports)
  - Wrapper.java (the two native declarations)
  - BabylonView.java (the secondary xrSurfaceView, its callback and
    the isXRActive() check in onDraw)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
embedding application as an asset named **`shadercache.bin`**:

```
app/src/main/assets/shadercache.bin
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How would a consumer of this package generate the shadercache.bin file? It seems like they would have to write some other BN code that loads their js bundle and then they save the shader cache or something?

Would it make more sense to have this lib save the shadercache.bin on first run, and automatically re-use it on subsequent runs, and allow the consumer to choose to grab that file off the device and include it in the app package if they want to avoid even the first run hit?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

shadercache.bin is generated by a specific user process (on the CI, or user is responsible to cache and push to repo) so glslang/spirv do not need to be linked with app (it's multiple Mb). Some users already use the cache the way you describe but it's for server side rendering. Here, the goal is to have a limited set of shaders that are bundled in the app.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok so effectively, the consumer would create a different app using the full BN api and the same shaders, and run that app in a CI to generate the shaderCache.bin, and then that cache is used with their client side app that can load that shaderCache.bin?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As discussed we can probably replace this with a reusable/shared interop layer, but that could be done in a separate PR.

Copy link
Copy Markdown
Collaborator Author

@CedricGuillemet CedricGuillemet Apr 30, 2026

Choose a reason for hiding this comment

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

Maybe! I'm curious to see if it's possible to have gradle/cmake/cpp sources in an aar.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was thinking more that you just use a shared layer up to the cpp/jni, and then provide your own Java layer and compile it all into your aar. Not a blocker for this PR.

* Image loading, shader compiler, NativeInput and XMLHttpRequest plugins
are disabled.
* Release build uses `MinSizeRel` (`-Os`), function/data sections,
hidden visibility, dead-code elimination and `--strip-all`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does it include debug symbols? I assume it would need to so that if there are crash reports, we'd be able to understand what is going on.

Copy link
Copy Markdown
Collaborator Author

@CedricGuillemet CedricGuillemet Apr 30, 2026

Choose a reason for hiding this comment

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

I guess it's possible. User would be responsible for stripping debug infos. Here, infos are stripped on purpose.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think stripped binaries are really usable in production. @bghgary

Wrapper.eval(source, sourceURL);
}

public void onPause() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could BabylonView self-register with the host Activity''s lifecycle so consumers don''t have to manually forward onPause / onResume from their Activity?

Since the constructor already takes a Context (and effectively an Application via getApplication()), the view could call Application.registerActivityLifecycleCallbacks(...) and react to onActivityPaused / onActivityResumed for the Activity that hosts it (filtering by walking getContext() up to the Activity). Unregister on onDetachedFromWindow to avoid leaks.

A more idiomatic alternative would be DefaultLifecycleObserver against the host''s Lifecycle, but that pulls in AndroidX which conflicts with the "smallest possible footprint" goal — so ActivityLifecycleCallbacks is probably the better fit here.

onRequestPermissionsResult would still need to be forwarded manually (it''s not part of ActivityLifecycleCallbacks), but eliminating the pause/resume boilerplate would noticeably simplify the embedding contract documented in the README.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have no idea. :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I know who does! 🤖 😂

@bghgary
Copy link
Copy Markdown
Contributor

bghgary commented May 1, 2026

[Responded by Copilot on behalf of @bghgary]

Stepping back from the inline comments — I want to raise an architectural-direction concern with this PR before we go deeper on the bug-level review.

This is the first time we'd publish a packaged binary artifact from this repo's CI:

  • Install/ today is purely a CMake SDK-install pattern (install(FILES ...)) plus installation tests that build a downstream consumer against an installed library. No artifact ever gets uploaded — the four test-install-*.yml workflows verify find_package(BabylonNative) works.
  • All current CI artifact uploads are diagnostic only — rendered pictures, error pictures, core dumps.
  • This PR adds an actions/upload-artifact@v4 step for BabylonNative-release.aar, making it the first deliverable this repo's CI produces.

The configuration is also very opinionated for one shape of consumer:

dimension this AAR dropped
JS engine QuickJS V8, JSC
GPU API OpenGL ES 3.0 only Vulkan
ABIs arm64-v8a, x86_64 armeabi-v7a, x86
Build MinSizeRel -Os, stripped RelWithDebInfo
Image loading OFF
Shader compiler OFF
NativeInput OFF (touch ignored)
XMLHttpRequest OFF
NativeXr implicit OFF

A consumer with different needs (XR, HTTP fetches, V8 perf, Vulkan, touch input, debuggable production binaries, etc.) would not use this AAR — they'd need their own. So one AAR per consumer-shape doesn't scale in the upstream repo.

The ownership questions that follow:

  1. Support policy. Once we publish an AAR from this repo's CI, is it supported? Versioned? Compatibility guarantees? SemVer? None of that is defined.
  2. Precedent. If this lands, what's the answer to the next contributor whose target shape is different? "Yes, also add yours"? We end up with N variants in Install/ each tuned for one consumer.
  3. Stripped-symbol concern @ryantrem raised connects to this. If the AAR is shipped as a deliverable, stripped binaries are a real problem for production crash diagnostics; if it's a sample, stripped is fine but it shouldn't be uploaded as a CI artifact for partners to consume.
  4. Mixing reusable scaffolding with opinionated tuning. The Java BabylonView and the JNI bridge are generic and reusable. The disable flags + ABI list + build mode are partner-specific. Bundling them in one tree conflates two different things.

Without rejecting the work — there's a real value capture here regardless of where it lives — a few alternatives I'd want to consider:

  • (a) Sample/template, not deliverable. Keep the directory but drop the upload-artifact step. Treat the CI job purely as a build smoke test (it does catch when CMake disable flags break — those paths bit-rot easily, so this is a real benefit). Move from Install/... to Samples/Android/... and document explicitly in README: "starting point, fork and tune for your scenario."
  • (b) Split as @ryantrem suggested. Install/Android/JNI-Core/ for the reusable C++/JNI layer, Samples/Android/Embed-QuickJSMinimal/ for the gradle module showing one tuning. This matches the "shareable interop layer" comment but takes it further and keeps the deliverable-vs-sample boundary clean.
  • (c) Move out of this repo. A separate BabylonNative-Android-AAR repo, or under the partner's org, where it can be versioned and supported on a different cadence.
  • (d) Accept as-is, but make it a real product. Define support policy, version-tag, SemVer, deprecation rules. Most expensive — requires team commitment we don't currently have.

My take: (a) or (b). The build smoke test is genuinely valuable — without something like it, the BABYLON_NATIVE_PLUGIN_*=OFF paths can break unnoticed. But shipping a CI-uploaded AAR turns a sample into a product without an explicit decision to do so. Splitting reusable from opinionated, plus dropping the upload step, gets us most of the value at a fraction of the maintenance commitment, and leaves the partner's actual AAR build in their repo where it belongs.

Curious what you both think before I do another pass through the inline review notes.

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.

3 participants