Browse Source

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
Fredia Huya-Kouadio 1 year ago
parent
commit
a57a99f5bc

+ 5 - 0
core/config/project_settings.cpp

@@ -1570,6 +1570,11 @@ ProjectSettings::ProjectSettings() {
 
 
 	GLOBAL_DEF("collada/use_ambient", false);
 	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().
 	// 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("application/config/features", PackedStringArray());
 	GLOBAL_DEF_INTERNAL("internationalization/locale/translation_remaps", PackedStringArray());
 	GLOBAL_DEF_INTERNAL("internationalization/locale/translation_remaps", PackedStringArray());

+ 33 - 0
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 {
 Vector3 Input::get_gravity() const {
 	_THREAD_SAFE_METHOD_
 	_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;
 	return gravity;
 }
 }
 
 
 Vector3 Input::get_accelerometer() const {
 Vector3 Input::get_accelerometer() const {
 	_THREAD_SAFE_METHOD_
 	_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;
 	return accelerometer;
 }
 }
 
 
 Vector3 Input::get_magnetometer() const {
 Vector3 Input::get_magnetometer() const {
 	_THREAD_SAFE_METHOD_
 	_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;
 	return magnetometer;
 }
 }
 
 
 Vector3 Input::get_gyroscope() const {
 Vector3 Input::get_gyroscope() const {
 	_THREAD_SAFE_METHOD_
 	_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;
 	return gyroscope;
 }
 }
 
 
@@ -1683,6 +1711,11 @@ Input::Input() {
 		// Always use standard behavior in the editor.
 		// Always use standard behavior in the editor.
 		legacy_just_pressed_behavior = false;
 		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() {
 Input::~Input() {

+ 4 - 0
core/input/input.h

@@ -92,9 +92,13 @@ private:
 	RBSet<JoyButton> joy_buttons_pressed;
 	RBSet<JoyButton> joy_buttons_pressed;
 	RBMap<JoyAxis, float> _joy_axis;
 	RBMap<JoyAxis, float> _joy_axis;
 	//RBMap<StringName,int> custom_action_press;
 	//RBMap<StringName,int> custom_action_press;
+	bool gravity_enabled = false;
 	Vector3 gravity;
 	Vector3 gravity;
+	bool accelerometer_enabled = false;
 	Vector3 accelerometer;
 	Vector3 accelerometer;
+	bool magnetometer_enabled = false;
 	Vector3 magnetometer;
 	Vector3 magnetometer;
+	bool gyroscope_enabled = false;
 	Vector3 gyroscope;
 	Vector3 gyroscope;
 	Vector2 mouse_pos;
 	Vector2 mouse_pos;
 	int64_t mouse_window = 0;
 	int64_t mouse_window = 0;

+ 12 - 0
doc/classes/ProjectSettings.xml

@@ -1422,6 +1422,18 @@
 		<member name="input_devices/pointing/emulate_touch_from_mouse" type="bool" setter="" getter="" default="false">
 		<member name="input_devices/pointing/emulate_touch_from_mouse" type="bool" setter="" getter="" default="false">
 			If [code]true[/code], sends touch input events when clicking or dragging the mouse.
 			If [code]true[/code], sends touch input events when clicking or dragging the mouse.
 		</member>
 		</member>
+		<member name="input_devices/sensors/enable_accelerometer" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], the accelerometer sensor is enabled and [method Input.get_accelerometer] returns valid data.
+		</member>
+		<member name="input_devices/sensors/enable_gravity" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], the gravity sensor is enabled and [method Input.get_gravity] returns valid data.
+		</member>
+		<member name="input_devices/sensors/enable_gyroscope" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], the gyroscope sensor is enabled and [method Input.get_gyroscope] returns valid data.
+		</member>
+		<member name="input_devices/sensors/enable_magnetometer" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], the magnetometer sensor is enabled and [method Input.get_magnetometer] returns valid data.
+		</member>
 		<member name="internationalization/locale/fallback" type="String" setter="" getter="" default="&quot;en&quot;">
 		<member name="internationalization/locale/fallback" type="String" setter="" getter="" default="&quot;en&quot;">
 			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.
 			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.
 		</member>
 		</member>

