diff --git a/.github/workflows/build-android-quickjs-minimal.yml b/.github/workflows/build-android-quickjs-minimal.yml
new file mode 100644
index 000000000..1338925e0
--- /dev/null
+++ b/.github/workflows/build-android-quickjs-minimal.yml
@@ -0,0 +1,58 @@
+name: Build Android QuickJS Minimal AAR
+
+on:
+ workflow_call:
+
+env:
+ NDK_VERSION: '28.2.13676358'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+ steps:
+ - uses: actions/checkout@v5
+
+ - uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: '17'
+
+ - name: Build AAR (release / MinSizeRel)
+ working-directory: Install/AndroidQuickJSMinimal
+ run: |
+ chmod +x gradlew
+ ./gradlew :BabylonNative:assembleRelease \
+ -PNDK_VERSION=${{ env.NDK_VERSION }}
+
+ - name: Print artifact sizes
+ working-directory: Install/AndroidQuickJSMinimal
+ run: |
+ AAR=BabylonNative/build/outputs/aar/BabylonNative-release.aar
+ if [ ! -f "$AAR" ]; then
+ echo "::error::AAR not found at $AAR"
+ exit 1
+ fi
+
+ echo "==> Artifact sizes"
+ AAR_BYTES=$(stat -c%s "$AAR")
+ AAR_MB=$(awk "BEGIN { printf \"%.2f\", $AAR_BYTES / 1048576 }")
+ printf 'AAR : %12d bytes (%6s MB) %s\n' "$AAR_BYTES" "$AAR_MB" "$AAR"
+
+ # Extract and report each libBabylonNativeJNI.so size
+ TMP=$(mktemp -d)
+ unzip -q "$AAR" "jni/*/libBabylonNativeJNI.so" -d "$TMP"
+ find "$TMP/jni" -name 'libBabylonNativeJNI.so' | sort | while read SO; do
+ REL=${SO#$TMP/}
+ SO_BYTES=$(stat -c%s "$SO")
+ SO_MB=$(awk "BEGIN { printf \"%.2f\", $SO_BYTES / 1048576 }")
+ printf ' - %-50s %12d bytes (%6s MB)\n' "$REL" "$SO_BYTES" "$SO_MB"
+ done
+ rm -rf "$TMP"
+
+ - name: Upload AAR
+ uses: actions/upload-artifact@v4
+ with:
+ name: BabylonNative-QuickJSMinimal-release-aar
+ path: Install/AndroidQuickJSMinimal/BabylonNative/build/outputs/aar/BabylonNative-release.aar
+ if-no-files-found: error
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a466d0c0a..ac9666b4b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -136,6 +136,10 @@ jobs:
runs-on: macos-latest
js-engine: V8
+ # ── Android QuickJS minimal AAR (publishable, MinSizeRel) ────
+ Android_QuickJSMinimal:
+ uses: ./.github/workflows/build-android-quickjs-minimal.yml
+
# ── Installation Tests ────────────────────────────────────────
iOS_Installation:
uses: ./.github/workflows/test-install-ios.yml
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 526a3b93c..c9b300a27 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -53,8 +53,8 @@ FetchContent_Declare(ios-cmake
GIT_TAG 4.5.0
EXCLUDE_FROM_ALL)
FetchContent_Declare(JsRuntimeHost
- GIT_REPOSITORY https://github.com/BabylonJS/JsRuntimeHost.git
- GIT_TAG 808601482588b7f806d91231288310b94766dc84)
+ GIT_REPOSITORY https://github.com/CedricGuillemet/JsRuntimeHost.git
+ GIT_TAG fe26c61ecdebf84e8617c234b1729b86b1650858)
FetchContent_Declare(libwebp
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
GIT_TAG 57e324e2eb99be46df46d77b65705e34a7ae616c
diff --git a/Install/AndroidQuickJSMinimal/.gitignore b/Install/AndroidQuickJSMinimal/.gitignore
new file mode 100644
index 000000000..2c68c85f0
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/.gitignore
@@ -0,0 +1,10 @@
+# Build outputs
+build/
+.cxx/
+.gradle/
+local.properties
+*.iml
+.idea/
+
+# AAR build staging (out-of-tree)
+../../Build/Android-QuickJSMinimal/
diff --git a/Install/AndroidQuickJSMinimal/BabylonNative/.gitignore b/Install/AndroidQuickJSMinimal/BabylonNative/.gitignore
new file mode 100644
index 000000000..d8f172b7d
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/BabylonNative/.gitignore
@@ -0,0 +1,3 @@
+/build
+*.iml
+.cxx/
diff --git a/Install/AndroidQuickJSMinimal/BabylonNative/CMakeLists.txt b/Install/AndroidQuickJSMinimal/BabylonNative/CMakeLists.txt
new file mode 100644
index 000000000..edef4d145
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/BabylonNative/CMakeLists.txt
@@ -0,0 +1,31 @@
+cmake_minimum_required(VERSION 3.18)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+project(BabylonNativeQuickJSMinimal)
+
+# Repo root: Install/AndroidQuickJSMinimal/BabylonNative/../../..
+get_filename_component(REPO_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE)
+
+add_subdirectory(${REPO_ROOT_DIR} "${CMAKE_CURRENT_BINARY_DIR}/BabylonNative")
+
+add_library(BabylonNativeJNI SHARED
+ src/main/cpp/BabylonNativeJNI.cpp)
+
+target_link_libraries(BabylonNativeJNI
+ PRIVATE GLESv3
+ PRIVATE android
+ PRIVATE EGL
+ PRIVATE log
+ PRIVATE -lz
+ PRIVATE AndroidExtensions
+ PRIVATE AppRuntime
+ PRIVATE Blob
+ PRIVATE Console
+ PRIVATE GraphicsDevice
+ PRIVATE NativeEngine
+ PRIVATE Performance
+ PRIVATE ScriptLoader
+ PRIVATE ShaderCache
+ PRIVATE Window)
diff --git a/Install/AndroidQuickJSMinimal/BabylonNative/build.gradle b/Install/AndroidQuickJSMinimal/BabylonNative/build.gradle
new file mode 100644
index 000000000..8aec7e88f
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/BabylonNative/build.gradle
@@ -0,0 +1,75 @@
+plugins {
+ id 'com.android.library'
+}
+
+def jsEngine = "QuickJS"
+if (project.hasProperty("jsEngine")) {
+ jsEngine = project.property("jsEngine")
+}
+
+def graphics_api = "OpenGL"
+if (project.hasProperty("GRAPHICS_API")) {
+ graphics_api = project.property("GRAPHICS_API")
+}
+
+android {
+ namespace = 'com.library.babylonnative'
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 25
+
+ consumerProguardFiles "consumer-rules.pro"
+ ndkVersion = "29.0.14206865"
+ if (project.hasProperty("NDK_VERSION")) {
+ ndkVersion = project.property("NDK_VERSION")
+ }
+ externalNativeBuild {
+ cmake {
+ abiFilters "arm64-v8a", "x86_64"
+ arguments "-DANDROID_STL=c++_static",
+ "-DENABLE_PCH=OFF",
+ "-DGRAPHICS_API=${graphics_api}",
+ "-DNAPI_JAVASCRIPT_ENGINE=${jsEngine}",
+ "-DBABYLON_NATIVE_BUILD_APPS=OFF",
+ "-DBABYLON_DEBUG_TRACE=ON",
+ "-DBABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES=OFF",
+ "-DBABYLON_NATIVE_PLUGIN_SHADERCOMPILER=OFF",
+ "-DBABYLON_NATIVE_PLUGIN_NATIVEENGINE_COMPILESHADERS=OFF"
+ cppFlags += ["-Wno-deprecated-literal-operator"]
+ }
+ }
+ ndk {
+ abiFilters "arm64-v8a", "x86_64"
+ }
+ }
+
+ externalNativeBuild {
+ cmake {
+ version = '3.19.6+'
+ path = 'CMakeLists.txt'
+ buildStagingDirectory = "../../../Build/Android-QuickJSMinimal"
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled = false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ externalNativeBuild {
+ cmake {
+ arguments "-DCMAKE_BUILD_TYPE=MinSizeRel",
+ "-DCMAKE_C_FLAGS_MINSIZEREL=-Os -DNDEBUG",
+ "-DCMAKE_CXX_FLAGS_MINSIZEREL=-Os -DNDEBUG",
+ "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,--strip-all,-gc-sections",
+ "-DCMAKE_C_FLAGS=-ffunction-sections -fdata-sections -fvisibility=hidden",
+ "-DCMAKE_CXX_FLAGS=-ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden"
+ }
+ }
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
diff --git a/Install/AndroidQuickJSMinimal/BabylonNative/consumer-rules.pro b/Install/AndroidQuickJSMinimal/BabylonNative/consumer-rules.pro
new file mode 100644
index 000000000..1c88924e1
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/BabylonNative/consumer-rules.pro
@@ -0,0 +1,3 @@
+# ProGuard rules consumed by applications using this AAR.
+# Keep the JNI bridge so applications calling its native methods are not stripped.
+-keep class com.library.babylonnative.** { *; }
diff --git a/Install/AndroidQuickJSMinimal/BabylonNative/proguard-rules.pro b/Install/AndroidQuickJSMinimal/BabylonNative/proguard-rules.pro
new file mode 100644
index 000000000..52b231153
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/BabylonNative/proguard-rules.pro
@@ -0,0 +1,2 @@
+# Keep all classes called from JNI by name.
+-keep class com.library.babylonnative.** { *; }
diff --git a/Install/AndroidQuickJSMinimal/BabylonNative/src/main/AndroidManifest.xml b/Install/AndroidQuickJSMinimal/BabylonNative/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9a40236b9
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/BabylonNative/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Install/AndroidQuickJSMinimal/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp b/Install/AndroidQuickJSMinimal/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp
new file mode 100644
index 000000000..8887951ba
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp
@@ -0,0 +1,307 @@
+// Babylon Native JNI bridge.
+//
+// All native lifecycle and runtime initialization logic lives in this single
+// translation unit so the AAR has no transitive dependency on the Playground
+// AppContext sources. The consuming application is expected to drive script
+// loading explicitly via Wrapper.loadScript / Wrapper.eval — nothing is
+// auto-loaded by this layer.
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+namespace
+{
+ constexpr const char* LOG_TAG = "BabylonNative";
+ constexpr const char* SHADER_CACHE_ASSET = "shadercache.bin";
+
+ struct Context
+ {
+ std::optional device;
+ std::optional deviceUpdate;
+ std::optional runtime;
+ std::optional scriptLoader;
+ };
+
+ std::optional g_context;
+
+ void DebugLog(const char* msg)
+ {
+ __android_log_write(ANDROID_LOG_INFO, LOG_TAG, msg);
+ }
+
+ const char* GetLogLevelString(Babylon::Polyfills::Console::LogLevel logLevel)
+ {
+ switch (logLevel)
+ {
+ case Babylon::Polyfills::Console::LogLevel::Log: return "Log";
+ case Babylon::Polyfills::Console::LogLevel::Warn: return "Warn";
+ case Babylon::Polyfills::Console::LogLevel::Error: return "Error";
+ default: return "";
+ }
+ }
+
+ // Loads `shadercache.bin` from the embedding application's asset folder
+ // and seeds the Babylon Native shader cache with it. If the asset is
+ // missing the runtime still works but every shader is compiled on first
+ // use.
+ void LoadShaderCacheFromAssets()
+ {
+ AAssetManager* assetMgr = android::global::GetAssetManager();
+ if (assetMgr == nullptr)
+ {
+ return;
+ }
+
+ AAsset* asset = AAssetManager_open(assetMgr, SHADER_CACHE_ASSET, AASSET_MODE_BUFFER);
+ if (asset == nullptr)
+ {
+ return;
+ }
+
+ const void* data = AAsset_getBuffer(asset);
+ off_t size = AAsset_getLength(asset);
+ if (data != nullptr && size > 0)
+ {
+ std::string buf(reinterpret_cast(data), static_cast(size));
+ std::istringstream stream(buf);
+ const auto entries = Babylon::Plugins::ShaderCache::Load(stream);
+ __android_log_print(ANDROID_LOG_INFO, LOG_TAG,
+ "Shader cache loaded from assets: %u entries", entries);
+ }
+ AAsset_close(asset);
+ }
+
+ void InitializeContext(ANativeWindow* window, size_t width, size_t height)
+ {
+ g_context.emplace();
+
+ Babylon::DebugTrace::EnableDebugTrace(true);
+ Babylon::DebugTrace::SetTraceOutput(DebugLog);
+ Babylon::PerfTrace::SetLevel(Babylon::PerfTrace::Level::Mark);
+
+ Babylon::Graphics::Configuration graphicsConfig{};
+ graphicsConfig.Window = window;
+ graphicsConfig.Width = width;
+ graphicsConfig.Height = height;
+ graphicsConfig.MSAASamples = 4;
+
+ g_context->device.emplace(graphicsConfig);
+ g_context->deviceUpdate.emplace(g_context->device->GetUpdate("update"));
+
+ Babylon::Plugins::ShaderCache::Enable();
+ LoadShaderCacheFromAssets();
+
+ g_context->device->StartRenderingCurrentFrame();
+ g_context->deviceUpdate->Start();
+
+ Babylon::AppRuntime::Options options{};
+ options.UnhandledExceptionHandler = [](const Napi::Error& error) {
+ std::ostringstream ss{};
+ ss << "[Uncaught Error] " << Napi::GetErrorString(error);
+ DebugLog(ss.str().c_str());
+ };
+
+ g_context->runtime.emplace(options);
+
+ g_context->runtime->Dispatch([](Napi::Env env) {
+ g_context->device->AddToJavaScript(env);
+
+ Babylon::Polyfills::Blob::Initialize(env);
+ Babylon::Polyfills::Console::Initialize(env,
+ [](const char* message, Babylon::Polyfills::Console::LogLevel logLevel) {
+ std::ostringstream ss{};
+ ss << "[" << GetLogLevelString(logLevel) << "] " << message;
+ DebugLog(ss.str().c_str());
+ });
+ Babylon::Polyfills::Window::Initialize(env);
+
+ Babylon::Plugins::NativeEngine::Initialize(env);
+ });
+
+ g_context->scriptLoader.emplace(*g_context->runtime);
+ }
+
+ void DestroyContext()
+ {
+ if (!g_context)
+ {
+ return;
+ }
+
+ if (g_context->device)
+ {
+ g_context->deviceUpdate->Finish();
+ g_context->device->FinishRenderingCurrentFrame();
+ }
+
+ Babylon::Plugins::ShaderCache::Disable();
+
+ g_context->scriptLoader.reset();
+ g_context->runtime.reset();
+ g_context->deviceUpdate.reset();
+ g_context->device.reset();
+ g_context.reset();
+ }
+}
+
+extern "C"
+{
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_initEngine(JNIEnv*, jclass)
+ {
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_finishEngine(JNIEnv*, jclass)
+ {
+ DestroyContext();
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_surfaceCreated(JNIEnv* env, jclass, jobject surface, jobject jniContext)
+ {
+ if (g_context)
+ {
+ return;
+ }
+
+ JavaVM* javaVM{};
+ if (env->GetJavaVM(&javaVM) != JNI_OK)
+ {
+ throw std::runtime_error("Failed to get Java VM");
+ }
+
+ android::global::Initialize(javaVM, jniContext);
+
+ ANativeWindow* window = ANativeWindow_fromSurface(env, surface);
+ const int32_t width = ANativeWindow_getWidth(window);
+ const int32_t height = ANativeWindow_getHeight(window);
+
+ InitializeContext(window, static_cast(width), static_cast(height));
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_surfaceChanged(JNIEnv* env, jclass, jint width, jint height, jobject surface)
+ {
+ if (g_context && g_context->runtime)
+ {
+ ANativeWindow* window = ANativeWindow_fromSurface(env, surface);
+ g_context->runtime->Dispatch(
+ [window, w = static_cast(width), h = static_cast(height)](auto) {
+ g_context->device->UpdateWindow(window);
+ g_context->device->UpdateSize(w, h);
+ });
+ }
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_setCurrentActivity(JNIEnv*, jclass, jobject currentActivity)
+ {
+ android::global::SetCurrentActivity(currentActivity);
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_activityOnPause(JNIEnv*, jclass)
+ {
+ android::global::Pause();
+ if (g_context && g_context->runtime)
+ {
+ g_context->runtime->Suspend();
+ }
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_activityOnResume(JNIEnv*, jclass)
+ {
+ if (g_context && g_context->runtime)
+ {
+ g_context->runtime->Resume();
+ }
+ android::global::Resume();
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_activityOnRequestPermissionsResult(
+ JNIEnv* env, jclass, jint requestCode, jobjectArray permissions, jintArray grantResults)
+ {
+ std::vector nativePermissions{};
+ const jsize permissionCount = env->GetArrayLength(permissions);
+ for (jsize i = 0; i < permissionCount; i++)
+ {
+ jstring permission = static_cast(env->GetObjectArrayElement(permissions, i));
+ const char* utfString = env->GetStringUTFChars(permission, nullptr);
+ nativePermissions.emplace_back(utfString);
+ env->ReleaseStringUTFChars(permission, utfString);
+ }
+
+ jint* grantResultElements = env->GetIntArrayElements(grantResults, nullptr);
+ const jsize grantResultCount = env->GetArrayLength(grantResults);
+ std::vector nativeGrantResults{grantResultElements, grantResultElements + grantResultCount};
+ env->ReleaseIntArrayElements(grantResults, grantResultElements, 0);
+
+ android::global::RequestPermissionsResult(requestCode, nativePermissions, nativeGrantResults);
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_loadScript(JNIEnv* env, jclass, jstring path)
+ {
+ if (g_context && g_context->scriptLoader)
+ {
+ const char* nativePath = env->GetStringUTFChars(path, nullptr);
+ g_context->scriptLoader->LoadScript(nativePath);
+ env->ReleaseStringUTFChars(path, nativePath);
+ }
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_eval(JNIEnv* env, jclass, jstring source, jstring sourceURL)
+ {
+ if (g_context && g_context->scriptLoader)
+ {
+ const char* nativeURL = env->GetStringUTFChars(sourceURL, nullptr);
+ const char* nativeSource = env->GetStringUTFChars(source, nullptr);
+ g_context->scriptLoader->Eval(std::string{nativeSource}, std::string{nativeURL});
+ env->ReleaseStringUTFChars(source, nativeSource);
+ env->ReleaseStringUTFChars(sourceURL, nativeURL);
+ }
+ }
+
+ // No NativeInput linked — touch events are ignored at this layer.
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_setTouchInfo(JNIEnv*, jclass, jint, jfloat, jfloat, jboolean, jint)
+ {
+ }
+
+ JNIEXPORT void JNICALL
+ Java_com_library_babylonnative_Wrapper_renderFrame(JNIEnv*, jclass)
+ {
+ if (g_context && g_context->device)
+ {
+ g_context->deviceUpdate->Finish();
+ g_context->device->FinishRenderingCurrentFrame();
+ g_context->device->StartRenderingCurrentFrame();
+ g_context->deviceUpdate->Start();
+ }
+ }
+}
diff --git a/Install/AndroidQuickJSMinimal/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java b/Install/AndroidQuickJSMinimal/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java
new file mode 100644
index 000000000..796eb032e
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/BabylonNative/src/main/java/com/library/babylonnative/BabylonView.java
@@ -0,0 +1,147 @@
+package com.library.babylonnative;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.FrameLayout;
+
+public class BabylonView extends FrameLayout implements SurfaceHolder.Callback2, View.OnTouchListener {
+ private static final FrameLayout.LayoutParams childViewLayoutParams = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ private static final String TAG = "BabylonView";
+ private boolean mViewReady = false;
+ private final ViewDelegate mViewDelegate;
+ private Activity mCurrentActivity;
+ private final SurfaceView primarySurfaceView;
+ private final float pixelDensityScale = getResources().getDisplayMetrics().density;
+
+ public BabylonView(Context context, ViewDelegate viewDelegate) {
+ this(context, viewDelegate, (Activity)viewDelegate);
+ }
+
+ public BabylonView(Context context, ViewDelegate viewDelegate, Activity currentActivity) {
+ super(context);
+
+ this.primarySurfaceView = new SurfaceView(context);
+ this.primarySurfaceView.setLayoutParams(BabylonView.childViewLayoutParams);
+ this.primarySurfaceView.getHolder().addCallback(this);
+ this.addView(this.primarySurfaceView);
+
+ this.mCurrentActivity = currentActivity;
+ SurfaceHolder holder = this.primarySurfaceView.getHolder();
+ holder.addCallback(this);
+ setOnTouchListener(this);
+ this.mViewDelegate = viewDelegate;
+
+ setWillNotDraw(false);
+
+ Wrapper.initEngine();
+ }
+
+ public void setCurrentActivity(Activity currentActivity)
+ {
+ if (currentActivity != this.mCurrentActivity) {
+ this.mCurrentActivity = currentActivity;
+ Wrapper.setCurrentActivity(this.mCurrentActivity);
+ }
+ }
+
+ public void loadScript(String path) {
+ Wrapper.loadScript(path);
+ }
+
+ public void eval(String source, String sourceURL) {
+ Wrapper.eval(source, sourceURL);
+ }
+
+ public void onPause() {
+ setVisibility(View.GONE);
+ Wrapper.activityOnPause();
+ }
+
+ public void onResume() {
+ Wrapper.activityOnResume();
+ }
+
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
+ Wrapper.activityOnRequestPermissionsResult(requestCode, permissions, results);
+ }
+
+ /**
+ * This method is part of the SurfaceHolder.Callback interface, and is
+ * not normally called or subclassed by clients of BabylonView.
+ */
+ public void surfaceCreated(SurfaceHolder holder) {
+ Wrapper.surfaceCreated(holder.getSurface(), this.getContext());
+ Wrapper.setCurrentActivity(this.mCurrentActivity);
+ if (!this.mViewReady) {
+ this.mViewDelegate.onViewReady();
+ this.mViewReady = true;
+ }
+ }
+
+ /**
+ * This method is part of the SurfaceHolder.Callback interface, and is
+ * not normally called or subclassed by clients of BabylonView.
+ */
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ }
+
+ /**
+ * This method is part of the SurfaceHolder.Callback interface, and is
+ * not normally called or subclassed by clients of BabylonView.
+ */
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ Wrapper.surfaceChanged((int)(w / this.pixelDensityScale), (int)(h / this.pixelDensityScale), holder.getSurface());
+ }
+
+ public interface ViewDelegate {
+ void onViewReady();
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ int pointerId = event.getPointerId(event.getActionIndex());
+ float mX = event.getX(event.getActionIndex()) / this.pixelDensityScale;
+ float mY = event.getY(event.getActionIndex()) / this.pixelDensityScale;
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ Wrapper.setTouchInfo(pointerId, mX, mY, true, 1);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ Wrapper.setTouchInfo(pointerId, mX, mY, false, 0);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ Wrapper.setTouchInfo(pointerId, mX, mY, true, 0);
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ Wrapper.finishEngine();
+ }
+
+ /**
+ * This method is part of the SurfaceHolder.Callback2 interface, and is
+ * not normally called or subclassed by clients of BabylonView.
+ */
+ @Deprecated
+ @Override
+ public void surfaceRedrawNeeded(SurfaceHolder holder) {
+ // Redraw happens in the bgfx thread. No need to handle it here.
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ Wrapper.renderFrame();
+ invalidate();
+ }
+}
diff --git a/Install/AndroidQuickJSMinimal/BabylonNative/src/main/java/com/library/babylonnative/Wrapper.java b/Install/AndroidQuickJSMinimal/BabylonNative/src/main/java/com/library/babylonnative/Wrapper.java
new file mode 100644
index 000000000..d7d626f99
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/BabylonNative/src/main/java/com/library/babylonnative/Wrapper.java
@@ -0,0 +1,36 @@
+package com.library.babylonnative;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.Surface;
+
+public class Wrapper {
+ // JNI interface
+ static {
+ System.loadLibrary("BabylonNativeJNI");
+ }
+
+ public static native void initEngine();
+
+ public static native void finishEngine();
+
+ public static native void surfaceCreated(Surface surface, Context context);
+
+ public static native void surfaceChanged(int width, int height, Surface surface);
+
+ public static native void setCurrentActivity(Activity currentActivity);
+
+ public static native void activityOnPause();
+
+ public static native void activityOnResume();
+
+ public static native void activityOnRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);
+
+ public static native void setTouchInfo(int pointerId, float dx, float dy, boolean button, int buttonValue);
+
+ public static native void loadScript(String path);
+
+ public static native void eval(String source, String sourceURL);
+
+ public static native void renderFrame();
+}
\ No newline at end of file
diff --git a/Install/AndroidQuickJSMinimal/README.md b/Install/AndroidQuickJSMinimal/README.md
new file mode 100644
index 000000000..4bf809e70
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/README.md
@@ -0,0 +1,93 @@
+# Babylon Native — Android QuickJS Minimal AAR
+
+This Gradle project produces a publishable Android AAR
+(`BabylonNative-release.aar`) embedding the [Babylon Native](https://github.com/BabylonJS/BabylonNative)
+runtime, intended to be embedded inside any Android application.
+
+The build is tuned for the **smallest possible footprint** suitable for
+production deployment.
+
+## Footprint optimizations
+
+* QuickJS JavaScript engine (no V8 / JSC dependency).
+* libc++ statically linked into `libBabylonNativeJNI.so` (no
+ `libc++_shared.so` companion).
+* Only `arm64-v8a` and `x86_64` ABIs.
+* OpenGL ES 3.0 backend only — Vulkan, D3D11, D3D12 and Metal are not
+ compiled into bgfx.
+* 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`.
+* No `Babylon::Plugins::NativeXr` (no ARCore dependency).
+
+## Shader cache
+
+The runtime expects a precompiled shader cache to be provided by the
+embedding application as an asset named **`shadercache.bin`**:
+
+```
+app/src/main/assets/shadercache.bin
+```
+
+If present, it is loaded at engine startup. If absent the runtime still
+works but every shader will be compiled on first use.
+
+## Public Java API
+
+Two classes are exposed under `com.library.babylonnative`:
+
+* `BabylonView` — a `FrameLayout` wrapping a Babylon Native rendering
+ surface. Forward `onPause()` / `onResume()` /
+ `onRequestPermissionsResult()` from your `Activity`.
+* `Wrapper` — a thin JNI bridge. Most consumers do not need to use it
+ directly.
+
+The consuming application is expected to call
+`BabylonView.loadScript(...)` (or `eval(...)`) explicitly from Java —
+**no script is auto-loaded by the AAR**.
+
+### Minimal usage
+
+```java
+public class MyActivity extends Activity implements BabylonView.ViewDelegate {
+ private BabylonView view;
+
+ @Override
+ protected void onCreate(Bundle s) {
+ super.onCreate(s);
+ view = new BabylonView(getApplication(), this, this);
+ setContentView(view);
+ }
+
+ @Override public void onViewReady() {
+ view.loadScript("app:///Scripts/myScene.js");
+ }
+
+ @Override protected void onPause() { view.onPause(); super.onPause(); }
+ @Override protected void onResume() { super.onResume(); view.onResume(); }
+}
+```
+
+## Building
+
+From this directory:
+
+```powershell
+.\build-release.ps1
+```
+
+or directly:
+
+```powershell
+.\gradlew :BabylonNative:assembleRelease
+```
+
+The script then prints the size of the produced `.aar` and of every
+`libBabylonNativeJNI.so` it contains.
+
+The AAR is written to:
+
+```
+BabylonNative\build\outputs\aar\BabylonNative-release.aar
+```
diff --git a/Install/AndroidQuickJSMinimal/build-release.ps1 b/Install/AndroidQuickJSMinimal/build-release.ps1
new file mode 100644
index 000000000..6a49f9db8
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/build-release.ps1
@@ -0,0 +1,56 @@
+#
+# Build the Babylon Native AAR in release / MinSizeRel mode and print the
+# size of the produced .aar and of every libBabylonNativeJNI.so it contains.
+#
+[CmdletBinding()]
+param(
+ [string] $JsEngine = "QuickJS",
+ [string] $GraphicsApi = "OpenGL",
+ [string] $NdkVersion = $null
+)
+
+$ErrorActionPreference = "Stop"
+$scriptDir = $PSScriptRoot
+Push-Location $scriptDir
+try {
+ $gradlewArgs = @(
+ ":BabylonNative:assembleRelease"
+ "-PjsEngine=$JsEngine"
+ "-PGRAPHICS_API=$GraphicsApi"
+ )
+ if ($NdkVersion) {
+ $gradlewArgs += "-PNDK_VERSION=$NdkVersion"
+ }
+
+ Write-Host "==> Building AAR (release / MinSizeRel)..." -ForegroundColor Cyan
+ & "$scriptDir\gradlew.bat" @gradlewArgs
+ if ($LASTEXITCODE -ne 0) {
+ throw "Gradle build failed with exit code $LASTEXITCODE"
+ }
+
+ $aar = Join-Path $scriptDir "BabylonNative\build\outputs\aar\BabylonNative-release.aar"
+ if (-not (Test-Path $aar)) {
+ throw "AAR not found at $aar"
+ }
+
+ Write-Host ""
+ Write-Host "==> Artifact sizes" -ForegroundColor Cyan
+ $aarBytes = (Get-Item $aar).Length
+ Write-Host ("AAR : {0,10:N0} bytes ({1,7:F2} MB) {2}" -f $aarBytes, ($aarBytes / 1MB), $aar)
+
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($aar)
+ try {
+ foreach ($entry in $zip.Entries | Where-Object { $_.FullName -like "jni/*libBabylonNativeJNI.so" } | Sort-Object FullName) {
+ $compressed = $entry.CompressedLength
+ $uncompressed = $entry.Length
+ Write-Host (" - {0,-50} {1,10:N0} B uncompressed ({2,6:F2} MB), {3,10:N0} B in AAR ({4,6:F2} MB)" -f `
+ $entry.FullName, $uncompressed, ($uncompressed / 1MB), $compressed, ($compressed / 1MB))
+ }
+ } finally {
+ $zip.Dispose()
+ }
+}
+finally {
+ Pop-Location
+}
diff --git a/Install/AndroidQuickJSMinimal/build.gradle b/Install/AndroidQuickJSMinimal/build.gradle
new file mode 100644
index 000000000..5c1441b12
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/build.gradle
@@ -0,0 +1,4 @@
+// Top-level build file for the Babylon Native AAR project.
+plugins {
+ id 'com.android.library' version '8.9.1' apply false
+}
diff --git a/Install/AndroidQuickJSMinimal/gradle.properties b/Install/AndroidQuickJSMinimal/gradle.properties
new file mode 100644
index 000000000..e892c3bc1
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/gradle.properties
@@ -0,0 +1,2 @@
+android.useAndroidX=true
+org.gradle.jvmargs=-Xmx4g
diff --git a/Install/AndroidQuickJSMinimal/gradle/wrapper/gradle-wrapper.jar b/Install/AndroidQuickJSMinimal/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..8bdaf60c7
Binary files /dev/null and b/Install/AndroidQuickJSMinimal/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Install/AndroidQuickJSMinimal/gradle/wrapper/gradle-wrapper.properties b/Install/AndroidQuickJSMinimal/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..2e1113280
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/Install/AndroidQuickJSMinimal/gradlew b/Install/AndroidQuickJSMinimal/gradlew
new file mode 100644
index 000000000..b2168455e
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/Install/AndroidQuickJSMinimal/gradlew.bat b/Install/AndroidQuickJSMinimal/gradlew.bat
new file mode 100644
index 000000000..db3a6ac20
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Install/AndroidQuickJSMinimal/settings.gradle b/Install/AndroidQuickJSMinimal/settings.gradle
new file mode 100644
index 000000000..898828b0f
--- /dev/null
+++ b/Install/AndroidQuickJSMinimal/settings.gradle
@@ -0,0 +1,18 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "BabylonNativeQuickJSMinimal"
+include ':BabylonNative'