diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..704f984 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + root: true, + extends: "@react-native", + overrides: [ + { + files: ['app.plugin.js'], + env: {node: true}, + parserOptions: {requireConfigFile: false}, + }, + ], +}; diff --git a/.gitignore b/.gitignore index 8abde9c..5d5c943 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /lib .yarn/ +.expo/ .gradle/ .cxx/ .DS_Store diff --git a/.tool-versions b/.tool-versions index 6a829fb..63aface 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ ruby 3.2.0 -nodejs 22.11.0 +nodejs 22.13.0 java oracle-21.0.3 diff --git a/README.md b/README.md index 15c8e39..13f8cd4 100644 --- a/README.md +++ b/README.md @@ -112,12 +112,36 @@ Born React Native Godot is distributed on npm. Just follow these steps to add it to your React Native application: +## Compatibility + +The current example is aligned with Expo SDK 57, React Native 0.86 and React 19.2. Node.js follows React Native's supported ranges (`^20.19.4`, `^22.13.0`, `^24.3.0` or `>=25`); this repository uses Node.js 22.13 as its development baseline. The example uses the New Architecture, Hermes and `react-native-worklets` 0.10, which is the Worklets version selected by Expo SDK 57. + +Expo monorepos are supported through Expo's automatic Metro configuration. Keep the example/app package inside the workspace list and base its Metro configuration on `expo/metro-config`; custom `watchFolders`, `resolver.nodeModulesPaths` and `disableHierarchicalLookup` settings are not required on Expo SDK 52 or newer. In this repository the library itself is the workspace root, so the example links it with Yarn's `portal:../` protocol. + +The Android integration requires Android 10 (API 29) because window embedding uses `SurfaceControl`. The bundled Godot binaries currently support `armeabi-v7a` and `arm64-v8a`; the example restricts `reactNativeArchitectures` to those ABIs so its APK never advertises an x86 variant that cannot load Godot. + ## Update `package.json` ```sh -yarn add @borndotcom/react-native-godot +npx expo install @borndotcom/react-native-godot react-native-worklets +``` + +Expo's install command adds the package config plugin automatically. The plugin keeps clean prebuilds on Android API 29 and the supported ARM ABIs without an `expo-build-properties` entry. To bundle Godot packs on iOS, pass their filenames to that plugin as shown in the example app's `app.json`. + +If the dependency was added manually (for example through a Git URL or a workspace), register the package plugin explicitly: + +```typescript +export default { + expo: { + plugins: ['@borndotcom/react-native-godot'], + }, +}; ``` +This is the only Android configuration the package needs. Do not duplicate its minimum SDK or add a relative LibGodot Maven path with `expo-build-properties`. + +For a React Native Community CLI project, use your package manager instead and ensure the Worklets Babel plugin is enabled according to the `react-native-worklets` installation guide. + ## Download the prebuilt LibGodot packages The LibGodot packages used by React Native Godot are not distributed on npm. Instead, they are downloaded separately by issuing the following command: @@ -128,6 +152,10 @@ yarn download-prebuilt This way React Native Godot can be updated independently from LibGodot, and also local, customized builds of LibGodot are supported. +On Expo, the package config plugin derives the downloaded LibGodot Maven repository from its installed location and writes `android.extraMavenRepos` automatically. Do not add that property or an `expo-build-properties` entry manually. The generated path works with hoisted dependencies, workspaces and regular `node_modules` layouts; React Native Community CLI builds retain the native Gradle fallback. + +The bundled Android LibGodot artifacts currently contain `arm64-v8a` and `armeabi-v7a`. The Gradle module automatically limits its native build to those ABIs even when Expo requests x86 variants for other dependencies. Running the Godot view itself therefore requires an ARM Android device or ARM emulator. + ## Import React Native Godot in your App code ```typescript @@ -372,7 +400,7 @@ iface.test_callable(function(s: string) { In a React Native app, the main JavaScript thread, where the bulk of the JavaScript code of the application runs is separate from the Android or iOS apps's main thread. -This way the JS Thread's processing does not affect the main application UI. Following the same pattern, the Godot Engine is also running on its own thread that is separate from both the application's and React Native's main JavaScript thread. As JavaScript is single-threaded by design, to be able to communicate with the Godot thread from JavaScript, we use the well-known [react-native-worklets-core](https://github.com/margelo/react-native-worklets-core) library, which allows us running JS code in the Godot thread using worklets. +This way the JS Thread's processing does not affect the main application UI. Following the same pattern, the Godot Engine is also running on its own thread that is separate from both the application's and React Native's main JavaScript thread. As JavaScript is single-threaded by design, to be able to communicate with the Godot thread from JavaScript, we use [react-native-worklets](https://docs.swmansion.com/react-native-worklets/), which allows us to run JS code in the Godot thread using worklets. Worklets are JavaScript functions designated with a 'worklet' keyword. @@ -382,7 +410,7 @@ function worklet() { } ``` -These functions and all their external dependencies are transpiled into self contained JS bundles so they can be executed in separate JS contexts associated with separate threads. For more information on how this works, please refer to the [React Native Worklets Core documentation](https://github.com/margelo/react-native-worklets-core/blob/main/docs/USAGE.md). +These functions and all their external dependencies are transformed so they can be executed in separate JS contexts associated with separate threads. For more information, refer to the [React Native Worklets documentation](https://docs.swmansion.com/react-native-worklets/). React Native Godot provides a helper function called `runOnGodotThread()` which will allow you to execute such _workletized_ JS functions on the Godot thread. diff --git a/android/build.gradle b/android/build.gradle index 0832e5d..f1a11de 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,9 +6,37 @@ def safeAppExtGet(prop, fallback) { appProject?.ext?.has(prop) ? appProject.ext.get(prop) : fallback } +def libGodotSupportedAbis() { + return ["armeabi-v7a", "arm64-v8a"] +} + def reactNativeArchitectures() { - def value = project.getProperties().get("reactNativeArchitectures") - return value ? value.split(",") : ["armeabi-v7a", "arm64-v8a"] + if (project.ext.has("rtngodotArchitectures")) { + return project.ext.get("rtngodotArchitectures") + } + + def availableAbis = libGodotSupportedAbis() + def requested = project.getProperties().get("reactNativeArchitectures") + def requestedAbis = requested ? requested.split(",")*.trim() : availableAbis + def supportedAbis = requestedAbis.findAll { availableAbis.contains(it) } + def unsupportedAbis = requestedAbis - supportedAbis + + if (!unsupportedAbis.isEmpty()) { + logger.lifecycle( + "[RTNGodot] Ignoring unsupported Android ABIs ${unsupportedAbis}. " + + "The bundled LibGodot binaries support ${availableAbis}." + ) + } + + if (supportedAbis.isEmpty()) { + throw new GradleException( + "[RTNGodot] None of the requested Android ABIs (${requestedAbis}) are supported. " + + "Use one of ${availableAbis}." + ) + } + + project.ext.set("rtngodotArchitectures", supportedAbis) + return supportedAbis } def resolveBuildType() { @@ -20,18 +48,33 @@ def resolveBuildType() { def resolveReactNativeDirectory() { def reactNativeLocation = safeAppExtGet("REACT_NATIVE_NODE_MODULES_DIR", null) if (reactNativeLocation != null) { - return file(reactNativeLocation) + return file(reactNativeLocation).canonicalFile } - // monorepo workaround - // react-native can be hoisted or in project's own node_modules - def reactNativeFromProjectNodeModules = file("${rootProject.projectDir}/../node_modules/react-native") - if (reactNativeFromProjectNodeModules.exists()) { - return reactNativeFromProjectNodeModules + def command = [ + "node", + "--print", + "require.resolve('react-native/package.json', { paths: [process.argv[1], process.argv[2]] })", + projectDir.absolutePath, + rootProject.projectDir.absolutePath, + ] + def process = command.execute(null, projectDir) + def stdout = new StringBuilder() + def stderr = new StringBuilder() + process.consumeProcessOutput(stdout, stderr) + def exitCode = process.waitFor() + + if (exitCode == 0) { + def packageJson = file(stdout.toString().trim()) + if (packageJson.exists()) { + return packageJson.parentFile.canonicalFile + } } throw new GradleException( - "[RTNGodot] Unable to resolve react-native location in node_modules. You should project extension property (in `app/build.gradle`) `REACT_NATIVE_NODE_MODULES_DIR` with path to react-native." + "[RTNGodot] Unable to resolve react-native from ${projectDir}. " + + "Set the app extension property REACT_NATIVE_NODE_MODULES_DIR when using a non-standard package layout. " + + stderr.toString().trim() ) } @@ -48,12 +91,6 @@ def isNewArchitectureEnabled() { def reactNativeRootDir = resolveReactNativeDirectory() -def reactProperties = new Properties() -file("$reactNativeRootDir/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) } - -def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME") -def REACT_NATIVE_MINOR_VERSION = REACT_NATIVE_VERSION.startsWith("0.0.0-") ? 1000 : REACT_NATIVE_VERSION.split("\\.")[1].toInteger() - def prefabHeadersDir = project.file("$buildDir/prefab-headers/rtngodot") def getPrebuiltLibraryVersion(libName) { @@ -76,6 +113,31 @@ def getPrebuiltLibraryVersion(libName) { def libGodotVersion = getPrebuiltLibraryVersion("libgodot-android") def godotCppVersion = getPrebuiltLibraryVersion("libgodot-cpp-android") +def libGodotMavenRepo = file("$projectDir/libs/libgodot-android/${libGodotVersion}").canonicalFile + +if (!libGodotMavenRepo.isDirectory()) { + throw new GradleException( + "[RTNGodot] Bundled LibGodot Maven repository was not found at ${libGodotMavenRepo}. " + + "Run the package's download-prebuilt command before building Android." + ) +} + +// Maven repositories are not transitive. Register the repository bundled with this +// package directly on every consumer project, using this module's real location so +// hoisted dependencies, workspaces, portals, and conventional node_modules layouts +// all resolve without android.extraMavenRepos or expo-build-properties. +rootProject.allprojects { targetProject -> + if (targetProject.repositories.findByName("libgodot") == null) { + targetProject.repositories.maven { + name = "libgodot" + url = uri(libGodotMavenRepo) + content { + includeGroup("com.migeran.libgodot") + } + } + } + +} buildscript { ext.safeExtGet = {prop, fallback -> @@ -104,7 +166,6 @@ android { buildFeatures { prefab true prefabPublishing true - buildConfig true } // If it doesn't exist @@ -120,11 +181,11 @@ android { defaultConfig { - minSdkVersion safeExtGet('minSdkVersion', 29) + // SurfaceControl is the foundation of the embedded window implementation + // and is only available from Android 10 (API 29). + minSdkVersion 29 targetSdkVersion safeExtGet('targetSdkVersion', 33) ndkVersion safeExtGet('ndkVersion', "26.1.10909125") - buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()) - buildConfigField("String", "FLAVOR", "\"rtngodot\"") // Just a placeholder value. ndk { // Specifies the ABI configurations of your native // libraries Gradle should build and package with your app. @@ -134,7 +195,6 @@ android { externalNativeBuild { cmake { arguments "-DANDROID_STL=c++_shared", - "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}", "-DANDROID_TOOLCHAIN=clang", "-DREACT_NATIVE_DIR=${toPlatformFileString(reactNativeRootDir.path)}", "-DIS_NEW_ARCHITECTURE_ENABLED=${isNewArchitectureEnabled().toString()}", @@ -177,7 +237,6 @@ android { "**/libreact_render_graphics.so", "**/libreact_render_imagemanager.so", "**/libreact_render_mapbuffer.so", - "**/libreact_render_debug.so", "**/libreact_utils.so", "**/librrc_image.so", "**/librrc_view.so", @@ -195,44 +254,22 @@ android { } } -/** - * Task: registerLibGodotRepo - * - * Adds a Maven repository named 'libgodot' that points to the locally bundled pre‑built - * Godot Android artifacts. The repository URL is relative to this library module: - * - * libs/libgodot-android/${libGodotVersion}/ - * - * The task is executed before any compile/assemble tasks so that the consuming app can resolve - * `com.migeran.libgodot:godot-dev` (or similar) from the local directory. - */ -task registerLibGodotRepo { - // Capture the version at configuration time - def libVersion = libGodotVersion - def repoUrl = uri("$projectDir/libs/libgodot-android/${libVersion}/") - rootProject.allprojects { proj -> - // Avoid adding the same repo multiple times - if (!proj.repositories.any { it instanceof MavenArtifactRepository && it.name == 'libgodot' }) { - proj.repositories.maven { - name = 'libgodot' - url = repoUrl - } - print("Added local Maven repository 'libgodot' to project ${proj.path}: ${repoUrl}\n") - } - } -} - - repositories { + maven { + name = "libgodot" + url = uri(libGodotMavenRepo) + content { + includeGroup("com.migeran.libgodot") + } + } mavenCentral() google() } dependencies { implementation 'com.facebook.react:react-native' - implementation project(':react-native-worklets-core') + implementation project(':react-native-worklets') api "com.migeran.libgodot:godot-debug:${libGodotVersion}-SNAPSHOT" - implementation('com.jakewharton.threetenabp:threetenabp:1.4.9') } task deleteCmakeCache() { diff --git a/android/fix-prefab.gradle b/android/fix-prefab.gradle index f0911b5..4d32eee 100644 --- a/android/fix-prefab.gradle +++ b/android/fix-prefab.gradle @@ -1,4 +1,4 @@ -// From: react-native-worklets-core/android/fix-prefab.gradle +// From: react-native-worklets/android/fix-prefab.gradle tasks.configureEach { task -> // Make sure that we generate our prefab publication file only after having built the native library diff --git a/android/src/main/cpp/CMakeLists.txt b/android/src/main/cpp/CMakeLists.txt index 88e056f..923182f 100644 --- a/android/src/main/cpp/CMakeLists.txt +++ b/android/src/main/cpp/CMakeLists.txt @@ -21,19 +21,14 @@ if(NOT ${CMAKE_BUILD_TYPE} MATCHES "Debug") endif() set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") # Prefab packages from React Native find_package(fbjni REQUIRED CONFIG) add_library(fbjni ALIAS fbjni::fbjni) - find_package(ReactAndroid REQUIRED CONFIG) add_library(jsi ALIAS ReactAndroid::jsi) add_library(reactnative ALIAS ReactAndroid::reactnative) - -# Consume shared libraries and headers from prefabs - -find_package(react-native-worklets-core REQUIRED CONFIG) +find_package(react-native-worklets REQUIRED CONFIG) add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../../../build/generated/source/codegen/jni" generated) set(LIBGODOT_CPP_VERSION ${GODOT_CPP_VERSION}) @@ -51,6 +46,10 @@ file(GLOB rtngodot_SRC CONFIGURE_DEPENDS *.cpp) file(GLOB rtngodot_common_SRC CONFIGURE_DEPENDS ../../../../common/*.cpp) add_library(rtngodot SHARED ${rtngodot_SRC} ${rtngodot_common_SRC}) +# Android 15+ devices may use 16 KiB memory pages. Keep this module loadable +# there instead of relying on the OS page-size compatibility mode. +target_link_options(rtngodot PRIVATE "-Wl,-z,max-page-size=16384") + target_include_directories(rtngodot PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} "../../../../common" @@ -61,7 +60,7 @@ target_link_libraries( fbjni::fbjni ReactAndroid::jsi ReactAndroid::reactnative - react-native-worklets-core::rnworklets + react-native-worklets::worklets react_codegen_RTNGodotSpec ) diff --git a/android/src/main/cpp/GodotModule.cpp b/android/src/main/cpp/GodotModule.cpp index 44f70de..0eaa055 100644 --- a/android/src/main/cpp/GodotModule.cpp +++ b/android/src/main/cpp/GodotModule.cpp @@ -231,7 +231,33 @@ GDExtensionBool GDE_EXPORT gdextension_default_init(GDExtensionInterfaceGetProcA } } -static void frameCallback64(int64_t frameTimeNanos, void *data) { +using PostFrameCallback64 = void (*)(AChoreographer *, AChoreographer_frameCallback64, void *); + +static void handleFrameCallback(void *data); + +static void frameCallback(long, void *data) { + handleFrameCallback(data); +} + +static void frameCallback64(int64_t, void *data) { + handleFrameCallback(data); +} + +static void postFrameCallback(AChoreographer *choreographer, void *data) { + static auto postFrameCallback64 = reinterpret_cast( + dlsym(RTLD_DEFAULT, "AChoreographer_postFrameCallback64")); + if (postFrameCallback64 != nullptr) { + postFrameCallback64(choreographer, frameCallback64, data); + return; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + AChoreographer_postFrameCallback(choreographer, frameCallback, data); +#pragma clang diagnostic pop +} + +static void handleFrameCallback(void *data) { GodotModule *self = (GodotModule *)data; if (!self->is_paused()) { godot::GodotInstance *instance = self->get_instance(); @@ -241,7 +267,7 @@ static void frameCallback64(int64_t frameTimeNanos, void *data) { instance->iteration(); } AChoreographer *choreographer = AChoreographer_getInstance(); - AChoreographer_postFrameCallback64(choreographer, frameCallback64, data); + postFrameCallback(choreographer, data); } } @@ -348,7 +374,7 @@ godot::GodotInstance *GodotModule::get_or_create_instance(std::vectorstart()) { AChoreographer *choreographer = AChoreographer_getInstance(); - AChoreographer_postFrameCallback64(choreographer, frameCallback64, this); + postFrameCallback(choreographer, this); } { @@ -523,7 +549,7 @@ void GodotModule::updateState() { // Register the frame callback again data->thread.enqueue([this]() { AChoreographer *choreographer = AChoreographer_getInstance(); - AChoreographer_postFrameCallback64(choreographer, frameCallback64, this); + postFrameCallback(choreographer, this); }); } } @@ -575,7 +601,7 @@ godot::Callable GodotModule::create_callable(std::function f, void *ref) { AndroidPlatformData *data = static_cast(_data); std::lock_guard lock(data->windowUpdateMutex); - LOGD("Registering Window: %llx, %s", (uint64_t)handle, name.c_str()); + LOGD("Registering Window: %p, %s", handle, name.c_str()); if (data->handleToWindowName.contains(handle)) { std::string currentName = data->handleToWindowName[handle]; if (currentName != name) { @@ -593,7 +619,7 @@ void GodotModule::unregisterWindowUpdateCallback(void *handle) { std::lock_guard lock(data->windowUpdateMutex); if (data->handleToWindowName.contains(handle)) { std::string name = data->handleToWindowName[handle]; - LOGD("Unregistering Window: %llx, %s", (uint64_t)handle, name.c_str()); + LOGD("Unregistering Window: %p, %s", handle, name.c_str()); WindowFuncData fd = data->windowUpdateCallbacks[name]; JNIEnv *env = LibGodot::get_jni_env(); env->DeleteGlobalRef(fd.ref); diff --git a/android/src/main/cpp/OnLoad.cpp b/android/src/main/cpp/OnLoad.cpp index 102ac35..eb79551 100644 --- a/android/src/main/cpp/OnLoad.cpp +++ b/android/src/main/cpp/OnLoad.cpp @@ -53,9 +53,13 @@ // } #include "native_godot_module_jni.h" +#include "libgodot_jni.h" #include JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + // LibGodot threads can request a JNIEnv before the engine-level initialize + // callback runs, so retain the VM as soon as this shared library is loaded. + LibGodot::set_java_vm(vm); return facebook::jni::initialize(vm, [] { NativeGodotModuleJNI::registerNatives(); }); diff --git a/android/src/main/cpp/libgodot_jni.cpp b/android/src/main/cpp/libgodot_jni.cpp index 113f32e..035bc12 100644 --- a/android/src/main/cpp/libgodot_jni.cpp +++ b/android/src/main/cpp/libgodot_jni.cpp @@ -73,7 +73,6 @@ void LibGodot::initialize(JNIEnv *env, jobject p_asset_manager, jobject p_net_ut godot_io = env->NewGlobalRef(p_godot_io); ANativeWindow *mainSurface = ANativeWindow_fromSurface(env, p_main_surface); windowMap[""] = WindowData(mainSurface, p_width, p_height, 0); - maxSize = fmax(p_width, p_height); godot_engine = env->NewGlobalRef(p_godot_engine); host_activity = env->NewGlobalRef(p_host_activity); class_loader = env->NewGlobalRef(p_class_loader); @@ -141,13 +140,21 @@ void LibGodot::cleanup(JNIEnv *env) { } JNIEnv *LibGodot::get_jni_env() { + if (!java_vm) { + LOGE("LibGodot::get_jni_env() called before JavaVM initialization"); + return nullptr; + } JNIEnv *env; java_vm->AttachCurrentThread(&env, nullptr); return env; } -static std::function createUpdateWindowFunc(std::string p_window_name, int p_width, int p_height, ANativeWindow *p_window_surface, bool p_change_surface) { - return [p_window_name, p_width, p_height, p_window_surface, p_change_surface]() { +void LibGodot::set_java_vm(JavaVM *vm) { + java_vm = vm; +} + +static std::function createUpdateWindowFunc(std::string p_window_name, int p_width, int p_height, ANativeWindow *p_window_surface) { + return [p_window_name, p_width, p_height, p_window_surface]() { godot::DisplayServerEmbedded *dse = godot::DisplayServerEmbedded::get_singleton(); int32_t windowId = -1; if (p_window_name == "") { @@ -232,7 +239,7 @@ void LibGodot::updateWindowNative(JNIEnv *env, jstring p_name, jobject p_surface } godot::GodotInstance *instance = GodotModule::get_singleton()->get_instance(); if (instance && instance->is_started()) { - GodotModule::get_singleton()->runOnGodotThread(createUpdateWindowFunc(windowName, p_width, p_height, windowSurface, changeSurface), true); + GodotModule::get_singleton()->runOnGodotThread(createUpdateWindowFunc(windowName, p_width, p_height, windowSurface), true); } } @@ -261,7 +268,6 @@ void LibGodot::removeWindowNative(JNIEnv *env, jstring p_name) { godot::GodotInstance *instance = GodotModule::get_singleton()->get_instance(); if (instance && instance->is_started()) { GodotModule::get_singleton()->runOnGodotThread([windowName, windowSurface]() { - godot::DisplayServerEmbedded *dse = godot::DisplayServerEmbedded::get_singleton(); { // Find window godot::MainLoop *mainLoop = godot::Engine::get_singleton()->get_main_loop(); @@ -293,7 +299,7 @@ void LibGodot::updateWindow(std::string windowName) { godot::GodotInstance *instance = GodotModule::get_singleton()->get_instance(); WindowData &data = windowMap[windowName]; if (instance && instance->is_started()) { - GodotModule::get_singleton()->runOnGodotThread(createUpdateWindowFunc(windowName, data.width, data.height, data.surface, windowName != "")); + GodotModule::get_singleton()->runOnGodotThread(createUpdateWindowFunc(windowName, data.width, data.height, data.surface)); } } } @@ -306,8 +312,7 @@ void LibGodot::updateWindows() { std::string windowName = item.first; WindowData &data = item.second; GodotModule::get_singleton()->runOnGodotThread( - createUpdateWindowFunc(windowName, data.width, data.height, data.surface, - windowName != "")); + createUpdateWindowFunc(windowName, data.width, data.height, data.surface)); } } } @@ -367,17 +372,6 @@ struct TouchPos { godot::Vector2 tilt; }; -static godot::String convertToGodotString(JNIEnv *env, jstring s) { - godot::String result; - { - jboolean isCopy; - const char *val = env->GetStringUTFChars(s, &isCopy); - result = godot::String::utf8(val); - env->ReleaseStringUTFChars(s, val); - } - return result; -} - static int32_t getWindowId(std::string p_name) { std::lock_guard lock(windowMapMutex); if (windowMap.contains(p_name)) { @@ -588,4 +582,4 @@ JNIEXPORT void JNICALL Java_com_rtngodot_RTNLibGodot_dispatchTouchEvent(JNIEnv * } break; } } -} \ No newline at end of file +} diff --git a/android/src/main/cpp/libgodot_jni.h b/android/src/main/cpp/libgodot_jni.h index 78fa34a..48bf843 100644 --- a/android/src/main/cpp/libgodot_jni.h +++ b/android/src/main/cpp/libgodot_jni.h @@ -41,7 +41,6 @@ class LibGodot { static inline jobject godot_engine = nullptr; static inline jobject class_loader = nullptr; static inline jobject host_activity = nullptr; - static inline jint maxSize = 0; public: static jobject get_asset_manager() { @@ -79,13 +78,9 @@ class LibGodot { static int get_main_height(); - static jint get_max_size() { - return maxSize; - } - static JNIEnv *get_jni_env(); - static JavaVM *get_java_vm(); + static void set_java_vm(JavaVM *vm); static void initialize(JNIEnv *env, jobject p_asset_manager, jobject p_net_utils, jobject p_dir_access_handler, jobject p_file_access_handler, jobject p_godot_io, jobject p_main_surface, jint p_width, jint p_height, jobject p_godot_engine, jobject p_host_activity, jobject p_class_loader); diff --git a/android/src/main/cpp/native_godot_module_jni.cpp b/android/src/main/cpp/native_godot_module_jni.cpp index 11841bf..1dbf8ac 100644 --- a/android/src/main/cpp/native_godot_module_jni.cpp +++ b/android/src/main/cpp/native_godot_module_jni.cpp @@ -49,6 +49,14 @@ void NativeGodotModuleJNI::registerNatives() { } bool NativeGodotModuleJNI::installTurboModule() { + // A zero JavaScript context must never be reported as a successful install: + // doing so leaves global.RTNGodot undefined and merely moves the crash to + // the first API call. Fail cleanly instead of dereferencing a null runtime. + if (rnRuntime_ == nullptr) { + LOGE("JavaScript runtime is unavailable; cannot install NativeGodotModule."); + return false; + } + jsi::Runtime &rnRuntime = *rnRuntime_; jsi::Value godotModule = createNativeGodotModule(rnRuntime, callInvoker_); if (!godotModule.isObject()) { @@ -64,4 +72,4 @@ NativeGodotModuleJNI::NativeGodotModuleJNI( const std::shared_ptr &jsCallInvoker) : javaPart_(jni::make_global(jThis)), rnRuntime_(rnRuntime), - callInvoker_(jsCallInvoker) {} \ No newline at end of file + callInvoker_(jsCallInvoker) {} diff --git a/android/src/main/cpp/native_godot_module_jni.h b/android/src/main/cpp/native_godot_module_jni.h index 36a34e7..3416404 100644 --- a/android/src/main/cpp/native_godot_module_jni.h +++ b/android/src/main/cpp/native_godot_module_jni.h @@ -26,7 +26,6 @@ #include #include #include -#include using namespace facebook; diff --git a/android/src/main/java/com/rtngodot/GodotPackage.java b/android/src/main/java/com/rtngodot/GodotPackage.java index 8aef380..f03b91d 100644 --- a/android/src/main/java/com/rtngodot/GodotPackage.java +++ b/android/src/main/java/com/rtngodot/GodotPackage.java @@ -25,8 +25,6 @@ package com.rtngodot; -import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -49,8 +47,6 @@ public class GodotPackage extends TurboReactPackage { System.loadLibrary("rtngodot"); } - private static final String TAG = "GodotPackage"; - @Override public List createViewManagers(@NonNull ReactApplicationContext reactContext) { return Collections.singletonList(new RTNGodotViewManager(reactContext)); @@ -59,15 +55,12 @@ public List createViewManagers(@NonNull ReactApplicationContext rea @Nullable @Override public NativeModule getModule(String name, ReactApplicationContext reactContext) { - Log.w(TAG, "Called getModule with name: " + name); if (NativeGodotModule.NAME.equals(name)) { return new NativeGodotModule(reactContext); } return null; } - public static String MODULE_NAME = "NativeGodotModule"; - @Override public ReactModuleInfoProvider getReactModuleInfoProvider() { return () -> { @@ -87,4 +80,4 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { return moduleInfos; }; } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/rtngodot/NativeGodotModule.java b/android/src/main/java/com/rtngodot/NativeGodotModule.java index 3f66edc..dcac6d3 100644 --- a/android/src/main/java/com/rtngodot/NativeGodotModule.java +++ b/android/src/main/java/com/rtngodot/NativeGodotModule.java @@ -36,6 +36,7 @@ import com.facebook.react.common.annotations.FrameworkAPI; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.turbomodule.core.CallInvokerHolderImpl; +import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder; import com.migeran.NativeGodotModuleSpec; @OptIn(markerClass = FrameworkAPI.class) @@ -49,11 +50,22 @@ public class NativeGodotModule extends NativeGodotModuleSpec { public NativeGodotModule(ReactApplicationContext context) { super(context); - CallInvokerHolderImpl holder = - (CallInvokerHolderImpl)context.getCatalystInstance().getJSCallInvokerHolder(); + // libgodot_create_godot_instance_android expects the Activity, Godot + // engine wrapper and Android services to have been registered first. + // Without this, starting an instance aborts in JNI GetLongField(null). + RTNLibGodot.getInstance().init(context.getCurrentActivity()); + + CallInvokerHolder callInvokerHolder = Objects.requireNonNull( + context.getJSCallInvokerHolder(), + "The JavaScript call invoker is not available"); + if (!(callInvokerHolder instanceof CallInvokerHolderImpl)) { + throw new IllegalStateException( + "Unsupported JavaScript call invoker implementation: " + + callInvokerHolder.getClass().getName()); + } mHybridData = initHybrid( Objects.requireNonNull(context.getJavaScriptContextHolder()).get(), - holder); + (CallInvokerHolderImpl)callInvokerHolder); } private native HybridData initHybrid(long jsContext, CallInvokerHolderImpl jsCallInvokerHolder); diff --git a/android/src/main/java/com/rtngodot/RTNGodotViewManager.java b/android/src/main/java/com/rtngodot/RTNGodotViewManager.java index f2c6605..518b94d 100644 --- a/android/src/main/java/com/rtngodot/RTNGodotViewManager.java +++ b/android/src/main/java/com/rtngodot/RTNGodotViewManager.java @@ -63,6 +63,10 @@ public String getName() { @NonNull @Override protected RTNGodotView createViewInstance(@NonNull ThemedReactContext context) { + // TurboModules can be created before React Native has attached the + // current Activity. The view is created on the UI thread, so retrying + // here makes initialization deterministic for bridgeless/Expo apps. + RTNLibGodot.getInstance().init(context.getCurrentActivity()); return new RTNGodotView(context); } diff --git a/android/src/main/java/com/rtngodot/RTNLibGodot.java b/android/src/main/java/com/rtngodot/RTNLibGodot.java index 934f5a5..17d5a6d 100644 --- a/android/src/main/java/com/rtngodot/RTNLibGodot.java +++ b/android/src/main/java/com/rtngodot/RTNLibGodot.java @@ -70,9 +70,7 @@ public class RTNLibGodot implements IGodotLib, GodotHost, GodotRenderView { private static Activity mActivity; - private static SurfaceControl mainSurfaceControl; - - private static RTNLibGodot instance = null; + private static final RTNLibGodot INSTANCE = new RTNLibGodot(); private Godot godot; @@ -83,15 +81,17 @@ public class RTNLibGodot implements IGodotLib, GodotHost, GodotRenderView { private RTNLibGodot() {} public static RTNLibGodot getInstance() { - if (RTNLibGodot.instance == null) { - RTNLibGodot.instance = new RTNLibGodot(); - } - return RTNLibGodot.instance; + return INSTANCE; } @Override public boolean initialize(Godot godot, AssetManager assetManager, GodotIO godotIO, GodotNetUtils godotNetUtils, DirectoryAccessHandler directoryAccessHandler, FileAccessHandler fileAccessHandler, boolean b) { ClassLoader loader = RTNLibGodot.class.getClassLoader(); + WindowSurfaceData mainWindow = windowData.get(""); + if (mainWindow == null || mainWindow.surface == null) { + Log.e(TAG, "Main window surface is unavailable; call init() before initialize()"); + return false; + } initialize( assetManager, @@ -99,9 +99,9 @@ public boolean initialize(Godot godot, AssetManager assetManager, GodotIO godotI directoryAccessHandler, fileAccessHandler, godotIO, - Objects.requireNonNull(windowData.get("")).surface, - surfaceSize, - surfaceSize, + mainWindow.surface, + mainWindow.width, + mainWindow.height, godot, mActivity, loader); @@ -356,18 +356,16 @@ public WindowSurfaceData(SurfaceControl ctrl, int width, int height, boolean per } } - private static Map windowData = new HashMap<>(); + private static final Map windowData = new HashMap<>(); - private static int surfaceSize; - - private static void createWindowSurface(String name, int width, int height, boolean persistent) { + private static void createWindowSurface(String name, int width, int height) { SurfaceControl.Builder b = new SurfaceControl.Builder(); SurfaceControl control = b.setBufferSize(width, height) .setFormat(PixelFormat.RGBA_8888) .setName(name) .build(); - WindowSurfaceData wsData = new WindowSurfaceData(control, width, height, persistent); + WindowSurfaceData wsData = new WindowSurfaceData(control, width, height, name.isEmpty()); windowData.put(name, wsData); } @@ -376,7 +374,7 @@ private static void createWindowSurface(String name, int width, int height, bool private static WindowSurfaceData getOrCreateWindowSurface(String name, int width, int height) { WindowSurfaceData wsData = windowData.get(name); if (wsData == null) { - createWindowSurface(name, width, height, false); + createWindowSurface(name, width, height); wsData = Objects.requireNonNull(windowData.get(name)); } return wsData; @@ -400,6 +398,7 @@ public void updateWindow(String name, SurfaceControl control, SurfaceHolder hold try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) { // Set new parent t.reparent(wsData.control, control); + t.setLayer(wsData.control, 1); t.setVisibility(wsData.control, true); if (wsData.width != width || wsData.height != height) { t.setBufferSize(wsData.control, width, height); @@ -445,7 +444,7 @@ public void removeWindow(String name) { } } - public void init(Activity activity) { + public synchronized void init(Activity activity) { if (inited) { return; } @@ -458,21 +457,21 @@ public void init(Activity activity) { } DisplayMetrics metrics = new DisplayMetrics(); mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics); - - createWindowSurface("", metrics.widthPixels, metrics.heightPixels, true); + getOrCreateWindowSurface("", metrics.widthPixels, metrics.heightPixels); GodotLib.setGodotLibImpl(RTNLibGodot.getInstance()); godot = Godot.getInstance(mActivity); godot.setActivity(mActivity); - Set runtimePlugins = new HashSet(); + Set runtimePlugins = new HashSet<>(); runtimePlugins.add(new AndroidRuntimePlugin(godot)); runtimePlugins.addAll(getHostPlugins()); - List commands = new ArrayList(); + List commands = new ArrayList<>(); if (!godot.initEngine(this, commands, runtimePlugins)) { Log.e(TAG, "Unable to initialize Godot engine layer"); + return; } mInputHandler = new GodotInputHandler(mActivity, godot); @@ -510,7 +509,7 @@ private static native void initialize(AssetManager asset_manager, private native void removeWindowNative(String windowName); - public Set hostPlugins = new HashSet<>(); + private final Set hostPlugins = new HashSet<>(); public void addHostPlugin(GodotPlugin plugin) { hostPlugins.add(plugin); diff --git a/app.plugin.js b/app.plugin.js new file mode 100644 index 0000000..2a7e4f7 --- /dev/null +++ b/app.plugin.js @@ -0,0 +1,120 @@ +const path = require("node:path"); +const { + IOSConfig, + withGradleProperties, + withXcodeProject, +} = require("expo/config-plugins"); +const { prebuiltFiles } = require("./package.json"); + +const ANDROID_PROPERTIES = { + "android.minSdkVersion": "29", + "reactNativeArchitectures": "armeabi-v7a,arm64-v8a", +}; +const GODOT_MAVEN_PROPERTY = "android.extraMavenRepos"; +const libGodotAndroid = prebuiltFiles.find( + (entry) => entry.name === "libgodot-android" +); + +if (!libGodotAndroid) { + throw new Error("Missing libgodot-android prebuilt metadata in package.json"); +} + +function setGradleProperty(properties, key, value) { + const property = properties.find( + (entry) => entry.type === "property" && entry.key === key + ); + + if (property) { + property.value = value; + } else { + properties.push({ type: "property", key, value }); + } +} + +function isGodotMavenRepository(repository) { + return ( + repository && + typeof repository.url === "string" && + /\/android\/libs\/libgodot-android\/[^/]+\/?$/.test( + repository.url.replaceAll("\\", "/") + ) + ); +} + +function addGodotMavenRepository(properties, platformProjectRoot) { + const property = properties.find( + (entry) => entry.type === "property" && entry.key === GODOT_MAVEN_PROPERTY + ); + let repositories = []; + + if (property) { + repositories = JSON.parse(property.value); + if (!Array.isArray(repositories)) { + throw new Error(`${GODOT_MAVEN_PROPERTY} must be a JSON array`); + } + } + + const repositoryPath = path + .relative( + path.join(platformProjectRoot, "app"), + path.join( + __dirname, + "android", + "libs", + "libgodot-android", + libGodotAndroid.version + ) + ) + .split(path.sep) + .join("/"); + repositories = repositories.filter( + (repository) => !isGodotMavenRepository(repository) + ); + repositories.push({ url: repositoryPath }); + setGradleProperty( + properties, + GODOT_MAVEN_PROPERTY, + JSON.stringify(repositories) + ); +} + +function withGodotAndroid(config) { + config = withGradleProperties(config, (projectConfig) => { + for (const [key, value] of Object.entries(ANDROID_PROPERTIES)) { + setGradleProperty(projectConfig.modResults, key, value); + } + addGodotMavenRepository( + projectConfig.modResults, + projectConfig.modRequest.platformProjectRoot + ); + + return projectConfig; + }); + return config; +} + +function withGodotPacks(config, iosPacks) { + if (!iosPacks.length) { + return config; + } + + return withXcodeProject(config, (projectConfig) => { + const project = projectConfig.modResults; + IOSConfig.XcodeUtils.ensureGroupRecursively(project, "Resources"); + + for (const pack of iosPacks) { + IOSConfig.XcodeUtils.addResourceFileToGroup({ + filepath: pack, + groupName: "Resources", + isBuildFile: true, + project, + verbose: true, + }); + } + + return projectConfig; + }); +} + +module.exports = (config, { iosPacks = [] } = {}) => + withGodotPacks(withGodotAndroid(config), iosPacks); diff --git a/borndotcom-react-native-godot.podspec b/borndotcom-react-native-godot.podspec index 319cad6..ccb740f 100644 --- a/borndotcom-react-native-godot.podspec +++ b/borndotcom-react-native-godot.podspec @@ -35,4 +35,5 @@ Pod::Spec.new do |s| s.header_mappings_dir = 'ios' install_modules_dependencies(s) + s.dependency 'RNWorklets' end diff --git a/common/NativeGodotModule.cpp b/common/NativeGodotModule.cpp index 0a4dc2d..9e1afc8 100644 --- a/common/NativeGodotModule.cpp +++ b/common/NativeGodotModule.cpp @@ -32,20 +32,51 @@ #include #include +#include #include #include #include #include +#include #ifdef ON_ANDROID #include #endif -#include -#include +#include +#include #define NATIVE_GODOT_MODULE_PROPERTY "RTNGodot" +class GodotAsyncQueue : public worklets::AsyncQueue { +public: + void push(std::function &&job) override { + GodotModule::get_singleton()->runOnGodotThread(std::move(job)); + } +}; + +class GodotWorkletContext : public std::enable_shared_from_this { + jsi::Runtime *_jsRuntime; + std::shared_ptr _jsCallInvoker; + +public: + GodotWorkletContext(jsi::Runtime *jsRuntime, std::shared_ptr jsCallInvoker) : + _jsRuntime(jsRuntime), _jsCallInvoker(std::move(jsCallInvoker)) {} + + jsi::Runtime *getJsRuntime() const { + return _jsRuntime; + } + + void invokeOnJsThread(std::function &&f) const { + _jsCallInvoker->invokeAsync([weakSelf = weak_from_this(), f = std::move(f)]() mutable { + auto self = weakSelf.lock(); + if (self) { + f(*self->_jsRuntime); + } + }); + } +}; + static std::string create_method_call_error_string(std::string methodName, GDExtensionCallError error) { std::string ret = "Method call error name: " + methodName + " "; switch (error.error) { @@ -69,6 +100,7 @@ static std::string create_method_call_error_string(std::string methodName, GDExt break; case GDEXTENSION_CALL_OK: ret += "Call OK (Should never happen)"; + break; default: ret += "Unknown Error"; } @@ -78,7 +110,7 @@ static std::string create_method_call_error_string(std::string methodName, GDExt static std::vector createVariantArgArray(const std::vector &args) { std::vector ret; ret.reserve(args.size()); - for (int i = 0; i < args.size(); ++i) { + for (size_t i = 0; i < args.size(); ++i) { ret.push_back(&args[i]); } return ret; @@ -86,13 +118,14 @@ static std::vector createVariantArgArray(const std::vect static const char *JAVASCRIPT_CALLABLE_NAME = "JavascriptCallable"; class JavascriptCallable : public godot::CallableCustom { - std::weak_ptr _workletContext; + std::weak_ptr _workletContext; + std::weak_ptr _workletRuntime; bool _isWorklet; jsi::Value _funcValue; static bool runInContext(const JavascriptCallable *c, std::function func) { - std::shared_ptr wc = c->_workletContext.lock(); + std::shared_ptr wc = c->_workletContext.lock(); if (!wc) { LOGE("WorkletContext is invalid"); return false; @@ -101,34 +134,50 @@ class JavascriptCallable : public godot::CallableCustom { std::mutex mtx; std::condition_variable cv; bool done = false; - bool err = false; - if (c->_isWorklet) { - wc->invokeOnWorkletThread([&err, &func, &c, &done, &mtx, &cv](RNWorklet::JsiWorkletContext *wc, jsi::Runtime &rt) { - err = func(c, rt); - std::unique_lock lock(mtx); + bool success = false; + auto execute = [&mtx, &cv, &done, &success, &func, c](jsi::Runtime &rt) { + try { + success = func(c, rt); + } catch (const std::exception &exc) { + LOGE("JavaScript callback failed: %s", exc.what()); + } catch (...) { + LOGE("JavaScript callback failed with an unknown error"); + } + { + std::lock_guard lock(mtx); done = true; - cv.notify_one(); - }); + } + cv.notify_one(); + }; + if (c->_isWorklet) { + std::shared_ptr runtime = c->_workletRuntime.lock(); + if (!runtime) { + LOGE("WorkletRuntime is invalid"); + return false; + } + runtime->schedule(std::move(execute)); } else { - wc->invokeOnJsThread([&err, &func, &c, &done, &mtx, &cv](jsi::Runtime &rt) { - err = func(c, rt); - std::unique_lock lock(mtx); - done = true; - cv.notify_one(); - }); + wc->invokeOnJsThread(std::move(execute)); } { std::unique_lock lock(mtx); cv.wait(lock, [&]() { return done; }); } - return err; + return success; } } public: - JavascriptCallable(std::shared_ptr workletContext, jsi::Runtime &rt, const jsi::Function &func) : + JavascriptCallable(std::shared_ptr workletContext, jsi::Runtime &rt, const jsi::Function &func) : _workletContext(workletContext), _funcValue(jsi::Value(rt, func)) { _isWorklet = workletContext->getJsRuntime() != &rt; + if (_isWorklet) { + try { + _workletRuntime = worklets::WorkletRuntime::getWeakRuntimeFromJSIRuntime(rt); + } catch (const std::exception &exc) { + LOGE("Unable to resolve WorkletRuntime: %s", exc.what()); + } + } } uint32_t hash() const override { @@ -164,13 +213,13 @@ class JavascriptCallable : public godot::CallableCustom { LOGE("Different WorkletContext: %d, %d", j_a->_isWorklet, j_b->_isWorklet); return false; } - std::shared_ptr j_a_wc = j_a->_workletContext.lock(); + std::shared_ptr j_a_wc = j_a->_workletContext.lock(); if (!j_a_wc) { LOGE("First WorkletContext is invalid"); return false; } - std::shared_ptr j_b_wc = j_a->_workletContext.lock(); + std::shared_ptr j_b_wc = j_b->_workletContext.lock(); if (!j_b_wc) { LOGE("Second WorkletContext is invalid"); return false; @@ -234,20 +283,18 @@ class JavascriptCallable : public godot::CallableCustom { } void call(const godot::Variant **p_arguments, int p_argcount, godot::Variant &r_return_value, GDExtensionCallError &r_call_error) const override; - ~JavascriptCallable() { - } }; -static godot::Callable createJSCallable(std::shared_ptr workletContext, jsi::Runtime &rt, jsi::Function func) { +static godot::Callable createJSCallable(std::shared_ptr workletContext, jsi::Runtime &rt, jsi::Function func) { return godot::Callable(memnew(JavascriptCallable(workletContext, rt, func))); } class GodotHostObject : public jsi::HostObject { - std::shared_ptr _workletContext; + std::shared_ptr _workletContext; godot::Variant _value; public: - static godot::Variant jsiValueToGodotVariant(std::shared_ptr workletContext, jsi::Runtime &rt, const jsi::Value &value) { + static godot::Variant jsiValueToGodotVariant(std::shared_ptr workletContext, jsi::Runtime &rt, const jsi::Value &value) { if (value.isNull() || value.isUndefined()) { return godot::Variant(nullptr); } @@ -286,7 +333,7 @@ class GodotHostObject : public jsi::HostObject { throw jsi::JSINativeException("Unhandled Object Type"); } - static jsi::Value godotVariantToJsiValue(std::shared_ptr workletContext, jsi::Runtime &rt, const godot::Variant &variant) { + static jsi::Value godotVariantToJsiValue(std::shared_ptr workletContext, jsi::Runtime &rt, const godot::Variant &variant) { switch (variant.get_type()) { case godot::Variant::Type::NIL: { return jsi::Value::null(); @@ -303,9 +350,7 @@ class GodotHostObject : public jsi::HostObject { } case godot::Variant::Type::STRING: { godot::String s = variant; - LOGI("Godot Variant String to JSI: %s", s.utf8().get_data()); jsi::String ret = jsi::String::createFromUtf8(rt, (uint8_t *)s.utf8().get_data(), s.length()); - LOGI("JSI String: %s", ret.utf8(rt).c_str()); return ret; } // math types @@ -372,20 +417,16 @@ class GodotHostObject : public jsi::HostObject { } } - GodotHostObject(std::shared_ptr workletContext, const godot::Variant v) : + GodotHostObject(std::shared_ptr workletContext, const godot::Variant v) : jsi::HostObject(), _workletContext(workletContext), _value(v) {} - ~GodotHostObject() { - LOGI("Destructing Godot object of type: %d", _value.get_type()); - } - jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override { godot::StringName propName(name.utf8(rt).c_str()); if (_value.get_type() == godot::Variant::Type::NIL) { return jsi::Value(nullptr); } if (_value.has_method(propName)) { - std::shared_ptr wc = _workletContext; + std::shared_ptr wc = _workletContext; return jsi::Function::createFromHostFunction(rt, name, 0, [propName, wc](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { // LOGI("Calling: %s", propName.to_utf8_buffer().ptr()); if (!thisVal.isObject()) { @@ -400,7 +441,7 @@ class GodotHostObject : public jsi::HostObject { std::shared_ptr ho = obj.getHostObject(rt); std::vector godotArgs; godotArgs.reserve(count); - for (int i = 0; i < count; ++i) { + for (size_t i = 0; i < count; ++i) { godotArgs.push_back(jsiValueToGodotVariant(wc, rt, args[i])); } @@ -436,11 +477,11 @@ class GodotHostObject : public jsi::HostObject { }; class GodotAPIObject : public jsi::HostObject { - std::shared_ptr _workletContext; + std::shared_ptr _workletContext; std::map builtin_types; public: - static jsi::Value createBuiltinTypeConstructor(std::shared_ptr workletContext, jsi::Runtime &rt, std::string name, std::function constructor) { + static jsi::Value createBuiltinTypeConstructor(std::shared_ptr workletContext, jsi::Runtime &rt, std::string name, std::function constructor) { jsi::Function ctorFunc = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forUtf8(rt, name), 0, @@ -450,11 +491,11 @@ class GodotAPIObject : public jsi::HostObject { return jsi::Value(rt, ctorFunc); } - static jsi::Value createStaticFunction(std::shared_ptr workletContext, jsi::Runtime &rt, std::string name, GDExtensionMethodBindPtr mb) { + static jsi::Value createStaticFunction(std::shared_ptr workletContext, jsi::Runtime &rt, std::string name, GDExtensionMethodBindPtr mb) { jsi::Function f = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forUtf8(rt, name), 0, [name, mb, workletContext](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { std::vector godotArgs; godotArgs.reserve(count); - for (int i = 0; i < count; ++i) { + for (size_t i = 0; i < count; ++i) { godotArgs.push_back(GodotHostObject::jsiValueToGodotVariant(workletContext, rt, args[i])); } std::vector variantArgs = createVariantArgArray(godotArgs); @@ -470,7 +511,7 @@ class GodotAPIObject : public jsi::HostObject { return jsi::Value(rt, f); } - static jsi::Value createClassConstructor(std::shared_ptr workletContext, jsi::Runtime &rt, std::string name) { + static jsi::Value createClassConstructor(std::shared_ptr workletContext, jsi::Runtime &rt, std::string name) { godot::StringName godotClassName(name.c_str()); jsi::Function ctorFunc = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forUtf8(rt, name), @@ -500,7 +541,7 @@ class GodotAPIObject : public jsi::HostObject { #define DECLARE_BUILTIN_TYPE(name) builtin_types[#name] = createBuiltinTypeConstructor(workletContext, rt, #name, []() { return godot::Variant(godot::name()); }) - GodotAPIObject(std::shared_ptr workletContext, jsi::Runtime &rt) : + GodotAPIObject(std::shared_ptr workletContext, jsi::Runtime &rt) : jsi::HostObject(), _workletContext(workletContext) { DECLARE_BUILTIN_TYPE(Vector2); DECLARE_BUILTIN_TYPE(Vector2i); @@ -568,7 +609,8 @@ class GodotAPIObject : public jsi::HostObject { }; void JavascriptCallable::call(const godot::Variant **p_arguments, int p_argcount, godot::Variant &r_return_value, GDExtensionCallError &r_call_error) const { - std::shared_ptr wc = _workletContext.lock(); + r_call_error.error = GDEXTENSION_CALL_ERROR_INVALID_METHOD; + std::shared_ptr wc = _workletContext.lock(); if (!wc) { // Func ref no longer valid r_call_error.error = GDEXTENSION_CALL_ERROR_INVALID_METHOD; @@ -614,74 +656,15 @@ void JavascriptCallable::call(const godot::Variant **p_arguments, int p_argcount jsi::Value createNativeGodotModule(jsi::Runtime &rt, const std::shared_ptr &callInvoker) { // Perform initialization - std::shared_ptr jsCallInvoker = callInvoker; - - auto runOnJS = [jsCallInvoker](std::function &&f) { - // Run on React JS Runtime - jsCallInvoker->invokeAsync(std::move(f)); - }; - - auto runOnWorklet = [](std::function &&f) { - GodotModule::get_singleton()->runOnGodotThread(std::move(f)); - }; - - std::shared_ptr workletContext = std::make_shared( - "ReactNativeGodot", - &rt, - runOnJS, - runOnWorklet); + std::shared_ptr workletContext = + std::make_shared(&rt, callInvoker); LOGI("NativeGodotModule::createNativeModule"); - auto runOnGodotThreadFunc = [workletContext](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value { - if (!arguments[0].isObject()) { - throw jsi::JSError(runtime, "runOnGodotThread: First argument has to be a function!"); - } - - auto worklet = std::make_shared(runtime, arguments[0]); - auto workletInvoker = std::make_shared(worklet); - - auto runOnGodotCallback = jsi::Function::createFromHostFunction(runtime, - jsi::PropNameID::forAscii(runtime, "runOnGodotCallback"), - 2, - [workletInvoker, workletContext](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value { - auto resolverValue = std::make_shared((arguments[0].asObject(runtime))); - auto rejecterValue = std::make_shared((arguments[1].asObject(runtime))); - - auto resolver = [resolverValue, workletContext](std::shared_ptr wrappedValue) { - workletContext->invokeOnJsThread([resolverValue, wrappedValue](jsi::Runtime &runtime) { - auto resolverFunc = resolverValue->asObject(runtime).asFunction(runtime); - auto resultValue = wrappedValue->unwrap(runtime); - resolverFunc.call(runtime, resultValue); - }); - }; - auto rejecter = [rejecterValue, workletContext](const std::string &message) { - workletContext->invokeOnJsThread([rejecterValue, message](jsi::Runtime &runtime) { - auto rejecterFunc = rejecterValue->asObject(runtime).asFunction(runtime); - auto messageValue = jsi::String::createFromUtf8(runtime, message); - rejecterFunc.call(runtime, messageValue); - }); - }; - - workletContext->invokeOnWorkletThread([resolver, rejecter, workletInvoker](RNWorklet::JsiWorkletContext *ctx, jsi::Runtime &workletRT) { - try { - auto resultValue = workletInvoker->call(workletRT, jsi::Value::undefined(), nullptr, 0); - auto result = RNWorklet::JsiWrapper::wrap(workletRT, resultValue); - resolver(result); - } catch (std::exception &exc) { - rejecter(exc.what()); - } - }); - return jsi::Value::undefined(); - }); - - auto newPromise = runtime.global().getProperty(runtime, "Promise"); - auto promise = newPromise - .asObject(runtime) - .asFunction(runtime) - .callAsConstructor(runtime, runOnGodotCallback); - - return promise; + auto createGodotQueueFunc = [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value { + jsi::Object queue(runtime); + queue.setNativeState(runtime, std::make_shared()); + return queue; }; auto isPausedFunc = [](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { @@ -727,7 +710,7 @@ jsi::Value createNativeGodotModule(jsi::Runtime &rt, const std::shared_ptr= 2) { GodotModule *mod = GodotModule::get_singleton(); @@ -774,66 +750,10 @@ jsi::Value createNativeGodotModule(jsi::Runtime &rt, const std::shared_ptrgetWorkletRuntime(); - - jsi::Function createInstance = jsi::Function::createFromHostFunction(workletRT, jsi::PropNameID::forUtf8(workletRT, "createInstance"), - 1, - createInstanceFunc); - - jsi::Function getInstance = jsi::Function::createFromHostFunction(workletRT, jsi::PropNameID::forUtf8(workletRT, "getInstance"), - 0, - getInstanceFunc); - - jsi::Function crash = jsi::Function::createFromHostFunction(workletRT, jsi::PropNameID::forUtf8(workletRT, "crash"), - 0, - crashFunc); - - jsi::Function is_paused = jsi::Function::createFromHostFunction(workletRT, jsi::PropNameID::forUtf8(workletRT, "is_paused"), - 0, - isPausedFunc); - - jsi::Function pause = jsi::Function::createFromHostFunction(workletRT, jsi::PropNameID::forUtf8(workletRT, "pause"), - 0, - pauseFunc); - - jsi::Function resume = jsi::Function::createFromHostFunction(workletRT, jsi::PropNameID::forUtf8(workletRT, "resume"), - 0, - resumeFunc); - - jsi::Function API = jsi::Function::createFromHostFunction(workletRT, jsi::PropNameID::forUtf8(workletRT, "API"), - 0, - APIFunc); - - jsi::Function updateWindow = jsi::Function::createFromHostFunction(workletRT, jsi::PropNameID::forUtf8(workletRT, "updateWindow"), - 1, - updateWindowFunc); - - jsi::Function destroyInstance = jsi::Function::createFromHostFunction(workletRT, jsi::PropNameID::forUtf8(workletRT, "destroyInstance"), - 0, - destroyInstanceFunc); - - jsi::Object o(workletRT); - // o.setProperty(workletRT, jsi::PropNameID::forUtf8(rt, "runOnGodotThread"), runOnGodotThread); - o.setProperty(workletRT, jsi::PropNameID::forUtf8(workletRT, "createInstance"), createInstance); - o.setProperty(workletRT, jsi::PropNameID::forUtf8(workletRT, "getInstance"), getInstance); - o.setProperty(workletRT, jsi::PropNameID::forUtf8(workletRT, "API"), API); - o.setProperty(workletRT, jsi::PropNameID::forUtf8(workletRT, "updateWindow"), updateWindow); - o.setProperty(workletRT, jsi::PropNameID::forUtf8(workletRT, "is_paused"), is_paused); - o.setProperty(workletRT, jsi::PropNameID::forUtf8(workletRT, "pause"), pause); - o.setProperty(workletRT, jsi::PropNameID::forUtf8(workletRT, "resume"), resume); - o.setProperty(workletRT, jsi::PropNameID::forUtf8(workletRT, "destroyInstance"), destroyInstance); - o.setProperty(workletRT, jsi::PropNameID::forUtf8(workletRT, "crash"), crash); - - auto result = jsi::Value(workletRT, o); - workletRT.global().setProperty(workletRT, NATIVE_GODOT_MODULE_PROPERTY, result); - } - - // runOnGodotThread(run: () => T): Promise - auto runOnGodotThread = jsi::Function::createFromHostFunction(rt, - jsi::PropNameID::forAscii(rt, "runOnGodotThread"), - 1, // run - runOnGodotThreadFunc); + jsi::Function createGodotQueue = jsi::Function::createFromHostFunction(rt, + jsi::PropNameID::forUtf8(rt, "createGodotQueue"), + 0, + createGodotQueueFunc); jsi::Function createInstance = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forUtf8(rt, "createInstance"), 1, @@ -843,16 +763,12 @@ jsi::Value createNativeGodotModule(jsi::Runtime &rt, const std::shared_ptr(); -function initGodot(name) { +function initGodot(name: string) { if (RTNGodot.getInstance() != null) { - console.log("Godot was already initialized."); return; } - console.log("Initializing Godot"); runOnGodotThread(() => { - "worklet"; - console.log("Running on Godot Thread"); + 'worklet'; - if (Platform.OS === "android") { + if (Platform.OS === 'android') { RTNGodot.createInstance([ // Uncomment and fill in the correct IP address and port for debugging in the Godot Editor. // Check the documentation for the complete procedure. // "--remote-debug", // "tcp://IP_ADDRESS:6007", - "--verbose", - "--path", - "/" + name, - "--rendering-driver", - "opengl3", - "--rendering-method", - "gl_compatibility", - "--display-driver", - "embedded", + '--verbose', + '--path', + '/' + name, + '--rendering-driver', + 'opengl3', + '--rendering-method', + 'gl_compatibility', + '--display-driver', + 'embedded', ]); } else { - let args = [ + const args = [ // Uncomment and fill in the correct IP address and port for debugging in the Godot Editor. // Check the documentation for the complete procedure. // "--remote-debug", // "tcp://IP_ADDRESS:6007", - "--verbose", - "--main-pack", - FileSystem.bundleDirectory + name + ".pck", - "--display-driver", - "embedded", + '--verbose', + '--main-pack', + FileSystem.bundleDirectory + name + '.pck', + '--display-driver', + 'embedded', ]; if (Device.isDevice) { args.push( - "--rendering-driver", - "opengl3", - "--rendering-method", - "gl_compatibility" + '--rendering-driver', + 'opengl3', + '--rendering-method', + 'gl_compatibility', ); } else { args.push( - "--rendering-driver", - "metal", - "--rendering-method", - "mobile" + '--rendering-driver', + 'metal', + '--rendering-method', + 'mobile', ); } RTNGodot.createInstance(args); } - - let Godot = RTNGodot.API(); - var v = Godot.Vector2(); - v.x = 1.0; - v.y = 2.0; - console.log("Godot Engine initialized:" + v.x + "," + v.y); - var engine = Godot.Engine; - console.log("After Engine"); - var sceneTree = engine.get_main_loop(); - console.log("After Main Loop"); - var root = sceneTree.get_root(); - console.log("After Get Root"); }); } -function pauseGodot(ev: any) { +function pauseGodot() { RTNGodot.pause(); } -function resumeGodot(ev: any) { +function resumeGodot() { RTNGodot.resume(); } function destroyGodot() { runOnGodotThread(() => { - "worklet"; + 'worklet'; RTNGodot.destroyInstance(); }); } @@ -117,13 +107,13 @@ export interface AppController { } const instance = () => { - "worklet"; + 'worklet'; return RTNGodot.getInstance(); }; const appController = () => { - "worklet"; + 'worklet'; if (!instance()) return null; const Godot = RTNGodot.API(); @@ -131,105 +121,86 @@ const appController = () => { const sceneTree = engine.get_main_loop(); const root = sceneTree.get_root(); const controller = root.find_child( - "AppController", + 'AppController', true, - false + false, ) as AppController; if (!controller) return null; - if (!controller.has_connections("window_status_update")) { - controller.window_status_update.connect(function (message: string) { - console.log(message); - }); - } - return controller; }; -const App = () => { - const openSubwindow = function () { - runOnGodotThread(() => { - "worklet"; - let controller = appController(); - if (!controller) return; - controller.open_window("subwindow"); - }); - }; +function openSubwindow() { + runOnGodotThread(() => { + 'worklet'; + const controller = appController(); + if (!controller) return; + controller.open_window('subwindow'); + }); +} - const closeSubwindow = function () { - runOnGodotThread(() => { - "worklet"; - let controller = appController(); - if (!controller) return; - controller.close_window("subwindow"); - }); - }; +function closeSubwindow() { + runOnGodotThread(() => { + 'worklet'; + const controller = appController(); + if (!controller) return; + controller.close_window('subwindow'); + }); +} - const MainWindow = ({ navigation }) => { - return ( - - -