From a57a99f5bcf38d4af4466b26f1b9effe3bedd870 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Wed, 10 Jul 2024 10:52:42 -0700 Subject: [PATCH] Memory cleanup and optimizations - Returns an empty list when there's not registered plugins, thus preventing the creation of spurious iterator objects - Inline `Godot#getRotatedValues(...)` given it only had a single caller. This allows to remove the allocation of a float array on each call and replace it with float variables - Disable sensor events by default. Sensor events can fired at 10-100s Hz taking cpu and memory resources. Now the use of sensor data is behind a project setting allowing projects that have use of it to enable it, while other projects don't pay the cost for a feature they don't use - Create a pool of specialized input `Runnable` objects to prevent spurious, unbounded `Runnable` allocations - Disable showing the boot logo for Android XR projects - Delete locale references of jni strings --- core/config/project_settings.cpp | 5 + core/input/input.cpp | 33 ++ core/input/input.h | 4 + doc/classes/ProjectSettings.xml | 12 + main/main.cpp | 3 - .../lib/src/org/godotengine/godot/Godot.kt | 149 +++----- .../godotengine/godot/GodotGLRenderView.java | 14 +- .../godotengine/godot/GodotRenderView.java | 4 - .../godot/GodotVulkanRenderView.java | 14 +- .../godot/input/GodotInputHandler.java | 184 ++++++--- .../godot/input/InputEventRunnable.java | 353 ++++++++++++++++++ .../godot/plugin/GodotPluginRegistry.java | 4 + platform/android/java_godot_lib_jni.cpp | 14 +- platform/android/java_godot_wrapper.cpp | 21 +- 14 files changed, 626 insertions(+), 188 deletions(-) create mode 100644 platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 37a2608c102..acf62f97c37 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1570,6 +1570,11 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF("collada/use_ambient", false); + // Input settings + GLOBAL_DEF_BASIC("input_devices/pointing/android/enable_long_press_as_right_click", false); + GLOBAL_DEF_BASIC("input_devices/pointing/android/enable_pan_and_scale_gestures", false); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "input_devices/pointing/android/rotary_input_scroll_axis", PROPERTY_HINT_ENUM, "Horizontal,Vertical"), 1); + // These properties will not show up in the dialog. If you want to exclude whole groups, use add_hidden_prefix(). GLOBAL_DEF_INTERNAL("application/config/features", PackedStringArray()); GLOBAL_DEF_INTERNAL("internationalization/locale/translation_remaps", PackedStringArray()); diff --git a/core/input/input.cpp b/core/input/input.cpp index 91378591b0a..eba7ded267b 100644 --- a/core/input/input.cpp +++ b/core/input/input.cpp @@ -513,21 +513,49 @@ void Input::joy_connection_changed(int p_idx, bool p_connected, const String &p_ Vector3 Input::get_gravity() const { _THREAD_SAFE_METHOD_ + +#ifdef DEBUG_ENABLED + if (!gravity_enabled) { + WARN_PRINT_ONCE("`input_devices/sensors/enable_gravity` is not enabled in project settings."); + } +#endif + return gravity; } Vector3 Input::get_accelerometer() const { _THREAD_SAFE_METHOD_ + +#ifdef DEBUG_ENABLED + if (!accelerometer_enabled) { + WARN_PRINT_ONCE("`input_devices/sensors/enable_accelerometer` is not enabled in project settings."); + } +#endif + return accelerometer; } Vector3 Input::get_magnetometer() const { _THREAD_SAFE_METHOD_ + +#ifdef DEBUG_ENABLED + if (!magnetometer_enabled) { + WARN_PRINT_ONCE("`input_devices/sensors/enable_magnetometer` is not enabled in project settings."); + } +#endif + return magnetometer; } Vector3 Input::get_gyroscope() const { _THREAD_SAFE_METHOD_ + +#ifdef DEBUG_ENABLED + if (!gyroscope_enabled) { + WARN_PRINT_ONCE("`input_devices/sensors/enable_gyroscope` is not enabled in project settings."); + } +#endif + return gyroscope; } @@ -1683,6 +1711,11 @@ Input::Input() { // Always use standard behavior in the editor. legacy_just_pressed_behavior = false; } + + accelerometer_enabled = GLOBAL_DEF_RST_BASIC("input_devices/sensors/enable_accelerometer", false); + gravity_enabled = GLOBAL_DEF_RST_BASIC("input_devices/sensors/enable_gravity", false); + gyroscope_enabled = GLOBAL_DEF_RST_BASIC("input_devices/sensors/enable_gyroscope", false); + magnetometer_enabled = GLOBAL_DEF_RST_BASIC("input_devices/sensors/enable_magnetometer", false); } Input::~Input() { diff --git a/core/input/input.h b/core/input/input.h index 89e48f53d74..95dd623cc03 100644 --- a/core/input/input.h +++ b/core/input/input.h @@ -92,9 +92,13 @@ private: RBSet joy_buttons_pressed; RBMap _joy_axis; //RBMap custom_action_press; + bool gravity_enabled = false; Vector3 gravity; + bool accelerometer_enabled = false; Vector3 accelerometer; + bool magnetometer_enabled = false; Vector3 magnetometer; + bool gyroscope_enabled = false; Vector3 gyroscope; Vector2 mouse_pos; int64_t mouse_window = 0; diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 4f7f3728646..2a7b5438a8d 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1422,6 +1422,18 @@ If [code]true[/code], sends touch input events when clicking or dragging the mouse. + + If [code]true[/code], the accelerometer sensor is enabled and [method Input.get_accelerometer] returns valid data. + + + If [code]true[/code], the gravity sensor is enabled and [method Input.get_gravity] returns valid data. + + + If [code]true[/code], the gyroscope sensor is enabled and [method Input.get_gyroscope] returns valid data. + + + If [code]true[/code], the magnetometer sensor is enabled and [method Input.get_magnetometer] returns valid data. + The locale to fall back to if a translation isn't available in a given language. If left empty, [code]en[/code] (English) will be used. diff --git a/main/main.cpp b/main/main.cpp index e1d53e7f1be..6309980667d 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -2942,9 +2942,6 @@ Error Main::setup2(bool p_show_boot_logo) { id->set_emulate_mouse_from_touch(bool(GLOBAL_DEF_BASIC("input_devices/pointing/emulate_mouse_from_touch", true))); } - GLOBAL_DEF_BASIC("input_devices/pointing/android/enable_long_press_as_right_click", false); - GLOBAL_DEF_BASIC("input_devices/pointing/android/enable_pan_and_scale_gestures", false); - GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "input_devices/pointing/android/rotary_input_scroll_axis", PROPERTY_HINT_ENUM, "Horizontal,Vertical"), 1); OS::get_singleton()->benchmark_end_measure("Startup", "Setup Window and Boot"); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index fa39ccb546f..111cd484050 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -39,8 +39,6 @@ import android.content.res.Configuration import android.content.res.Resources import android.graphics.Color import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener import android.hardware.SensorManager import android.os.* import android.util.Log @@ -53,6 +51,7 @@ import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsCompat import com.google.android.vending.expansion.downloader.* import org.godotengine.godot.input.GodotEditText +import org.godotengine.godot.input.GodotInputHandler import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.file.FileAccessHandler import org.godotengine.godot.plugin.GodotPluginRegistry @@ -73,6 +72,7 @@ import java.io.InputStream import java.lang.Exception import java.security.MessageDigest import java.util.* +import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference /** @@ -81,7 +81,7 @@ import java.util.concurrent.atomic.AtomicReference * Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its * lifecycle methods are properly invoked. */ -class Godot(private val context: Context) : SensorEventListener { +class Godot(private val context: Context) { private companion object { private val TAG = Godot::class.java.simpleName @@ -99,15 +99,23 @@ class Godot(private val context: Context) : SensorEventListener { private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() } + + private val accelerometer_enabled = AtomicBoolean(false) private val mAccelerometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) } + + private val gravity_enabled = AtomicBoolean(false) private val mGravity: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) } + + private val magnetometer_enabled = AtomicBoolean(false) private val mMagnetometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) } + + private val gyroscope_enabled = AtomicBoolean(false) private val mGyroscope: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) } @@ -127,6 +135,7 @@ class Godot(private val context: Context) : SensorEventListener { val fileAccessHandler = FileAccessHandler(context) val netUtils = GodotNetUtils(context) private val commandLineFileParser = CommandLineFileParser() + private val godotInputHandler = GodotInputHandler(context, this) /** * Task to run when the engine terminates. @@ -154,6 +163,17 @@ class Godot(private val context: Context) : SensorEventListener { private var renderViewInitialized = false private var primaryHost: GodotHost? = null + /** + * Tracks whether we're in the RESUMED lifecycle state. + * See [onResume] and [onPause] + */ + private var resumed = false + + /** + * Tracks whether [onGodotSetupCompleted] fired. + */ + private val godotMainLoopStarted = AtomicBoolean(false) + var io: GodotIO? = null private var commandLine : MutableList = ArrayList() @@ -416,10 +436,10 @@ class Godot(private val context: Context) : SensorEventListener { if (!meetsVulkanRequirements(activity.packageManager)) { throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message)) } - GodotVulkanRenderView(host, this) + GodotVulkanRenderView(host, this, godotInputHandler) } else { // Fallback to openGl - GodotGLRenderView(host, this, xrMode, useDebugOpengl) + GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl) } if (host == primaryHost) { @@ -520,23 +540,13 @@ class Godot(private val context: Context) : SensorEventListener { fun onResume(host: GodotHost) { Log.v(TAG, "OnResume: $host") + resumed = true if (host != primaryHost) { return } renderView?.onActivityResumed() - if (mAccelerometer != null) { - mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) - } - if (mGravity != null) { - mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME) - } - if (mMagnetometer != null) { - mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME) - } - if (mGyroscope != null) { - mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME) - } + registerSensorsIfNeeded() if (useImmersive) { val window = requireActivity().window window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or @@ -551,14 +561,34 @@ class Godot(private val context: Context) : SensorEventListener { } } + private fun registerSensorsIfNeeded() { + if (!resumed || !godotMainLoopStarted.get()) { + return + } + + if (accelerometer_enabled.get() && mAccelerometer != null) { + mSensorManager.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) + } + if (gravity_enabled.get() && mGravity != null) { + mSensorManager.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME) + } + if (magnetometer_enabled.get() && mMagnetometer != null) { + mSensorManager.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME) + } + if (gyroscope_enabled.get() && mGyroscope != null) { + mSensorManager.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME) + } + } + fun onPause(host: GodotHost) { Log.v(TAG, "OnPause: $host") + resumed = false if (host != primaryHost) { return } renderView?.onActivityPaused() - mSensorManager.unregisterListener(this) + mSensorManager.unregisterListener(godotInputHandler) for (plugin in pluginRegistry.allPlugins) { plugin.onMainPause() } @@ -659,6 +689,16 @@ class Godot(private val context: Context) : SensorEventListener { */ private fun onGodotMainLoopStarted() { Log.v(TAG, "OnGodotMainLoopStarted") + godotMainLoopStarted.set(true) + + accelerometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_accelerometer"))) + gravity_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gravity"))) + gyroscope_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gyroscope"))) + magnetometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer"))) + + runOnUiThread { + registerSensorsIfNeeded() + } for (plugin in pluginRegistry.allPlugins) { plugin.onGodotMainLoopStarted() @@ -858,77 +898,6 @@ class Godot(private val context: Context) : SensorEventListener { } } - private fun getRotatedValues(values: FloatArray?): FloatArray? { - if (values == null || values.size != 3) { - return null - } - val rotatedValues = FloatArray(3) - when (windowManager.defaultDisplay.rotation) { - Surface.ROTATION_0 -> { - rotatedValues[0] = values[0] - rotatedValues[1] = values[1] - rotatedValues[2] = values[2] - } - Surface.ROTATION_90 -> { - rotatedValues[0] = -values[1] - rotatedValues[1] = values[0] - rotatedValues[2] = values[2] - } - Surface.ROTATION_180 -> { - rotatedValues[0] = -values[0] - rotatedValues[1] = -values[1] - rotatedValues[2] = values[2] - } - Surface.ROTATION_270 -> { - rotatedValues[0] = values[1] - rotatedValues[1] = -values[0] - rotatedValues[2] = values[2] - } - } - return rotatedValues - } - - override fun onSensorChanged(event: SensorEvent) { - if (renderView == null) { - return - } - - val rotatedValues = getRotatedValues(event.values) - - when (event.sensor.type) { - Sensor.TYPE_ACCELEROMETER -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.accelerometer(-it[0], -it[1], -it[2]) - } - } - } - Sensor.TYPE_GRAVITY -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.gravity(-it[0], -it[1], -it[2]) - } - } - } - Sensor.TYPE_MAGNETIC_FIELD -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.magnetometer(-it[0], -it[1], -it[2]) - } - } - } - Sensor.TYPE_GYROSCOPE -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.gyroscope(it[0], it[1], it[2]) - } - } - } - } - } - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} - /** * Used by the native code (java_godot_wrapper.h) to vibrate the device. * @param durationMs @@ -1063,7 +1032,7 @@ class Godot(private val context: Context) : SensorEventListener { @Keep private fun initInputDevices() { - renderView?.initInputDevices() + godotInputHandler.initInputDevices() } @Keep diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index 7fbdb34047b..15a811ce833 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -83,12 +83,12 @@ class GodotGLRenderView extends GLSurfaceView implements GodotRenderView { private final GodotRenderer godotRenderer; private final SparseArray customPointerIcons = new SparseArray<>(); - public GodotGLRenderView(GodotHost host, Godot godot, XRMode xrMode, boolean useDebugOpengl) { + public GodotGLRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl) { super(host.getActivity()); this.host = host; this.godot = godot; - this.inputHandler = new GodotInputHandler(this); + this.inputHandler = inputHandler; this.godotRenderer = new GodotRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); @@ -101,11 +101,6 @@ class GodotGLRenderView extends GLSurfaceView implements GodotRenderView { return this; } - @Override - public void initInputDevices() { - this.inputHandler.initInputDevices(); - } - @Override public void queueOnRenderThread(Runnable event) { queueEvent(event); @@ -144,11 +139,6 @@ class GodotGLRenderView extends GLSurfaceView implements GodotRenderView { requestRenderThreadExitAndWait(); } - @Override - public void onBackPressed() { - godot.onBackPressed(); - } - @Override public GodotInputHandler getInputHandler() { return inputHandler; diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java index 19ec0fd1a4a..30821eaa8e1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -37,8 +37,6 @@ import android.view.SurfaceView; public interface GodotRenderView { SurfaceView getView(); - void initInputDevices(); - /** * Starts the thread that will drive Godot's rendering. */ @@ -59,8 +57,6 @@ public interface GodotRenderView { void onActivityDestroyed(); - void onBackPressed(); - GodotInputHandler getInputHandler(); void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index f4411ddf2c3..d5b05913d8d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -57,12 +57,12 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { private final VkRenderer mRenderer; private final SparseArray customPointerIcons = new SparseArray<>(); - public GodotVulkanRenderView(GodotHost host, Godot godot) { + public GodotVulkanRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler) { super(host.getActivity()); this.host = host; this.godot = godot; - mInputHandler = new GodotInputHandler(this); + mInputHandler = inputHandler; mRenderer = new VkRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); @@ -80,11 +80,6 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { return this; } - @Override - public void initInputDevices() { - mInputHandler.initInputDevices(); - } - @Override public void queueOnRenderThread(Runnable event) { queueOnVkThread(event); @@ -123,11 +118,6 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { requestRenderThreadExitAndWait(); } - @Override - public void onBackPressed() { - godot.onBackPressed(); - } - @Override public GodotInputHandler getInputHandler() { return mInputHandler; diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index 273774a33d1..fb41cd00c02 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -32,10 +32,14 @@ package org.godotengine.godot.input; import static org.godotengine.godot.utils.GLUtils.DEBUG; +import org.godotengine.godot.Godot; import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotRenderView; import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; import android.hardware.input.InputManager; import android.os.Build; import android.util.Log; @@ -46,6 +50,10 @@ import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ScaleGestureDetector; +import android.view.Surface; +import android.view.WindowManager; + +import androidx.annotation.NonNull; import java.util.Collections; import java.util.HashSet; @@ -54,7 +62,7 @@ import java.util.Set; /** * Handles input related events for the {@link GodotRenderView} view. */ -public class GodotInputHandler implements InputManager.InputDeviceListener { +public class GodotInputHandler implements InputManager.InputDeviceListener, SensorEventListener { private static final String TAG = GodotInputHandler.class.getSimpleName(); private static final int ROTARY_INPUT_VERTICAL_AXIS = 1; @@ -64,8 +72,9 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { private final SparseArray mJoysticksDevices = new SparseArray<>(4); private final HashSet mHardwareKeyboardIds = new HashSet<>(); - private final GodotRenderView mRenderView; + private final Godot godot; private final InputManager mInputManager; + private final WindowManager windowManager; private final GestureDetector gestureDetector; private final ScaleGestureDetector scaleGestureDetector; private final GodotGestureHandler godotGestureHandler; @@ -77,12 +86,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { private int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS; - public GodotInputHandler(GodotRenderView godotView) { - final Context context = godotView.getView().getContext(); - mRenderView = godotView; + public GodotInputHandler(Context context, Godot godot) { + this.godot = godot; mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); mInputManager.registerInputDeviceListener(this, null); + windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + this.godotGestureHandler = new GodotGestureHandler(this); this.gestureDetector = new GestureDetector(context, godotGestureHandler); this.gestureDetector.setIsLongpressEnabled(false); @@ -174,7 +184,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { public boolean onKeyDown(final int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { - mRenderView.onBackPressed(); + godot.onBackPressed(); // press 'back' button should not terminate program //normal handle 'back' event in game logic return true; @@ -507,7 +517,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return handleTouchEvent(event, eventActionOverride, doubleTap); } - private static float getEventTiltX(MotionEvent event) { + static float getEventTiltX(MotionEvent event) { // Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise. final float orientation = event.getOrientation(); @@ -520,7 +530,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return (float)-Math.sin(orientation) * tiltMult; } - private static float getEventTiltY(MotionEvent event) { + static float getEventTiltY(MotionEvent event) { // Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise. final float orientation = event.getOrientation(); @@ -579,6 +589,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return false; + } + // Fix the buttonsMask switch (eventAction) { case MotionEvent.ACTION_CANCEL: @@ -594,7 +609,6 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { break; } - final int updatedButtonsMask = buttonsMask; // We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically // follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate // stream of events to the engine. @@ -607,11 +621,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_SCROLL: { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY)); - } else { - GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); - } + runnable.setMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); + dispatchInputEventRunnable(runnable); return true; } } @@ -627,22 +638,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { - final int pointerCount = event.getPointerCount(); - if (pointerCount == 0) { + if (event.getPointerCount() == 0) { return true; } - final float[] positions = new float[pointerCount * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc... - - for (int i = 0; i < pointerCount; i++) { - positions[i * 6 + 0] = event.getPointerId(i); - positions[i * 6 + 1] = event.getX(i); - positions[i * 6 + 2] = event.getY(i); - positions[i * 6 + 3] = event.getPressure(i); - positions[i * 6 + 4] = getEventTiltX(event); - positions[i * 6 + 5] = getEventTiltY(event); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return false; } - final int actionPointerId = event.getPointerId(event.getActionIndex()); switch (eventActionOverride) { case MotionEvent.ACTION_DOWN: @@ -651,11 +654,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_POINTER_DOWN: { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap)); - } else { - GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap); - } + runnable.setTouchEvent(event, eventActionOverride, doubleTap); + dispatchInputEventRunnable(runnable); return true; } } @@ -663,58 +663,128 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } void handleMagnifyEvent(float x, float y, float factor) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.magnify(x, y, factor)); - } else { - GodotLib.magnify(x, y, factor); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setMagnifyEvent(x, y, factor); + dispatchInputEventRunnable(runnable); } void handlePanEvent(float x, float y, float deltaX, float deltaY) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.pan(x, y, deltaX, deltaY)); - } else { - GodotLib.pan(x, y, deltaX, deltaY); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setPanEvent(x, y, deltaX, deltaY); + dispatchInputEventRunnable(runnable); } private void handleJoystickButtonEvent(int device, int button, boolean pressed) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.joybutton(device, button, pressed)); - } else { - GodotLib.joybutton(device, button, pressed); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setJoystickButtonEvent(device, button, pressed); + dispatchInputEventRunnable(runnable); } private void handleJoystickAxisEvent(int device, int axis, float value) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.joyaxis(device, axis, value)); - } else { - GodotLib.joyaxis(device, axis, value); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setJoystickAxisEvent(device, axis, value); + dispatchInputEventRunnable(runnable); } private void handleJoystickHatEvent(int device, int hatX, int hatY) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.joyhat(device, hatX, hatY)); - } else { - GodotLib.joyhat(device, hatX, hatY); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setJoystickHatEvent(device, hatX, hatY); + dispatchInputEventRunnable(runnable); } private void handleJoystickConnectionChangedEvent(int device, boolean connected, String name) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.joyconnectionchanged(device, connected, name)); - } else { - GodotLib.joyconnectionchanged(device, connected, name); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setJoystickConnectionChangedEvent(device, connected, name); + dispatchInputEventRunnable(runnable); } void handleKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + runnable.setKeyEvent(physicalKeycode, unicode, keyLabel, pressed, echo); + dispatchInputEventRunnable(runnable); + } + + private void dispatchInputEventRunnable(@NonNull InputEventRunnable runnable) { if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo)); + godot.runOnRenderThread(runnable); } else { - GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo); + runnable.run(); } } + + @Override + public void onSensorChanged(SensorEvent event) { + final float[] values = event.values; + if (values == null || values.length != 3) { + return; + } + + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + float rotatedValue0 = 0f; + float rotatedValue1 = 0f; + float rotatedValue2 = 0f; + switch (windowManager.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + rotatedValue0 = values[0]; + rotatedValue1 = values[1]; + rotatedValue2 = values[2]; + break; + + case Surface.ROTATION_90: + rotatedValue0 = -values[1]; + rotatedValue1 = values[0]; + rotatedValue2 = values[2]; + break; + + case Surface.ROTATION_180: + rotatedValue0 = -values[0]; + rotatedValue1 = -values[1]; + rotatedValue2 = values[2]; + break; + + case Surface.ROTATION_270: + rotatedValue0 = values[1]; + rotatedValue1 = -values[0]; + rotatedValue2 = values[2]; + break; + } + + runnable.setSensorEvent(event.sensor.getType(), rotatedValue0, rotatedValue1, rotatedValue2); + godot.runOnRenderThread(runnable); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java new file mode 100644 index 00000000000..a282791b2e2 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java @@ -0,0 +1,353 @@ +/**************************************************************************/ +/* InputEventRunnable.java */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.input; + +import org.godotengine.godot.GodotLib; + +import android.hardware.Sensor; +import android.util.Log; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pools; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Used to dispatch input events. + * + * This is a specialized version of @{@link Runnable} which allows to allocate a finite pool of + * objects for input events dispatching, thus avoid the creation (and garbage collection) of + * spurious @{@link Runnable} objects. + */ +final class InputEventRunnable implements Runnable { + private static final String TAG = InputEventRunnable.class.getSimpleName(); + + private static final int MAX_TOUCH_POINTER_COUNT = 10; // assuming 10 fingers as max supported concurrent touch pointers + + private static final Pools.Pool POOL = new Pools.Pool<>() { + private static final int MAX_POOL_SIZE = 120 * 10; // up to 120Hz input events rate for up to 5 secs (ANR limit) * 2 + + private final ArrayBlockingQueue queue = new ArrayBlockingQueue<>(MAX_POOL_SIZE); + private final AtomicInteger createdCount = new AtomicInteger(); + + @Nullable + @Override + public InputEventRunnable acquire() { + InputEventRunnable instance = queue.poll(); + if (instance == null) { + int creationCount = createdCount.incrementAndGet(); + if (creationCount <= MAX_POOL_SIZE) { + instance = new InputEventRunnable(creationCount - 1); + } + } + + return instance; + } + + @Override + public boolean release(@NonNull InputEventRunnable instance) { + return queue.offer(instance); + } + }; + + @Nullable + static InputEventRunnable obtain() { + InputEventRunnable runnable = POOL.acquire(); + if (runnable == null) { + Log.w(TAG, "Input event pool is at capacity"); + } + return runnable; + } + + /** + * Used to track when this instance was created and added to the pool. Primarily used for + * debug purposes. + */ + private final int creationRank; + + private InputEventRunnable(int creationRank) { + this.creationRank = creationRank; + } + + /** + * Set of supported input events. + */ + private enum EventType { + MOUSE, + TOUCH, + MAGNIFY, + PAN, + JOYSTICK_BUTTON, + JOYSTICK_AXIS, + JOYSTICK_HAT, + JOYSTICK_CONNECTION_CHANGED, + KEY, + SENSOR + } + + private EventType currentEventType = null; + + // common event fields + private float eventX; + private float eventY; + private float eventDeltaX; + private float eventDeltaY; + private boolean eventPressed; + + // common touch / mouse fields + private int eventAction; + private boolean doubleTap; + + // Mouse event fields and setter + private int buttonsMask; + private boolean sourceMouseRelative; + private float pressure; + private float tiltX; + private float tiltY; + void setMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { + this.currentEventType = EventType.MOUSE; + this.eventAction = eventAction; + this.buttonsMask = buttonsMask; + this.eventX = x; + this.eventY = y; + this.eventDeltaX = deltaX; + this.eventDeltaY = deltaY; + this.doubleTap = doubleClick; + this.sourceMouseRelative = sourceMouseRelative; + this.pressure = pressure; + this.tiltX = tiltX; + this.tiltY = tiltY; + } + + // Touch event fields and setter + private int actionPointerId; + private int pointerCount; + private final float[] positions = new float[MAX_TOUCH_POINTER_COUNT * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc... + void setTouchEvent(MotionEvent event, int eventAction, boolean doubleTap) { + this.currentEventType = EventType.TOUCH; + this.eventAction = eventAction; + this.doubleTap = doubleTap; + this.actionPointerId = event.getPointerId(event.getActionIndex()); + this.pointerCount = Math.min(event.getPointerCount(), MAX_TOUCH_POINTER_COUNT); + for (int i = 0; i < pointerCount; i++) { + positions[i * 6 + 0] = event.getPointerId(i); + positions[i * 6 + 1] = event.getX(i); + positions[i * 6 + 2] = event.getY(i); + positions[i * 6 + 3] = event.getPressure(i); + positions[i * 6 + 4] = GodotInputHandler.getEventTiltX(event); + positions[i * 6 + 5] = GodotInputHandler.getEventTiltY(event); + } + } + + // Magnify event fields and setter + private float magnifyFactor; + void setMagnifyEvent(float x, float y, float factor) { + this.currentEventType = EventType.MAGNIFY; + this.eventX = x; + this.eventY = y; + this.magnifyFactor = factor; + } + + // Pan event setter + void setPanEvent(float x, float y, float deltaX, float deltaY) { + this.currentEventType = EventType.PAN; + this.eventX = x; + this.eventY = y; + this.eventDeltaX = deltaX; + this.eventDeltaY = deltaY; + } + + // common joystick field + private int joystickDevice; + + // Joystick button event fields and setter + private int button; + void setJoystickButtonEvent(int device, int button, boolean pressed) { + this.currentEventType = EventType.JOYSTICK_BUTTON; + this.joystickDevice = device; + this.button = button; + this.eventPressed = pressed; + } + + // Joystick axis event fields and setter + private int axis; + private float value; + void setJoystickAxisEvent(int device, int axis, float value) { + this.currentEventType = EventType.JOYSTICK_AXIS; + this.joystickDevice = device; + this.axis = axis; + this.value = value; + } + + // Joystick hat event fields and setter + private int hatX; + private int hatY; + void setJoystickHatEvent(int device, int hatX, int hatY) { + this.currentEventType = EventType.JOYSTICK_HAT; + this.joystickDevice = device; + this.hatX = hatX; + this.hatY = hatY; + } + + // Joystick connection changed event fields and setter + private boolean connected; + private String joystickName; + void setJoystickConnectionChangedEvent(int device, boolean connected, String name) { + this.currentEventType = EventType.JOYSTICK_CONNECTION_CHANGED; + this.joystickDevice = device; + this.connected = connected; + this.joystickName = name; + } + + // Key event fields and setter + private int physicalKeycode; + private int unicode; + private int keyLabel; + private boolean echo; + void setKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) { + this.currentEventType = EventType.KEY; + this.physicalKeycode = physicalKeycode; + this.unicode = unicode; + this.keyLabel = keyLabel; + this.eventPressed = pressed; + this.echo = echo; + } + + // Sensor event fields and setter + private int sensorType; + private float rotatedValue0; + private float rotatedValue1; + private float rotatedValue2; + void setSensorEvent(int sensorType, float rotatedValue0, float rotatedValue1, float rotatedValue2) { + this.currentEventType = EventType.SENSOR; + this.sensorType = sensorType; + this.rotatedValue0 = rotatedValue0; + this.rotatedValue1 = rotatedValue1; + this.rotatedValue2 = rotatedValue2; + } + + @Override + public void run() { + try { + if (currentEventType == null) { + Log.w(TAG, "Invalid event type"); + return; + } + + switch (currentEventType) { + case MOUSE: + GodotLib.dispatchMouseEvent( + eventAction, + buttonsMask, + eventX, + eventY, + eventDeltaX, + eventDeltaY, + doubleTap, + sourceMouseRelative, + pressure, + tiltX, + tiltY); + break; + + case TOUCH: + GodotLib.dispatchTouchEvent( + eventAction, + actionPointerId, + pointerCount, + positions, + doubleTap); + break; + + case MAGNIFY: + GodotLib.magnify(eventX, eventY, magnifyFactor); + break; + + case PAN: + GodotLib.pan(eventX, eventY, eventDeltaX, eventDeltaY); + break; + + case JOYSTICK_BUTTON: + GodotLib.joybutton(joystickDevice, button, eventPressed); + break; + + case JOYSTICK_AXIS: + GodotLib.joyaxis(joystickDevice, axis, value); + break; + + case JOYSTICK_HAT: + GodotLib.joyhat(joystickDevice, hatX, hatY); + break; + + case JOYSTICK_CONNECTION_CHANGED: + GodotLib.joyconnectionchanged(joystickDevice, connected, joystickName); + break; + + case KEY: + GodotLib.key(physicalKeycode, unicode, keyLabel, eventPressed, echo); + break; + + case SENSOR: + switch (sensorType) { + case Sensor.TYPE_ACCELEROMETER: + GodotLib.accelerometer(-rotatedValue0, -rotatedValue1, -rotatedValue2); + break; + + case Sensor.TYPE_GRAVITY: + GodotLib.gravity(-rotatedValue0, -rotatedValue1, -rotatedValue2); + break; + + case Sensor.TYPE_MAGNETIC_FIELD: + GodotLib.magnetometer(-rotatedValue0, -rotatedValue1, -rotatedValue2); + break; + + case Sensor.TYPE_GYROSCOPE: + GodotLib.gyroscope(rotatedValue0, rotatedValue1, rotatedValue2); + break; + } + break; + } + } finally { + recycle(); + } + } + + /** + * Release the current instance back to the pool + */ + private void recycle() { + currentEventType = null; + POOL.release(this); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java index 711bca02e7d..8976dd65db6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java @@ -43,6 +43,7 @@ import androidx.annotation.Nullable; import java.lang.reflect.Constructor; import java.util.Collection; +import java.util.Collections; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -82,6 +83,9 @@ public final class GodotPluginRegistry { * Retrieve the full set of loaded plugins. */ public Collection getAllPlugins() { + if (registry.isEmpty()) { + return Collections.emptyList(); + } return registry.values(); } diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index fec317ecb88..f491574f515 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -51,6 +51,7 @@ #include "core/config/project_settings.h" #include "core/input/input.h" #include "main/main.h" +#include "servers/xr_server.h" #ifdef TOOLS_ENABLED #include "editor/editor_settings.h" @@ -266,7 +267,18 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, } if (step.get() == STEP_SHOW_LOGO) { - Main::setup_boot_logo(); + bool xr_enabled; + if (XRServer::get_xr_mode() == XRServer::XRMODE_DEFAULT) { + xr_enabled = GLOBAL_GET("xr/shaders/enabled"); + } else { + xr_enabled = XRServer::get_xr_mode() == XRServer::XRMODE_ON; + } + // Unlike PCVR, there's no additional 2D screen onto which to render the boot logo, + // so we skip this step if xr is enabled. + if (!xr_enabled) { + Main::setup_boot_logo(); + } + step.increment(); return true; } diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 70ea4b09c10..91bf7b48a6f 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -213,25 +213,27 @@ bool GodotJavaWrapper::has_get_clipboard() { } String GodotJavaWrapper::get_clipboard() { + String clipboard; if (_get_clipboard) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_clipboard); - return jstring_to_string(s, env); - } else { - return String(); + clipboard = jstring_to_string(s, env); + env->DeleteLocalRef(s); } + return clipboard; } String GodotJavaWrapper::get_input_fallback_mapping() { + String input_fallback_mapping; if (_get_input_fallback_mapping) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, String()); jstring fallback_mapping = (jstring)env->CallObjectMethod(godot_instance, _get_input_fallback_mapping); - return jstring_to_string(fallback_mapping, env); - } else { - return String(); + input_fallback_mapping = jstring_to_string(fallback_mapping, env); + env->DeleteLocalRef(fallback_mapping); } + return input_fallback_mapping; } bool GodotJavaWrapper::has_set_clipboard() { @@ -324,14 +326,15 @@ Vector GodotJavaWrapper::get_gdextension_list_config_file() const { } String GodotJavaWrapper::get_ca_certificates() const { + String ca_certificates; if (_get_ca_certificates) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_ca_certificates); - return jstring_to_string(s, env); - } else { - return String(); + ca_certificates = jstring_to_string(s, env); + env->DeleteLocalRef(s); } + return ca_certificates; } void GodotJavaWrapper::init_input_devices() {