using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using UnityEditor; #if UNITY_EDITOR using System.IO; using UnityEditor.XR.OpenXR.Features; using UnityEngine.Rendering; using UnityEngine.XR.OpenXR.Features.Interactions; #endif [assembly: InternalsVisibleTo("Unity.XR.OpenXR.Features.OculusQuestSupport")] [assembly: InternalsVisibleTo("Unity.XR.OpenXR.Features.MetaQuestSupport.Editor")] [assembly: InternalsVisibleTo("Unity.XRTesting")] namespace UnityEngine.XR.OpenXR.Features.MetaQuestSupport { /// /// Enables the Meta mobile OpenXR Loader for Android, and modifies the AndroidManifest to be compatible with Quest. /// #if UNITY_EDITOR [OpenXRFeature(UiName = "Meta Quest Support", Desc = "Necessary to deploy a Meta Quest compatible app.", Company = "Unity", DocumentationLink = "https://developer.oculus.com/downloads/package/oculus-openxr-mobile-sdk/", OpenxrExtensionStrings = "XR_OCULUS_android_initialize_loader", Version = "1.0.0", BuildTargetGroups = new[] { BuildTargetGroup.Android }, FeatureId = featureId )] #endif public class MetaQuestFeature : OpenXRFeature { [Serializable] internal struct TargetDevice { public string visibleName; public string manifestName; public bool enabled; [NonSerialized] public bool active; } /// /// The feature id string. This is used to give the feature a well known id for reference. /// public const string featureId = "com.unity.openxr.feature.metaquest"; /// /// The name of the ambient occlusion render feature script. /// Used for validation regarding ambient occlusion on meta quest devices. /// private const string ambientOcclusionScriptName = "ScreenSpaceAmbientOcclusion"; #if UNITY_EDITOR /// /// Adds devices to the supported devices list in the Android manifest. /// [SerializeField] internal List targetDevices; /// /// Forces the removal of Internet permissions added to the Android Manifest. /// [SerializeField, Tooltip("Forces the removal of Internet permissions added to the Android Manifest.")] internal bool forceRemoveInternetPermission = false; [SerializeField] internal bool symmetricProjection = false; /// /// Different APIs to use in the backend. /// On Built-in Render Pipeline, only Legacy will be used. /// On Scriptable Render Pipelines, it is highly recommended to use the SRPFoveation API. More textures will use FDM with the SRPFoveation API. /// [SerializeField, Tooltip("On Scriptable Render Pipelines, it is highly recommended to use the SRPFoveation API. More textures will use FDM with the SRPFoveation API.")] internal OpenXRSettings.BackendFovationApi foveatedRenderingApi = OpenXRSettings.BackendFovationApi.Legacy; /// /// Uses a PNG in the Assets folder as the system splash screen image. If set, the OS will display the system splash screen as a high quality compositor layer as soon as the app is starting to launch until the app submits the first frame. /// [SerializeField, Tooltip("Uses a PNG in the Assets folder as the system splash screen image. If set, the OS will display the system splash screen as a high quality compositor layer as soon as the app is starting to launch until the app submits the first frame.")] public Texture2D systemSplashScreen; [SerializeField, Tooltip("Optimization that allows 4x MSAA textures to be memoryless on Vulkan")] internal bool optimizeBufferDiscards = true; /// /// Caches validation rules for each build target group requested by . /// private Dictionary validationRules = new Dictionary(); /// Holding the Late Latching mode here for the editor (so we get undo/redo functionality) /// [SerializeField] internal bool lateLatchingMode; /// /// Holding the Late Latching mode here for the editor (so we get undo/redo functionality) /// [SerializeField] internal bool lateLatchingDebug; /// /// If enabled, the application can use Multi-View Per View Viewports functionality. This feature requires Unity 6.1 or later, and usage of the Vulkan renderer. /// [SerializeField] internal bool optimizeMultiviewRenderRegions; /// /// Holding the Space Warp motion vector texture format /// [SerializeField] internal OpenXRSettings.SpaceWarpMotionVectorTextureFormat spacewarpMotionVectorTextureFormat = OpenXRSettings.SpaceWarpMotionVectorTextureFormat.RGBA16f; /// /// Forces the removal of Internet permissions added to the Android Manifest. /// public bool ForceRemoveInternetPermission { get => forceRemoveInternetPermission; set => forceRemoveInternetPermission = value; } public new void OnEnable() { // add known devices AddTargetDevice("quest", "Quest", true); AddTargetDevice("quest2", "Quest 2", true); AddTargetDevice("cambria", "Quest Pro", true); AddTargetDevice("eureka", "Quest 3", true); AddTargetDevice("quest3s", "Quest 3S", true); } /// /// Adds additional target devices to the devices list in the MetaQuestFeatureEditor. Added target devices will /// be serialized into the settings asset and will persist across editor sessions, but will only be visible to users /// and the manifest if they've been added in the active editor session. /// /// Target device name that will be added to AndroidManifest /// Device name that will be displayed in feature configuration UI /// Target device should be enabled by default or not public void AddTargetDevice(string manifestName, string visibleName, bool enabledByDefault) { if (targetDevices == null) targetDevices = new List(); // don't add devices that already exist, but do mark them active for this session for (int i = 0; i < targetDevices.Count; ++i) { var dev = targetDevices[i]; if (dev.manifestName == manifestName) { dev.active = true; targetDevices[i] = dev; return; } } TargetDevice targetDevice = new TargetDevice { manifestName = manifestName, visibleName = visibleName, enabled = enabledByDefault, active = true }; targetDevices.Add(targetDevice); } private bool SettingsUseVulkan() { if (!PlayerSettings.GetUseDefaultGraphicsAPIs(BuildTarget.Android)) { GraphicsDeviceType[] apis = PlayerSettings.GetGraphicsAPIs(BuildTarget.Android); if (apis.Length >= 1 && apis[0] == GraphicsDeviceType.Vulkan) { return true; } return false; } return true; } protected override void GetValidationChecks(List rules, BuildTargetGroup targetGroup) { if (!validationRules.ContainsKey(targetGroup)) validationRules.Add(targetGroup, CreateValidationRules(targetGroup)); rules.AddRange(validationRules[targetGroup]); } private ValidationRule[] CreateValidationRules(BuildTargetGroup targetGroup) => new ValidationRule[] { new ValidationRule(this) { message = "Select Oculus Touch Interaction Profile, Meta Quest Touch Pro Interaction Profile, or Meta Quest Touch Plus Interaction Profile to pair with.", checkPredicate = () => { var settings = OpenXRSettings.GetSettingsForBuildTargetGroup(targetGroup); if (null == settings) return false; bool touchFeatureEnabled = false; foreach (var feature in settings.GetFeatures()) { if (feature.enabled) { if ((feature is OculusTouchControllerProfile) || (feature is MetaQuestTouchProControllerProfile) || (feature is MetaQuestTouchPlusControllerProfile)) touchFeatureEnabled = true; } } return touchFeatureEnabled; }, error = false, fixIt = () => { SettingsService.OpenProjectSettings("Project/XR Plug-in Management/OpenXR"); }, fixItAutomatic = false, fixItMessage = "Open Project Settings to select Oculus Touch, Meta Quest Touch Pro, or Meta Quest Touch Plus interaction profiles or select all." }, new ValidationRule(this) { message = "No Quest target devices selected.", checkPredicate = () => { foreach (var device in targetDevices) { if (device.enabled) return true; } return false; }, fixIt = () => { var window = MetaQuestFeatureEditorWindow.Create(this); window.ShowPopup(); }, error = true, fixItAutomatic = false, }, new ValidationRule(this) { message = "Using the Screen Space Ambient Occlusion render feature results in significant performance overhead when the application is running natively on device. Disabling or removing that render feature is recommended.", helpText = "Only removing the Screen Space Ambient Occlusion render feature from all UniversalRenderer assets that may be used will make this warning go away, but just disabling the render feature will still prevent the performance overhead.", checkPredicate = () => { // Checks the dependencies of all configured render pipeline assets. foreach(var renderPipeline in GraphicsSettings.allConfiguredRenderPipelines) { var dependencies = AssetDatabase.GetDependencies(AssetDatabase.GetAssetPath(renderPipeline)); foreach(var dependency in dependencies) { if (dependency.Contains(ambientOcclusionScriptName)) return false; } } return true; }, fixItAutomatic = false, }, new ValidationRule(this) { message = "System Splash Screen must be a PNG texture asset.", checkPredicate = () => { if (systemSplashScreen == null) return true; string splashScreenAssetPath = AssetDatabase.GetAssetPath(systemSplashScreen); if (Path.GetExtension(splashScreenAssetPath).ToLower() != ".png") return false; return true; }, fixIt = () => { var window = MetaQuestFeatureEditorWindow.Create(this); window.ShowPopup(); }, error = true, fixItAutomatic = false, }, // OptimizeMultiviewRenderRegions (aka MVPVV) only supported on Unity 6.1 onwards #if UNITY_6000_1_OR_NEWER new ValidationRule(this) { message = "Optimize Multiview Render Regions requires symmetric projection setting turned on.", checkPredicate = () => { if (optimizeMultiviewRenderRegions) { return symmetricProjection; } return true; }, error = true, fixIt = () => { var settings = OpenXRSettings.GetSettingsForBuildTargetGroup(targetGroup); var feature = settings.GetFeature(); feature.symmetricProjection = true; } }, new ValidationRule(this) { message = "Optimize Multiview Render Regions requires Render Mode set to \"Single Pass Instanced / Multi-view\".", checkPredicate = () => { if (optimizeMultiviewRenderRegions) { var settings = OpenXRSettings.GetSettingsForBuildTargetGroup(targetGroup); return (settings.renderMode == OpenXRSettings.RenderMode.SinglePassInstanced); } return true; }, error = true, fixIt = () => { var settings = OpenXRSettings.GetSettingsForBuildTargetGroup(targetGroup); settings.renderMode = OpenXRSettings.RenderMode.SinglePassInstanced; } }, new ValidationRule(this) { message = "Optimize Multiview Render Regions needs the Vulkan Graphics API to be the default Graphics API to work at runtime.", helpText = "The Optimize Multiview Render Regions feature only works with the Vulkan Graphics API, which needs to be set as the first Graphics API to be loaded at application startup. Choosing other Graphics API may require to switch to Vulkan and restart the application.", checkPredicate = () => { if (optimizeMultiviewRenderRegions) { var graphicsApis = PlayerSettings.GetGraphicsAPIs(BuildTarget.Android); return graphicsApis[0] == GraphicsDeviceType.Vulkan; } return true; }, error = false }, #endif #if UNITY_ANDROID new ValidationRule(this) { message = "Symmetric Projection is only supported on Vulkan graphics API", checkPredicate = () => { if (symmetricProjection && !SettingsUseVulkan()) { return false; } return true; }, fixIt = () => { PlayerSettings.SetGraphicsAPIs(BuildTarget.Android, new[] { GraphicsDeviceType.Vulkan }); }, error = true, fixItAutomatic = true, fixItMessage = "Set Vulkan as Graphics API" }, new ValidationRule(this) { message = "Symmetric Projection is only supported when using Multi-view", checkPredicate = () => { var settings = OpenXRSettings.GetSettingsForBuildTargetGroup(targetGroup); if (null == settings) return false; if (symmetricProjection && (settings.renderMode != OpenXRSettings.RenderMode.SinglePassInstanced)) { return false; } return true; }, fixIt = () => { var settings = OpenXRSettings.GetSettingsForBuildTargetGroup(targetGroup); if (null != settings) { settings.renderMode = OpenXRSettings.RenderMode.SinglePassInstanced; } }, error = true, fixItAutomatic = true, fixItMessage = "Set Render Mode to Multi-view" }, new ValidationRule(this) { message = "Only Legacy Foveated Rendering API usage is possible on Built-in Render Pipeline", checkPredicate = () => { return GraphicsSettings.defaultRenderPipeline != null || foveatedRenderingApi == OpenXRSettings.BackendFovationApi.Legacy; }, fixIt = () => { foveatedRenderingApi = OpenXRSettings.BackendFovationApi.Legacy; }, error = true, fixItAutomatic = true, fixItMessage = "Set Foveated Rendering API to Legacy" }, new ValidationRule(this) { message = "Symmetric Projection is only available on Quest 2 or higher", checkPredicate = () => { if (symmetricProjection) { foreach (var device in targetDevices) { if (device.enabled && device.manifestName == "quest") { return false; } } } return true; }, fixIt = () => { var window = MetaQuestFeatureEditorWindow.Create(this); window.ShowPopup(); }, error = true, fixItAutomatic = false, }, new ValidationRule(this) { message = "Optimize Buffer Discards is only supported on Vulkan graphics API", checkPredicate = () => { if (optimizeBufferDiscards && !SettingsUseVulkan()) { return false; } return true; } }, #endif new ValidationRule(this) { message = "Meta Quest HMDs only support Landscape Left orientation.", checkPredicate = () => { if (PlayerSettings.defaultInterfaceOrientation == UIOrientation.AutoRotation) { if (!PlayerSettings.allowedAutorotateToLandscapeLeft) return false; } else { if (PlayerSettings.defaultInterfaceOrientation != UIOrientation.LandscapeLeft) return false; } return true; }, error = true, fixIt = () => { if (PlayerSettings.defaultInterfaceOrientation == UIOrientation.AutoRotation) { PlayerSettings.allowedAutorotateToLandscapeLeft = true; } else { PlayerSettings.defaultInterfaceOrientation = UIOrientation.LandscapeLeft; } }, fixItAutomatic = true, } }; internal class MetaQuestFeatureEditorWindow : EditorWindow { private Object feature; private Editor featureEditor; public static EditorWindow Create(Object feature) { var window = EditorWindow.GetWindow(true, "Meta Quest Feature Configuration", true); window.feature = feature; window.featureEditor = Editor.CreateEditor(feature); return window; } private void OnGUI() { featureEditor.OnInspectorGUI(); } } #endif } }