Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3b28873
First pass, iOS only
ryantrem Dec 16, 2020
0d33202
Don't even initialize Babylon Native if we have remote debugging enabled
ryantrem Dec 16, 2020
722e908
First pass cleanup
ryantrem Dec 16, 2020
06f26e8
JS cleanup
ryantrem Dec 17, 2020
8c9f977
Remove comments and logs
ryantrem Dec 17, 2020
fd28325
Remove dead code
ryantrem Dec 17, 2020
f65336c
Convert BabylonReactNative shared lib to just a shared CMakeLists.txt…
ryantrem Dec 17, 2020
209f2d3
Move shared code
ryantrem Dec 17, 2020
2d54fdc
Update Android to use shared code
ryantrem Dec 18, 2020
6690729
Cleanup and queue updateView on JS thread
ryantrem Dec 18, 2020
f56d5e9
Fix crash when toggling engine screen off from XR and then toggling e…
ryantrem Dec 18, 2020
1701e2d
Ensure UpdateView is called after Initialize (from the JS side)
ryantrem Dec 18, 2020
2aeb5d2
Minor cleanup
ryantrem Dec 18, 2020
17111f2
Fix package build
ryantrem Dec 18, 2020
9f34496
Remove unused decl of jsCallInvoker
ryantrem Dec 18, 2020
e13f54c
Remove old setJSThread stuff from EngineView.tsx
ryantrem Dec 19, 2020
895e0c9
std::move jsCallInvoker in dispatcher
ryantrem Dec 19, 2020
6031d69
Remove use of getActionButton, which is only available in API levle 2…
ryantrem Dec 19, 2020
d34d0db
Add ReactNativeEngine
ryantrem Dec 21, 2020
bbe9e60
Address feedback on C++ side
ryantrem Dec 22, 2020
9a8e53b
Move ReactNativeEngine to its own file and clean it up more
ryantrem Dec 22, 2020
cc32e2d
Add ReactNativeEngine to package
ryantrem Dec 22, 2020
8c9848c
Switch from CallInvoker to a Dispatcher function since on UWP CallInv…
ryantrem Dec 22, 2020
9d351b7
Fix bad import
ryantrem Dec 22, 2020
100f491
Merge master
ryantrem Jan 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 18 additions & 27 deletions Modules/@babylonjs/react-native/BabylonModule.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
engineInstance: NativeEngine;
}
declare const global: {
nativeCallSyncHook: any;
};
const isRemoteDebuggingEnabled = !global.nativeCallSyncHook;

const NativeBabylonModule: {
initialize(): Promise<boolean>;
whenInitialized(): Promise<boolean>;
reset(): Promise<boolean>;
// 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<void>;
} = 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<boolean> {
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;
}
};
}
14 changes: 0 additions & 14 deletions Modules/@babylonjs/react-native/EngineHelpers.ts

This file was deleted.

27 changes: 7 additions & 20 deletions Modules/@babylonjs/react-native/EngineHook.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -66,29 +65,17 @@ export function useEngine(): Engine | undefined {
const [engine, setEngine] = useState<Engine>();

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);
};
}, []);
Expand Down
42 changes: 15 additions & 27 deletions Modules/@babylonjs/react-native/EngineView.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,7 +13,7 @@ interface NativeEngineViewProps extends ViewProps {
const NativeEngineView: {
prototype: Component<NativeEngineViewProps>;
new(props: Readonly<NativeEngineViewProps>): Component<NativeEngineViewProps>;
} = requireNativeComponent('EngineView');
} = global['EngineView'] || (global['EngineView'] = requireNativeComponent('EngineView'));

export interface EngineViewProps extends ViewProps {
camera?: Camera;
Expand All @@ -36,17 +26,15 @@ export interface EngineViewCallbacks {
}

export const EngineView: FunctionComponent<EngineViewProps> = (props: EngineViewProps) => {
const [failedInitialization, setFailedInitialization] = useState(false);
const [initialized, setInitialized] = useState<boolean>();
const [appState, setAppState] = useState(AppState.currentState);
const [fps, setFps] = useState<number>();
const engineViewRef = useRef<Component<NativeEngineViewProps>>(null);
const snapshotPromise = useRef<{ promise: Promise<string>, resolve: (data: string) => void }>();

useEffect(() => {
(async () => {
if (!await BabylonModule.whenInitialized()) {
setFailedInitialization(true);
}
setInitialized(await ensureInitialized());
})();
}, []);

Expand All @@ -64,17 +52,17 @@ export const EngineView: FunctionComponent<EngineViewProps> = (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();
}
});

return () => {
if (!IsEngineDisposed(engine)) {
if (!engine.isDisposed) {
engine.stopRenderLoop();
}
};
Expand All @@ -86,9 +74,9 @@ export const EngineView: FunctionComponent<EngineViewProps> = (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());
Expand Down Expand Up @@ -145,11 +133,11 @@ export const EngineView: FunctionComponent<EngineViewProps> = (props: EngineView
}
}, []);

if (!failedInitialization) {
if (initialized !== false) {
return (
<View style={[props.style, { overflow: "hidden" }]}>
<NativeEngineView ref={engineViewRef} style={{ flex: 1 }} onSnapshotDataReturned={snapshotDataReturnedHandler} />
{ fps && <Text style={{ color: 'yellow', position: 'absolute', margin: 10, right: 0, top: 0 }}>FPS: {Math.round(fps)}</Text>}
{ initialized && <NativeEngineView ref={engineViewRef} style={{ flex: 1 }} onSnapshotDataReturned={snapshotDataReturnedHandler} /> }
{ fps && <Text style={{ color: 'yellow', position: 'absolute', margin: 10, right: 0, top: 0 }}>FPS: {Math.round(fps)}</Text> }
</View>
);
} else {
Expand All @@ -161,7 +149,7 @@ export const EngineView: FunctionComponent<EngineViewProps> = (props: EngineView
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 24 }}>{message}</Text>
{ isRemoteDebuggingEnabled && <Text style={{ fontSize: 12 }}>React Native remote debugging does not work with Babylon Native.</Text>}
<Text style={{ fontSize: 12 }}>React Native remote debugging does not work with Babylon Native.</Text>
</View>
);
}
Expand Down
69 changes: 69 additions & 0 deletions Modules/@babylonjs/react-native/ReactNativeEngine.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
};

// 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<void>;
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<ReactNativeEngine | null> {
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);
}
}
8 changes: 6 additions & 2 deletions Modules/@babylonjs/react-native/android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -67,7 +72,6 @@ target_link_libraries(BabylonNative
fbjni
jsi
turbomodulejsijni
BabylonReactNativeShared
AndroidExtensions
Graphics
JsRuntime
Expand Down
Loading