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'