diff --git a/CMakeLists.txt b/CMakeLists.txt index 57b9a4f9d2..9eb93a6999 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,7 +93,7 @@ option(ENABLE_CUBEB "Enables Cubeb sound backend" ON) option(ENABLE_LLVM "Enables LLVM support, for disassembly" ON) option(ENABLE_TESTS "Enables building the unit tests" ON) option(ENABLE_VULKAN "Enables vulkan video backend" ON) -option(ENABLE_VR "Enables VR support via OpenXR (Windows, D3D11 + Vulkan)" OFF) +option(ENABLE_VR "Enables VR support via OpenXR (Windows, Android Vulkan)" OFF) option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence, show the current game on Discord" ON) option(USE_MGBA "Enables GBA controllers emulation using libmgba" ON) option(ENABLE_AUTOUPDATE "Enables support for automatic updates" ON) @@ -654,7 +654,7 @@ if(ENABLE_VULKAN) endif() endif() -if(ENABLE_VR AND WIN32) +if(ENABLE_VR AND (WIN32 OR ANDROID)) add_definitions(-DENABLE_VR) # Build only the OpenXR loader from the Khronos SDK; skip tests, conformance, and samples. set(BUILD_TESTS OFF CACHE BOOL "" FORCE) diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 64d909423c..a5adf79386 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -17,6 +17,8 @@ android { buildConfig = true } + flavorDimensions += "device" + compileOptions { // Flag to enable support for the new language APIs isCoreLibraryDesugaringEnabled = true @@ -94,6 +96,37 @@ android { } } + productFlavors { + create("standard") { + dimension = "device" + buildConfigField("boolean", "IS_QUEST", "false") + ndk { + abiFilters += listOf("arm64-v8a", "x86_64") + } + externalNativeBuild { + cmake { + arguments += "-DENABLE_VR=OFF" + } + } + } + + create("quest") { + dimension = "device" + applicationIdSuffix = ".quest" + versionNameSuffix = "-quest" + buildConfigField("boolean", "IS_QUEST", "true") + manifestPlaceholders["questSupportedDevices"] = "quest2|quest3|quest3s|questpro" + ndk { + abiFilters += listOf("arm64-v8a") + } + externalNativeBuild { + cmake { + arguments += "-DENABLE_VR=ON" + } + } + } + } + externalNativeBuild { cmake { path = file("../../../CMakeLists.txt") @@ -111,7 +144,6 @@ android { "-DCMAKE_BUILD_TYPE=RelWithDebInfo" // , "-DENABLE_GENERIC=ON" ) - abiFilters("arm64-v8a", "x86_64") //, "armeabi-v7a", "x86" // Uncomment the line below if you don't want to build the C++ unit tests //targets("main", "hook_impl", "main_hook", "gsl_alloc_hook", "file_redirect_hook") diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt index 0edc0f91f9..3c85ce39e9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt @@ -366,6 +366,9 @@ object NativeLibrary { @JvmStatic external fun StopEmulation() + @JvmStatic + external fun RequestOpenXRRecenter() + /** * Ensures that IsRunning will return true from now on until emulation exits. * (If this is not called, IsRunning will start returning true at some point diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt index 8a1d8addbd..e531762905 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -44,6 +44,7 @@ import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface import org.dolphinemu.dolphinemu.features.input.model.DolphinSensorEventListener import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.features.settings.model.QuestVrSettings import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.StringSetting import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag @@ -435,6 +436,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { val wii = NativeLibrary.IsEmulatingWii() val id = if (wii) R.menu.menu_overlay_controls_wii else R.menu.menu_overlay_controls_gc popup.menuInflater.inflate(id, menu) + menu.findItem(R.id.menu_emulation_recenter_vr)?.isVisible = QuestVrSettings.isQuestBuild() // Populate the switch value for joystick center on touch menu.findItem(R.id.menu_emulation_joystick_rel_center).isChecked = @@ -517,6 +519,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { MENU_ACTION_SETTINGS -> SettingsActivity.launch(this, MenuTag.SETTINGS) MENU_ACTION_SKYLANDERS -> showSkylanderPortalSettings() MENU_ACTION_INFINITY_BASE -> showInfinityBaseSettings() + MENU_ACTION_RECENTER_OPENXR -> NativeLibrary.RequestOpenXRRecenter() MENU_ACTION_EXIT -> emulationFragment!!.stopEmulation() } } @@ -1077,6 +1080,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { const val MENU_ACTION_SKYLANDERS = 36 const val MENU_ACTION_INFINITY_BASE = 37 const val MENU_ACTION_LATCHING_CONTROLS = 38 + const val MENU_ACTION_RECENTER_OPENXR = 39 init { buttonsActionsMap.apply { @@ -1087,6 +1091,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { append(R.id.menu_emulation_choose_controller, MENU_ACTION_CHOOSE_CONTROLLER) append(R.id.menu_emulation_joystick_rel_center, MENU_ACTION_JOYSTICK_REL_CENTER) append(R.id.menu_emulation_reset_overlay, MENU_ACTION_RESET_OVERLAY) + append(R.id.menu_emulation_recenter_vr, MENU_ACTION_RECENTER_OPENXR) append(R.id.menu_emulation_ir_recenter, MENU_SET_IR_RECENTER) append(R.id.menu_emulation_set_ir_mode, MENU_SET_IR_MODE) append(R.id.menu_emulation_choose_doubletap, MENU_ACTION_CHOOSE_DOUBLETAP) @@ -1111,10 +1116,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { riivolution: Boolean ) { ignoreLaunchRequests = true + prepareQuestLaunchSettings(false) val launcher = Intent(activity, EmulationActivity::class.java) launcher.putExtra(EXTRA_SELECTED_GAMES, filePaths) launcher.putExtra(EXTRA_RIIVOLUTION, riivolution) - activity.startActivity(launcher) + startEmulationActivity(activity, launcher) } private fun performLaunchChecks(activity: FragmentActivity, fromIntent: Boolean, continueCallback: Runnable) { @@ -1165,9 +1171,21 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { private fun launchSystemMenuWithoutChecks(activity: FragmentActivity) { ignoreLaunchRequests = true + prepareQuestLaunchSettings(true) val launcher = Intent(activity, EmulationActivity::class.java) launcher.putExtra(EXTRA_SYSTEM_MENU, true) - activity.startActivity(launcher) + startEmulationActivity(activity, launcher) + } + + private fun startEmulationActivity(activity: FragmentActivity, launcher: Intent) { + if (QuestVrSettings.isLaunchInVrEnabled()) { + launcher.addCategory("com.oculus.intent.category.VR") + launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + activity.startActivity(launcher) + activity.finish() + } else { + activity.startActivity(launcher) + } } @JvmStatic @@ -1175,6 +1193,21 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { ignoreLaunchRequests = false } + private fun prepareQuestLaunchSettings(launchSystemMenu: Boolean) { + if (!QuestVrSettings.isQuestBuild()) { + return + } + + val settings = Settings() + settings.loadSettings() + try { + QuestVrSettings.prepareLaunchSettings(settings, launchSystemMenu) + settings.saveSettings() + } finally { + settings.close() + } + } + private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean { if (view == null) return true diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index 1f68bbbd1a..8fc2f1d589 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui +import org.dolphinemu.dolphinemu.BuildConfig import android.content.Context import android.content.pm.PackageManager import android.net.Uri @@ -1659,7 +1660,9 @@ class SettingsFragmentPresenter( // If we only support OpenGLES then we need both OpenGLES 3.1 and AEP val helper = EGLHelper(EGLHelper.EGL_OPENGL_ES2_BIT) - if (helper.supportsOpenGL() && helper.GetVersion() >= 320 || helper.supportsGLES3() && helper.GetVersion() >= 310 && helper.SupportsExtension( + if (BuildConfig.IS_QUEST || + helper.supportsOpenGL() && helper.GetVersion() >= 320 || + helper.supportsGLES3() && helper.GetVersion() >= 310 && helper.SupportsExtension( "GL_ANDROID_extension_pack_es31a" ) ) { @@ -2358,6 +2361,145 @@ class SettingsFragmentPresenter( R.string.stereoscopy_swap_eyes_description ) ) + + if (BuildConfig.IS_QUEST && gameId.isNullOrEmpty()) { + addQuestVrSettings(sl) + } + } + + private fun addQuestVrSettings(sl: ArrayList) { + sl.add(HeaderSetting(context, R.string.quest_vr_runtime, 0)) + sl.add( + SwitchSetting( + context, + QuestVrSettings.openXrEnabledSetting(), + R.string.quest_enable_openxr, + R.string.quest_enable_openxr_description + ) + ) + sl.add( + SwitchSetting( + context, + QuestVrSettings.launchInVrSetting(), + R.string.quest_launch_in_vr, + R.string.quest_launch_in_vr_description + ) + ) + sl.add( + SwitchSetting( + context, + QuestVrSettings.recenterOnLaunchSetting(), + R.string.quest_recenter_on_launch, + R.string.quest_recenter_on_launch_description + ) + ) + sl.add( + SwitchSetting( + context, + QuestVrSettings.lockHeadPoseSetting(), + R.string.quest_lock_head_pose, + R.string.quest_lock_head_pose_description + ) + ) + + sl.add(HeaderSetting(context, R.string.quest_vr_presets, 0)) + sl.add( + SingleChoiceSetting( + context, + QuestVrSettings.controllerPresetSetting(), + R.string.quest_controller_preset, + R.string.quest_controller_preset_description, + R.array.questControllerPresetEntries, + R.array.questControllerPresetValues + ) + ) + sl.add( + RunRunnable( + context, + R.string.quest_apply_controller_preset, + R.string.quest_apply_controller_preset_description, + 0, + R.string.quest_controller_preset_applied, + false + ) { + runQuestSettingsMutation { settings -> + QuestVrSettings.applySelectedControllerPreset(settings) + } + } + ) + sl.add( + RunRunnable( + context, + R.string.quest_apply_recommended_defaults, + R.string.quest_apply_recommended_defaults_description, + 0, + R.string.quest_recommended_defaults_applied, + false + ) { + runQuestSettingsMutation { settings -> + QuestVrSettings.applyRecommendedDefaults(settings) + } + } + ) + + sl.add(HeaderSetting(context, R.string.quest_vr_debug, 0)) + sl.add( + SwitchSetting( + context, + QuestVrSettings.leftHandedSetting(), + R.string.quest_left_handed_wiimote, + R.string.quest_left_handed_wiimote_description + ) + ) + sl.add( + SwitchSetting( + context, + QuestVrSettings.showMirrorSurfaceSetting(), + R.string.quest_show_mirror_surface, + R.string.quest_show_mirror_surface_description + ) + ) + sl.add( + SwitchSetting( + context, + QuestVrSettings.passthroughSetting(), + R.string.quest_passthrough, + R.string.quest_passthrough_description + ) + ) + sl.add( + SwitchSetting( + context, + QuestVrSettings.debugPassthroughSetting(), + R.string.quest_passthrough_debug, + R.string.quest_passthrough_debug_description + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.GFX_SHOW_FPS, + R.string.quest_show_perf_hud, + R.string.quest_show_perf_hud_description + ) + ) + sl.add( + RunRunnable( + context, + R.string.quest_recenter_now, + R.string.quest_recenter_now_description, + 0, + 0, + true + ) { NativeLibrary.RequestOpenXRRecenter() } + ) + } + + private fun runQuestSettingsMutation(block: (Settings) -> Unit) { + val activeSettings = settings ?: return + block(activeSettings) + activeSettings.saveSettings() + fragmentView.adapter?.notifyAllSettingsChanged() } private fun addGcPadSubSettings(sl: ArrayList, gcPadNumber: Int, gcPadType: Int) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index e4698ba43c..ad614a0ca6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -14,6 +14,7 @@ import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.QuestVrSettings import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.overlay.InputOverlay import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner @@ -67,6 +68,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { // The new Surface created here will get passed to the native code via onSurfaceChanged. val surfaceView = binding.surfaceEmulation surfaceView.holder.addCallback(this) + surfaceView.alpha = if (QuestVrSettings.shouldShowMirrorSurface()) 1.0f else 0.0f inputOverlay = binding.surfaceInputOverlay diff --git a/Source/Android/app/src/main/res/menu/menu_overlay_controls_gc.xml b/Source/Android/app/src/main/res/menu/menu_overlay_controls_gc.xml index e4ef459487..f840337937 100644 --- a/Source/Android/app/src/main/res/menu/menu_overlay_controls_gc.xml +++ b/Source/Android/app/src/main/res/menu/menu_overlay_controls_gc.xml @@ -27,6 +27,10 @@ android:id="@+id/menu_emulation_choose_controller" android:title="@string/emulation_choose_controller"/> + + diff --git a/Source/Android/app/src/main/res/menu/menu_overlay_controls_wii.xml b/Source/Android/app/src/main/res/menu/menu_overlay_controls_wii.xml index 066889493f..5972a01429 100644 --- a/Source/Android/app/src/main/res/menu/menu_overlay_controls_wii.xml +++ b/Source/Android/app/src/main/res/menu/menu_overlay_controls_wii.xml @@ -29,6 +29,10 @@ android:id="@+id/menu_emulation_choose_controller" android:title="@string/emulation_choose_controller"/> + + diff --git a/Source/Android/app/src/main/res/values/arrays.xml b/Source/Android/app/src/main/res/values/arrays.xml index d919edb1d2..5aead3908d 100644 --- a/Source/Android/app/src/main/res/values/arrays.xml +++ b/Source/Android/app/src/main/res/values/arrays.xml @@ -339,6 +339,15 @@ 3 + + @string/quest_controller_preset_gamecube + @string/quest_controller_preset_wiimote + + + 0 + 1 + + @string/aspect_ratio_auto diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 855cb56ab8..24a2948857 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -783,6 +783,41 @@ It can efficiently compress both junk data and encrypted Wii data. Side-by-Side Top-and-Bottom Anaglyph + OpenXR + Quest VR + Controller Presets + Runtime + Comfort and Debug + Enable OpenXR on Quest + Allows Quest launches to switch Dolphin into the OpenXR renderer. + Launch games in VR + Starts emulation in the headset instead of keeping games on the 2D Android surface. + Recenter on launch + Recenters the active OpenXR view as soon as emulation starts. + Left-handed Wii Remote + Uses the left Quest controller as the primary OpenXR Wii Remote hand. + Show 2D mirror surface + Keeps the Android surface visible while OpenXR is active for debugging and capture. + Show performance HUD + Displays Dolphin frame rate statistics in the headset mirror. + Enable passthrough + Submits the OpenXR layer with transparent background so headset passthrough can show through empty regions. + Passthrough debug tint + Uses a solid debug clear color to make passthrough masking issues obvious. + Lock head pose per frame + Keeps pose updates aligned with emulation frame boundaries for more stable stereo output. + Default controller preset + Selects which stock Quest controller mapping Dolphin reapplies before launch. + GameCube pad + Wii Remote + Apply selected preset now + Loads the selected Quest controller profile immediately. + Quest controller preset applied. + Apply recommended Quest defaults + Forces Vulkan, conservative rendering defaults, and Quest-safe VR options. + Quest defaults applied. + Recenter VR view now + Requests an OpenXR recenter while emulation is running. Auto diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 7112833403..3d890e7634 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -20,6 +20,7 @@ #include "Common/Assert.h" #include "Common/CPUDetect.h" +#include "Common/Config/Config.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" #include "Common/Event.h" @@ -64,6 +65,9 @@ #include "VideoCommon/OnScreenDisplay.h" #include "VideoCommon/Present.h" #include "VideoCommon/VideoBackendBase.h" +#ifdef ENABLE_VR +#include "VideoCommon/VR/OpenXRManager.h" +#endif #include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/IDCache.h" @@ -72,6 +76,10 @@ namespace { constexpr char DOLPHIN_TAG[] = "DolphinEmuNative"; +constexpr char NATIVE_LIBRARY_CLASS[] = "org/dolphinemu/dolphinemu/NativeLibrary"; +constexpr char GET_EMULATION_ACTIVITY_METHOD[] = "getEmulationActivity"; +constexpr char GET_EMULATION_ACTIVITY_SIG[] = + "()Lorg/dolphinemu/dolphinemu/activities/EmulationActivity;"; ANativeWindow* s_surf; @@ -262,6 +270,16 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_StopEmulatio s_update_main_frame_event.Set(); } +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RequestOpenXRRecenter( + JNIEnv*, jclass) +{ +#ifdef ENABLE_VR + HostThreadLock guard; + if (VR::g_openxr) + VR::g_openxr->RequestRecenter(); +#endif +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetIsBooting(JNIEnv*, jclass) { s_is_booting.Set(); @@ -584,6 +602,26 @@ static float GetRenderSurfaceScale(JNIEnv* env) return env->CallStaticFloatMethod(native_library_class, get_render_surface_scale_method); } +#ifdef ENABLE_VR +static jobject GetEmulationActivity(JNIEnv* env) +{ + jclass native_library_class = env->FindClass(NATIVE_LIBRARY_CLASS); + jmethodID get_emulation_activity = + env->GetStaticMethodID(native_library_class, GET_EMULATION_ACTIVITY_METHOD, + GET_EMULATION_ACTIVITY_SIG); + jobject activity = env->CallStaticObjectMethod(native_library_class, get_emulation_activity); + env->DeleteLocalRef(native_library_class); + return activity; +} + +static bool IsQuestRecenterOnLaunchEnabled() +{ + static const Config::Info quest_recenter_on_launch{ + {Config::System::Main, "Android", "QuestRecenterOnLaunch"}, true}; + return Config::Get(quest_recenter_on_launch); +} +#endif + static void Run(JNIEnv* env, std::unique_ptr&& boot, bool riivolution) { HostThreadLock host_identity_guard; @@ -606,11 +644,27 @@ static void Run(JNIEnv* env, std::unique_ptr&& boot, bool riivol WindowSystemInfo wsi(WindowSystemType::Android, nullptr, s_surf, s_surf); wsi.render_surface_scale = GetRenderSurfaceScale(env); +#ifdef ENABLE_VR + JavaVM* vm = nullptr; + env->GetJavaVM(&vm); + jobject activity = GetEmulationActivity(env); + if (activity) + { + VR::OpenXRManager::SetAndroidAppInfo(vm, env, activity); + env->DeleteLocalRef(activity); + } +#endif + if (BootManager::BootCore(Core::System::GetInstance(), std::move(boot), wsi)) { static constexpr int WAIT_STEP = 25; while (Core::GetState(Core::System::GetInstance()) == Core::State::Starting) std::this_thread::sleep_for(std::chrono::milliseconds(WAIT_STEP)); + +#ifdef ENABLE_VR + if (IsQuestRecenterOnLaunchEnabled() && VR::g_openxr) + VR::g_openxr->RequestRecenter(); +#endif } s_is_booting.Clear(); @@ -627,6 +681,9 @@ static void Run(JNIEnv* env, std::unique_ptr&& boot, bool riivol s_game_metadata_is_valid = false; Core::Shutdown(Core::System::GetInstance()); +#ifdef ENABLE_VR + VR::OpenXRManager::ClearAndroidAppInfo(env); +#endif host_identity_guard.Unlock(); env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), diff --git a/Source/Core/Core/Config/GraphicsSettings.cpp b/Source/Core/Core/Config/GraphicsSettings.cpp index 48a90f6245..866096510d 100644 --- a/Source/Core/Core/Config/GraphicsSettings.cpp +++ b/Source/Core/Core/Config/GraphicsSettings.cpp @@ -189,7 +189,9 @@ const Info GFX_VR_ENABLE_OPENXR{{System::GFX, "VR", "EnableOpenXR"}, false const Info GFX_VR_AUTO_IMMEDIATE_XFB{{System::GFX, "VR", "AutoImmediateXFB"}, true}; const Info GFX_VR_UNITS_PER_METER{{System::GFX, "VR", "UnitsPerMeter"}, 1.0f}; const Info GFX_VR_LEAN_BACK_ANGLE{{System::GFX, "VR", "LeanBackAngle"}, 0.0f}; +const Info GFX_VR_ENABLE_CAMERA_FORWARD{{System::GFX, "VR", "EnableCameraForward"}, true}; const Info GFX_VR_CAMERA_FORWARD{{System::GFX, "VR", "CameraForward"}, 0.0f}; +const Info GFX_VR_ENABLE_CAMERA_HEIGHT{{System::GFX, "VR", "EnableCameraHeight"}, true}; const Info GFX_VR_CAMERA_HEIGHT{{System::GFX, "VR", "CameraHeight"}, 0.0f}; const Info GFX_VR_VIRTUAL_SCREEN{{System::GFX, "VR", "VirtualScreen"}, true}; const Info GFX_VR_SCREEN_DISTANCE{{System::GFX, "VR", "ScreenDistance"}, 1.5f}; diff --git a/Source/Core/Core/Config/GraphicsSettings.h b/Source/Core/Core/Config/GraphicsSettings.h index ec8ea0b321..5389444910 100644 --- a/Source/Core/Core/Config/GraphicsSettings.h +++ b/Source/Core/Core/Config/GraphicsSettings.h @@ -165,7 +165,9 @@ extern const Info GFX_VR_ENABLE_OPENXR; extern const Info GFX_VR_AUTO_IMMEDIATE_XFB; extern const Info GFX_VR_UNITS_PER_METER; extern const Info GFX_VR_LEAN_BACK_ANGLE; +extern const Info GFX_VR_ENABLE_CAMERA_FORWARD; extern const Info GFX_VR_CAMERA_FORWARD; +extern const Info GFX_VR_ENABLE_CAMERA_HEIGHT; extern const Info GFX_VR_CAMERA_HEIGHT; static constexpr float GFX_VR_UNITS_PER_METER_MIN = 0.1f; diff --git a/Source/Core/Core/HW/Wiimote.cpp b/Source/Core/Core/HW/Wiimote.cpp index 07a5e5787f..7987d35423 100644 --- a/Source/Core/Core/HW/Wiimote.cpp +++ b/Source/Core/Core/HW/Wiimote.cpp @@ -421,9 +421,13 @@ void UpdateOpenXRInputOverride(unsigned int index, WiimoteSource source) if (source == WiimoteSource::OpenXR) { + const bool prefer_left_hand = + Config::Get(Config::Info{{Config::System::Main, "Android", "QuestLeftHanded"}, + false}); s_openxr_ir_centers[index] = {}; - wiimote->SetInputOverrideFunction(CreateOpenXRInputOverrideFunction(index, false)); - apply_to_attachments(CreateOpenXRInputOverrideFunction(index, true)); + wiimote->SetInputOverrideFunction( + CreateOpenXRInputOverrideFunction(index, prefer_left_hand)); + apply_to_attachments(CreateOpenXRInputOverrideFunction(index, !prefer_left_hand)); s_openxr_overrides_enabled[index] = true; } else if (s_openxr_overrides_enabled[index]) @@ -522,6 +526,16 @@ std::optional GetOpenXRWiiRemoteState(unsigned int index) if (index >= MAX_WIIMOTES || s_wiimote_sources[index].load() != WiimoteSource::OpenXR) return std::nullopt; + const bool prefer_left_hand = + Config::Get(Config::Info{{Config::System::Main, "Android", "QuestLeftHanded"}, + false}); + if (prefer_left_hand) + { + if (const auto left = GetOpenXRHandState(true)) + return left; + return GetOpenXRHandState(false); + } + if (const auto right = GetOpenXRHandState(false)) return right; return GetOpenXRHandState(true); diff --git a/Source/Core/Core/HotkeyManager.cpp b/Source/Core/Core/HotkeyManager.cpp index 41f796d776..8b3025ae82 100644 --- a/Source/Core/Core/HotkeyManager.cpp +++ b/Source/Core/Core/HotkeyManager.cpp @@ -120,8 +120,12 @@ constexpr std::array s_hotkey_labels{{ _trans("Increase Units Per Meter"), _trans("Decrease Lean Back Angle"), _trans("Increase Lean Back Angle"), + _trans("Toggle Enable Camera Forward"), _trans("Decrease Camera Forward"), _trans("Increase Camera Forward"), + _trans("Toggle Enable Camera Height"), + _trans("Decrease Camera Height"), + _trans("Increase Camera Height"), _trans("Toggle Virtual Screen"), _trans("Decrease Screen Distance"), _trans("Increase Screen Distance"), diff --git a/Source/Core/Core/HotkeyManager.h b/Source/Core/Core/HotkeyManager.h index eb885c2faa..1ee79f088d 100644 --- a/Source/Core/Core/HotkeyManager.h +++ b/Source/Core/Core/HotkeyManager.h @@ -108,8 +108,12 @@ enum Hotkey HK_VR_INCREASE_UNITS_PER_METER, HK_VR_DECREASE_LEAN_BACK_ANGLE, HK_VR_INCREASE_LEAN_BACK_ANGLE, + HK_VR_TOGGLE_ENABLE_CAMERA_FORWARD, HK_VR_DECREASE_CAMERA_FORWARD, HK_VR_INCREASE_CAMERA_FORWARD, + HK_VR_TOGGLE_ENABLE_CAMERA_HEIGHT, + HK_VR_DECREASE_CAMERA_HEIGHT, + HK_VR_INCREASE_CAMERA_HEIGHT, HK_VR_TOGGLE_VIRTUAL_SCREEN, HK_VR_DECREASE_SCREEN_DISTANCE, HK_VR_INCREASE_SCREEN_DISTANCE, diff --git a/Source/Core/DolphinQt/HotkeyScheduler.cpp b/Source/Core/DolphinQt/HotkeyScheduler.cpp index 13bc44464a..b621c659da 100644 --- a/Source/Core/DolphinQt/HotkeyScheduler.cpp +++ b/Source/Core/DolphinQt/HotkeyScheduler.cpp @@ -552,6 +552,9 @@ void HotkeyScheduler::Run() Config::GFX_VR_LEAN_BACK_ANGLE_MAX, "Lean Back Angle", 1); } + if (IsHotkey(HK_VR_TOGGLE_ENABLE_CAMERA_FORWARD)) + ToggleVRSetting(Config::GFX_VR_ENABLE_CAMERA_FORWARD, "Enable Camera Forward"); + if (IsHotkey(HK_VR_DECREASE_CAMERA_FORWARD)) { AdjustVRFloatSetting(Config::GFX_VR_CAMERA_FORWARD, -Config::GFX_VR_CAMERA_FORWARD_STEP, @@ -565,6 +568,22 @@ void HotkeyScheduler::Run() "Camera Forward", 2); } + if (IsHotkey(HK_VR_TOGGLE_ENABLE_CAMERA_HEIGHT)) + ToggleVRSetting(Config::GFX_VR_ENABLE_CAMERA_HEIGHT, "Enable Camera Height"); + + if (IsHotkey(HK_VR_DECREASE_CAMERA_HEIGHT)) + { + AdjustVRFloatSetting(Config::GFX_VR_CAMERA_HEIGHT, -Config::GFX_VR_CAMERA_HEIGHT_STEP, + Config::GFX_VR_CAMERA_HEIGHT_MIN, Config::GFX_VR_CAMERA_HEIGHT_MAX, + "Camera Height", 2); + } + if (IsHotkey(HK_VR_INCREASE_CAMERA_HEIGHT)) + { + AdjustVRFloatSetting(Config::GFX_VR_CAMERA_HEIGHT, Config::GFX_VR_CAMERA_HEIGHT_STEP, + Config::GFX_VR_CAMERA_HEIGHT_MIN, Config::GFX_VR_CAMERA_HEIGHT_MAX, + "Camera Height", 2); + } + if (IsHotkey(HK_VR_TOGGLE_VIRTUAL_SCREEN)) ToggleVRSetting(Config::GFX_VR_VIRTUAL_SCREEN, "Virtual Screen"); diff --git a/Source/Core/DolphinQt/Settings/VRPane.cpp b/Source/Core/DolphinQt/Settings/VRPane.cpp index 81b61829c3..7a72906172 100644 --- a/Source/Core/DolphinQt/Settings/VRPane.cpp +++ b/Source/Core/DolphinQt/Settings/VRPane.cpp @@ -63,11 +63,15 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) Config::GFX_VR_LEAN_BACK_ANGLE, Config::GFX_VR_LEAN_BACK_ANGLE_STEP); m_lean_back_angle_value = new QLabel(); + m_enable_camera_forward = + new ConfigBool(tr("Enable Camera Forward"), Config::GFX_VR_ENABLE_CAMERA_FORWARD); m_camera_forward = new ConfigFloatSlider(Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX, Config::GFX_VR_CAMERA_FORWARD, Config::GFX_VR_CAMERA_FORWARD_STEP); m_camera_forward_value = new QLabel(); + m_enable_camera_height = + new ConfigBool(tr("Enable Camera Height"), Config::GFX_VR_ENABLE_CAMERA_HEIGHT); m_camera_height = new ConfigFloatSlider(Config::GFX_VR_CAMERA_HEIGHT_MIN, Config::GFX_VR_CAMERA_HEIGHT_MAX, Config::GFX_VR_CAMERA_HEIGHT, @@ -86,9 +90,9 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) Config::GFX_VR_AR_BACKGROUND_ALPHA_STEP); m_ar_background_alpha_value = new QLabel(); openxr_layout->addWidget( - new ConfigFloatLabel(tr("AR Background Alpha:"), m_ar_background_alpha), 17, 0); - openxr_layout->addWidget(m_ar_background_alpha, 17, 1); - openxr_layout->addWidget(m_ar_background_alpha_value, 17, 2); + new ConfigFloatLabel(tr("AR Background Alpha:"), m_ar_background_alpha), 19, 0); + openxr_layout->addWidget(m_ar_background_alpha, 19, 1); + openxr_layout->addWidget(m_ar_background_alpha_value, 19, 2); m_ar_background_alpha_value->setText( QString::asprintf("%.2f", m_ar_background_alpha->GetValue())); connect(m_ar_background_alpha, &ConfigFloatSlider::valueChanged, this, [this] { @@ -103,12 +107,14 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) 0); openxr_layout->addWidget(m_lean_back_angle, 3, 1); openxr_layout->addWidget(m_lean_back_angle_value, 3, 2); - openxr_layout->addWidget(new ConfigFloatLabel(tr("Camera Forward (m):"), m_camera_forward), 4, 0); - openxr_layout->addWidget(m_camera_forward, 4, 1); - openxr_layout->addWidget(m_camera_forward_value, 4, 2); - openxr_layout->addWidget(new ConfigFloatLabel(tr("Camera Height (m):"), m_camera_height), 5, 0); - openxr_layout->addWidget(m_camera_height, 5, 1); - openxr_layout->addWidget(m_camera_height_value, 5, 2); + openxr_layout->addWidget(m_enable_camera_forward, 4, 0, 1, 3); + openxr_layout->addWidget(new ConfigFloatLabel(tr("Camera Forward (m):"), m_camera_forward), 5, 0); + openxr_layout->addWidget(m_camera_forward, 5, 1); + openxr_layout->addWidget(m_camera_forward_value, 5, 2); + openxr_layout->addWidget(m_enable_camera_height, 6, 0, 1, 3); + openxr_layout->addWidget(new ConfigFloatLabel(tr("Camera Height (m):"), m_camera_height), 7, 0); + openxr_layout->addWidget(m_camera_height, 7, 1); + openxr_layout->addWidget(m_camera_height_value, 7, 2); m_units_per_meter_value->setText(QString::asprintf("%.1f", m_units_per_meter->GetValue())); connect(m_units_per_meter, &ConfigFloatSlider::valueChanged, this, [this] { @@ -126,10 +132,25 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) connect(m_camera_height, &ConfigFloatSlider::valueChanged, this, [this] { m_camera_height_value->setText(QString::asprintf("%.1f", m_camera_height->GetValue())); }); + const auto update_camera_offset_controls = [this] { + const bool camera_forward_enabled = m_enable_camera_forward->isChecked(); + m_camera_forward->setEnabled(camera_forward_enabled); + m_camera_forward_value->setEnabled(camera_forward_enabled); + + const bool camera_height_enabled = m_enable_camera_height->isChecked(); + m_camera_height->setEnabled(camera_height_enabled); + m_camera_height_value->setEnabled(camera_height_enabled); + }; + connect(m_enable_camera_forward, &QCheckBox::toggled, this, + [update_camera_offset_controls](bool) { update_camera_offset_controls(); }); + connect(m_enable_camera_height, &QCheckBox::toggled, this, + [update_camera_offset_controls](bool) { update_camera_offset_controls(); }); + connect(&Settings::Instance(), &Settings::ConfigChanged, this, update_camera_offset_controls); + update_camera_offset_controls(); // Virtual screen settings m_virtual_screen = new ConfigBool(tr("Virtual Screen for 2D Content"), Config::GFX_VR_VIRTUAL_SCREEN); - openxr_layout->addWidget(m_virtual_screen, 6, 0, 1, 3); + openxr_layout->addWidget(m_virtual_screen, 8, 0, 1, 3); m_screen_distance = new ConfigFloatSlider(Config::GFX_VR_SCREEN_DISTANCE_MIN, Config::GFX_VR_SCREEN_DISTANCE_MAX, @@ -148,16 +169,16 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP); m_head_locked_curvature_value = new QLabel(); - openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Distance (m):"), m_screen_distance), 7, 0); - openxr_layout->addWidget(m_screen_distance, 7, 1); - openxr_layout->addWidget(m_screen_distance_value, 7, 2); - openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Size (m):"), m_screen_size), 8, 0); - openxr_layout->addWidget(m_screen_size, 8, 1); - openxr_layout->addWidget(m_screen_size_value, 8, 2); + openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Distance (m):"), m_screen_distance), 9, 0); + openxr_layout->addWidget(m_screen_distance, 9, 1); + openxr_layout->addWidget(m_screen_distance_value, 9, 2); + openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Size (m):"), m_screen_size), 10, 0); + openxr_layout->addWidget(m_screen_size, 10, 1); + openxr_layout->addWidget(m_screen_size_value, 10, 2); openxr_layout->addWidget( - new ConfigFloatLabel(tr("Head Locked Curvature:"), m_head_locked_curvature), 9, 0); - openxr_layout->addWidget(m_head_locked_curvature, 9, 1); - openxr_layout->addWidget(m_head_locked_curvature_value, 9, 2); + new ConfigFloatLabel(tr("Head Locked Curvature:"), m_head_locked_curvature), 11, 0); + openxr_layout->addWidget(m_head_locked_curvature, 11, 1); + openxr_layout->addWidget(m_head_locked_curvature_value, 11, 2); m_screen_distance_value->setText(QString::asprintf("%.1f", m_screen_distance->GetValue())); connect(m_screen_distance, &ConfigFloatSlider::valueChanged, this, [this] { @@ -179,12 +200,12 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) {tr("60 to 90"), OpenXROpcodeReplayMode::Replay60To90}, {tr("30 to 90"), OpenXROpcodeReplayMode::Replay30To90}}, Config::GFX_VR_OPCODE_REPLAY); - openxr_layout->addWidget(new QLabel(tr("Opcode Replay:")), 10, 0); - openxr_layout->addWidget(m_opcode_replay_mode, 10, 1, 1, 2); + openxr_layout->addWidget(new QLabel(tr("Opcode Replay:")), 12, 0); + openxr_layout->addWidget(m_opcode_replay_mode, 12, 1, 1, 2); m_auto_vbi_from_hmd = new ConfigBool(tr("Force VBI Frequency to 90 Hz in VR"), Config::GFX_VR_AUTO_VBI_FROM_HMD); - openxr_layout->addWidget(m_auto_vbi_from_hmd, 11, 0, 1, 3); + openxr_layout->addWidget(m_auto_vbi_from_hmd, 13, 0, 1, 3); m_clear_efb_slider = new ConfigSlider(Config::GFX_VR_CLEAR_EFB_MIN, Config::GFX_VR_CLEAR_EFB_MAX, @@ -192,9 +213,9 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) Config::GFX_VR_CLEAR_EFB_STEP); m_clear_efb_value = new QLabel(); openxr_layout->addWidget(new ConfigSliderLabel(tr("Clear EFB (min width):"), m_clear_efb_slider), - 12, 0); - openxr_layout->addWidget(m_clear_efb_slider, 12, 1); - openxr_layout->addWidget(m_clear_efb_value, 12, 2); + 14, 0); + openxr_layout->addWidget(m_clear_efb_slider, 14, 1); + openxr_layout->addWidget(m_clear_efb_value, 14, 2); auto update_clear_efb_label = [this] { const int val = m_clear_efb_slider->value(); @@ -207,9 +228,9 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) m_vr_gamma = new ConfigFloatSlider(Config::GFX_VR_GAMMA_MIN, Config::GFX_VR_GAMMA_MAX, Config::GFX_VR_GAMMA, Config::GFX_VR_GAMMA_STEP); m_vr_gamma_value = new QLabel(); - openxr_layout->addWidget(new ConfigFloatLabel(tr("VR Gamma:"), m_vr_gamma), 13, 0); - openxr_layout->addWidget(m_vr_gamma, 13, 1); - openxr_layout->addWidget(m_vr_gamma_value, 13, 2); + openxr_layout->addWidget(new ConfigFloatLabel(tr("VR Gamma:"), m_vr_gamma), 15, 0); + openxr_layout->addWidget(m_vr_gamma, 15, 1); + openxr_layout->addWidget(m_vr_gamma_value, 15, 2); auto update_gamma_label = [this] { const float val = m_vr_gamma->GetValue(); @@ -221,16 +242,16 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) // Auto Layer Spread m_auto_layer_spread = new ConfigBool(tr("Auto Layer Spread"), Config::GFX_VR_AUTO_LAYER_SPREAD); - openxr_layout->addWidget(m_auto_layer_spread, 14, 0, 1, 3); + openxr_layout->addWidget(m_auto_layer_spread, 16, 0, 1, 3); m_layer_offset = new ConfigFloatSlider(Config::GFX_VR_LAYER_OFFSET_MIN, Config::GFX_VR_LAYER_OFFSET_MAX, Config::GFX_VR_LAYER_OFFSET, Config::GFX_VR_LAYER_OFFSET_STEP); m_layer_offset_value = new QLabel(); - openxr_layout->addWidget(new ConfigFloatLabel(tr("Layer Offset:"), m_layer_offset), 15, 0); - openxr_layout->addWidget(m_layer_offset, 15, 1); - openxr_layout->addWidget(m_layer_offset_value, 15, 2); + openxr_layout->addWidget(new ConfigFloatLabel(tr("Layer Offset:"), m_layer_offset), 17, 0); + openxr_layout->addWidget(m_layer_offset, 17, 1); + openxr_layout->addWidget(m_layer_offset_value, 17, 2); m_layer_offset_value->setText(QString::asprintf("%.4f", m_layer_offset->GetValue())); connect(m_layer_offset, &ConfigFloatSlider::valueChanged, this, [this] { @@ -242,9 +263,9 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent) Config::GFX_VR_ELEMENT_DEPTH, Config::GFX_VR_ELEMENT_DEPTH_STEP); m_element_depth_value = new QLabel(); - openxr_layout->addWidget(new ConfigFloatLabel(tr("Element Depth:"), m_element_depth), 16, 0); - openxr_layout->addWidget(m_element_depth, 16, 1); - openxr_layout->addWidget(m_element_depth_value, 16, 2); + openxr_layout->addWidget(new ConfigFloatLabel(tr("Element Depth:"), m_element_depth), 18, 0); + openxr_layout->addWidget(m_element_depth, 18, 1); + openxr_layout->addWidget(m_element_depth_value, 18, 2); m_element_depth_value->setText(QString::asprintf("%.4f", m_element_depth->GetValue())); connect(m_element_depth, &ConfigFloatSlider::valueChanged, this, [this] { @@ -426,9 +447,15 @@ void VRPane::AddDescriptions() "Applies a constant pitch offset (in degrees) to the VR camera." "

Use this to compensate games where the default viewpoint feels tilted." "

Positive values lean the camera back, negative values lean it forward."); + static constexpr char TR_ENABLE_CAMERA_FORWARD_DESCRIPTION[] = QT_TR_NOOP( + "Turns the Camera Forward offset on or off without resetting its saved value." + "

Disable this to restore neutral forward/backward camera placement."); static constexpr char TR_CAMERA_FORWARD_DESCRIPTION[] = QT_TR_NOOP( "Applies a constant forward/backward offset (in meters) to the VR camera." "

Positive values move the camera forward, negative values move it backward."); + static constexpr char TR_ENABLE_CAMERA_HEIGHT_DESCRIPTION[] = QT_TR_NOOP( + "Turns the Camera Height offset on or off without resetting its saved value." + "

Disable this to restore neutral vertical camera placement."); static constexpr char TR_CAMERA_HEIGHT_DESCRIPTION[] = QT_TR_NOOP( "Applies a constant vertical offset (in meters) to the VR camera." "

Positive values move the camera up, negative values move it down."); @@ -561,7 +588,9 @@ void VRPane::AddDescriptions() m_auto_immediate_xfb->SetDescription(tr(TR_AUTO_IMMEDIATE_XFB_DESCRIPTION)); m_units_per_meter->SetDescription(tr(TR_UNITS_PER_METER_DESCRIPTION)); m_lean_back_angle->SetDescription(tr(TR_LEAN_BACK_ANGLE_DESCRIPTION)); + m_enable_camera_forward->SetDescription(tr(TR_ENABLE_CAMERA_FORWARD_DESCRIPTION)); m_camera_forward->SetDescription(tr(TR_CAMERA_FORWARD_DESCRIPTION)); + m_enable_camera_height->SetDescription(tr(TR_ENABLE_CAMERA_HEIGHT_DESCRIPTION)); m_camera_height->SetDescription(tr(TR_CAMERA_HEIGHT_DESCRIPTION)); m_virtual_screen->SetDescription(tr(TR_VIRTUAL_SCREEN_DESCRIPTION)); m_screen_distance->SetDescription(tr(TR_SCREEN_DISTANCE_DESCRIPTION)); diff --git a/Source/Core/DolphinQt/Settings/VRPane.h b/Source/Core/DolphinQt/Settings/VRPane.h index 7552301020..48205992f0 100644 --- a/Source/Core/DolphinQt/Settings/VRPane.h +++ b/Source/Core/DolphinQt/Settings/VRPane.h @@ -37,8 +37,10 @@ private: QLabel* m_units_per_meter_value = nullptr; ConfigFloatSlider* m_lean_back_angle = nullptr; QLabel* m_lean_back_angle_value = nullptr; + ConfigBool* m_enable_camera_forward = nullptr; ConfigFloatSlider* m_camera_forward = nullptr; QLabel* m_camera_forward_value = nullptr; + ConfigBool* m_enable_camera_height = nullptr; ConfigFloatSlider* m_camera_height = nullptr; QLabel* m_camera_height_value = nullptr; ConfigBool* m_virtual_screen = nullptr; diff --git a/Source/Core/InputCommon/CMakeLists.txt b/Source/Core/InputCommon/CMakeLists.txt index 23e168c9f1..d2d0f04dad 100644 --- a/Source/Core/InputCommon/CMakeLists.txt +++ b/Source/Core/InputCommon/CMakeLists.txt @@ -145,6 +145,12 @@ elseif(ANDROID) ControllerInterface/Touch/InputOverrider.cpp ControllerInterface/Touch/InputOverrider.h ) + if(ENABLE_VR) + target_sources(inputcommon PRIVATE + ControllerInterface/OpenXR/OpenXR.cpp + ControllerInterface/OpenXR/OpenXR.h + ) + endif() target_link_libraries(inputcommon PRIVATE androidcommon ) diff --git a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp index 609986e459..a2c616075f 100644 --- a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp +++ b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp @@ -9,11 +9,11 @@ #include "Common/Logging/Log.h" #include "Core/HW/WiimoteReal/WiimoteReal.h" -#ifdef CIFACE_USE_WIN32 -#include "InputCommon/ControllerInterface/Win32/Win32.h" -#ifdef ENABLE_VR +#if defined(ENABLE_VR) && (defined(CIFACE_USE_WIN32) || defined(CIFACE_USE_ANDROID)) #include "InputCommon/ControllerInterface/OpenXR/OpenXR.h" #endif +#ifdef CIFACE_USE_WIN32 +#include "InputCommon/ControllerInterface/Win32/Win32.h" #endif #ifdef CIFACE_USE_XLIB #include "InputCommon/ControllerInterface/Xlib/XInput2.h" @@ -78,6 +78,9 @@ void ControllerInterface::Initialize(const WindowSystemInfo& wsi) #endif #ifdef CIFACE_USE_ANDROID m_input_backends.emplace_back(ciface::Android::CreateInputBackend(this)); +#ifdef ENABLE_VR + m_input_backends.emplace_back(ciface::OpenXR::CreateInputBackend(this)); +#endif #endif #ifdef CIFACE_USE_EVDEV m_input_backends.emplace_back(ciface::evdev::CreateInputBackend(this)); diff --git a/Source/Core/VideoBackends/D3D/D3DOpenXR.cpp b/Source/Core/VideoBackends/D3D/D3DOpenXR.cpp index 89ced31337..c1c1bb6c43 100644 --- a/Source/Core/VideoBackends/D3D/D3DOpenXR.cpp +++ b/Source/Core/VideoBackends/D3D/D3DOpenXR.cpp @@ -148,7 +148,13 @@ bool D3DOpenXR::CreateSwapchains() info.sampleCount = 1; // We render into this swapchain image as a color target, but never sample from it. // Some runtimes reject the extra SAMPLED usage for certain formats. - info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; + // + // MUTABLE_FORMAT lets us keep the swapchain declared as sRGB (so the compositor + // decodes correctly) while creating a UNORM RTV on the underlying texture, so + // BlitFromTexture writes raw sRGB-encoded bytes without a double gamma encode. + // With this flag the runtime must back the texture with a DXGI _TYPELESS format. + info.usageFlags = + XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT; XrResult result = xrCreateSwapchain(VR::g_openxr->GetSession(), &info, &sc.swapchain); if (XR_FAILED(result)) diff --git a/Source/Core/VideoBackends/D3DCommon/D3DCommon.cpp b/Source/Core/VideoBackends/D3DCommon/D3DCommon.cpp index d987107123..e7e2babb33 100644 --- a/Source/Core/VideoBackends/D3DCommon/D3DCommon.cpp +++ b/Source/Core/VideoBackends/D3DCommon/D3DCommon.cpp @@ -240,10 +240,12 @@ AbstractTextureFormat GetAbstractFormatForDXGIFormat(DXGI_FORMAT format) { case DXGI_FORMAT_R8G8B8A8_UINT: case DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: case DXGI_FORMAT_R8G8B8A8_TYPELESS: return AbstractTextureFormat::RGBA8; case DXGI_FORMAT_B8G8R8A8_UNORM: + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: case DXGI_FORMAT_B8G8R8A8_TYPELESS: return AbstractTextureFormat::BGRA8; diff --git a/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.cpp b/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.cpp index 8da2759c65..bd62774d4b 100644 --- a/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.cpp +++ b/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.cpp @@ -35,6 +35,11 @@ namespace Vulkan { std::unique_ptr g_openxr_vk; +XRVkEyeSwapchain::XRVkEyeSwapchain() = default; +XRVkEyeSwapchain::~XRVkEyeSwapchain() = default; +XRVkEyeSwapchain::XRVkEyeSwapchain(XRVkEyeSwapchain&&) noexcept = default; +XRVkEyeSwapchain& XRVkEyeSwapchain::operator=(XRVkEyeSwapchain&&) noexcept = default; + static const char* VkFormatToString(int64_t format) { switch (static_cast(format)) @@ -101,16 +106,19 @@ static bool SelectSwapchainFormat(XrSession session, int64_t* out_format) VkFormatToString(format)); } - // Prefer formats that map cleanly through AbstractTextureFormat without SRGB/UNORM mismatch. - // sRGB formats are avoided because Dolphin's PostProcessing pipeline cache is keyed on - // AbstractTextureFormat (RGBA8), and a pipeline compiled for a UNORM render pass is NOT - // compatible with an SRGB render pass in Vulkan — causing silently dropped draws. - // RGB10_A2 (4 bytes/pixel, 2-bit alpha) and RGBA16F (8 bytes/pixel) both round-trip cleanly. - // UNORM 8-bit would also work but SteamVR typically doesn't offer them. + // Prefer sRGB formats so the OpenXR compositor correctly decodes Dolphin's + // gamma-encoded XFB. With a UNORM swapchain the runtime treats our sRGB bytes + // as linear and the image appears ~2x too bright. + // + // The image is created with MUTABLE_FORMAT_BIT so we render through a UNORM + // VkImageView alias — raw sRGB-encoded bytes land in memory without a double + // gamma encode, and the compositor reads them through the image's sRGB format. + // This also keeps the PostProcessing pipeline cache (keyed on AbstractTextureFormat + // RGBA8 / VK_FORMAT_*_UNORM) compatible with the eye framebuffer's render pass. static constexpr std::array preferred_formats = { + VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM, - VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_R16G16B16A16_SFLOAT, - VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB}; + VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_R16G16B16A16_SFLOAT}; for (const VkFormat preferred : preferred_formats) { @@ -175,6 +183,9 @@ bool VulkanOpenXR::PreQueryVulkanExtensions(VulkanExtensionRequirements& out) auto mgr = std::make_unique(); std::vector extensions = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME}; +#if defined(ANDROID) + extensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME); +#endif const auto controller_exts = VR::OpenXRManager::GetAvailableControllerExtensions(); extensions.insert(extensions.end(), controller_exts.begin(), controller_exts.end()); if (!mgr->CreateInstance(extensions)) @@ -271,6 +282,9 @@ bool VulkanOpenXR::Initialize() auto mgr = std::make_unique(); std::vector extensions = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME}; +#if defined(ANDROID) + extensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME); +#endif const auto controller_exts = VR::OpenXRManager::GetAvailableControllerExtensions(); extensions.insert(extensions.end(), controller_exts.begin(), controller_exts.end()); if (!mgr->CreateInstance(extensions)) @@ -504,8 +518,9 @@ bool VulkanOpenXR::CreateSwapchains() info.mipCount = 1; info.faceCount = 1; info.sampleCount = 1; - info.usageFlags = - XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT; + info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | + XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT | + XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT; XrResult result = xrCreateSwapchain(VR::g_openxr->GetSession(), &info, &sc.swapchain); if (XR_FAILED(result)) @@ -534,12 +549,17 @@ bool VulkanOpenXR::CreateSwapchains() abstract_format, AbstractTextureFlag_RenderTarget, AbstractTextureType::Texture_2D); - // Adopt the runtime-owned VkImage. Pass the actual VkFormat so the VkImageView - // matches the runtime's image format (critical for sRGB formats where - // AbstractTextureFormat::RGBA8 would otherwise map to VK_FORMAT_R8G8B8A8_UNORM). - sc.textures[i] = VKTexture::CreateAdopted( - tex_config, images[i].image, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_LAYOUT_UNDEFINED, - static_cast(swapchain_format)); + // Adopt the runtime-owned VkImage. For sRGB swapchains we create the + // VkImageView with the UNORM alias so BlitFromTexture writes raw bytes + // without a gamma encode — the runtime still sees the image as sRGB and + // the compositor decodes correctly. This relies on + // XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT being set, which forces the + // runtime to create the VkImage with VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT. + const VkFormat view_format = + VKTexture::GetLinearFormat(static_cast(swapchain_format)); + sc.textures[i] = + VKTexture::CreateAdopted(tex_config, images[i].image, VK_IMAGE_VIEW_TYPE_2D, + VK_IMAGE_LAYOUT_UNDEFINED, view_format); if (!sc.textures[i]) { ERROR_LOG_FMT(VIDEO, "OpenXR: VKTexture::CreateAdopted failed for eye {}, image {}.", eye, @@ -664,31 +684,48 @@ void VulkanOpenXR::ReleaseEyeTexture(uint32_t eye_index) if (!m_image_acquired[eye_index]) return; - // End any active render pass and submit the command buffer so the GPU actually - // executes the blit before we tell the runtime the image is ready. - // Without this flush, the draw commands sit in an unsubmitted command buffer - // and the runtime reads an uninitialized image, causing driver crashes. + // End any active render pass so the eye image is no longer bound for rendering. + // We defer the Vulkan submit/xrReleaseSwapchainImage pairing until SubmitFrame() + // so both eyes stay within one command-buffer lifetime. StateTracker::GetInstance()->EndRenderPass(); - g_command_buffer_mgr->SubmitCommandBuffer(false, false); - StateTracker::GetInstance()->InvalidateCachedState(); - - XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - const XrResult result = - xrReleaseSwapchainImage(m_eye_swapchains[eye_index].swapchain, &release_info); - if (XR_FAILED(result)) - { - WARN_LOG_FMT(VIDEO, "OpenXR: xrReleaseSwapchainImage failed for eye {} ({}).", eye_index, - static_cast(result)); - } - - m_image_acquired[eye_index] = false; } bool VulkanOpenXR::SubmitFrame() { ASSERT(VR::g_openxr != nullptr); - // Images are already released in ReleaseEyeTexture(). Just compose layers and call xrEndFrame. + bool has_acquired_images = false; + for (bool acquired : m_image_acquired) + { + has_acquired_images |= acquired; + } + + if (has_acquired_images) + { + // Submit the eye rendering work once per frame before releasing the swapchain + // images back to the runtime. + StateTracker::GetInstance()->EndRenderPass(); + g_command_buffer_mgr->SubmitCommandBuffer(false, true); + StateTracker::GetInstance()->InvalidateCachedState(); + + XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; + for (uint32_t eye = 0; eye < 2; ++eye) + { + if (!m_image_acquired[eye]) + continue; + + const XrResult result = + xrReleaseSwapchainImage(m_eye_swapchains[eye].swapchain, &release_info); + if (XR_FAILED(result)) + { + WARN_LOG_FMT(VIDEO, "OpenXR: xrReleaseSwapchainImage failed for eye {} ({}).", eye, + static_cast(result)); + } + + m_image_acquired[eye] = false; + } + } + const auto& eye_views = VR::g_openxr->GetEyeViews(); for (uint32_t eye = 0; eye < 2; ++eye) diff --git a/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.h b/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.h index a7af6a4b65..939caa6839 100644 --- a/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.h +++ b/Source/Core/VideoBackends/Vulkan/VulkanOpenXR.h @@ -14,6 +14,10 @@ #include "VideoBackends/Vulkan/VulkanLoader.h" #define XR_USE_GRAPHICS_API_VULKAN +#if defined(ANDROID) +#include +#define XR_USE_PLATFORM_ANDROID +#endif #include #include @@ -29,6 +33,14 @@ class VKFramebuffer; // (without allocation) and VKFramebuffer provides a render pass for rendering. struct XRVkEyeSwapchain { + XRVkEyeSwapchain(); + ~XRVkEyeSwapchain(); + XRVkEyeSwapchain(XRVkEyeSwapchain&&) noexcept; + XRVkEyeSwapchain& operator=(XRVkEyeSwapchain&&) noexcept; + + XRVkEyeSwapchain(const XRVkEyeSwapchain&) = delete; + XRVkEyeSwapchain& operator=(const XRVkEyeSwapchain&) = delete; + XrSwapchain swapchain = XR_NULL_HANDLE; uint32_t width = 0; uint32_t height = 0; diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt index 679695d22e..b42fceff9a 100644 --- a/Source/Core/VideoCommon/CMakeLists.txt +++ b/Source/Core/VideoCommon/CMakeLists.txt @@ -308,13 +308,18 @@ endif() if(ENABLE_VR) target_sources(videocommon PRIVATE - VR/OpenXRD3D11Common.cpp - VR/OpenXRD3D11Common.h VR/OpenXRManager.cpp VR/OpenXRManager.h - VR/OpenXRUtilitySession.cpp - VR/OpenXRUtilitySession.h ) # Expose the OpenXR headers to all targets that depend on videocommon. target_include_directories(videocommon PUBLIC "${CMAKE_SOURCE_DIR}/Externals/OpenXR/include") endif() + +if(ENABLE_VR AND WIN32) + target_sources(videocommon PRIVATE + VR/OpenXRD3D11Common.cpp + VR/OpenXRD3D11Common.h + VR/OpenXRUtilitySession.cpp + VR/OpenXRUtilitySession.h + ) +endif() diff --git a/Source/Core/VideoCommon/GeometryShaderManager.cpp b/Source/Core/VideoCommon/GeometryShaderManager.cpp index 42eff51d71..557c42469f 100644 --- a/Source/Core/VideoCommon/GeometryShaderManager.cpp +++ b/Source/Core/VideoCommon/GeometryShaderManager.cpp @@ -182,6 +182,9 @@ void GeometryShaderManager::SetConstants(PrimitiveType prim) constants.eye_z_row[0] = m_cached_eye_z_row[0]; constants.eye_z_row[1] = m_cached_eye_z_row[1]; + // The Vulkan legacy projected-space fallback behaves like a flat stereo projection on + // Quest. Android should use the full per-eye OpenXR projection path instead. +#if !defined(ANDROID) if (perspective && g_backend_info.api_type == APIType::Vulkan) { const Common::Vec2 fov_multiplier = g_freelook_camera.IsActive() ? @@ -217,6 +220,7 @@ void GeometryShaderManager::SetConstants(PrimitiveType prim) constants.legacy_eye_projection_y[1] = legacy_eye_projection_y_rows[0]; constants.stereoparams[1] = 1.0f; } +#endif // Unrotated per-eye projection rows for head-locked content (cached above). constants.head_projection[0] = m_cached_head_projection[0]; diff --git a/Source/Core/VideoCommon/Present.cpp b/Source/Core/VideoCommon/Present.cpp index c365a91a19..1a9dea7860 100644 --- a/Source/Core/VideoCommon/Present.cpp +++ b/Source/Core/VideoCommon/Present.cpp @@ -103,6 +103,16 @@ static void TryToSnapToXFBSize(int& width, int& height, int xfb_width, int xfb_h } } +#ifdef ENABLE_VR +static int GetOpenXRSourceLayer(const AbstractTexture* source_texture, int requested_layer) +{ + if (!source_texture || source_texture->GetLayers() == 0) + return 0; + + return std::min(requested_layer, static_cast(source_texture->GetLayers() - 1)); +} +#endif + Presenter::Presenter() { auto& video_events = GetVideoEvents(); @@ -936,8 +946,8 @@ void Presenter::FinishReplayOpenXRFrameWithoutSource() void Presenter::BlitCurrentSourceToOpenXREyes(const AbstractTexture* source_texture, const MathUtil::Rectangle& source_rc) { - if (!source_texture || source_texture->GetLayers() < 2 || !VR::g_openxr || - !VR::g_openxr->GetSwapchain() || !VR::g_openxr->ShouldRender()) + if (!source_texture || !VR::g_openxr || !VR::g_openxr->GetSwapchain() || + !VR::g_openxr->ShouldRender()) { return; } @@ -973,7 +983,8 @@ void Presenter::BlitCurrentSourceToOpenXREyes(const AbstractTexture* source_text const float clear_alpha = g_ActiveConfig.vr_ar_mode ? g_ActiveConfig.vr_ar_background_alpha : 1.0f; g_gfx->SetAndClearFramebuffer(eye_fb, {0.f, 0.f, 0.f, clear_alpha}); - m_post_processor->BlitFromTexture(eye_rect, source_rc, source_texture, static_cast(eye)); + m_post_processor->BlitFromTexture(eye_rect, source_rc, source_texture, + GetOpenXRSourceLayer(source_texture, static_cast(eye))); sc->ReleaseEyeTexture(eye); } @@ -1187,8 +1198,10 @@ void Presenter::RenderXFBToScreen(const MathUtil::Rectangle& target_rc, // Mirror view on the desktop window (side-by-side, left eye | right eye). // The HMD receives full-resolution images below. const auto [left_rc, right_rc] = ConvertStereoRectangle(target_rc); - m_post_processor->BlitFromTexture(left_rc, source_rc, source_texture, 0); - m_post_processor->BlitFromTexture(right_rc, source_rc, source_texture, 1); + m_post_processor->BlitFromTexture(left_rc, source_rc, source_texture, + GetOpenXRSourceLayer(source_texture, 0)); + m_post_processor->BlitFromTexture(right_rc, source_rc, source_texture, + GetOpenXRSourceLayer(source_texture, 1)); BlitCurrentSourceToOpenXREyes(source_texture, source_rc); } #endif diff --git a/Source/Core/VideoCommon/VertexShaderManager.cpp b/Source/Core/VideoCommon/VertexShaderManager.cpp index 218cb456d9..8f72960f12 100644 --- a/Source/Core/VideoCommon/VertexShaderManager.cpp +++ b/Source/Core/VideoCommon/VertexShaderManager.cpp @@ -119,6 +119,7 @@ Common::Matrix44 VertexShaderManager::LoadProjectionMatrix(bool apply_openxr_leg if (xfmem.projection.type == ProjectionType::Perspective) { #ifdef ENABLE_VR +#if !defined(ANDROID) if (apply_openxr_legacy_view && g_ActiveConfig.stereo_mode == StereoMode::OpenXR && g_backend_info.api_type == APIType::Vulkan && VR::g_openxr && VR::g_openxr->IsSessionRunning()) { @@ -137,6 +138,7 @@ Common::Matrix44 VertexShaderManager::LoadProjectionMatrix(bool apply_openxr_leg corrected_matrix *= openxr_view; } } +#endif #endif if (g_freelook_camera.IsActive()) diff --git a/Source/Core/VideoCommon/VideoConfig.cpp b/Source/Core/VideoCommon/VideoConfig.cpp index 1dba08c90e..a9972e3f79 100644 --- a/Source/Core/VideoCommon/VideoConfig.cpp +++ b/Source/Core/VideoCommon/VideoConfig.cpp @@ -188,9 +188,11 @@ void VideoConfig::Refresh() vr_lean_back_angle = std::clamp(Config::Get(Config::GFX_VR_LEAN_BACK_ANGLE), Config::GFX_VR_LEAN_BACK_ANGLE_MIN, Config::GFX_VR_LEAN_BACK_ANGLE_MAX); + vr_enable_camera_forward = Config::Get(Config::GFX_VR_ENABLE_CAMERA_FORWARD); vr_camera_forward = std::clamp(Config::Get(Config::GFX_VR_CAMERA_FORWARD), Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX); + vr_enable_camera_height = Config::Get(Config::GFX_VR_ENABLE_CAMERA_HEIGHT); vr_camera_height = std::clamp(Config::Get(Config::GFX_VR_CAMERA_HEIGHT), Config::GFX_VR_CAMERA_HEIGHT_MIN, Config::GFX_VR_CAMERA_HEIGHT_MAX); diff --git a/Source/Core/VideoCommon/VideoConfig.h b/Source/Core/VideoCommon/VideoConfig.h index fcefea2712..a501d95ea0 100644 --- a/Source/Core/VideoCommon/VideoConfig.h +++ b/Source/Core/VideoCommon/VideoConfig.h @@ -326,7 +326,9 @@ struct VideoConfig final bool bStereoEFBMonoDepth = false; float vr_units_per_meter = 1.0f; float vr_lean_back_angle = 0.0f; + bool vr_enable_camera_forward = true; float vr_camera_forward = 0.0f; + bool vr_enable_camera_height = true; float vr_camera_height = 0.0f; bool vr_virtual_screen = true; float vr_screen_distance = 1.5f;