1
0
Fork 0

Clean up the XR editor logic

- Coalesce common logic into the `main` flavor to avoid duplication
- Code cleanup
This commit is contained in:
Fredia Huya-Kouadio 2025-01-16 09:40:30 -08:00
parent d33da79d3f
commit b4f25b1863
13 changed files with 86 additions and 214 deletions

View File

@ -2840,6 +2840,13 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
command_line_strings.push_back("--xr_mode_openxr"); command_line_strings.push_back("--xr_mode_openxr");
} else { // XRMode.REGULAR is the default. } else { // XRMode.REGULAR is the default.
command_line_strings.push_back("--xr_mode_regular"); command_line_strings.push_back("--xr_mode_regular");
// Also override the 'xr/openxr/enabled' project setting.
// This is useful for multi-platforms projects supporting both XR and non-XR devices. The project would need
// to enable openxr for development, and would create multiple XR and non-XR export presets.
// These command line args ensure that the non-XR export presets will have openxr disabled.
command_line_strings.push_back("--xr-mode");
command_line_strings.push_back("off");
} }
bool immersive = p_preset->get("screen/immersive_mode"); bool immersive = p_preset->get("screen/immersive_mode");

View File

@ -35,5 +35,4 @@ package org.godotengine.editor
* *
* This is the implementation of the editor used when running on regular Android devices. * This is the implementation of the editor used when running on regular Android devices.
*/ */
open class GodotEditor : BaseGodotEditor() { open class GodotEditor : BaseGodotEditor()
}

View File

@ -57,15 +57,8 @@
<activity <activity
android:name=".GodotXRGame" android:name=".GodotXRGame"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:process=":GodotXRGame"
android:launchMode="singleTask"
android:icon="@mipmap/ic_play_window"
android:label="@string/godot_game_activity_name"
android:exported="false" android:exported="false"
android:screenOrientation="landscape" tools:node="merge">
android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />

View File

@ -30,9 +30,6 @@
package org.godotengine.editor package org.godotengine.editor
import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.isNativeXRDevice
/** /**
* Primary window of the Godot Editor. * Primary window of the Godot Editor.
* *
@ -40,66 +37,10 @@ import org.godotengine.godot.utils.isNativeXRDevice
*/ */
open class GodotEditor : BaseGodotEditor() { open class GodotEditor : BaseGodotEditor() {
companion object { override fun getXRRuntimePermissions(): MutableSet<String> {
private val TAG = GodotEditor::class.java.simpleName val xrRuntimePermissions = super.getXRRuntimePermissions()
xrRuntimePermissions.add("com.oculus.permission.USE_SCENE")
/** Default behavior, means we check project settings **/ xrRuntimePermissions.add("horizonos.permission.USE_SCENE")
private const val XR_MODE_DEFAULT = "default" return xrRuntimePermissions
/**
* Ignore project settings, OpenXR is disabled
*/
private const val XR_MODE_OFF = "off"
/**
* Ignore project settings, OpenXR is enabled
*/
private const val XR_MODE_ON = "on"
internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
internal val USE_SCENE_PERMISSIONS = listOf("com.oculus.permission.USE_SCENE", "horizonos.permission.USE_SCENE")
}
override fun getExcludedPermissions(): MutableSet<String> {
val excludedPermissions = super.getExcludedPermissions()
// The USE_SCENE permission is requested when the "xr/openxr/enabled" project setting
// is enabled.
excludedPermissions.addAll(USE_SCENE_PERMISSIONS)
return excludedPermissions
}
override fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
var hasEditor = false
var xrMode = XR_MODE_DEFAULT
var i = 0
while (i < args.size) {
when (args[i++]) {
EDITOR_ARG, EDITOR_ARG_SHORT, EDITOR_PROJECT_MANAGER_ARG, EDITOR_PROJECT_MANAGER_ARG_SHORT -> hasEditor = true
XR_MODE_ARG -> {
xrMode = args[i++]
}
}
}
return if (hasEditor) {
EDITOR_MAIN_INFO
} else {
val openxrEnabled = xrMode == XR_MODE_ON ||
(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())
if (openxrEnabled && isNativeXRDevice()) {
XR_RUN_GAME_INFO
} else {
RUN_GAME_INFO
}
}
}
override fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? {
return when (instanceId) {
XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
else -> super.getEditorWindowInfoForInstanceId(instanceId)
}
} }
} }

View File

