Skip to content

Commit 91cdda2

Browse files
authored
Converge react native integration layers across platforms (#135)
1 parent c2f3d78 commit 91cdda2

24 files changed

+583
-706
lines changed
Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,24 @@
11
import { NativeModules } from 'react-native';
2-
import { NativeEngine } from '@babylonjs/core';
32

4-
// This global object is part of Babylon Native.
5-
declare const _native: {
6-
whenGraphicsReady: () => Promise<void>;
7-
engineInstance: NativeEngine;
8-
}
3+
declare const global: {
4+
nativeCallSyncHook: any;
5+
};
6+
const isRemoteDebuggingEnabled = !global.nativeCallSyncHook;
97

10-
const NativeBabylonModule: {
11-
initialize(): Promise<boolean>;
12-
whenInitialized(): Promise<boolean>;
13-
reset(): Promise<boolean>;
8+
// This legacy React Native module is created by Babylon React Native, and is only used to bootstrap the JSI object creation.
9+
// This will likely be removed when the BabylonNative global object is eventually converted to a TurboModule.
10+
const BabylonModule: {
11+
initialize(): Promise<void>;
1412
} = NativeModules.BabylonModule;
1513

16-
export const BabylonModule = {
17-
initialize: async () => {
18-
const initialized = await NativeBabylonModule.initialize();
19-
if (initialized) {
20-
await _native.whenGraphicsReady();
21-
}
22-
return initialized;
23-
},
24-
25-
whenInitialized: NativeBabylonModule.whenInitialized,
26-
reset: NativeBabylonModule.reset,
27-
28-
createEngine: () => {
29-
const engine = new NativeEngine();
30-
_native.engineInstance = engine;
31-
return engine;
14+
export async function ensureInitialized(): Promise<boolean> {
15+
if (isRemoteDebuggingEnabled) {
16+
// When remote debugging is enabled, JavaScript runs on the debugging host machine, not on the device where the app is running.
17+
// 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.
18+
return false;
19+
} else {
20+
// This does the first stage of Babylon Native initialization, including creating the BabylonNative JSI object.
21+
await BabylonModule.initialize();
22+
return true;
3223
}
33-
};
24+
}

Modules/@babylonjs/react-native/EngineHelpers.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

Modules/@babylonjs/react-native/EngineHook.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { useEffect, useState } from 'react';
22
import { Platform } from 'react-native';
33
import { PERMISSIONS, check, request } from 'react-native-permissions';
4-
import { Engine, NativeEngine, WebXRSessionManager } from '@babylonjs/core';
5-
import { BabylonModule } from './BabylonModule';
6-
import { DisposeEngine } from './EngineHelpers';
4+
import { Engine, WebXRSessionManager } from '@babylonjs/core';
5+
import { ReactNativeEngine } from './ReactNativeEngine';
76
import * as base64 from 'base-64';
87

98
// 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 {
6665
const [engine, setEngine] = useState<Engine>();
6766

6867
useEffect(() => {
69-
let disposed = false;
70-
let engine: Engine | undefined = undefined;
68+
const abortController = new AbortController();
69+
let engine: ReactNativeEngine | undefined = undefined;
7170

7271
(async () => {
73-
if (await BabylonModule.initialize() && !disposed)
74-
{
75-
engine = BabylonModule.createEngine();
76-
setEngine(engine);
77-
}
72+
setEngine(engine = await ReactNativeEngine.tryCreateAsync(abortController.signal) ?? undefined);
7873
})();
7974

8075
return () => {
81-
disposed = true;
76+
abortController.abort();
8277
// 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.
83-
if (engine) {
84-
DisposeEngine(engine);
85-
}
86-
// 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
87-
// 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).
88-
// TODO: https://github.com/BabylonJS/BabylonReactNative/issues/125
89-
if (!__DEV__) {
90-
BabylonModule.reset();
91-
}
78+
engine?.dispose();
9279
setEngine(undefined);
9380
};
9481
}, []);

Modules/@babylonjs/react-native/EngineView.tsx

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,10 @@
11
import React, { Component, FunctionComponent, SyntheticEvent, useCallback, useEffect, useState, useRef } from 'react';
2-
import { requireNativeComponent, NativeModules, ViewProps, AppState, AppStateStatus, View, Text, findNodeHandle, UIManager } from 'react-native';
2+
import { requireNativeComponent, ViewProps, AppState, AppStateStatus, View, Text, findNodeHandle, UIManager } from 'react-native';
33
import { Camera } from '@babylonjs/core';
4-
import { IsEngineDisposed } from './EngineHelpers';
5-
import { BabylonModule } from './BabylonModule';
4+
import { ensureInitialized } from './BabylonModule';
5+
import { ReactNativeEngine } from './ReactNativeEngine';
66

77
declare const global: any;
8-
const isRemoteDebuggingEnabled = !global['nativeCallSyncHook'];
9-
10-
const EngineViewManager: {
11-
setJSThread(): void;
12-
} = NativeModules.EngineViewManager;
13-
14-
// 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.
15-
if (EngineViewManager && EngineViewManager.setJSThread && !isRemoteDebuggingEnabled) {
16-
EngineViewManager.setJSThread();
17-
}
188

199
interface NativeEngineViewProps extends ViewProps {
2010
onSnapshotDataReturned: (event: SyntheticEvent) => void;
@@ -23,7 +13,7 @@ interface NativeEngineViewProps extends ViewProps {
2313
const NativeEngineView: {
2414
prototype: Component<NativeEngineViewProps>;
2515
new(props: Readonly<NativeEngineViewProps>): Component<NativeEngineViewProps>;
26-
} = requireNativeComponent('EngineView');
16+
} = global['EngineView'] || (global['EngineView'] = requireNativeComponent('EngineView'));
2717

2818
export interface EngineViewProps extends ViewProps {
2919
camera?: Camera;
@@ -36,17 +26,15 @@ export interface EngineViewCallbacks {
3626
}
3727

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

4535
useEffect(() => {
4636
(async () => {
47-
if (!await BabylonModule.whenInitialized()) {
48-
setFailedInitialization(true);
49-
}
37+
setInitialized(await ensureInitialized());
5038
})();
5139
}, []);
5240

@@ -64,17 +52,17 @@ export const EngineView: FunctionComponent<EngineViewProps> = (props: EngineView
6452

6553
useEffect(() => {
6654
if (props.camera && appState === "active") {
67-
const engine = props.camera.getScene().getEngine();
55+
const engine = props.camera.getScene().getEngine() as ReactNativeEngine;
6856

69-
if (!IsEngineDisposed(engine)) {
57+
if (!engine.isDisposed) {
7058
engine.runRenderLoop(() => {
7159
for (let scene of engine.scenes) {
7260
scene.render();
7361
}
7462
});
7563

7664
return () => {
77-
if (!IsEngineDisposed(engine)) {
65+
if (!engine.isDisposed) {
7866
engine.stopRenderLoop();
7967
}
8068
};
@@ -86,9 +74,9 @@ export const EngineView: FunctionComponent<EngineViewProps> = (props: EngineView
8674

8775
useEffect(() => {
8876
if (props.camera && (props.displayFrameRate ?? __DEV__)) {
89-
const engine = props.camera.getScene().getEngine();
77+
const engine = props.camera.getScene().getEngine() as ReactNativeEngine;
9078

91-
if (!IsEngineDisposed(engine)) {
79+
if (!engine.isDisposed) {
9280
setFps(engine.getFps());
9381
const timerHandle = setInterval(() => {
9482
setFps(engine.getFps());
@@ -145,11 +133,11 @@ export const EngineView: FunctionComponent<EngineViewProps> = (props: EngineView
145133
}
146134
}, []);
147135

148-
if (!failedInitialization) {
136+
if (initialized !== false) {
149137
return (
150138
<View style={[props.style, { overflow: "hidden" }]}>
151-
<NativeEngineView ref={engineViewRef} style={{ flex: 1 }} onSnapshotDataReturned={snapshotDataReturnedHandler} />
152-
{ fps && <Text style={{ color: 'yellow', position: 'absolute', margin: 10, right: 0, top: 0 }}>FPS: {Math.round(fps)}</Text>}
139+
{ initialized && <NativeEngineView ref={engineViewRef} style={{ flex: 1 }} onSnapshotDataReturned={snapshotDataReturnedHandler} /> }
140+
{ fps && <Text style={{ color: 'yellow', position: 'absolute', margin: 10, right: 0, top: 0 }}>FPS: {Math.round(fps)}</Text> }
153141
</View>
154142
);
155143
} else {
@@ -161,7 +149,7 @@ export const EngineView: FunctionComponent<EngineViewProps> = (props: EngineView
161149
return (
162150
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
163151
<Text style={{ fontSize: 24 }}>{message}</Text>
164-
{ isRemoteDebuggingEnabled && <Text style={{ fontSize: 12 }}>React Native remote debugging does not work with Babylon Native.</Text>}
152+
<Text style={{ fontSize: 12 }}>React Native remote debugging does not work with Babylon Native.</Text>
165153
</View>
166154
);
167155
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { ensureInitialized } from './BabylonModule';
2+
import { NativeEngine } from '@babylonjs/core';
3+
4+
// This global object is owned by Babylon Native.
5+
declare const _native: {
6+
whenGraphicsReady: () => Promise<void>;
7+
};
8+
9+
// This JSI-based global object is owned by Babylon React Native.
10+
// This will likely be converted to a TurboModule when they are fully supported.
11+
declare const BabylonNative: {
12+
readonly initializationPromise: Promise<void>;
13+
setEngineInstance: (engine: NativeEngine | null) => void;
14+
reset: () => void;
15+
};
16+
17+
export class ReactNativeEngine extends NativeEngine {
18+
private _isDisposed = false;
19+
20+
private constructor() {
21+
super();
22+
BabylonNative.setEngineInstance(this);
23+
}
24+
25+
public static async tryCreateAsync(abortSignal: AbortSignal): Promise<ReactNativeEngine | null> {
26+
if (!await ensureInitialized() || abortSignal.aborted) {
27+
return null;
28+
}
29+
30+
// This waits Graphics/NativeEngine to be created (which in turn makes the whenGraphicsReady available).
31+
await BabylonNative.initializationPromise;
32+
33+
// Check for cancellation.
34+
if (abortSignal.aborted) {
35+
return null;
36+
}
37+
38+
// This waits for the Graphics system to be up and running.
39+
await _native.whenGraphicsReady();
40+
41+
// Check for cancellation.
42+
if (abortSignal.aborted) {
43+
return null;
44+
}
45+
46+
return new ReactNativeEngine();
47+
}
48+
49+
public get isDisposed() {
50+
return this._isDisposed;
51+
}
52+
53+
public dispose(): void {
54+
if (!this.isDisposed) {
55+
super.dispose();
56+
57+
// 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
58+
// 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).
59+
// TODO: https://github.com/BabylonJS/BabylonReactNative/issues/125
60+
if (!__DEV__) {
61+
BabylonNative.reset();
62+
}
63+
64+
this._isDisposed = true;
65+
}
66+
67+
BabylonNative.setEngineInstance(null);
68+
}
69+
}

Modules/@babylonjs/react-native/android/CMakeLists.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ cmake_minimum_required(VERSION 3.13.2)
1919
# detection.
2020
# [24, infinite) ES2 & ES3 & Vulkan
2121
project(ReactNativeBabylon)
22+
include(${CMAKE_CURRENT_LIST_DIR}/../shared/CMakeLists.txt)
23+
2224
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall")
2325
set(BABYLON_NATIVE_PLATFORM "Android")
2426
set(CMAKE_CXX_EXTENSIONS OFF)
@@ -55,7 +57,10 @@ target_link_libraries(turbomodulejsijni
5557
fbjni)
5658

5759
add_library(BabylonNative SHARED
58-
src/main/cpp/BabylonNativeInterop.cpp)
60+
src/main/cpp/BabylonNativeInterop.cpp
61+
${SHARED_SOURCES})
62+
63+
target_include_directories(BabylonNative PRIVATE ${SHARED_INCLUDES})
5964

6065
target_link_libraries(BabylonNative
6166
GLESv3
@@ -67,7 +72,6 @@ target_link_libraries(BabylonNative
6772
fbjni
6873
jsi
6974
turbomodulejsijni
70-
BabylonReactNativeShared
7175
AndroidExtensions
7276
Graphics
7377
JsRuntime

0 commit comments

Comments
 (0)