diff --git a/Modules/@babylonjs/react-native/BabylonModule.ts b/Modules/@babylonjs/react-native/BabylonModule.ts index 9c03716d0..479887089 100644 --- a/Modules/@babylonjs/react-native/BabylonModule.ts +++ b/Modules/@babylonjs/react-native/BabylonModule.ts @@ -1,33 +1,24 @@ import { NativeModules } from 'react-native'; -import { NativeEngine } from '@babylonjs/core'; -// This global object is part of Babylon Native. -declare const _native: { - whenGraphicsReady: () => Promise; - engineInstance: NativeEngine; -} +declare const global: { + nativeCallSyncHook: any; +}; +const isRemoteDebuggingEnabled = !global.nativeCallSyncHook; -const NativeBabylonModule: { - initialize(): Promise; - whenInitialized(): Promise; - reset(): Promise; +// This legacy React Native module is created by Babylon React Native, and is only used to bootstrap the JSI object creation. +// This will likely be removed when the BabylonNative global object is eventually converted to a TurboModule. +const BabylonModule: { + initialize(): Promise; } = NativeModules.BabylonModule; -export const BabylonModule = { - initialize: async () => { - const initialized = await NativeBabylonModule.initialize(); - if (initialized) { - await _native.whenGraphicsReady(); - } - return initialized; - }, - - whenInitialized: NativeBabylonModule.whenInitialized, - reset: NativeBabylonModule.reset, - - createEngine: () => { - const engine = new NativeEngine(); - _native.engineInstance = engine; - return engine; +export async function ensureInitialized(): Promise { + if (isRemoteDebuggingEnabled) { + // When remote debugging is enabled, JavaScript runs on the debugging host machine, not on the device where the app is running. + // JSI (which Babylon Native uses heavily) can not work in this mode. In the future, this debugging mode will be phased out as it is incompatible with TurboModules for the same reason. + return false; + } else { + // This does the first stage of Babylon Native initialization, including creating the BabylonNative JSI object. + await BabylonModule.initialize(); + return true; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/Modules/@babylonjs/react-native/EngineHelpers.ts b/Modules/@babylonjs/react-native/EngineHelpers.ts deleted file mode 100644 index cb3051adc..000000000 --- a/Modules/@babylonjs/react-native/EngineHelpers.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Engine } from '@babylonjs/core'; - -const disposedPropertyName = "EngineHelper_IsDisposed"; - -export function IsEngineDisposed(engine: Engine): boolean { - return (engine as any)[disposedPropertyName]; -} - -export function DisposeEngine(engine: Engine) { - if (engine && !IsEngineDisposed(engine)) { - engine.dispose(); - (engine as any)[disposedPropertyName] = true; - } -} \ No newline at end of file diff --git a/Modules/@babylonjs/react-native/EngineHook.ts b/Modules/@babylonjs/react-native/EngineHook.ts index 162be3c4b..566ca2cd2 100644 --- a/Modules/@babylonjs/react-native/EngineHook.ts +++ b/Modules/@babylonjs/react-native/EngineHook.ts @@ -1,9 +1,8 @@ import { useEffect, useState } from 'react'; import { Platform } from 'react-native'; import { PERMISSIONS, check, request } from 'react-native-permissions'; -import { Engine, NativeEngine, WebXRSessionManager } from '@babylonjs/core'; -import { BabylonModule } from './BabylonModule'; -import { DisposeEngine } from './EngineHelpers'; +import { Engine, WebXRSessionManager } from '@babylonjs/core'; +import { ReactNativeEngine } from './ReactNativeEngine'; import * as base64 from 'base-64'; // These are errors that are normally thrown by WebXR's requestSession, so we should throw the same errors under similar circumstances so app code can be written the same for browser or native. @@ -66,29 +65,17 @@ export function useEngine(): Engine | undefined { const [engine, setEngine] = useState(); useEffect(() => { - let disposed = false; - let engine: Engine | undefined = undefined; + const abortController = new AbortController(); + let engine: ReactNativeEngine | undefined = undefined; (async () => { - if (await BabylonModule.initialize() && !disposed) - { - engine = BabylonModule.createEngine(); - setEngine(engine); - } + setEngine(engine = await ReactNativeEngine.tryCreateAsync(abortController.signal) ?? undefined); })(); return () => { - disposed = true; + abortController.abort(); // NOTE: Do not use setEngine with a callback to dispose the engine instance as that callback does not get called during component unmount when compiled in release. - if (engine) { - DisposeEngine(engine); - } - // Ideally we would always do a reset here as we don't want different behavior between debug and release. Unfortunately, fast refresh has some strange behavior that - // makes it quite difficult to get this to work correctly (e.g. it re-runs previous useEffect instances, which means it can try to use Babylon Native in a de-initialized state). - // TODO: https://github.com/BabylonJS/BabylonReactNative/issues/125 - if (!__DEV__) { - BabylonModule.reset(); - } + engine?.dispose(); setEngine(undefined); }; }, []); diff --git a/Modules/@babylonjs/react-native/EngineView.tsx b/Modules/@babylonjs/react-native/EngineView.tsx index 038bc572a..30b87207a 100644 --- a/Modules/@babylonjs/react-native/EngineView.tsx +++ b/Modules/@babylonjs/react-native/EngineView.tsx @@ -1,20 +1,10 @@ import React, { Component, FunctionComponent, SyntheticEvent, useCallback, useEffect, useState, useRef } from 'react'; -import { requireNativeComponent, NativeModules, ViewProps, AppState, AppStateStatus, View, Text, findNodeHandle, UIManager } from 'react-native'; +import { requireNativeComponent, ViewProps, AppState, AppStateStatus, View, Text, findNodeHandle, UIManager } from 'react-native'; import { Camera } from '@babylonjs/core'; -import { IsEngineDisposed } from './EngineHelpers'; -import { BabylonModule } from './BabylonModule'; +import { ensureInitialized } from './BabylonModule'; +import { ReactNativeEngine } from './ReactNativeEngine'; declare const global: any; -const isRemoteDebuggingEnabled = !global['nativeCallSyncHook']; - -const EngineViewManager: { - setJSThread(): void; -} = NativeModules.EngineViewManager; - -// Not all platforms need this, but for those that do, this is intended to be a synchronous call to boostrap the ability to run native code on the JavaScript thread. -if (EngineViewManager && EngineViewManager.setJSThread && !isRemoteDebuggingEnabled) { - EngineViewManager.setJSThread(); -} interface NativeEngineViewProps extends ViewProps { onSnapshotDataReturned: (event: SyntheticEvent) => void; @@ -23,7 +13,7 @@ interface NativeEngineViewProps extends ViewProps { const NativeEngineView: { prototype: Component; new(props: Readonly): Component; -} = requireNativeComponent('EngineView'); +} = global['EngineView'] || (global['EngineView'] = requireNativeComponent('EngineView')); export interface EngineViewProps extends ViewProps { camera?: Camera; @@ -36,7 +26,7 @@ export interface EngineViewCallbacks { } export const EngineView: FunctionComponent = (props: EngineViewProps) => { - const [failedInitialization, setFailedInitialization] = useState(false); + const [initialized, setInitialized] = useState(); const [appState, setAppState] = useState(AppState.currentState); const [fps, setFps] = useState(); const engineViewRef = useRef>(null); @@ -44,9 +34,7 @@ export const EngineView: FunctionComponent = (props: EngineView useEffect(() => { (async () => { - if (!await BabylonModule.whenInitialized()) { - setFailedInitialization(true); - } + setInitialized(await ensureInitialized()); })(); }, []); @@ -64,9 +52,9 @@ export const EngineView: FunctionComponent = (props: EngineView useEffect(() => { if (props.camera && appState === "active") { - const engine = props.camera.getScene().getEngine(); + const engine = props.camera.getScene().getEngine() as ReactNativeEngine; - if (!IsEngineDisposed(engine)) { + if (!engine.isDisposed) { engine.runRenderLoop(() => { for (let scene of engine.scenes) { scene.render(); @@ -74,7 +62,7 @@ export const EngineView: FunctionComponent = (props: EngineView }); return () => { - if (!IsEngineDisposed(engine)) { + if (!engine.isDisposed) { engine.stopRenderLoop(); } }; @@ -86,9 +74,9 @@ export const EngineView: FunctionComponent = (props: EngineView useEffect(() => { if (props.camera && (props.displayFrameRate ?? __DEV__)) { - const engine = props.camera.getScene().getEngine(); + const engine = props.camera.getScene().getEngine() as ReactNativeEngine; - if (!IsEngineDisposed(engine)) { + if (!engine.isDisposed) { setFps(engine.getFps()); const timerHandle = setInterval(() => { setFps(engine.getFps()); @@ -145,11 +133,11 @@ export const EngineView: FunctionComponent = (props: EngineView } }, []); - if (!failedInitialization) { + if (initialized !== false) { return ( - - { fps && FPS: {Math.round(fps)}} + { initialized && } + { fps && FPS: {Math.round(fps)} } ); } else { @@ -161,7 +149,7 @@ export const EngineView: FunctionComponent = (props: EngineView return ( {message} - { isRemoteDebuggingEnabled && React Native remote debugging does not work with Babylon Native.} + React Native remote debugging does not work with Babylon Native. ); } diff --git a/Modules/@babylonjs/react-native/ReactNativeEngine.ts b/Modules/@babylonjs/react-native/ReactNativeEngine.ts new file mode 100644 index 000000000..9287f2a75 --- /dev/null +++ b/Modules/@babylonjs/react-native/ReactNativeEngine.ts @@ -0,0 +1,69 @@ +import { ensureInitialized } from './BabylonModule'; +import { NativeEngine } from '@babylonjs/core'; + +// This global object is owned by Babylon Native. +declare const _native: { + whenGraphicsReady: () => Promise; +}; + +// This JSI-based global object is owned by Babylon React Native. +// This will likely be converted to a TurboModule when they are fully supported. +declare const BabylonNative: { + readonly initializationPromise: Promise; + setEngineInstance: (engine: NativeEngine | null) => void; + reset: () => void; +}; + +export class ReactNativeEngine extends NativeEngine { + private _isDisposed = false; + + private constructor() { + super(); + BabylonNative.setEngineInstance(this); + } + + public static async tryCreateAsync(abortSignal: AbortSignal): Promise { + if (!await ensureInitialized() || abortSignal.aborted) { + return null; + } + + // This waits Graphics/NativeEngine to be created (which in turn makes the whenGraphicsReady available). + await BabylonNative.initializationPromise; + + // Check for cancellation. + if (abortSignal.aborted) { + return null; + } + + // This waits for the Graphics system to be up and running. + await _native.whenGraphicsReady(); + + // Check for cancellation. + if (abortSignal.aborted) { + return null; + } + + return new ReactNativeEngine(); + } + + public get isDisposed() { + return this._isDisposed; + } + + public dispose(): void { + if (!this.isDisposed) { + super.dispose(); + + // Ideally we would always do a reset here as we don't want different behavior between debug and release. Unfortunately, fast refresh has some strange behavior that + // makes it quite difficult to get this to work correctly (e.g. it re-runs previous useEffect instances, which means it can try to use Babylon Native in a de-initialized state). + // TODO: https://github.com/BabylonJS/BabylonReactNative/issues/125 + if (!__DEV__) { + BabylonNative.reset(); + } + + this._isDisposed = true; + } + + BabylonNative.setEngineInstance(null); + } +} \ No newline at end of file diff --git a/Modules/@babylonjs/react-native/android/CMakeLists.txt b/Modules/@babylonjs/react-native/android/CMakeLists.txt index dd11974d1..c9d01b8e8 100644 --- a/Modules/@babylonjs/react-native/android/CMakeLists.txt +++ b/Modules/@babylonjs/react-native/android/CMakeLists.txt @@ -19,6 +19,8 @@ cmake_minimum_required(VERSION 3.13.2) # detection. # [24, infinite) ES2 & ES3 & Vulkan project(ReactNativeBabylon) +include(${CMAKE_CURRENT_LIST_DIR}/../shared/CMakeLists.txt) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") set(BABYLON_NATIVE_PLATFORM "Android") set(CMAKE_CXX_EXTENSIONS OFF) @@ -55,7 +57,10 @@ target_link_libraries(turbomodulejsijni fbjni) add_library(BabylonNative SHARED - src/main/cpp/BabylonNativeInterop.cpp) + src/main/cpp/BabylonNativeInterop.cpp + ${SHARED_SOURCES}) + +target_include_directories(BabylonNative PRIVATE ${SHARED_INCLUDES}) target_link_libraries(BabylonNative GLESv3 @@ -67,7 +72,6 @@ target_link_libraries(BabylonNative fbjni jsi turbomodulejsijni - BabylonReactNativeShared AndroidExtensions Graphics JsRuntime diff --git a/Modules/@babylonjs/react-native/android/src/main/cpp/BabylonNativeInterop.cpp b/Modules/@babylonjs/react-native/android/src/main/cpp/BabylonNativeInterop.cpp index a69bd1b82..182f6f004 100644 --- a/Modules/@babylonjs/react-native/android/src/main/cpp/BabylonNativeInterop.cpp +++ b/Modules/@babylonjs/react-native/android/src/main/cpp/BabylonNativeInterop.cpp @@ -10,176 +10,79 @@ #include -#include #include #include -#include -#include -#include - #include #include -#include +#include using namespace facebook; -namespace Babylon +extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_00024BabylonNative_initialize(JNIEnv* env, jclass obj, jobject context, jlong jsiRuntimeRef, jobject jsCallInvokerHolder) { - namespace + static bool initializedJVM{ false }; + if (!initializedJVM) { - void log(const char *str) + JavaVM* javaVM{}; + if (env->GetJavaVM(&javaVM) != JNI_OK) { - __android_log_print(ANDROID_LOG_VERBOSE, "BabylonNative", "%s", str); + throw std::runtime_error("Failed to get Java VM"); } - bool isShuttingDown{false}; + android::global::Initialize(javaVM, context); + + initializedJVM = true; } - class Native final + auto jsiRuntime{ reinterpret_cast(jsiRuntimeRef) }; + auto jsCallInvoker{ jni::alias_ref{ reinterpret_cast(jsCallInvokerHolder) }->cthis()->getCallInvoker() }; + auto jsDispatcher{ [jsCallInvoker{ std::move(jsCallInvoker) }](std::function func) { - public: - // This class must be constructed from the JavaScript thread - Native(jsi::Runtime& jsiRuntime, std::shared_ptr callInvoker, ANativeWindow* windowPtr) - : m_env{ Napi::Attach(jsiRuntime) } + jsCallInvoker->invokeAsync([func{ std::move(func) }] { - isShuttingDown = false; - - m_runtime = &JsRuntime::CreateForJavaScript(m_env, CreateJsRuntimeDispatcher(m_env, jsiRuntime, std::move(callInvoker), isShuttingDown)); - - auto width = static_cast(ANativeWindow_getWidth(windowPtr)); - auto height = static_cast(ANativeWindow_getHeight(windowPtr)); - - m_graphics = Graphics::CreateGraphics(reinterpret_cast(windowPtr), width, height); - m_graphics->AddToJavaScript(m_env); - - Plugins::NativeEngine::Initialize(m_env, true); - Plugins::NativeXr::Initialize(m_env); - - Polyfills::Window::Initialize(m_env); - // NOTE: React Native's XMLHttpRequest is slow and allocates a lot of memory. This does not override - // React Native's implementation, but rather adds a second one scoped to Babylon and used by WebRequest.ts. - Polyfills::XMLHttpRequest::Initialize(m_env); - - m_nativeInput = &Babylon::Plugins::NativeInput::CreateForJavaScript(m_env); - } - - // NOTE: This only happens when the JS engine is shutting down (other than when the app exits, this only - // happens during a dev mode reload). In this case, EngineHook.ts won't call NativeEngine.dispose, - // so we need to manually do it here to properly clean up these resources. - ~Native() - { - auto native = JsRuntime::NativeObject::GetFromJavaScript(m_env); - auto engine = native.Get("engineInstance").As(); - auto dispose = engine.Get("dispose").As(); - dispose.Call(engine, {}); - isShuttingDown = true; - - Napi::Detach(m_env); - } - - void Refresh(ANativeWindow* windowPtr) - { - auto width = static_cast(ANativeWindow_getWidth(windowPtr)); - auto height = static_cast(ANativeWindow_getHeight(windowPtr)); - m_graphics->UpdateWindow(windowPtr); - m_graphics->UpdateSize(width, height); - m_graphics->EnableRendering(); - } - - void Reset() - { - m_graphics->DisableRendering(); - } + func(); + }); + } }; - void SetPointerButtonState(uint32_t pointerId, uint32_t buttonId, bool isDown, uint32_t x, uint32_t y) - { - if (isDown) - { - m_nativeInput->PointerDown(pointerId, buttonId, x, y); - } - else - { - m_nativeInput->PointerUp(pointerId, buttonId, x, y); - } - } - - void SetPointerPosition(uint32_t pointerId, uint32_t x, uint32_t y) - { - m_nativeInput->PointerMove(pointerId, x, y); - } - - private: - std::unique_ptr m_graphics{}; - Napi::Env m_env; - JsRuntime* m_runtime; - Plugins::NativeInput* m_nativeInput; - }; + Babylon::Initialize(*jsiRuntime, std::move(jsDispatcher)); } -extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_initialize(JNIEnv* env, jclass obj, jobject context) +extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_00024BabylonNative_deinitialize(JNIEnv* env, jclass obj) { - JavaVM* javaVM{}; - if (env->GetJavaVM(&javaVM) != JNI_OK) - { - throw std::runtime_error("Failed to get Java VM"); - } - - android::global::Initialize(javaVM, context); + Babylon::Deinitialize(); } -extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_setCurrentActivity(JNIEnv* env, jclass obj, jobject activity) +extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_00024BabylonNative_setCurrentActivity(JNIEnv* env, jclass obj, jobject activity) { android::global::SetCurrentActivity(activity); } -extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_pause(JNIEnv* env, jclass obj) +extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_00024BabylonNative_pause(JNIEnv* env, jclass obj) { android::global::Pause(); } -extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_resume(JNIEnv* env, jclass obj) +extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_00024BabylonNative_resume(JNIEnv* env, jclass obj) { android::global::Resume(); } -extern "C" JNIEXPORT jlong JNICALL Java_com_babylonreactnative_BabylonNativeInterop_create(JNIEnv* env, jclass obj, jlong jsiRuntimeRef, jobject jsCallInvokerHolder, jobject surface) -{ - auto jsiRuntime = reinterpret_cast(jsiRuntimeRef); - auto callInvoker = jni::alias_ref {reinterpret_cast(jsCallInvokerHolder)}->cthis()->getCallInvoker(); - ANativeWindow* windowPtr = ANativeWindow_fromSurface(env, surface); - auto native = new Babylon::Native(*jsiRuntime, callInvoker, windowPtr); - return reinterpret_cast(native); -} - -extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_refresh(JNIEnv* env, jclass obj, jlong instanceRef, jobject surface) -{ - auto native = reinterpret_cast(instanceRef); - ANativeWindow* windowPtr = ANativeWindow_fromSurface(env, surface); - native->Refresh(windowPtr); -} - -extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_setPointerButtonState(JNIEnv* env, jclass obj, jlong instanceRef, jint pointerId, jint buttonId, jboolean isDown, jint x, jint y) -{ - auto native = reinterpret_cast(instanceRef); - native->SetPointerButtonState(static_cast(pointerId), static_cast(buttonId), isDown, static_cast(x), static_cast(y)); -} - -extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_setPointerPosition(JNIEnv* env, jclass obj, jlong instanceRef, jint pointerId, jint x, jint y) +extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_00024BabylonNative_updateView(JNIEnv* env, jclass obj, jobject surface) { - auto native = reinterpret_cast(instanceRef); - native->SetPointerPosition(static_cast(pointerId), static_cast(x), static_cast(y)); + ANativeWindow* windowPtr{ ANativeWindow_fromSurface(env, surface) }; + auto width{ static_cast(ANativeWindow_getWidth(windowPtr)) }; + auto height{ static_cast(ANativeWindow_getHeight(windowPtr)) }; + Babylon::UpdateView(windowPtr, width, height); } -extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_reset(JNIEnv* env, jclass obj, jlong instanceRef) +extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_00024BabylonNative_setPointerButtonState(JNIEnv* env, jclass obj, jint pointerId, jint buttonId, jboolean isDown, jint x, jint y) { - auto native = reinterpret_cast(instanceRef); - native->Reset(); + Babylon::SetPointerButtonState(static_cast(pointerId), static_cast(buttonId), isDown, static_cast(x), static_cast(y)); } -extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_destroy(JNIEnv* env, jclass obj, jlong instanceRef) +extern "C" JNIEXPORT void JNICALL Java_com_babylonreactnative_BabylonNativeInterop_00024BabylonNative_setPointerPosition(JNIEnv* env, jclass obj, jint pointerId, jint x, jint y) { - auto native = reinterpret_cast(instanceRef); - delete native; + Babylon::SetPointerPosition(static_cast(pointerId), static_cast(x), static_cast(y)); } diff --git a/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonModule.java b/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonModule.java index 573bc0e76..94ae5fb77 100644 --- a/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonModule.java +++ b/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonModule.java @@ -1,8 +1,5 @@ package com.babylonreactnative; -import android.os.Handler; -import android.os.Looper; - import androidx.annotation.NonNull; import com.facebook.react.bridge.Promise; @@ -22,29 +19,17 @@ public String getName() { return "BabylonModule"; } - // NOTE: This happens during dev mode reload, when the JS engine is being shutdown and restarted. - @Override - public void onCatalystInstanceDestroy() { - this.getReactApplicationContext().runOnJSQueueThread(BabylonNativeInterop::deinitialize); - } - @ReactMethod public void initialize(Promise promise) { - // Ideally we'd do all the initialization here that is scoped to a javascript engine instance, but this is tied up in the view initialization in Babylon Native currently. - // For now, just await initialization by the first EngineView that is created. - BabylonNativeInterop.whenInitialized(this.getReactApplicationContext()).thenAccept(instanceRef -> promise.resolve(instanceRef != 0)); - } - - @ReactMethod - public void whenInitialized(Promise promise) { - BabylonNativeInterop.whenInitialized(this.getReactApplicationContext()).thenAccept(instanceRef -> promise.resolve(instanceRef != 0)); - } - - @ReactMethod - public void reset(Promise promise) { this.getReactApplicationContext().runOnJSQueueThread(() -> { - BabylonNativeInterop.reset(this.getReactApplicationContext()); + BabylonNativeInterop.initialize(this.getReactApplicationContext()); promise.resolve(null); }); } + + // NOTE: This happens during dev mode reload, when the JS engine is being shutdown and restarted. + @Override + public void onCatalystInstanceDestroy() { + this.getReactApplicationContext().runOnJSQueueThread(BabylonNativeInterop::deinitialize); + } } diff --git a/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonNativeInterop.java b/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonNativeInterop.java index fa4d8231f..319bc9fb1 100644 --- a/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonNativeInterop.java +++ b/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonNativeInterop.java @@ -7,169 +7,104 @@ import android.view.Surface; import com.facebook.react.bridge.ActivityEventListener; -import com.facebook.react.bridge.JavaScriptContextHolder; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReactContext; import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; +public final class BabylonNativeInterop { -final class BabylonNativeInterop { - // JNI interface - static { - System.loadLibrary("BabylonNative"); + private static class BabylonNative { + static { + System.loadLibrary("BabylonNative"); + } + + public static native void initialize(Context context, long jsiRuntimeRef, CallInvokerHolder jsCallInvokerHolder); + public static native void deinitialize(); + public static native void setCurrentActivity(Activity activity); + public static native void pause(); + public static native void resume(); + public static native void updateView(Surface surface); + public static native void setPointerButtonState(int pointerId, int buttonId, boolean isDown, int x, int y); + public static native void setPointerPosition(int pointerId, int x, int y); } - private static boolean isInitialized; - private static final Hashtable> nativeInstances = new Hashtable<>(); - - private static native void initialize(Context context); - private static native void setCurrentActivity(Activity activity); - private static native void pause(); - private static native void resume(); - private static native long create(long jsiRuntimeRef, CallInvokerHolder callInvokerHolder, Surface surface); - private static native void refresh(long instanceRef, Surface surface); - private static native void setPointerButtonState(long instanceRef, int pointerId, int buttonId, boolean isDown, int x, int y); - private static native void setPointerPosition(long instanceRef, int pointerId, int x, int y); - private static native void reset(long instanceRef); - private static native void destroy(long instanceRef); - - // Must be called from the Android UI thread - static void setView(ReactContext reactContext, Surface surface) { - // This is global initialization that only needs to happen once - if (!BabylonNativeInterop.isInitialized) { - BabylonNativeInterop.initialize(reactContext); - BabylonNativeInterop.isInitialized = true; + private static LifecycleEventListener lifeCycleEventListener; + private static ActivityEventListener activityEventListener; + + public static void initialize(ReactContext reactContext) { + long jsiRuntimeRef = reactContext.getJavaScriptContextHolder().get(); + CallInvokerHolder jsCallInvokerHolder = reactContext.getCatalystInstance().getJSCallInvokerHolder(); + BabylonNative.initialize(reactContext, jsiRuntimeRef, jsCallInvokerHolder); + + if (BabylonNativeInterop.lifeCycleEventListener != null) { + reactContext.removeLifecycleEventListener(lifeCycleEventListener); } - BabylonNativeInterop.destroyOldNativeInstances(reactContext); - - CompletableFuture instanceRefFuture = BabylonNativeInterop.getOrCreateFuture(reactContext); - - reactContext.runOnJSQueueThread(() -> { - Long instanceRef = instanceRefFuture.getNow(null); - if (instanceRef == null) - { - long jsiRuntimeRef = reactContext.getJavaScriptContextHolder().get(); - if (jsiRuntimeRef == 0) { - instanceRefFuture.complete(0L); - } else { - instanceRef = BabylonNativeInterop.create(jsiRuntimeRef, reactContext.getCatalystInstance().getJSCallInvokerHolder(), surface); - final long finalInstanceRef = instanceRef; - - reactContext.addLifecycleEventListener(new LifecycleEventListener() { - @Override - public void onHostResume() { - BabylonNativeInterop.setCurrentActivity(reactContext.getCurrentActivity()); - BabylonNativeInterop.resume(); - } - - @Override - public void onHostPause() { - BabylonNativeInterop.pause(); - } - - @Override - public void onHostDestroy() { - BabylonNativeInterop.deinitialize(); - } - }); - - reactContext.addActivityEventListener(new ActivityEventListener() { - @Override - public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { - // Nothing to do here - } - - @Override - public void onNewIntent(Intent intent) { - BabylonNativeInterop.setCurrentActivity(reactContext.getCurrentActivity()); - } - }); - - instanceRefFuture.complete(finalInstanceRef); - } - } else if (instanceRef != 0) { - BabylonNativeInterop.refresh(instanceRef, surface); + BabylonNativeInterop.lifeCycleEventListener = new LifecycleEventListener() { + @Override + public void onHostResume() { + BabylonNative.setCurrentActivity(reactContext.getCurrentActivity()); + BabylonNative.resume(); } - }); - } - static void reportMotionEvent(ReactContext reactContext, MotionEvent motionEvent) { - CompletableFuture instanceRefFuture = BabylonNativeInterop.nativeInstances.get(reactContext.getJavaScriptContextHolder()); - if (instanceRefFuture != null) { - Long instanceRef = instanceRefFuture.getNow(null); - if (instanceRef != null) { - int maskedAction = motionEvent.getActionMasked(); - boolean isPointerDown = maskedAction == MotionEvent.ACTION_DOWN || maskedAction == MotionEvent.ACTION_POINTER_DOWN; - boolean isPointerUp = maskedAction == MotionEvent.ACTION_UP || maskedAction == MotionEvent.ACTION_POINTER_UP; - boolean isPointerMove = maskedAction == MotionEvent.ACTION_MOVE; - - if (isPointerDown || isPointerUp) { - int pointerIndex = motionEvent.getActionIndex(); - int pointerId = motionEvent.getPointerId(pointerIndex); - int buttonId = motionEvent.getActionButton(); - int x = (int)motionEvent.getX(pointerIndex); - int y = (int)motionEvent.getY(pointerIndex); - BabylonNativeInterop.setPointerButtonState(instanceRef, pointerId, buttonId, isPointerDown, x, y); - } else if (isPointerMove) { - for (int pointerIndex = 0; pointerIndex < motionEvent.getPointerCount(); pointerIndex++) { - int pointerId = motionEvent.getPointerId(pointerIndex); - int x = (int)motionEvent.getX(pointerIndex); - int y = (int)motionEvent.getY(pointerIndex); - BabylonNativeInterop.setPointerPosition(instanceRef, pointerId, x, y); - } - } + @Override + public void onHostPause() { + BabylonNative.pause(); } + + @Override + public void onHostDestroy() { + BabylonNative.deinitialize(); + } + }; + + reactContext.addLifecycleEventListener(lifeCycleEventListener); + + if (BabylonNativeInterop.activityEventListener != null) { + reactContext.removeActivityEventListener(BabylonNativeInterop.activityEventListener); } - } - // Must be called from the Android UI thread - static CompletionStage whenInitialized(ReactContext reactContext) { - return BabylonNativeInterop.getOrCreateFuture(reactContext); - } + BabylonNativeInterop.activityEventListener = new ActivityEventListener() { + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + // Nothing to do here + } + + @Override + public void onNewIntent(Intent intent) { + BabylonNative.setCurrentActivity(reactContext.getCurrentActivity()); + } + }; - // Must be called from the JavaScript thread - static void deinitialize() { - BabylonNativeInterop.destroyOldNativeInstances(null); + reactContext.addActivityEventListener(BabylonNativeInterop.activityEventListener); } - static void reset(ReactContext reactContext) { - JavaScriptContextHolder jsContext = reactContext.getJavaScriptContextHolder(); - CompletableFuture instanceRefFuture = BabylonNativeInterop.nativeInstances.get(jsContext); - if (instanceRefFuture != null) { - Long instanceRef = instanceRefFuture.getNow(null); - if (instanceRef != null) { - BabylonNativeInterop.reset(instanceRef); - } - } + public static void deinitialize() { + BabylonNative.deinitialize(); } - private static CompletableFuture getOrCreateFuture(ReactContext reactContext) { - JavaScriptContextHolder jsContext = reactContext.getJavaScriptContextHolder(); - CompletableFuture instanceRefFuture = BabylonNativeInterop.nativeInstances.get(jsContext); - if (instanceRefFuture == null) - { - instanceRefFuture = new CompletableFuture<>(); - BabylonNativeInterop.nativeInstances.put(jsContext, instanceRefFuture); - } - return instanceRefFuture; + public static void updateView(Surface surface) { + BabylonNative.updateView(surface); } - private static void destroyOldNativeInstances(ReactContext currentReactContext) { - Iterator>> nativeInstanceIterator = BabylonNativeInterop.nativeInstances.entrySet().iterator(); - while (nativeInstanceIterator.hasNext()) { - Map.Entry> nativeInstanceInfo = nativeInstanceIterator.next(); - if (currentReactContext == null || nativeInstanceInfo.getKey() != currentReactContext.getJavaScriptContextHolder()) { - Long oldInstanceRef = nativeInstanceInfo.getValue().getNow(null); - if (oldInstanceRef != null && oldInstanceRef != 0) { - BabylonNativeInterop.destroy(oldInstanceRef); - } - nativeInstanceIterator.remove(); + public static void reportMotionEvent(MotionEvent motionEvent) { + int maskedAction = motionEvent.getActionMasked(); + boolean isPointerDown = maskedAction == MotionEvent.ACTION_DOWN || maskedAction == MotionEvent.ACTION_POINTER_DOWN; + boolean isPointerUp = maskedAction == MotionEvent.ACTION_UP || maskedAction == MotionEvent.ACTION_POINTER_UP; + boolean isPointerMove = maskedAction == MotionEvent.ACTION_MOVE; + + if (isPointerDown || isPointerUp) { + int pointerIndex = motionEvent.getActionIndex(); + int pointerId = motionEvent.getPointerId(pointerIndex); + int x = (int)motionEvent.getX(pointerIndex); + int y = (int)motionEvent.getY(pointerIndex); + BabylonNative.setPointerButtonState(pointerId, 0, isPointerDown, x, y); + } else if (isPointerMove) { + for (int pointerIndex = 0; pointerIndex < motionEvent.getPointerCount(); pointerIndex++) { + int pointerId = motionEvent.getPointerId(pointerIndex); + int x = (int)motionEvent.getX(pointerIndex); + int y = (int)motionEvent.getY(pointerIndex); + BabylonNative.setPointerPosition(pointerId, x, y); } } } diff --git a/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/EngineView.java b/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/EngineView.java index d3bccab57..ed36fb336 100644 --- a/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/EngineView.java +++ b/Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/EngineView.java @@ -19,15 +19,13 @@ import java.io.ByteArrayOutputStream; public final class EngineView extends SurfaceView implements SurfaceHolder.Callback, View.OnTouchListener { - private final ReactContext reactContext; private final EventDispatcher reactEventDispatcher; public EngineView(ReactContext reactContext) { super(reactContext); - this.reactContext = reactContext; this.getHolder().addCallback(this); this.setOnTouchListener(this); - this.reactEventDispatcher = this.reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + this.reactEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); } @Override @@ -37,17 +35,16 @@ public void surfaceCreated(SurfaceHolder surfaceHolder) { @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int width, int height) { - BabylonNativeInterop.setView(this.reactContext, surfaceHolder.getSurface()); + BabylonNativeInterop.updateView(surfaceHolder.getSurface()); } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { - } @Override public boolean onTouch(View view, MotionEvent motionEvent) { - BabylonNativeInterop.reportMotionEvent(this.reactContext, motionEvent); + BabylonNativeInterop.reportMotionEvent(motionEvent); return true; } diff --git a/Modules/@babylonjs/react-native/ios/BabylonModule.mm b/Modules/@babylonjs/react-native/ios/BabylonModule.mm index 05af48afc..3769a55d1 100644 --- a/Modules/@babylonjs/react-native/ios/BabylonModule.mm +++ b/Modules/@babylonjs/react-native/ios/BabylonModule.mm @@ -19,16 +19,8 @@ @implementation BabylonModule @synthesize bridge = _bridge; RCT_EXPORT_METHOD(initialize:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - [BabylonNativeInterop whenInitialized:self.bridge resolve:resolve]; -} - -RCT_EXPORT_METHOD(whenInitialized:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - [BabylonNativeInterop whenInitialized:self.bridge resolve:resolve]; -} - -RCT_EXPORT_METHOD(reset:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - self.bridge.jsCallInvoker->invokeAsync([resolve]() { - [BabylonNativeInterop reset]; + self.bridge.jsCallInvoker->invokeAsync([bridge{ self.bridge }, resolve]() { + [BabylonNativeInterop initialize:bridge]; resolve([NSNull null]); }); } diff --git a/Modules/@babylonjs/react-native/ios/BabylonNative.cpp b/Modules/@babylonjs/react-native/ios/BabylonNative.cpp deleted file mode 100644 index 541c63d6c..000000000 --- a/Modules/@babylonjs/react-native/ios/BabylonNative.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "BabylonNative.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include - -#include -#include -#include - -#include - -namespace Babylon -{ - using namespace facebook; - - namespace - { - bool isShuttingDown{false}; - } - - class Native::Impl - { - public: - Impl(facebook::jsi::Runtime& jsiRuntime, std::shared_ptr callInvoker) - : env{ Napi::Attach(jsiRuntime) } - , jsCallInvoker{ callInvoker } - { - } - - ~Impl() - { - Napi::Detach(env); - } - - Napi::Env env; - std::shared_ptr jsCallInvoker; - std::unique_ptr graphics{}; - JsRuntime* runtime{}; - Plugins::NativeInput* nativeInput{}; - }; - - Native::Native(facebook::jsi::Runtime& jsiRuntime, std::shared_ptr callInvoker, void* windowPtr, size_t width, size_t height) - : m_impl{ std::make_unique(jsiRuntime, callInvoker) } - { - isShuttingDown = false; - m_impl->graphics = Graphics::CreateGraphics(reinterpret_cast(windowPtr), width, height); - - m_impl->runtime = &JsRuntime::CreateForJavaScript(m_impl->env, CreateJsRuntimeDispatcher(m_impl->env, jsiRuntime, std::move(callInvoker), isShuttingDown)); - - m_impl->graphics->AddToJavaScript(m_impl->env); - - Polyfills::Window::Initialize(m_impl->env); - // NOTE: React Native's XMLHttpRequest is slow and allocates a lot of memory. This does not override - // React Native's implementation, but rather adds a second one scoped to Babylon and used by WebRequest.ts. - Polyfills::XMLHttpRequest::Initialize(m_impl->env); - - Plugins::NativeEngine::Initialize(m_impl->env, true); - Plugins::NativeXr::Initialize(m_impl->env); - - m_impl->nativeInput = &Babylon::Plugins::NativeInput::CreateForJavaScript(m_impl->env); - } - - // NOTE: This only happens when the JS engine is shutting down (other than when the app exits, this only - // happens during a dev mode reload). In this case, EngineHook.ts won't call NativeEngine.dispose, - // so we need to manually do it here to properly clean up these resources. - Native::~Native() - { - auto native = JsRuntime::NativeObject::GetFromJavaScript(m_impl->env); - auto engine = native.Get("engineInstance").As(); - auto dispose = engine.Get("dispose").As(); - dispose.Call(engine, {}); - isShuttingDown = true; - } - - void Native::Refresh(void* windowPtr, size_t width, size_t height) - { - m_impl->graphics->UpdateWindow(windowPtr); - m_impl->graphics->UpdateSize(width, height); - m_impl->graphics->EnableRendering(); - } - - void Native::Resize(size_t width, size_t height) - { - m_impl->graphics->UpdateSize(width, height); - } - - void Native::Reset() - { - m_impl->graphics->DisableRendering(); - } - - void Native::SetPointerButtonState(uint32_t pointerId, uint32_t buttonId, bool isDown, uint32_t x, uint32_t y) - { - if (isDown) - { - m_impl->nativeInput->PointerDown(pointerId, buttonId, x, y); - } - else - { - m_impl->nativeInput->PointerUp(pointerId, buttonId, x, y); - } - } - - void Native::SetPointerPosition(uint32_t pointerId, uint32_t x, uint32_t y) - { - m_impl->nativeInput->PointerMove(pointerId, x, y); - } -} diff --git a/Modules/@babylonjs/react-native/ios/BabylonNative.h b/Modules/@babylonjs/react-native/ios/BabylonNative.h deleted file mode 100644 index d7ce8fa97..000000000 --- a/Modules/@babylonjs/react-native/ios/BabylonNative.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include -#include - -namespace Babylon -{ - class Native final - { - public: - // This class must be constructed from the JavaScript thread - Native(facebook::jsi::Runtime& jsiRuntime, std::shared_ptr callInvoker, void* windowPtr, size_t width, size_t height); - ~Native(); - void Refresh(void* windowPtr, size_t width, size_t height); - void Resize(size_t width, size_t height); - void Reset(); - void SetPointerButtonState(uint32_t pointerId, uint32_t buttonId, bool isDown, uint32_t x, uint32_t y); - void SetPointerPosition(uint32_t pointerId, uint32_t x, uint32_t y); - - private: - class Impl; - std::unique_ptr m_impl{}; - }; -} diff --git a/Modules/@babylonjs/react-native/ios/BabylonNativeInterop.h b/Modules/@babylonjs/react-native/ios/BabylonNativeInterop.h index 2ecca3399..c7417ba46 100644 --- a/Modules/@babylonjs/react-native/ios/BabylonNativeInterop.h +++ b/Modules/@babylonjs/react-native/ios/BabylonNativeInterop.h @@ -4,8 +4,7 @@ #import @interface BabylonNativeInterop : NSObject -+ (void)setView:(RCTBridge*)bridge jsRunLoop:(NSRunLoop*)jsRunLoop mktView:(MTKView*)mtkView; -+ (void)reportTouchEvent:(NSSet*)touches withEvent:(UIEvent*)event; -+ (void)whenInitialized:(RCTBridge*)bridge resolve:(RCTPromiseResolveBlock)resolve; -+ (void)reset; ++ (void)initialize:(RCTBridge*)bridge; ++ (void)updateView:(MTKView*)mtkView; ++ (void)reportTouchEvent:(MTKView*)mtkView touches:(NSSet*)touches event:(UIEvent*)event; @end diff --git a/Modules/@babylonjs/react-native/ios/BabylonNativeInterop.mm b/Modules/@babylonjs/react-native/ios/BabylonNativeInterop.mm index 857b45ee1..9b0afe1c0 100644 --- a/Modules/@babylonjs/react-native/ios/BabylonNativeInterop.mm +++ b/Modules/@babylonjs/react-native/ios/BabylonNativeInterop.mm @@ -1,16 +1,13 @@ #import "BabylonNativeInterop.h" -#import "BabylonNative.h" +#import "../shared/BabylonNative.h" #import #import +#include #import -#import #import -#import -#import -#import using namespace facebook; @@ -27,144 +24,84 @@ @interface RCTBridge (RCTTurboModule) @implementation BabylonNativeInterop -static RCTBridge* currentBridge; -static MTKView* currentView; -static std::unique_ptr currentNativeInstance; -static std::unordered_map> initializationPromises; -static std::mutex mapMutex; static NSMutableArray* activeTouches; -+ (void)setView:(RCTBridge*)bridge jsRunLoop:(NSRunLoop*)jsRunLoop mktView:(MTKView*)mtkView { ++ (void)initialize:(RCTBridge*)bridge { + auto jsCallInvoker{ bridge.jsCallInvoker }; + auto jsDispatcher{ [jsCallInvoker{ std::move(jsCallInvoker) }](std::function func) + { + jsCallInvoker->invokeAsync([func{ std::move(func) }] + { + func(); + }); + } }; + + Babylon::Initialize(*GetJSIRuntime(bridge), std::move(jsDispatcher)); + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:RCTBridgeWillInvalidateModulesNotification + object:bridge.parentBridge]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onBridgeWillInvalidate:) + name:RCTBridgeWillInvalidateModulesNotification + object:bridge.parentBridge]; +} + +// NOTE: This happens during dev mode reload, when the JS engine is being shutdown and restarted. ++ (void)onBridgeWillInvalidate:(NSNotification*)notification +{ + Babylon::Deinitialize(); +} + ++ (void)updateView:(MTKView*)mtkView { const int width = static_cast(mtkView.bounds.size.width * UIScreen.mainScreen.scale); const int height = static_cast(mtkView.bounds.size.height * UIScreen.mainScreen.scale); if (width != 0 && height != 0) { - // NOTE: jsRunLoop should only be null when remote debugging is enabled. - // In this case, we can just use the main loop, because we are only - // going to set an error state (which can happen on any thread). - if (!jsRunLoop) { - jsRunLoop = NSRunLoop.mainRunLoop; - } - - [jsRunLoop performBlock:^{ - if (bridge != currentBridge) { - [BabylonNativeInterop setCurrentNativeInstance:bridge mtkView:mtkView width:width height:height]; - } else if (currentNativeInstance) { - if (mtkView != currentView) { - [BabylonNativeInterop setCurrentView:mtkView]; - currentNativeInstance->Refresh((__bridge void*)currentView, width, height); - } else { - // NOTE: This will cause Metal API Validation to fail if it is enabled when the debugger is attached, which stops app execution. - // For now, be sure to disable Metal API Validation under Product->Scheme->Edit Scheme. - currentNativeInstance->Resize(width, height); - } - } - }]; + Babylon::UpdateView((__bridge void*)mtkView, width, height); } } -+ (void)reportTouchEvent:(NSSet*)touches withEvent:(UIEvent*)event { - if (currentNativeInstance) { - for (UITouch* touch in touches) { - if (touch.view == currentView) { - const CGFloat scale = UIScreen.mainScreen.scale; - const CGPoint pointerPosition = [touch locationInView:currentView]; - const uint32_t x = static_cast(pointerPosition.x * scale); - const uint32_t y = static_cast(pointerPosition.y * scale); - - switch (touch.phase) { - case UITouchPhaseBegan: { - NSUInteger pointerId = [activeTouches indexOfObject:[NSNull null]]; - if (pointerId == NSNotFound) { - pointerId = [activeTouches count]; - [activeTouches addObject:touch]; - } else { - [activeTouches replaceObjectAtIndex:pointerId withObject:touch]; - } - currentNativeInstance->SetPointerButtonState(static_cast(pointerId), 0, true, x, y); - break; - } - - case UITouchPhaseMoved: { - NSUInteger pointerId = [activeTouches indexOfObject:touch]; - currentNativeInstance->SetPointerPosition(static_cast(pointerId), x, y); - break; ++ (void)reportTouchEvent:(MTKView*)mtkView touches:(NSSet*)touches event:(UIEvent*)event { + for (UITouch* touch in touches) { + if (touch.view == mtkView) { + const CGFloat scale = UIScreen.mainScreen.scale; + const CGPoint pointerPosition = [touch locationInView:mtkView]; + const uint32_t x = static_cast(pointerPosition.x * scale); + const uint32_t y = static_cast(pointerPosition.y * scale); + + switch (touch.phase) { + case UITouchPhaseBegan: { + NSUInteger pointerId = [activeTouches indexOfObject:[NSNull null]]; + if (pointerId == NSNotFound) { + pointerId = [activeTouches count]; + [activeTouches addObject:touch]; + } else { + [activeTouches replaceObjectAtIndex:pointerId withObject:touch]; } - - case UITouchPhaseEnded: - case UITouchPhaseCancelled: { - NSUInteger pointerId = [activeTouches indexOfObject:touch]; - [activeTouches replaceObjectAtIndex:pointerId withObject:[NSNull null]]; - currentNativeInstance->SetPointerButtonState(static_cast(pointerId), 0, false, x, y); - break; - } - - default: - break; + Babylon::SetPointerButtonState(static_cast(pointerId), 0, true, x, y); + break; } - } - } - } -} - -+ (void)whenInitialized:(RCTBridge*)bridge resolve:(RCTPromiseResolveBlock)resolve { - const std::lock_guard lock(mapMutex); - if (bridge == currentBridge) { - resolve([NSNumber numberWithUnsignedLong:reinterpret_cast(currentNativeInstance.get())]); - } else { - initializationPromises[(__bridge void*)bridge].push_back(resolve); - } -} - -+ (void)reset { - if (currentNativeInstance) { - currentNativeInstance->Reset(); - } -} -+ (void)setCurrentView:(MTKView*)mtkView { - currentView = mtkView; - activeTouches = [NSMutableArray new]; -} + case UITouchPhaseMoved: { + NSUInteger pointerId = [activeTouches indexOfObject:touch]; + Babylon::SetPointerPosition(static_cast(pointerId), x, y); + break; + } -+ (void)setCurrentNativeInstance:(RCTBridge*)bridge mtkView:(MTKView*)mtkView width:(int)width height:(int)height { - [BabylonNativeInterop setCurrentView:mtkView]; + case UITouchPhaseEnded: + case UITouchPhaseCancelled: { + NSUInteger pointerId = [activeTouches indexOfObject:touch]; + [activeTouches replaceObjectAtIndex:pointerId withObject:[NSNull null]]; + Babylon::SetPointerButtonState(static_cast(pointerId), 0, false, x, y); + break; + } - { - const std::lock_guard lock(mapMutex); - - if (bridge != currentBridge) { - if (currentBridge == nil || currentBridge.parentBridge != bridge.parentBridge) { - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onBridgeWillInvalidate:) - name:RCTBridgeWillInvalidateModulesNotification - object:bridge.parentBridge]; + default: + break; } - - currentBridge = bridge; - } - - currentNativeInstance.reset(); - - jsi::Runtime* jsiRuntime = GetJSIRuntime(currentBridge); - if (jsiRuntime) { - currentNativeInstance = std::make_unique(*jsiRuntime, currentBridge.jsCallInvoker, (__bridge void*)mtkView, width, height); - } - } - - auto initializationPromisesIterator = initializationPromises.find((__bridge void*)currentBridge); - if (initializationPromisesIterator != initializationPromises.end()) { - for (RCTPromiseResolveBlock resolve : initializationPromisesIterator->second) { - resolve([NSNumber numberWithUnsignedLong:reinterpret_cast(currentNativeInstance.get())]); } - - initializationPromises.erase(initializationPromisesIterator); } } -// NOTE: This happens during dev mode reload, when the JS engine is being shutdown and restarted. -+ (void)onBridgeWillInvalidate:(NSNotification*)notification -{ - currentNativeInstance.reset(); -} - @end diff --git a/Modules/@babylonjs/react-native/ios/CMakeLists.txt b/Modules/@babylonjs/react-native/ios/CMakeLists.txt index 4dbf11777..5b32f7ae4 100644 --- a/Modules/@babylonjs/react-native/ios/CMakeLists.txt +++ b/Modules/@babylonjs/react-native/ios/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.13.2) - project(ReactNativeBabylon) +include(${CMAKE_CURRENT_LIST_DIR}/../shared/CMakeLists.txt) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") @@ -27,12 +28,12 @@ set(BABYLON_REACT_NATIVE_SHARED_DIR "${CMAKE_CURRENT_LIST_DIR}/../shared") add_subdirectory(${BABYLON_REACT_NATIVE_SHARED_DIR} ${CMAKE_CURRENT_BINARY_DIR}/shared) add_library(BabylonNative - ${CMAKE_CURRENT_LIST_DIR}/BabylonNative.h - ${CMAKE_CURRENT_LIST_DIR}/BabylonNative.cpp) + ${SHARED_SOURCES}) target_compile_definitions(BabylonNative PRIVATE UNICODE) target_compile_definitions(BabylonNative PRIVATE _UNICODE) +target_include_directories(BabylonNative PRIVATE ${SHARED_INCLUDES}) target_include_directories(BabylonNative PUBLIC ${CMAKE_CURRENT_LIST_DIR}) target_link_libraries(BabylonNative @@ -41,7 +42,6 @@ target_link_libraries(BabylonNative Graphics jsi reactnative - BabylonReactNativeShared JsRuntime NativeEngine NativeInput diff --git a/Modules/@babylonjs/react-native/ios/EngineViewManager.mm b/Modules/@babylonjs/react-native/ios/EngineViewManager.mm index 00066406c..4291514c6 100644 --- a/Modules/@babylonjs/react-native/ios/EngineViewManager.mm +++ b/Modules/@babylonjs/react-native/ios/EngineViewManager.mm @@ -2,6 +2,7 @@ #import #import +#import #import #import @@ -15,13 +16,11 @@ @interface EngineView : MTKView @implementation EngineView { RCTBridge* bridge; - NSRunLoop* runLoop; } -- (instancetype)init:(RCTBridge*)_bridge runLoop:(NSRunLoop*)_runLoop { +- (instancetype)init:(RCTBridge*)_bridge { if (self = [super initWithFrame:CGRectZero device:MTLCreateSystemDefaultDevice()]) { bridge = _bridge; - runLoop = _runLoop; super.translatesAutoresizingMaskIntoConstraints = false; super.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB; @@ -32,23 +31,23 @@ - (instancetype)init:(RCTBridge*)_bridge runLoop:(NSRunLoop*)_runLoop { - (void)setBounds:(CGRect)bounds { [super setBounds:bounds]; - [BabylonNativeInterop setView:bridge jsRunLoop:runLoop mktView:self]; + [BabylonNativeInterop updateView:self]; } - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { - [BabylonNativeInterop reportTouchEvent:touches withEvent:event]; + [BabylonNativeInterop reportTouchEvent:self touches:touches event:event]; } - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { - [BabylonNativeInterop reportTouchEvent:touches withEvent:event]; + [BabylonNativeInterop reportTouchEvent:self touches:touches event:event]; } - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { - [BabylonNativeInterop reportTouchEvent:touches withEvent:event]; + [BabylonNativeInterop reportTouchEvent:self touches:touches event:event]; } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { - [BabylonNativeInterop reportTouchEvent:touches withEvent:event]; + [BabylonNativeInterop reportTouchEvent:self touches:touches event:event]; } - (void)takeSnapshot { @@ -79,9 +78,7 @@ - (void)takeSnapshot { @interface EngineViewManager : RCTViewManager @end -@implementation EngineViewManager { - NSRunLoop* runLoop; -} +@implementation EngineViewManager RCT_EXPORT_MODULE(EngineViewManager) @@ -98,13 +95,8 @@ @implementation EngineViewManager { }]; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(setJSThread) { - runLoop = [NSRunLoop currentRunLoop]; - return nil; -} - - (UIView*)view { - return [[EngineView alloc] init:self.bridge runLoop:runLoop]; + return [[EngineView alloc] init:self.bridge]; } @end diff --git a/Modules/@babylonjs/react-native/react-native-babylon.podspec b/Modules/@babylonjs/react-native/react-native-babylon.podspec index 8638e7abb..2e9be0360 100644 --- a/Modules/@babylonjs/react-native/react-native-babylon.podspec +++ b/Modules/@babylonjs/react-native/react-native-babylon.podspec @@ -14,7 +14,6 @@ Pod::Spec.new do |s| s.source = { :git => package["repository"]["url"], :tag => s.version } s.source_files = "ios/**/*.{h,m,mm}" - s.exclude_files = "ios/BabylonNative.h" s.requires_arc = true s.libraries = 'astc-codec', diff --git a/Modules/@babylonjs/react-native/shared/BabylonNative.cpp b/Modules/@babylonjs/react-native/shared/BabylonNative.cpp new file mode 100644 index 000000000..3ef50c425 --- /dev/null +++ b/Modules/@babylonjs/react-native/shared/BabylonNative.cpp @@ -0,0 +1,232 @@ +#include "BabylonNative.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Babylon +{ + using namespace facebook; + + class ReactNativeModule : public jsi::HostObject + { + public: + ReactNativeModule(jsi::Runtime& jsiRuntime, Dispatcher jsDispatcher) + : m_env{ Napi::Attach(jsiRuntime) } + , m_jsDispatcher{ std::move(jsDispatcher) } + , m_isRunning{ std::make_shared(true) } + { + // Initialize a JS promise that will be returned by whenInitialized, and completed when NativeEngine is initialized. + m_initPromise = jsiRuntime.global().getPropertyAsFunction(jsiRuntime, "Promise").callAsConstructor + ( + jsiRuntime, + jsi::Function::createFromHostFunction(jsiRuntime, jsi::PropNameID::forAscii(jsiRuntime, "executor"), 0, [this](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t) -> jsi::Value + { + m_resolveInitPromise = [&rt, resolve{ std::make_shared(rt, args[0]) }]() + { + resolve->asObject(rt).asFunction(rt).call(rt); + }; + return {}; + }) + ); + + // Initialize Babylon Native core components + JsRuntime::CreateForJavaScript(m_env, CreateJsRuntimeDispatcher(m_env, jsiRuntime, m_jsDispatcher, m_isRunning)); + + // Initialize Babylon Native plugins + Plugins::NativeXr::Initialize(m_env); + m_nativeInput = &Babylon::Plugins::NativeInput::CreateForJavaScript(m_env); + + // Initialize Babylon Native polyfills + Polyfills::Window::Initialize(m_env); + + // NOTE: React Native's XMLHttpRequest is slow and allocates a lot of memory. This does not override + // React Native's implementation, but rather adds a second one scoped to Babylon and used by WebRequest.ts. + Polyfills::XMLHttpRequest::Initialize(m_env); + } + + ~ReactNativeModule() override + { + *m_isRunning = false; + Napi::Detach(m_env); + } + + // NOTE: This only happens when the JS engine is shutting down (other than when the app exits, this only + // happens during a dev mode reload). In this case, EngineHook.ts won't call NativeEngine.dispose, + // so we need to manually do it here to properly clean up these resources. + void Deinitialize() + { + if (m_disposeEngine) + { + m_disposeEngine(); + m_disposeEngine = {}; + } + } + + void UpdateView(void* windowPtr, size_t width, size_t height) + { + // TODO: We shouldn't have to dispatch to the JS thread most of this, but not doing so results in a crash. + // I don't understand the issue yet, but for now just retain the pre-refactor logic. We'll need to + // resolve this to enable manual non-JS thread rendering. Note this only repros in release builds + // where we actually call ResetView. + m_jsDispatcher([this, windowPtr, width, height]() + { + if (!m_graphics) + { + m_graphics = Graphics::CreateGraphics(windowPtr, width, height); + m_graphics->AddToJavaScript(m_env); + Plugins::NativeEngine::Initialize(m_env, true); + m_resolveInitPromise(); + } + else + { + m_graphics->UpdateWindow(windowPtr); + m_graphics->UpdateSize(width, height); + m_graphics->EnableRendering(); + } + }); + } + + void ResetView() + { + // TODO: We shouldn't have to dispatch to the JS thread for this since we are already on the JS thread, + // but there is an issue in NativeEngine where it will Dispatch a call to RenderCurrentFrame, then + // get disposed, then try to actually render the frame. This results in immediately re-enabling + // graphics after disabling it here. For now, retain the pre-refactor logic (queueing on the JS thread). + m_jsDispatcher([this]() + { + if (m_graphics) + { + m_graphics->DisableRendering(); + } + }); + } + + void SetPointerButtonState(uint32_t pointerId, uint32_t buttonId, bool isDown, uint32_t x, uint32_t y) + { + if (isDown) + { + m_nativeInput->PointerDown(pointerId, buttonId, x, y); + } + else + { + m_nativeInput->PointerUp(pointerId, buttonId, x, y); + } + } + + void SetPointerPosition(uint32_t pointerId, uint32_t x, uint32_t y) + { + m_nativeInput->PointerMove(pointerId, x, y); + } + + jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& prop) override + { + const auto propName{ prop.utf8(runtime) }; + + if (propName == "initializationPromise") + { + return { runtime, m_initPromise }; + } + else if (propName == "reset") + { + return jsi::Function::createFromHostFunction(runtime, prop, 0, [this](jsi::Runtime& rt, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value + { + this->ResetView(); + return {}; + }); + } + else if (propName == "setEngineInstance") + { + return jsi::Function::createFromHostFunction(runtime, prop, 0, [this](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value + { + if (count == 0 || !args[0].isObject()) + { + m_disposeEngine = {}; + } + else + { + m_disposeEngine = [&rt, engineInstanceValue{ std::make_shared(rt, args[0]) }]() + { + auto engineInstance{ engineInstanceValue->getObject(rt) }; + engineInstance.getPropertyAsFunction(rt, "dispose").callWithThis(rt, engineInstance); + }; + } + return {}; + }); + } + + return {}; + } + + private: + jsi::Value m_initPromise{}; + std::function m_resolveInitPromise{}; + + Napi::Env m_env; + Dispatcher m_jsDispatcher{}; + + std::shared_ptr m_isRunning{}; + std::unique_ptr m_graphics{}; + Plugins::NativeInput* m_nativeInput{}; + + std::function m_disposeEngine{}; + }; + + namespace + { + constexpr auto JS_INSTANCE_NAME{ "BabylonNative" }; + std::weak_ptr g_nativeModule{}; + } + + void Initialize(facebook::jsi::Runtime& jsiRuntime, Dispatcher jsDispatcher) + { + if (!jsiRuntime.global().hasProperty(jsiRuntime, JS_INSTANCE_NAME)) + { + auto nativeModule{ std::make_shared(jsiRuntime, jsDispatcher) }; + jsiRuntime.global().setProperty(jsiRuntime, JS_INSTANCE_NAME, jsi::Object::createFromHostObject(jsiRuntime, nativeModule)); + g_nativeModule = nativeModule; + } + } + + void Deinitialize() + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->Deinitialize(); + } + } + + void UpdateView(void* windowPtr, size_t width, size_t height) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->UpdateView(windowPtr, width, height); + } + else + { + throw std::runtime_error{ "UpdateView must not be called before Initialize." }; + } + } + + void SetPointerButtonState(uint32_t pointerId, uint32_t buttonId, bool isDown, uint32_t x, uint32_t y) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->SetPointerButtonState(pointerId, buttonId, isDown, x, y); + } + } + + void SetPointerPosition(uint32_t pointerId, uint32_t x, uint32_t y) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->SetPointerPosition(pointerId, x, y); + } + } +} diff --git a/Modules/@babylonjs/react-native/shared/BabylonNative.h b/Modules/@babylonjs/react-native/shared/BabylonNative.h new file mode 100644 index 000000000..4dcadfd6e --- /dev/null +++ b/Modules/@babylonjs/react-native/shared/BabylonNative.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace Babylon +{ + using Dispatcher = std::function)>; + + void Initialize(facebook::jsi::Runtime& jsiRuntime, Dispatcher jsDispatcher); + void Deinitialize(); + void UpdateView(void* windowPtr, size_t width, size_t height); + void SetPointerButtonState(uint32_t pointerId, uint32_t buttonId, bool isDown, uint32_t x, uint32_t y); + void SetPointerPosition(uint32_t pointerId, uint32_t x, uint32_t y); +} diff --git a/Modules/@babylonjs/react-native/shared/CMakeLists.txt b/Modules/@babylonjs/react-native/shared/CMakeLists.txt index ea2151292..2744bc19c 100644 --- a/Modules/@babylonjs/react-native/shared/CMakeLists.txt +++ b/Modules/@babylonjs/react-native/shared/CMakeLists.txt @@ -1,2 +1,7 @@ -add_library(BabylonReactNativeShared INTERFACE) -target_include_directories(BabylonReactNativeShared INTERFACE ".") \ No newline at end of file +set(SHARED_INCLUDES + "${CMAKE_CURRENT_LIST_DIR}") + +set(SHARED_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/DispatchFunction.h" + "${CMAKE_CURRENT_LIST_DIR}/BabylonNative.h" + "${CMAKE_CURRENT_LIST_DIR}/BabylonNative.cpp") \ No newline at end of file diff --git a/Modules/@babylonjs/react-native/shared/DispatchFunction.h b/Modules/@babylonjs/react-native/shared/DispatchFunction.h index ae05a5706..c3fb19a26 100644 --- a/Modules/@babylonjs/react-native/shared/DispatchFunction.h +++ b/Modules/@babylonjs/react-native/shared/DispatchFunction.h @@ -3,16 +3,15 @@ #include #include -#include namespace Babylon { using namespace facebook; // Creates a JsRuntime::DispatchFunctionT that integrates with the React Native execution environment. - inline JsRuntime::DispatchFunctionT CreateJsRuntimeDispatcher(Napi::Env env, jsi::Runtime& jsiRuntime, std::shared_ptr callInvoker, const bool& isShuttingDown) + inline JsRuntime::DispatchFunctionT CreateJsRuntimeDispatcher(Napi::Env env, jsi::Runtime& jsiRuntime, Dispatcher dispatcher, const std::shared_ptr isRunning) { - return [env, &jsiRuntime, callInvoker, &isShuttingDown](std::function func) + return [env, &jsiRuntime, dispatcher{ std::move(dispatcher) }, isRunning{ std::move(isRunning) }](std::function func) { // Ideally we would just use CallInvoker::invokeAsync directly, but currently it does not seem to integrate well with the React Native logbox. // To work around this, we wrap all functions in a try/catch, and when there is an exception, we do the following: @@ -23,12 +22,12 @@ namespace Babylon // 1. setImmediate queues the callback, and that queue is drained immediately following the invocation of the function passed to CallInvoker::invokeAsync. // 2. The immediates queue is drained as part of the class bridge, which knows how to display the logbox for unhandled exceptions. // In the future, CallInvoker::invokeAsync likely will properly integrate with logbox, at which point we can remove the try/catch and just call func directly. - callInvoker->invokeAsync([env, &jsiRuntime, func{std::move(func)}, &isShuttingDown] + dispatcher([env, &jsiRuntime, isRunning{ std::move(isRunning) }, func{ std::move(func) }] { try { // If JS engine shutdown is in progress, don't dispatch any new work. - if (!isShuttingDown) + if (*isRunning) { func(env); } diff --git a/Modules/@babylonjs/react-native/submodules/BabylonNative b/Modules/@babylonjs/react-native/submodules/BabylonNative index e228c93b6..4fb4abede 160000 --- a/Modules/@babylonjs/react-native/submodules/BabylonNative +++ b/Modules/@babylonjs/react-native/submodules/BabylonNative @@ -1 +1 @@ -Subproject commit e228c93b6ea21efe1588437c85d08f3050f92b40 +Subproject commit 4fb4abede658640965ff61c84cdbee098cdd0089 diff --git a/Package/gulpfile.js b/Package/gulpfile.js index 4cd308add..3eb917a27 100644 --- a/Package/gulpfile.js +++ b/Package/gulpfile.js @@ -48,6 +48,11 @@ const copyCommonFiles = () => { .pipe(gulp.dest('Assembled')); }; +const copySharedFiles = () => { + return gulp.src('../Apps/Playground/node_modules/@babylonjs/react-native/shared/BabylonNative.h') + .pipe(gulp.dest('Assembled/shared')); +}; + const copyIOSFiles = () => { return gulp.src('../Apps/Playground/node_modules/@babylonjs/react-native/ios/*.h') .pipe(gulp.src('../Apps/Playground/node_modules/@babylonjs/react-native/ios/*.mm')) @@ -84,6 +89,8 @@ const validate = async () => { const expected = `Assembled Assembled/EngineHook.ts +Assembled/shared +Assembled/shared/BabylonNative.h Assembled/EngineView.tsx Assembled/ios Assembled/ios/BabylonNativeInterop.mm @@ -122,9 +129,7 @@ Assembled/ios/ReactNativeBabylon.xcodeproj/project.xcworkspace Assembled/ios/ReactNativeBabylon.xcodeproj/project.xcworkspace/xcshareddata Assembled/ios/ReactNativeBabylon.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings Assembled/ios/BabylonModule.mm -Assembled/ios/BabylonNative.h Assembled/README.md -Assembled/EngineHelpers.ts Assembled/package.json Assembled/android Assembled/android/build.gradle @@ -153,6 +158,7 @@ Assembled/android/src/main/jniLibs/arm64-v8a/libBabylonNative.so Assembled/react-native-babylon.podspec Assembled/index.ts Assembled/BabylonModule.ts +Assembled/ReactNativeEngine.ts `; const result = shelljs.exec('find Assembled', {silent: true}); @@ -165,7 +171,7 @@ const createPackage = async () => { exec('npm pack', 'Assembled'); }; -const copyFiles = gulp.parallel(copyCommonFiles, copyIOSFiles, copyAndroidFiles); +const copyFiles = gulp.parallel(copyCommonFiles, copySharedFiles, copyIOSFiles, copyAndroidFiles); const build = gulp.series(buildIOS, buildAndroid, createIOSUniversalLibs, copyFiles, validate); const rebuild = gulp.series(clean, build);