@ -71,6 +71,18 @@
android:defaultWidth="@dimen/editor_default_window_width" android:defaultWidth="@dimen/editor_default_window_width"
android:defaultHeight="@dimen/editor_default_window_height" /> android:defaultHeight="@dimen/editor_default_window_height" />
</activity> </activity>
<activity
android:name=".GodotXRGame"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:process=":GodotXRGame"
android:launchMode="singleTask"
android:icon="@mipmap/ic_play_window"
android:label="@string/godot_game_activity_name"
android:exported="false"
android:screenOrientation="landscape"
android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
</activity>
</application> </application>
</manifest> </manifest>

View File

@ -47,13 +47,12 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.window.layout.WindowMetricsCalculator import androidx.window.layout.WindowMetricsCalculator
import org.godotengine.editor.utils.signApk import org.godotengine.editor.utils.signApk
import org.godotengine.editor.utils.verifyApk import org.godotengine.editor.utils.verifyApk
import org.godotengine.godot.BuildConfig
import org.godotengine.godot.GodotActivity import org.godotengine.godot.GodotActivity
import org.godotengine.godot.GodotLib import org.godotengine.godot.GodotLib
import org.godotengine.godot.error.Error import org.godotengine.godot.error.Error
import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.utils.isHorizonOSDevice
import org.godotengine.godot.utils.isPicoOSDevice
import org.godotengine.godot.utils.isNativeXRDevice import org.godotengine.godot.utils.isNativeXRDevice
import java.util.* import java.util.*
import kotlin.math.min import kotlin.math.min
@ -93,6 +92,20 @@ abstract class BaseGodotEditor : GodotActivity() {
// Info for the various classes used by the editor // Info for the various classes used by the editor
internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "") internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO, true) internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO, true)
internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
/** Default behavior, means we check project settings **/
private const val XR_MODE_DEFAULT = "default"
/**
* Ignore project settings, OpenXR is disabled
*/
private const val XR_MODE_OFF = "off"
/**
* Ignore project settings, OpenXR is enabled
*/
private const val XR_MODE_ON = "on"
/** /**
* Sets of constants to specify the window to use to run the project. * Sets of constants to specify the window to use to run the project.
@ -129,8 +142,7 @@ abstract class BaseGodotEditor : GodotActivity() {
* *
* The permissions in this set will be requested on demand based on use cases. * The permissions in this set will be requested on demand based on use cases.
*/ */
@CallSuper private fun getExcludedPermissions(): MutableSet<String> {
protected open fun getExcludedPermissions(): MutableSet<String> {
val excludedPermissions = mutableSetOf( val excludedPermissions = mutableSetOf(
// The RECORD_AUDIO permission is requested when the "audio/driver/enable_input" project // The RECORD_AUDIO permission is requested when the "audio/driver/enable_input" project
// setting is enabled. // setting is enabled.
@ -144,9 +156,21 @@ abstract class BaseGodotEditor : GodotActivity() {
Manifest.permission.REQUEST_INSTALL_PACKAGES, Manifest.permission.REQUEST_INSTALL_PACKAGES,
) )
} }
// XR runtime permissions should only be requested when the "xr/openxr/enabled" project setting
// is enabled.
excludedPermissions.addAll(getXRRuntimePermissions())
return excludedPermissions return excludedPermissions
} }
/**
* Set of permissions to request when the "xr/openxr/enabled" project setting is enabled.
*/
@CallSuper
protected open fun getXRRuntimePermissions(): MutableSet<String> {
return mutableSetOf()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
@ -208,27 +232,38 @@ abstract class BaseGodotEditor : GodotActivity() {
final override fun getCommandLine() = commandLineParams final override fun getCommandLine() = commandLineParams
protected open fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo { protected fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
var hasEditor = false var hasEditor = false
var xrMode = XR_MODE_DEFAULT
var i = 0 var i = 0
while (i < args.size) { while (i < args.size) {
when (args[i++]) { when (args[i++]) {
EDITOR_ARG, EDITOR_ARG_SHORT, EDITOR_PROJECT_MANAGER_ARG, EDITOR_PROJECT_MANAGER_ARG_SHORT -> hasEditor = true EDITOR_ARG, EDITOR_ARG_SHORT, EDITOR_PROJECT_MANAGER_ARG, EDITOR_PROJECT_MANAGER_ARG_SHORT -> hasEditor = true
XR_MODE_ARG -> {
xrMode = args[i++]
}
} }
} }
return if (hasEditor) { return if (hasEditor) {
EDITOR_MAIN_INFO EDITOR_MAIN_INFO
} else { } else {
RUN_GAME_INFO val openxrEnabled = xrMode == XR_MODE_ON ||
(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())
if (openxrEnabled && isNativeXRDevice(applicationContext)) {
XR_RUN_GAME_INFO
} else {
RUN_GAME_INFO
}
} }
} }
protected open fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? { private fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? {
return when (instanceId) { return when (instanceId) {
RUN_GAME_INFO.windowId -> RUN_GAME_INFO RUN_GAME_INFO.windowId -> RUN_GAME_INFO
EDITOR_MAIN_INFO.windowId -> EDITOR_MAIN_INFO EDITOR_MAIN_INFO.windowId -> EDITOR_MAIN_INFO
XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
else -> null else -> null
} }
} }
@ -417,8 +452,8 @@ abstract class BaseGodotEditor : GodotActivity() {
return when (policy) { return when (policy) {
LaunchPolicy.AUTO -> { LaunchPolicy.AUTO -> {
if (isHorizonOSDevice()) { if (isNativeXRDevice(applicationContext)) {
// Horizon OS UX is more desktop-like and has support for launching adjacent // Native XR devices are more desktop-like and have support for launching adjacent
// windows. So we always want to launch in adjacent mode when auto is selected. // windows. So we always want to launch in adjacent mode when auto is selected.
LaunchPolicy.ADJACENT LaunchPolicy.ADJACENT
} else { } else {
@ -450,12 +485,6 @@ abstract class BaseGodotEditor : GodotActivity() {
* Returns true the if the device supports picture-in-picture (PiP) * Returns true the if the device supports picture-in-picture (PiP)
*/ */
protected open fun hasPiPSystemFeature(): Boolean { protected open fun hasPiPSystemFeature(): Boolean {
if (isNativeXRDevice()) {
// Known native XR devices do not support PiP.
// Will need to revisit as they update their OS.
return false
}
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
} }
@ -534,15 +563,15 @@ abstract class BaseGodotEditor : GodotActivity() {
override fun supportsFeature(featureTag: String): Boolean { override fun supportsFeature(featureTag: String): Boolean {
if (featureTag == "xr_editor") { if (featureTag == "xr_editor") {
return isNativeXRDevice() return isNativeXRDevice(applicationContext)
} }
if (featureTag == "horizonos") { if (featureTag == "horizonos") {
return isHorizonOSDevice() return BuildConfig.FLAVOR == "horizonos"
} }
if (featureTag == "picoos") { if (featureTag == "picoos") {
return isPicoOSDevice() return BuildConfig.FLAVOR == "picoos"
} }
return false return false

View File

@ -59,8 +59,8 @@ open class GodotXRGame: GodotGame() {
override fun getProjectPermissionsToEnable(): MutableList<String> { override fun getProjectPermissionsToEnable(): MutableList<String> {
val permissionsToEnable = super.getProjectPermissionsToEnable() val permissionsToEnable = super.getProjectPermissionsToEnable()
val openxrEnabled = GodotLib.getGlobal("xr/openxr/enabled").toBoolean() val xrRuntimePermission = getXRRuntimePermissions()
if (openxrEnabled) { if (xrRuntimePermission.isNotEmpty() && GodotLib.getGlobal("xr/openxr/enabled").toBoolean()) {
// We only request permissions when the `automatically_request_runtime_permissions` // We only request permissions when the `automatically_request_runtime_permissions`
// project setting is enabled. // project setting is enabled.
// If the project setting is not defined, we fall-back to the default behavior which is // If the project setting is not defined, we fall-back to the default behavior which is
@ -69,7 +69,7 @@ open class GodotXRGame: GodotGame() {
val automaticPermissionsRequestEnabled = automaticallyRequestPermissionsSetting.isNullOrEmpty() || val automaticPermissionsRequestEnabled = automaticallyRequestPermissionsSetting.isNullOrEmpty() ||
automaticallyRequestPermissionsSetting.toBoolean() automaticallyRequestPermissionsSetting.toBoolean()
if (automaticPermissionsRequestEnabled) { if (automaticPermissionsRequestEnabled) {
permissionsToEnable.addAll(USE_SCENE_PERMISSIONS) permissionsToEnable.addAll(xrRuntimePermission)
} }
} }

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="editor_default_window_height">640dp</dimen> <dimen name="editor_default_window_height">720dp</dimen>
<dimen name="editor_default_window_width">1024dp</dimen> <dimen name="editor_default_window_width">1024dp</dimen>
</resources> </resources>

View File

@ -29,15 +29,8 @@
<activity <activity
android:name=".GodotXRGame" android:name=".GodotXRGame"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:process=":GodotXRGame"
android:launchMode="singleTask"
android:icon="@mipmap/ic_play_window"
android:label="@string/godot_game_activity_name"
android:exported="false" android:exported="false"
android:screenOrientation="landscape" tools:node="merge">
android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />

View File

@ -30,53 +30,9 @@
package org.godotengine.editor package org.godotengine.editor
import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.isNativeXRDevice
/** /**
* Primary window of the Godot Editor. * Primary window of the Godot Editor.
* *
* This is the implementation of the editor used when running on PicoOS devices. * This is the implementation of the editor used when running on PicoOS devices.
*/ */
open class GodotEditor : BaseGodotEditor() { open class GodotEditor : BaseGodotEditor()
companion object {
private val TAG = GodotEditor::class.java.simpleName
internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
}
override fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
var hasEditor = false
var xrModeOn = false
var i = 0
while (i < args.size) {
when (args[i++]) {
EDITOR_ARG, EDITOR_ARG_SHORT, EDITOR_PROJECT_MANAGER_ARG, EDITOR_PROJECT_MANAGER_ARG_SHORT -> hasEditor = true
XR_MODE_ARG -> {
val argValue = args[i++]
xrModeOn = xrModeOn || ("on" == argValue)
}
}
}
return if (hasEditor) {
EDITOR_MAIN_INFO
} else {
val openxrEnabled = GodotLib.getGlobal("xr/openxr/enabled").toBoolean()
if (openxrEnabled && isNativeXRDevice()) {
XR_RUN_GAME_INFO
} else {
RUN_GAME_INFO
}
}
}
override fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? {
return when (instanceId) {
XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
else -> super.getEditorWindowInfoForInstanceId(instanceId)
}
}
}

View File

@ -1,59 +0,0 @@
/*************************************************************************/
/* GodotXRGame.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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.editor
import org.godotengine.godot.GodotLib
import org.godotengine.godot.xr.XRMode
/**
* Provide support for running XR apps / games from the editor window.
*/
open class GodotXRGame: GodotGame() {
override fun overrideOrientationRequest() = true
override fun updateCommandLineParams(args: List<String>) {
val updatedArgs = ArrayList<String>()
if (!args.contains(XRMode.OPENXR.cmdLineArg)) {
updatedArgs.add(XRMode.OPENXR.cmdLineArg)
}
if (!args.contains(XR_MODE_ARG)) {
updatedArgs.add(XR_MODE_ARG)
updatedArgs.add("on")
}
updatedArgs.addAll(args)
super.updateCommandLineParams(updatedArgs)
}
override fun getEditorWindowInfo() = XR_RUN_GAME_INFO
}

View File

@ -68,7 +68,7 @@ public interface GodotRenderView {
* @return true if pointer capture is supported. * @return true if pointer capture is supported.
*/ */
default boolean canCapturePointer() { default boolean canCapturePointer() {
// Pointer capture is not supported on Horizon OS // Pointer capture is not supported on native XR devices.
return !DeviceUtils.isHorizonOSDevice() && getInputHandler().canCapturePointer(); return !DeviceUtils.isNativeXRDevice(getView().getContext()) && getInputHandler().canCapturePointer();
} }
} }

View File

@ -35,13 +35,14 @@
package org.godotengine.godot.utils package org.godotengine.godot.utils
import android.content.Context
import android.os.Build import android.os.Build
/** /**
* Returns true if running on Meta Horizon OS. * Returns true if running on Meta Horizon OS.
*/ */
fun isHorizonOSDevice(): Boolean { fun isHorizonOSDevice(context: Context): Boolean {
return "Oculus".equals(Build.BRAND, true) return context.packageManager.hasSystemFeature("oculus.hardware.standalone_vr")
} }
/** /**
@ -54,6 +55,6 @@ fun isPicoOSDevice(): Boolean {
/** /**
* Returns true if running on a native Android XR device. * Returns true if running on a native Android XR device.
*/ */
fun isNativeXRDevice(): Boolean { fun isNativeXRDevice(context: Context): Boolean {
return isHorizonOSDevice() || isPicoOSDevice() return isHorizonOSDevice(context) || isPicoOSDevice()
} }