+ 0 - 3
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)));
 			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");
 		OS::get_singleton()->benchmark_end_measure("Startup", "Setup Window and Boot");
 	}
 	}
 
 

+ 59 - 90
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.content.res.Resources
 import android.graphics.Color
 import android.graphics.Color
 import android.hardware.Sensor
 import android.hardware.Sensor
-import android.hardware.SensorEvent
-import android.hardware.SensorEventListener
 import android.hardware.SensorManager
 import android.hardware.SensorManager
 import android.os.*
 import android.os.*
 import android.util.Log
 import android.util.Log
@@ -53,6 +51,7 @@ import androidx.core.view.WindowInsetsAnimationCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsCompat
 import com.google.android.vending.expansion.downloader.*
 import com.google.android.vending.expansion.downloader.*
 import org.godotengine.godot.input.GodotEditText
 import org.godotengine.godot.input.GodotEditText
+import org.godotengine.godot.input.GodotInputHandler
 import org.godotengine.godot.io.directory.DirectoryAccessHandler
 import org.godotengine.godot.io.directory.DirectoryAccessHandler
 import org.godotengine.godot.io.file.FileAccessHandler
 import org.godotengine.godot.io.file.FileAccessHandler
 import org.godotengine.godot.plugin.GodotPluginRegistry
 import org.godotengine.godot.plugin.GodotPluginRegistry
@@ -73,6 +72,7 @@ import java.io.InputStream
 import java.lang.Exception
 import java.lang.Exception
 import java.security.MessageDigest
 import java.security.MessageDigest
 import java.util.*
 import java.util.*
+import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicReference
 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
  * Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its
  * lifecycle methods are properly invoked.
  * lifecycle methods are properly invoked.
  */
  */
