Skip to content

Commit a8cf342

Browse files
authored
Get dev mode reload working (#112)
This change includes a bunch of fixes for React Native dev mode reload (which reloads the JS engine). - Update to the latest BabylonNative submodule which includes a fix to actually delete the napi instance on Napi::Detach. - Set the current NativeEngine instance on the _native object so we can get it on the native side and manually dispose it (since during a reload the EngineHook's cleanup function will not be called, but we still need to release these resources). - Have the native interop code grab the current NativeEngine instance off of _native during teardown and call dispose on it. - Prevent the JS dispatcher from actually doing anything if we are in the process of shutting down the JS engine. These changes are a little icky and there is a fair bit of duplication across Android and iOS. I have a bigger cleanup planned for this code that will consolidate a lot of the Android and iOS code, but that will happen later. Fixes #84
1 parent c9f2935 commit a8cf342

File tree

9 files changed

+99
-31
lines changed

9 files changed

+99
-31
lines changed
Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
import { NativeModules } from 'react-native';
2+
import { NativeEngine } from '@babylonjs/core';
23

3-
export const BabylonModule: {
4+
// This global object is part of Babylon Native.
5+
declare const _native: {
6+
graphicsInitializationPromise: Promise<void>;
7+
engineInstance: NativeEngine;
8+
}
9+
10+
const NativeBabylonModule: {
411
initialize(): Promise<boolean>;
512
whenInitialized(): Promise<boolean>;
6-
} = NativeModules.BabylonModule;
13+
} = NativeModules.BabylonModule;
14+
15+
export const BabylonModule = {
16+
initialize: async () => {
17+
const initialized = await NativeBabylonModule.initialize();
18+
if (initialized) {
19+
await _native.graphicsInitializationPromise;
20+
}
21+
return initialized;
22+
},
23+
24+
whenInitialized: NativeBabylonModule.whenInitialized,
25+
26+
createEngine: () => {
27+
const engine = new NativeEngine();
28+
_native.engineInstance = engine;
29+
return engine;
30+
}
31+
};

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,6 @@ class DOMException {
6262
declare const global: any;
6363
global.atob = base64.decode;
6464

65-
// This global object is part of Babylon Native.
66-
declare const _native: {
67-
graphicsInitializationPromise: Promise<void>;
68-
}
69-
7065
export function useEngine(): Engine | undefined {
7166
const [engine, setEngine] = useState<Engine>();
7267

@@ -77,8 +72,7 @@ export function useEngine(): Engine | undefined {
7772
(async () => {
7873
if (await BabylonModule.initialize() && !disposed)
7974
{
80-
await _native.graphicsInitializationPromise;
81-
engine = new NativeEngine();
75+
engine = BabylonModule.createEngine();
8276
setEngine(engine);
8377
}
8478
})();

Modules/@babylonjs/react-native/android/src/main/cpp/BabylonNativeInterop.cpp

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ namespace Babylon
3333
{
3434
__android_log_print(ANDROID_LOG_VERBOSE, "BabylonNative", "%s", str);
3535
}
36+
37+
bool isShuttingDown{false};
3638
}
3739

3840
class Native final
@@ -42,7 +44,9 @@ namespace Babylon
4244
Native(jsi::Runtime& jsiRuntime, std::shared_ptr<react::CallInvoker> callInvoker, ANativeWindow* windowPtr)
4345
: m_env{ Napi::Attach<jsi::Runtime&>(jsiRuntime) }
4446
{
45-
m_runtime = &JsRuntime::CreateForJavaScript(m_env, CreateJsRuntimeDispatcher(m_env, jsiRuntime, callInvoker));
47+
isShuttingDown = false;
48+
49+
m_runtime = &JsRuntime::CreateForJavaScript(m_env, CreateJsRuntimeDispatcher(m_env, jsiRuntime, std::move(callInvoker), isShuttingDown));
4650

4751
auto width = static_cast<size_t>(ANativeWindow_getWidth(windowPtr));
4852
auto height = static_cast<size_t>(ANativeWindow_getHeight(windowPtr));
@@ -61,10 +65,18 @@ namespace Babylon
6165
m_nativeInput = &Babylon::Plugins::NativeInput::CreateForJavaScript(m_env);
6266
}
6367

68+
// NOTE: This only happens when the JS engine is shutting down (other than when the app exits, this only
69+
// happens during a dev mode reload). In this case, EngineHook.ts won't call NativeEngine.dispose,
70+
// so we need to manually do it here to properly clean up these resources.
6471
~Native()
6572
{
66-
// TODO: Figure out why this causes the app to crash
67-
//Napi::Detach(m_env);
73+
auto native = JsRuntime::NativeObject::GetFromJavaScript(m_env);
74+
auto engine = native.Get("engineInstance").As<Napi::Object>();
75+
auto dispose = engine.Get("dispose").As<Napi::Function>();
76+
dispose.Call(engine, {});
77+
isShuttingDown = true;
78+
79+
Napi::Detach(m_env);
6880
}
6981

7082
void Refresh(ANativeWindow* windowPtr)

Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonModule.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ public String getName() {
2222
return "BabylonModule";
2323
}
2424

25+
// NOTE: This happens during dev mode reload, when the JS engine is being shutdown and restarted.
2526
@Override
2627
public void onCatalystInstanceDestroy() {
27-
new Handler(Looper.getMainLooper()).post(BabylonNativeInterop::deinitialize);
28+
this.getReactApplicationContext().runOnJSQueueThread(BabylonNativeInterop::deinitialize);
2829
}
2930

3031
@ReactMethod

Modules/@babylonjs/react-native/android/src/main/java/com/babylonreactnative/BabylonNativeInterop.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ static CompletionStage<Long> whenInitialized(ReactContext reactContext) {
132132
return BabylonNativeInterop.getOrCreateFuture(reactContext);
133133
}
134134

135-
// Must be called from the Android UI thread
135+
// Must be called from the JavaScript thread
136136
static void deinitialize() {
137137
BabylonNativeInterop.destroyOldNativeInstances(null);
138138
}

Modules/@babylonjs/react-native/ios/BabylonNative.cpp

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ namespace Babylon
2424
{
2525
using namespace facebook;
2626

27+
namespace
28+
{
29+
bool isShuttingDown{false};
30+
}
31+
2732
class Native::Impl
2833
{
2934
public:
@@ -32,24 +37,28 @@ namespace Babylon
3237
, jsCallInvoker{ callInvoker }
3338
{
3439
}
40+
41+
~Impl()
42+
{
43+
Napi::Detach(env);
44+
}
3545

3646
Napi::Env env;
3747
std::shared_ptr<facebook::react::CallInvoker> jsCallInvoker;
38-
std::unique_ptr<Graphics> m_graphics{};
48+
std::unique_ptr<Graphics> graphics{};
3949
JsRuntime* runtime{};
4050
Plugins::NativeInput* nativeInput{};
4151
};
4252

4353
Native::Native(facebook::jsi::Runtime& jsiRuntime, std::shared_ptr<facebook::react::CallInvoker> callInvoker, void* windowPtr, size_t width, size_t height)
4454
: m_impl{ std::make_unique<Native::Impl>(jsiRuntime, callInvoker) }
4555
{
46-
dispatch_sync(dispatch_get_main_queue(), ^{
47-
m_impl->m_graphics = Graphics::CreateGraphics(reinterpret_cast<void*>(windowPtr), width, height);
48-
});
56+
isShuttingDown = false;
57+
m_impl->graphics = Graphics::CreateGraphics(reinterpret_cast<void*>(windowPtr), width, height);
4958

50-
m_impl->runtime = &JsRuntime::CreateForJavaScript(m_impl->env, CreateJsRuntimeDispatcher(m_impl->env, jsiRuntime, callInvoker));
51-
52-
m_impl->m_graphics->AddToJavaScript(m_impl->env);
59+
m_impl->runtime = &JsRuntime::CreateForJavaScript(m_impl->env, CreateJsRuntimeDispatcher(m_impl->env, jsiRuntime, std::move(callInvoker), isShuttingDown));
60+
61+
m_impl->graphics->AddToJavaScript(m_impl->env);
5362

5463
Polyfills::Window::Initialize(m_impl->env);
5564
// NOTE: React Native's XMLHttpRequest is slow and allocates a lot of memory. This does not override
@@ -62,19 +71,27 @@ namespace Babylon
6271
m_impl->nativeInput = &Babylon::Plugins::NativeInput::CreateForJavaScript(m_impl->env);
6372
}
6473

74+
// NOTE: This only happens when the JS engine is shutting down (other than when the app exits, this only
75+
// happens during a dev mode reload). In this case, EngineHook.ts won't call NativeEngine.dispose,
76+
// so we need to manually do it here to properly clean up these resources.
6577
Native::~Native()
6678
{
79+
auto native = JsRuntime::NativeObject::GetFromJavaScript(m_impl->env);
80+
auto engine = native.Get("engineInstance").As<Napi::Object>();
81+
auto dispose = engine.Get("dispose").As<Napi::Function>();
82+
dispose.Call(engine, {});
83+
isShuttingDown = true;
6784
}
6885

6986
void Native::Refresh(void* windowPtr, size_t width, size_t height)
7087
{
71-
m_impl->m_graphics->UpdateWindow<void*>(windowPtr);
72-
m_impl->m_graphics->UpdateSize(width, height);
88+
m_impl->graphics->UpdateWindow<void*>(windowPtr);
89+
m_impl->graphics->UpdateSize(width, height);
7390
}
7491

7592
void Native::Resize(size_t width, size_t height)
7693
{
77-
m_impl->m_graphics->UpdateSize(width, height);
94+
m_impl->graphics->UpdateSize(width, height);
7895
}
7996

8097
void Native::SetPointerButtonState(uint32_t pointerId, uint32_t buttonId, bool isDown, uint32_t x, uint32_t y)

Modules/@babylonjs/react-native/ios/BabylonNativeInterop.mm

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,16 @@ + (void)setCurrentNativeInstance:(RCTBridge*)bridge mtkView:(MTKView*)mtkView wi
126126
{
127127
const std::lock_guard<std::mutex> lock(mapMutex);
128128

129-
currentBridge = bridge;
129+
if (bridge != currentBridge) {
130+
if (currentBridge == nil || currentBridge.parentBridge != bridge.parentBridge) {
131+
[[NSNotificationCenter defaultCenter] addObserver:self
132+
selector:@selector(onBridgeWillInvalidate:)
133+
name:RCTBridgeWillInvalidateModulesNotification
134+
object:bridge.parentBridge];
135+
}
136+
137+
currentBridge = bridge;
138+
}
130139

131140
currentNativeInstance.reset();
132141

@@ -146,4 +155,10 @@ + (void)setCurrentNativeInstance:(RCTBridge*)bridge mtkView:(MTKView*)mtkView wi
146155
}
147156
}
148157

158+
// NOTE: This happens during dev mode reload, when the JS engine is being shutdown and restarted.
159+
+ (void)onBridgeWillInvalidate:(NSNotification*)notification
160+
{
161+
currentNativeInstance.reset();
162+
}
163+
149164
@end

Modules/@babylonjs/react-native/shared/DispatchFunction.h

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ namespace Babylon
1010
using namespace facebook;
1111

1212
// Creates a JsRuntime::DispatchFunctionT that integrates with the React Native execution environment.
13-
inline JsRuntime::DispatchFunctionT CreateJsRuntimeDispatcher(Napi::Env env, jsi::Runtime& jsiRuntime, std::shared_ptr<react::CallInvoker> callInvoker)
13+
inline JsRuntime::DispatchFunctionT CreateJsRuntimeDispatcher(Napi::Env env, jsi::Runtime& jsiRuntime, std::shared_ptr<react::CallInvoker> callInvoker, const bool& isShuttingDown)
1414
{
15-
return [env, &jsiRuntime, callInvoker](std::function<void(Napi::Env)> func)
15+
return [env, &jsiRuntime, callInvoker, &isShuttingDown](std::function<void(Napi::Env)> func)
1616
{
1717
// Ideally we would just use CallInvoker::invokeAsync directly, but currently it does not seem to integrate well with the React Native logbox.
1818
// To work around this, we wrap all functions in a try/catch, and when there is an exception, we do the following:
@@ -23,11 +23,15 @@ namespace Babylon
2323
// 1. setImmediate queues the callback, and that queue is drained immediately following the invocation of the function passed to CallInvoker::invokeAsync.
2424
// 2. The immediates queue is drained as part of the class bridge, which knows how to display the logbox for unhandled exceptions.
2525
// 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.
26-
callInvoker->invokeAsync([env, &jsiRuntime, func{std::move(func)}]
26+
callInvoker->invokeAsync([env, &jsiRuntime, func{std::move(func)}, &isShuttingDown]
2727
{
2828
try
2929
{
30-
func(env);
30+
// If JS engine shutdown is in progress, don't dispatch any new work.
31+
if (!isShuttingDown)
32+
{
33+
func(env);
34+
}
3135
}
3236
catch (...)
3337
{
@@ -43,4 +47,4 @@ namespace Babylon
4347
});
4448
};
4549
}
46-
}
50+
}

0 commit comments

Comments
 (0)