-class Godot(private val context: Context) : SensorEventListener {
+class Godot(private val context: Context) {
 
 
 	private companion object {
 	private companion object {
 		private val TAG = Godot::class.java.simpleName
 		private val TAG = Godot::class.java.simpleName
@@ -99,15 +99,23 @@ class Godot(private val context: Context) : SensorEventListener {
 	private val pluginRegistry: GodotPluginRegistry by lazy {
 	private val pluginRegistry: GodotPluginRegistry by lazy {
 		GodotPluginRegistry.getPluginRegistry()
 		GodotPluginRegistry.getPluginRegistry()
 	}
 	}
+
+	private val accelerometer_enabled = AtomicBoolean(false)
 	private val mAccelerometer: Sensor? by lazy {
 	private val mAccelerometer: Sensor? by lazy {
 		mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
 		mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
 	}
 	}
+
+	private val gravity_enabled = AtomicBoolean(false)
 	private val mGravity: Sensor? by lazy {
 	private val mGravity: Sensor? by lazy {
 		mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
 		mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
 	}
 	}
+
+	private val magnetometer_enabled = AtomicBoolean(false)
 	private val mMagnetometer: Sensor? by lazy {
 	private val mMagnetometer: Sensor? by lazy {
 		mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
 		mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
 	}
 	}
+
+	private val gyroscope_enabled = AtomicBoolean(false)
 	private val mGyroscope: Sensor? by lazy {
 	private val mGyroscope: Sensor? by lazy {
 		mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
 		mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
 	}
 	}
@@ -127,6 +135,7 @@ class Godot(private val context: Context) : SensorEventListener {
 	val fileAccessHandler = FileAccessHandler(context)
 	val fileAccessHandler = FileAccessHandler(context)
 	val netUtils = GodotNetUtils(context)
 	val netUtils = GodotNetUtils(context)
 	private val commandLineFileParser = CommandLineFileParser()
 	private val commandLineFileParser = CommandLineFileParser()
+	private val godotInputHandler = GodotInputHandler(context, this)
 
 
 	/**
 	/**
 	 * Task to run when the engine terminates.
 	 * Task to run when the engine terminates.
@@ -154,6 +163,17 @@ class Godot(private val context: Context) : SensorEventListener {
 	private var renderViewInitialized = false
 	private var renderViewInitialized = false
 	private var primaryHost: GodotHost? = null
 	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
 	var io: GodotIO? = null
 
 
 	private var commandLine : MutableList<String> = ArrayList<String>()
 	private var commandLine : MutableList<String> = ArrayList<String>()
@@ -416,10 +436,10 @@ class Godot(private val context: Context) : SensorEventListener {
 				if (!meetsVulkanRequirements(activity.packageManager)) {
 				if (!meetsVulkanRequirements(activity.packageManager)) {
 					throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message))
 					throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message))
 				}
 				}
-				GodotVulkanRenderView(host, this)
+				GodotVulkanRenderView(host, this, godotInputHandler)
 			} else {
 			} else {
 				// Fallback to openGl
 				// Fallback to openGl
-				GodotGLRenderView(host, this, xrMode, useDebugOpengl)
+				GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl)
 			}
 			}
 
 
 			if (host == primaryHost) {
 			if (host == primaryHost) {
@@ -520,23 +540,13 @@ class Godot(private val context: Context) : SensorEventListener {
 
 
 	fun onResume(host: GodotHost) {
 	fun onResume(host: GodotHost) {
 		Log.v(TAG, "OnResume: $host")
 		Log.v(TAG, "OnResume: $host")
+		resumed = true
 		if (host != primaryHost) {
 		if (host != primaryHost) {
 			return
 			return
 		}
 		}
 
 
 		renderView?.onActivityResumed()
 		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) {
 		if (useImmersive) {
 			val window = requireActivity().window
 			val window = requireActivity().window
 			window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
 			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) {
 	fun onPause(host: GodotHost) {
 		Log.v(TAG, "OnPause: $host")
 		Log.v(TAG, "OnPause: $host")
+		resumed = false
 		if (host != primaryHost) {
 		if (host != primaryHost) {
 			return
 			return
 		}
 		}
 
 
 		renderView?.onActivityPaused()
 		renderView?.onActivityPaused()
-		mSensorManager.unregisterListener(this)
+		mSensorManager.unregisterListener(godotInputHandler)
 		for (plugin in pluginRegistry.allPlugins) {
 		for (plugin in pluginRegistry.allPlugins) {
 			plugin.onMainPause()
 			plugin.onMainPause()
 		}
 		}
@@ -659,6 +689,16 @@ class Godot(private val context: Context) : SensorEventListener {
 	 */
 	 */
 	private fun onGodotMainLoopStarted() {
 	private fun onGodotMainLoopStarted() {
 		Log.v(TAG, "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) {
 		for (plugin in pluginRegistry.allPlugins) {
 			plugin.onGodotMainLoopStarted()
 			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.
 	 * Used by the native code (java_godot_wrapper.h) to vibrate the device.
 	 * @param durationMs
 	 * @param durationMs
@@ -1063,7 +1032,7 @@ class Godot(private val context: Context) : SensorEventListener {
 
 
 	@Keep
 	@Keep
 	private fun initInputDevices() {
 	private fun initInputDevices() {
-		renderView?.initInputDevices()
+		godotInputHandler.initInputDevices()
 	}
 	}
 
 
 	@Keep
 	@Keep

+ 2 - 12
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 GodotRenderer godotRenderer;
 	private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
 	private final SparseArray<PointerIcon> 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());
 		super(host.getActivity());
 
 
 		this.host = host;
 		this.host = host;
 		this.godot = godot;
 		this.godot = godot;
-		this.inputHandler = new GodotInputHandler(this);
+		this.inputHandler = inputHandler;
 		this.godotRenderer = new GodotRenderer();
 		this.godotRenderer = new GodotRenderer();
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 			setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
 			setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
@@ -101,11 +101,6 @@ class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
 		return this;
 		return this;
 	}
 	}
 
 
-	@Override
-	public void initInputDevices() {
-		this.inputHandler.initInputDevices();
-	}
-
 	@Override
 	@Override
 	public void queueOnRenderThread(Runnable event) {
 	public void queueOnRenderThread(Runnable event) {
 		queueEvent(event);
 		queueEvent(event);
@@ -144,11 +139,6 @@ class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
 		requestRenderThreadExitAndWait();
 		requestRenderThreadExitAndWait();
 	}
 	}
 
 
-	@Override
-	public void onBackPressed() {
-		godot.onBackPressed();
-	}
-
 	@Override
 	@Override
 	public GodotInputHandler getInputHandler() {
 	public GodotInputHandler getInputHandler() {
 		return inputHandler;
 		return inputHandler;

+ 0 - 4
platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java

@@ -37,8 +37,6 @@ import android.view.SurfaceView;
 public interface GodotRenderView {
 public interface GodotRenderView {
 	SurfaceView getView();
 	SurfaceView getView();
 
 
-	void initInputDevices();
-
 	/**
 	/**
 	 * Starts the thread that will drive Godot's rendering.
 	 * Starts the thread that will drive Godot's rendering.
 	 */
 	 */
@@ -59,8 +57,6 @@ public interface GodotRenderView {
 
 
 	void onActivityDestroyed();
 	void onActivityDestroyed();
 
 
-	void onBackPressed();
-
 	GodotInputHandler getInputHandler();
 	GodotInputHandler getInputHandler();
 
 
 	void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY);
 	void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY);

+ 2 - 12
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 VkRenderer mRenderer;
 	private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
 	private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
 
 
-	public GodotVulkanRenderView(GodotHost host, Godot godot) {
+	public GodotVulkanRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler) {
 		super(host.getActivity());
 		super(host.getActivity());
 
 
 		this.host = host;
 		this.host = host;
 		this.godot = godot;
 		this.godot = godot;
-		mInputHandler = new GodotInputHandler(this);
+		mInputHandler = inputHandler;
 		mRenderer = new VkRenderer();
 		mRenderer = new VkRenderer();
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 			setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
 			setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
@@ -80,11 +80,6 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
 		return this;
 		return this;
 	}
 	}
 
 
-	@Override
-	public void initInputDevices() {
-		mInputHandler.initInputDevices();
-	}
-
 	@Override
 	@Override
 	public void queueOnRenderThread(Runnable event) {
 	public void queueOnRenderThread(Runnable event) {
 		queueOnVkThread(event);
 		queueOnVkThread(event);
@@ -123,11 +118,6 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
 		requestRenderThreadExitAndWait();
 		requestRenderThreadExitAndWait();
 	}
 	}
 
 
-	@Override
-	public void onBackPressed() {
-		godot.onBackPressed();
-	}
-
 	@Override
 	@Override
 	public GodotInputHandler getInputHandler() {
 	public GodotInputHandler getInputHandler() {
 		return mInputHandler;
 		return mInputHandler;

+ 127 - 57
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 static org.godotengine.godot.utils.GLUtils.DEBUG;
 
 
+import org.godotengine.godot.Godot;
 import org.godotengine.godot.GodotLib;
 import org.godotengine.godot.GodotLib;
 import org.godotengine.godot.GodotRenderView;
 import org.godotengine.godot.GodotRenderView;
 
 
 import android.content.Context;
 import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
 import android.hardware.input.InputManager;
 import android.hardware.input.InputManager;
 import android.os.Build;
 import android.os.Build;
 import android.util.Log;
 import android.util.Log;
@@ -46,6 +50,10 @@ import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
 import android.view.ScaleGestureDetector;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
 
 
 import java.util.Collections;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.HashSet;
@@ -54,7 +62,7 @@ import java.util.Set;
 /**
 /**
  * Handles input related events for the {@link GodotRenderView} view.
  * 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 String TAG = GodotInputHandler.class.getSimpleName();
 
 
 	private static final int ROTARY_INPUT_VERTICAL_AXIS = 1;
 	private static final int ROTARY_INPUT_VERTICAL_AXIS = 1;
@@ -64,8 +72,9 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 	private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4);
 	private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4);
 	private final HashSet<Integer> mHardwareKeyboardIds = new HashSet<>();
 	private final HashSet<Integer> mHardwareKeyboardIds = new HashSet<>();
 
 
-	private final GodotRenderView mRenderView;
+	private final Godot godot;
 	private final InputManager mInputManager;
 	private final InputManager mInputManager;
+	private final WindowManager windowManager;
 	private final GestureDetector gestureDetector;
 	private final GestureDetector gestureDetector;
 	private final ScaleGestureDetector scaleGestureDetector;
 	private final ScaleGestureDetector scaleGestureDetector;
 	private final GodotGestureHandler godotGestureHandler;
 	private final GodotGestureHandler godotGestureHandler;
@@ -77,12 +86,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 
 
 	private int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS;
 	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 = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
 		mInputManager.registerInputDeviceListener(this, null);
 		mInputManager.registerInputDeviceListener(this, null);
 
 
+		windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+
 		this.godotGestureHandler = new GodotGestureHandler(this);
 		this.godotGestureHandler = new GodotGestureHandler(this);
 		this.gestureDetector = new GestureDetector(context, godotGestureHandler);
 		this.gestureDetector = new GestureDetector(context, godotGestureHandler);
 		this.gestureDetector.setIsLongpressEnabled(false);
 		this.gestureDetector.setIsLongpressEnabled(false);
@@ -174,7 +184,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 
 
 	public boolean onKeyDown(final int keyCode, KeyEvent event) {
 	public boolean onKeyDown(final int keyCode, KeyEvent event) {
 		if (keyCode == KeyEvent.KEYCODE_BACK) {
 		if (keyCode == KeyEvent.KEYCODE_BACK) {
-			mRenderView.onBackPressed();
+			godot.onBackPressed();
 			// press 'back' button should not terminate program
 			// press 'back' button should not terminate program
 			//normal handle 'back' event in game logic
 			//normal handle 'back' event in game logic
 			return true;
 			return true;
@@ -507,7 +517,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 		return handleTouchEvent(event, eventActionOverride, doubleTap);
 		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.
 		// Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise.
 		final float orientation = event.getOrientation();
 		final float orientation = event.getOrientation();
 
 
@@ -520,7 +530,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 		return (float)-Math.sin(orientation) * tiltMult;
 		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.
 		// Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise.
 		final float orientation = event.getOrientation();
 		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) {
 	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
 		// Fix the buttonsMask
 		switch (eventAction) {
 		switch (eventAction) {
 			case MotionEvent.ACTION_CANCEL:
 			case MotionEvent.ACTION_CANCEL:
@@ -594,7 +609,6 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 				break;
 				break;
 		}
 		}
 
 
-		final int updatedButtonsMask = buttonsMask;
 		// We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically
 		// 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
 		// follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate
 		// stream of events to the engine.
 		// stream of events to the engine.
@@ -607,11 +621,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 			case MotionEvent.ACTION_HOVER_MOVE:
 			case MotionEvent.ACTION_HOVER_MOVE:
 			case MotionEvent.ACTION_MOVE:
 			case MotionEvent.ACTION_MOVE:
 			case MotionEvent.ACTION_SCROLL: {
 			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;
 				return true;
 			}
 			}
 		}
 		}
@@ -627,22 +638,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 	}
 	}
 
 
 	boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
 	boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
-		final int pointerCount = event.getPointerCount();
-		if (pointerCount == 0) {
+		if (event.getPointerCount() == 0) {
 			return true;
 			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) {
 		switch (eventActionOverride) {
 			case MotionEvent.ACTION_DOWN:
 			case MotionEvent.ACTION_DOWN:
@@ -651,11 +654,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 			case MotionEvent.ACTION_MOVE:
 			case MotionEvent.ACTION_MOVE:
 			case MotionEvent.ACTION_POINTER_UP:
 			case MotionEvent.ACTION_POINTER_UP:
 			case MotionEvent.ACTION_POINTER_DOWN: {
 			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;
 				return true;
 			}
 			}
 		}
 		}
@@ -663,58 +663,128 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
 	}
 	}
 
 
 	void handleMagnifyEvent(float x, float y, float factor) {
 	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) {
 	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) {
 	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) {
 	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) {
 	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) {
 	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) {
 	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()) {
 		if (shouldDispatchInputToRenderThread()) {
-			mRenderView.queueOnRenderThread(() -> GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo));
+			godot.runOnRenderThread(runnable);
 		} else {
 		} 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) {}
 }
 }

+ 353 - 0
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<InputEventRunnable> 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<InputEventRunnable> 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);
+	}
+}

+ 4 - 0
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.lang.reflect.Constructor;
 import java.util.Collection;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Set;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentHashMap;
 
 
@@ -82,6 +83,9 @@ public final class GodotPluginRegistry {
 	 * Retrieve the full set of loaded plugins.
 	 * Retrieve the full set of loaded plugins.
 	 */
 	 */
 	public Collection<GodotPlugin> getAllPlugins() {
 	public Collection<GodotPlugin> getAllPlugins() {
+		if (registry.isEmpty()) {
+			return Collections.emptyList();
+		}
 		return registry.values();
 		return registry.values();
 	}
 	}
 
 

+ 13 - 1
platform/android/java_godot_lib_jni.cpp

@@ -51,6 +51,7 @@
 #include "core/config/project_settings.h"
 #include "core/config/project_settings.h"
 #include "core/input/input.h"
 #include "core/input/input.h"
 #include "main/main.h"
 #include "main/main.h"
+#include "servers/xr_server.h"
 
 
 #ifdef TOOLS_ENABLED
 #ifdef TOOLS_ENABLED
 #include "editor/editor_settings.h"
 #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) {
 	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();
 		step.increment();
 		return true;
 		return true;
 	}
 	}

+ 12 - 9
platform/android/java_godot_wrapper.cpp

@@ -213,25 +213,27 @@ bool GodotJavaWrapper::has_get_clipboard() {
 }
 }
 
 
 String GodotJavaWrapper::get_clipboard() {
 String GodotJavaWrapper::get_clipboard() {
+	String clipboard;
 	if (_get_clipboard) {
 	if (_get_clipboard) {
 		JNIEnv *env = get_jni_env();
 		JNIEnv *env = get_jni_env();
 		ERR_FAIL_NULL_V(env, String());
 		ERR_FAIL_NULL_V(env, String());
 		jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_clipboard);
 		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 GodotJavaWrapper::get_input_fallback_mapping() {
+	String input_fallback_mapping;
 	if (_get_input_fallback_mapping) {
 	if (_get_input_fallback_mapping) {
 		JNIEnv *env = get_jni_env();
 		JNIEnv *env = get_jni_env();
 		ERR_FAIL_NULL_V(env, String());
 		ERR_FAIL_NULL_V(env, String());
 		jstring fallback_mapping = (jstring)env->CallObjectMethod(godot_instance, _get_input_fallback_mapping);
 		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() {
 bool GodotJavaWrapper::has_set_clipboard() {
@@ -324,14 +326,15 @@ Vector<String> GodotJavaWrapper::get_gdextension_list_config_file() const {
 }
 }
 
 
 String GodotJavaWrapper::get_ca_certificates() const {
 String GodotJavaWrapper::get_ca_certificates() const {
+	String ca_certificates;
 	if (_get_ca_certificates) {
 	if (_get_ca_certificates) {
 		JNIEnv *env = get_jni_env();
 		JNIEnv *env = get_jni_env();
 		ERR_FAIL_NULL_V(env, String());
 		ERR_FAIL_NULL_V(env, String());
 		jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_ca_certificates);
 		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() {
 void GodotJavaWrapper::init_input_devices() {