diff --git a/android/Build-Quest.ps1 b/android/Build-Quest.ps1 index 6e86b77..340796e 100644 --- a/android/Build-Quest.ps1 +++ b/android/Build-Quest.ps1 @@ -1,7 +1,10 @@ # Builds the standalone Meta Quest APK on a Windows host. # # powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 [-Generated ] -# [-Headset modern|quest1] [-Configuration debug|release] [-Install] +# [-Headset modern|quest1|frame] [-Configuration debug|release] [-Install] +# +# -Headset frame builds the Steam Frame flavour (steamFrame), which runs under Lepton, SteamOS's +# Android layer, with SteamVR's OpenXR runtime (docs/quest-port.md, "Steam Frame"). # # One app offers both games: the APK carries a kit for the base game, and for Retro Rewind too # when -Generated holds a translation that includes the mod. @@ -25,7 +28,7 @@ param( [string]$Generated = '', [string]$Dependencies = '', [string]$CMakeDir = '', - [ValidateSet('modern', 'quest1')] [string]$Headset = 'modern', + [ValidateSet('modern', 'quest1', 'frame')] [string]$Headset = 'modern', [ValidateSet('debug', 'release')] [string]$Configuration = 'debug', [switch]$Install, [switch]$StockDawn @@ -80,8 +83,13 @@ if (-not $Dependencies) { } $variant = (Get-Culture).TextInfo.ToTitleCase($Configuration) -$flavour = if ($Headset -eq 'quest1') { 'Quest1' } else { 'ModernQuest' } -$expectedCpu = if ($Headset -eq 'quest1') { 'kryo' } else { 'cortex-a77' } +# The Gradle flavour and the -mcpu target its kit must record (headsetCpus in app/build.gradle.kts). +$flavourDir, $expectedCpu = switch ($Headset) { + 'quest1' { 'quest1', 'kryo' } + 'frame' { 'steamFrame', 'cortex-x4+nosve' } + default { 'modernQuest', 'cortex-a77' } +} +$flavour = $flavourDir.Substring(0, 1).ToUpperInvariant() + $flavourDir.Substring(1) $task = "app:assemble$flavour$variant" $gradleArgs = @( '--project-dir', $root, @@ -98,7 +106,6 @@ Write-Host "gradlew $($gradleArgs -join ' ')" & (Join-Path $root 'gradlew.bat') @gradleArgs if ($LASTEXITCODE -ne 0) { throw "Gradle failed ($LASTEXITCODE)" } -$flavourDir = if ($Headset -eq 'quest1') { 'quest1' } else { 'modernQuest' } $apkDir = Join-Path $root "app\build\outputs\apk\$flavourDir\$Configuration" $apk = Get-ChildItem -Path $apkDir -Filter '*.apk' | Select-Object -First 1 if (-not $apk) { throw "No APK under $apkDir" } @@ -128,6 +135,8 @@ if ($Install) { if ($LASTEXITCODE -ne 0) { throw "adb install failed ($LASTEXITCODE)" } if ($Headset -eq 'quest1') { Write-Host 'Installed. Use WiiCompiled Settings for setup, then launch WiiCompiled VR directly from the library.' + } elseif ($Headset -eq 'frame') { + Write-Host 'Installed. Build a game on this PC with android/Build-QuestGame.ps1 -Headset frame -Install, then start WiiCompiled VR from the Steam library.' } else { Write-Host 'Installed. Build a game with Build on this Quest in the launcher, or on this PC with android/Build-QuestGame.ps1 -Install.' } diff --git a/android/Build-QuestGame.ps1 b/android/Build-QuestGame.ps1 index 3dbc754..c63877b 100644 --- a/android/Build-QuestGame.ps1 +++ b/android/Build-QuestGame.ps1 @@ -6,7 +6,7 @@ # SDK's NDK and ninja: # # powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 [-Generated ] [-Kit ] -# [-Headset modern|quest1] [-Configuration debug|release] +# [-Headset modern|quest1|frame] [-Configuration debug|release] # [-Data ] [-Output ] [-Install] # # WiiCompiled Setup's --build-quest runs the copy staged in an installation's BuildWorkspace\android, @@ -33,7 +33,7 @@ param( [string]$Sysroot = '', [string]$Ninja = '', [string]$BuiltBy = 'android/Build-QuestGame.ps1', - [ValidateSet('modern', 'quest1')] [string]$Headset = 'modern', + [ValidateSet('modern', 'quest1', 'frame')] [string]$Headset = 'modern', [ValidateSet('debug', 'release')] [string]$Configuration = 'debug', [ValidateSet('base', 'retro_rewind')] [string]$Product = 'base', [string]$Mod = '', @@ -47,7 +47,7 @@ $root = $PSScriptRoot $workspace = (Resolve-Path (Join-Path $root '..')).Path if (-not $Generated) { $Generated = Join-Path $workspace '.scratch\vr-build-workspace\BuildWorkspace\generated' } if (-not $Manifest) { $Manifest = Join-Path $workspace 'projects\mkwii\recomp.yml' } -$variant = if ($Headset -eq 'quest1') { 'quest1' } else { 'modernQuest' } +$variant = switch ($Headset) { 'quest1' { 'quest1' } 'frame' { 'steamFrame' } default { 'modernQuest' } } $variant += (Get-Culture).TextInfo.ToTitleCase($Configuration) if (-not $BuildDir) { $BuildDir = Join-Path $root "app\build\questGame\$variant\$Product" } if (-not $Kit) { diff --git a/android/Run-Quest.ps1 b/android/Run-Quest.ps1 index f2c3c40..5a193bd 100644 --- a/android/Run-Quest.ps1 +++ b/android/Run-Quest.ps1 @@ -3,7 +3,7 @@ # diagnostics for one session. # # powershell -ExecutionPolicy Bypass -File android/Run-Quest.ps1 [-Apk ] [-Data ] -# [-Headset modern|quest1] [-Configuration debug|release] +# [-Headset modern|quest1|frame] [-Configuration debug|release] # [-Seconds 60] [-NoLaunch] [-SkipInstall] # # -Data names the extracted disc partition (the directory holding sys/, files/, @@ -25,7 +25,7 @@ param( [string]$Apk = '', [string]$Data = '', - [ValidateSet('modern', 'quest1')] [string]$Headset = 'modern', + [ValidateSet('modern', 'quest1', 'frame')] [string]$Headset = 'modern', [ValidateSet('debug', 'release')] [string]$Configuration = 'debug', [int]$Seconds = 60, [switch]$NoLaunch, @@ -52,7 +52,7 @@ if (-not $devices) { throw 'No device in "device" state; check the Quest is conn if (-not $SkipInstall) { if (-not $Apk) { - $flavour = if ($Headset -eq 'quest1') { 'quest1' } else { 'modernQuest' } + $flavour = switch ($Headset) { 'quest1' { 'quest1' } 'frame' { 'steamFrame' } default { 'modernQuest' } } $apkDir = Join-Path $root "app\build\outputs\apk\$flavour\$Configuration" $Apk = Get-ChildItem -Path $apkDir -Filter '*.apk' -ErrorAction SilentlyContinue | Select-Object -First 1 | ForEach-Object FullName diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b22638a..e71493b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -53,6 +53,19 @@ val hasRetroRewindShards: Boolean = run { shards.isFile && shards.readText().contains("set(MKW_HAVE_RETRO_REWIND_SHARDS ON)") } +// Each headset flavour's AArch64 -mcpu target. The game kit records it, and the kit export, the APK +// audit (Build-Quest.ps1), the PC build (QuestGameKit.psm1), the on-headset build and the package +// import all refuse a game built for another one. +val headsetCpus = mapOf( + // Snapdragon XR2 and newer. Keep this as the default build target. + "modernQuest" to "cortex-a77", + // Snapdragon 835. cortex-a77 binaries terminate with SIGILL on Quest 1. + "quest1" to "kryo", + // Snapdragon 8 Gen 3 (Cortex-X4, A720 and A520, all ARMv9.2). Its firmware does not expose SVE, + // which clang would otherwise auto-vectorise with for a cortex-x4. + "steamFrame" to "cortex-x4+nosve", +) + // android/nod-jni: nod, the disc image library the PC installer runs as nodtool, // cross-compiled with cargo for the launcher's "Select disc image". Needs a Rust // toolchain with the aarch64-linux-android target (see docs/quest-port.md). @@ -152,11 +165,11 @@ android { dimension = "headset" manifestPlaceholders["mkwQuestSupportedDevices"] = "quest2|quest3|quest3s|questpro" buildConfigField("boolean", "QUEST1_DIRECT_LAUNCH", "false") - buildConfigField("String", "ANDROID_CPU", "\"cortex-a77\"") + buildConfigField("boolean", "STEAM_FRAME", "false") + buildConfigField("String", "ANDROID_CPU", "\"${headsetCpus.getValue("modernQuest")}\"") externalNativeBuild { cmake { - // Snapdragon XR2 and newer. Keep this as the default build target. - arguments += "-DMKW_ANDROID_CPU=cortex-a77" + arguments += "-DMKW_ANDROID_CPU=${headsetCpus.getValue("modernQuest")}" } } } @@ -164,11 +177,30 @@ android { dimension = "headset" manifestPlaceholders["mkwQuestSupportedDevices"] = "quest|quest2" buildConfigField("boolean", "QUEST1_DIRECT_LAUNCH", "true") - buildConfigField("String", "ANDROID_CPU", "\"kryo\"") + buildConfigField("boolean", "STEAM_FRAME", "false") + buildConfigField("String", "ANDROID_CPU", "\"${headsetCpus.getValue("quest1")}\"") externalNativeBuild { cmake { - // Snapdragon 835. cortex-a77 binaries terminate with SIGILL on Quest 1. - arguments += "-DMKW_ANDROID_CPU=kryo" + arguments += "-DMKW_ANDROID_CPU=${headsetCpus.getValue("quest1")}" + } + } + } + // Valve's Steam Frame, through Lepton (SteamOS's Android layer) and SteamVR's Android + // OpenXR runtime. Its manifest (src/steamFrame) drops the Horizon OS entries, and + // MKW_ANDROID_HEADSET gives the runtime the Frame's defaults (runtime_config.h). + create("steamFrame") { + dimension = "headset" + // Horizon OS only; src/steamFrame/AndroidManifest.xml removes the entry. + manifestPlaceholders["mkwQuestSupportedDevices"] = "" + buildConfigField("boolean", "QUEST1_DIRECT_LAUNCH", "false") + buildConfigField("boolean", "STEAM_FRAME", "true") + buildConfigField("String", "ANDROID_CPU", "\"${headsetCpus.getValue("steamFrame")}\"") + externalNativeBuild { + cmake { + arguments += listOf( + "-DMKW_ANDROID_CPU=${headsetCpus.getValue("steamFrame")}", + "-DMKW_ANDROID_HEADSET=steam_frame", + ) } } } @@ -324,7 +356,7 @@ androidComponents { dependsOn("merge${capitalized}NativeLibs") appDir.set(layout.projectDirectory) script.set(rootProject.layout.projectDirectory.file("Export-QuestGameKit.ps1")) - androidCpu.set(if (variant.name.startsWith("quest1", ignoreCase = true)) "kryo" else "cortex-a77") + androidCpu.set(headsetCpus.getValue(checkNotNull(variant.flavorName) { "${variant.name} has no headset flavour" })) outputDir.set(layout.buildDirectory.dir("generated/assets/questGameKit/${variant.name}")) llvmStrip.set(sdkComponents.ndkDirectory.map { it.file("toolchains/llvm/prebuilt/$host/bin/llvm-strip" + if (host.startsWith("windows")) ".exe" else "") diff --git a/android/app/src/steamFrame/AndroidManifest.xml b/android/app/src/steamFrame/AndroidManifest.xml new file mode 100644 index 0000000..4979970 --- /dev/null +++ b/android/app/src/steamFrame/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/steamFrame/java/org/wiicompiled/quest/FrameEntryActivity.kt b/android/app/src/steamFrame/java/org/wiicompiled/quest/FrameEntryActivity.kt new file mode 100644 index 0000000..6099588 --- /dev/null +++ b/android/app/src/steamFrame/java/org/wiicompiled/quest/FrameEntryActivity.kt @@ -0,0 +1,52 @@ +package org.wiicompiled.quest + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.util.Log +import org.wiicompiled.quest.launcher.LauncherActivity +import org.wiicompiled.quest.launcher.ModLibrary + +/** + * The Steam Frame's library entry. Lepton starts the one real activity that is both MAIN and + * LAUNCHER, in VR when it carries a VR category, and ignores activity-aliases, so neither of the + * Quest flavours' entries works there. This one shows nothing: the setup panel (LauncherActivity) + * always opens, and when the selected game can start as it is, the game opens on top of it, so the + * headset goes straight into VR and quitting the game comes back to the panel. + */ +class FrameEntryActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val launcher = Intent(this, LauncherActivity::class.java) + if (readyToPlay()) { + Log.i(TAG, "Starting the game over the setup panel") + startActivities(arrayOf(launcher, Intent(this, QuestActivity::class.java))) + } else { + Log.i(TAG, "The game is not ready to start; opening the setup panel") + startActivity(launcher) + } + finish() + } + + /** + * What Home's Play needs (LauncherActivity), short of the steps only the panel performs: the + * game files and the selected game built for this app, Retro Rewind's pack, and no enabled mods + * to copy into the pack's Patches folder first. + */ + private fun readyToPlay(): Boolean = runCatching { + val profile = GameProfile.selected(this) + GameStorage.discStatus(this) == GameStorage.DiscStatus.Ready && + GameLibrary.status(this, profile) == GameLibrary.Status.Ready && + GameStorage.modContentReady(this, profile) && + (!profile.modPack || ModLibrary.load(GameStorage.modsDirectory(this)).none { it.enabled }) + }.getOrElse { + Log.w(TAG, "Could not check the installed game", it) + false + } + + private companion object { + // The launcher's tag, so the usual `adb logcat -s WiiCompiledLauncher` shows the routing. + const val TAG = "WiiCompiledLauncher" + } +} diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 3a2cb1e..5820ffb 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -37,6 +37,21 @@ option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON) # aurora-main and its third-party tree stay unaffected. set(MKW_PROJECT_COMPILE_DEFINITIONS NOMINMAX) +# The Android headset family the build is for (the Gradle flavour passes it). The +# Steam Frame build runs under SteamVR's Android runtime rather than Horizon OS and +# takes its own defaults (runtime_config.h) through MKW_HEADSET_STEAM_FRAME. The +# definition reaches every target below, so the game kit's recorded compile flags +# carry it too. +if(MKW_PLATFORM_ANDROID) + set(MKW_ANDROID_HEADSET "quest" CACHE STRING "Android headset family: quest (Meta Quest, any flavour) or steam_frame") + set_property(CACHE MKW_ANDROID_HEADSET PROPERTY STRINGS quest steam_frame) + if(MKW_ANDROID_HEADSET STREQUAL "steam_frame") + list(APPEND MKW_PROJECT_COMPILE_DEFINITIONS MKW_HEADSET_STEAM_FRAME=1) + elseif(NOT MKW_ANDROID_HEADSET STREQUAL "quest") + message(FATAL_ERROR "MKW_ANDROID_HEADSET must be quest or steam_frame, not '${MKW_ANDROID_HEADSET}'") + endif() +endif() + set(CMAKE_CXX_STANDARD 17) # Declared before the vendored libraries so the third-party block below can diff --git a/runtime/cmake/PublicProducts.cmake b/runtime/cmake/PublicProducts.cmake index 9c78816..4cc7ba2 100644 --- a/runtime/cmake/PublicProducts.cmake +++ b/runtime/cmake/PublicProducts.cmake @@ -448,6 +448,7 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") elseif(MKW_PLATFORM_ANDROID) # Cross-compiled, so "native" would describe the build host. Cortex-A77 is # the Snapdragon XR2 Gen 1 (Quest 2) core; Quest 3 / Pro are supersets. + # Each Gradle headset flavour passes its own (android/app/build.gradle.kts). set(MKW_ANDROID_CPU "cortex-a77" CACHE STRING "AArch64 -mcpu target for the Android products") set(MKW_BASELINE_ARCH_FLAG -mcpu=${MKW_ANDROID_CPU}) elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") diff --git a/runtime/include/vr/openxr_input.h b/runtime/include/vr/openxr_input.h index e1f3b4e..759b40c 100644 --- a/runtime/include/vr/openxr_input.h +++ b/runtime/include/vr/openxr_input.h @@ -193,6 +193,11 @@ private: XrAction m_button_primary = XR_NULL_HANDLE; // A / X XrAction m_button_secondary = XR_NULL_HANDLE; // B / Y XrAction m_menu = XR_NULL_HANDLE; + // The Steam Frame's left D-pad (valve/frame_controller_valve); Touch has none. + XrAction m_dpad_up = XR_NULL_HANDLE; + XrAction m_dpad_down = XR_NULL_HANDLE; + XrAction m_dpad_left = XR_NULL_HANDLE; + XrAction m_dpad_right = XR_NULL_HANDLE; XrAction m_aim_pose = XR_NULL_HANDLE; XrAction m_grip_pose = XR_NULL_HANDLE; XrAction m_haptic = XR_NULL_HANDLE; diff --git a/runtime/include/vr/openxr_wii_remote.h b/runtime/include/vr/openxr_wii_remote.h index 666b2f6..bacbbc0 100644 --- a/runtime/include/vr/openxr_wii_remote.h +++ b/runtime/include/vr/openxr_wii_remote.h @@ -341,6 +341,12 @@ struct HandInputs { float squeeze = 0.0f; float stick_x = 0.0f; float stick_y = 0.0f; // +up + // A D-pad, which only the Steam Frame's left controller has + // (valve/frame_controller_valve). + bool dpad_up = false; + bool dpad_down = false; + bool dpad_left = false; + bool dpad_right = false; }; // Adapted from DolphinXR's default "OpenXR Wii Remote" profile @@ -353,6 +359,8 @@ struct HandInputs { // HOME has no button; left Y opens the settings panel (openxr_settings_panel.h). // The grips press nothing: they take hold of the wheel (openxr_driving.h), and C // is the game's look-behind, which a hand on the wheel would otherwise hold down. +// A controller D-pad (the Steam Frame's left one) is the remote's D-pad; the +// Frame's left View button is the left menu and its left shoulder the left Y. inline uint32_t RemoteButtons(const HandInputs& left, const HandInputs& right) noexcept { uint32_t hold = 0; const auto press = [&hold](bool held, uint32_t bit) { @@ -367,6 +375,10 @@ inline uint32_t RemoteButtons(const HandInputs& left, const HandInputs& right) n press(right.stick_y < -kPressThreshold, kButtonTwo); press(left.primary || left.menu, kButtonPlus); press(left.trigger > kPressThreshold, kButtonZ); + press(left.dpad_up || right.dpad_up, kButtonUp); + press(left.dpad_down || right.dpad_down, kButtonDown); + press(left.dpad_left || right.dpad_left, kButtonLeft); + press(left.dpad_right || right.dpad_right, kButtonRight); return hold; } diff --git a/runtime/src/vr/openxr_input.cpp b/runtime/src/vr/openxr_input.cpp index bc4b3e7..001baf2 100644 --- a/runtime/src/vr/openxr_input.cpp +++ b/runtime/src/vr/openxr_input.cpp @@ -146,7 +146,9 @@ private: (1u << SDL_GAMEPAD_BUTTON_START) | (1u << SDL_GAMEPAD_BUTTON_LEFT_STICK) | (1u << SDL_GAMEPAD_BUTTON_RIGHT_STICK) | (1u << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) | - (1u << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER); + (1u << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) | + (1u << SDL_GAMEPAD_BUTTON_DPAD_UP) | (1u << SDL_GAMEPAD_BUTTON_DPAD_DOWN) | + (1u << SDL_GAMEPAD_BUTTON_DPAD_LEFT) | (1u << SDL_GAMEPAD_BUTTON_DPAD_RIGHT); desc.axis_mask = (1u << SDL_GAMEPAD_AXIS_LEFTX) | (1u << SDL_GAMEPAD_AXIS_LEFTY) | (1u << SDL_GAMEPAD_AXIS_RIGHTX) | (1u << SDL_GAMEPAD_AXIS_RIGHTY) | (1u << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1u << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER); @@ -394,7 +396,7 @@ bool OpenXRInput::CreateActions() { const char* localized; XrActionType type; }; - const std::array specs{{ + const std::array specs{{ {&m_thumbstick, "thumbstick", "Thumbstick", XR_ACTION_TYPE_VECTOR2F_INPUT}, {&m_thumbstick_click, "thumbstick_click", "Thumbstick Click", XR_ACTION_TYPE_BOOLEAN_INPUT}, {&m_trigger, "trigger", "Trigger", XR_ACTION_TYPE_FLOAT_INPUT}, @@ -402,6 +404,10 @@ bool OpenXRInput::CreateActions() { {&m_button_primary, "button_primary", "A / X", XR_ACTION_TYPE_BOOLEAN_INPUT}, {&m_button_secondary, "button_secondary", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT}, {&m_menu, "menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT}, + {&m_dpad_up, "dpad_up", "D-pad Up", XR_ACTION_TYPE_BOOLEAN_INPUT}, + {&m_dpad_down, "dpad_down", "D-pad Down", XR_ACTION_TYPE_BOOLEAN_INPUT}, + {&m_dpad_left, "dpad_left", "D-pad Left", XR_ACTION_TYPE_BOOLEAN_INPUT}, + {&m_dpad_right, "dpad_right", "D-pad Right", XR_ACTION_TYPE_BOOLEAN_INPUT}, {&m_aim_pose, "aim_pose", "Pointer", XR_ACTION_TYPE_POSE_INPUT}, {&m_grip_pose, "grip_pose", "Motion", XR_ACTION_TYPE_POSE_INPUT}, {&m_haptic, "haptic", "Haptic", XR_ACTION_TYPE_VIBRATION_OUTPUT}, @@ -493,6 +499,40 @@ bool OpenXRInput::SuggestBindings() { {&m_haptic, "/user/hand/right/output/haptic"}, }; suggest("/interaction_profiles/khr/simple_controller", simple, false); + + // The Steam Frame's controllers (XR_VALVE_frame_controller_interaction). Without the profile + // SteamVR presents them as Touch controllers, which loses the left D-pad. Its left hand has a + // D-pad where Touch has X and Y, a View button for the menu and a shoulder button, which takes + // left Y's place as the settings panel's button. Right X, Y, menu and shoulder stay free. + const auto& extensions = m_runtime->EnabledExtensions(); + if (std::find(extensions.begin(), extensions.end(), "XR_VALVE_frame_controller_interaction") != + extensions.end()) { + const std::vector frame{ + {&m_thumbstick, "/user/hand/left/input/thumbstick"}, + {&m_thumbstick, "/user/hand/right/input/thumbstick"}, + {&m_thumbstick_click, "/user/hand/left/input/thumbstick/click"}, + {&m_thumbstick_click, "/user/hand/right/input/thumbstick/click"}, + {&m_trigger, "/user/hand/left/input/trigger/value"}, + {&m_trigger, "/user/hand/right/input/trigger/value"}, + {&m_squeeze, "/user/hand/left/input/squeeze/value"}, + {&m_squeeze, "/user/hand/right/input/squeeze/value"}, + {&m_button_primary, "/user/hand/right/input/a/click"}, + {&m_button_secondary, "/user/hand/right/input/b/click"}, + {&m_button_secondary, "/user/hand/left/input/shoulder/click"}, + {&m_menu, "/user/hand/left/input/view/click"}, + {&m_dpad_up, "/user/hand/left/input/dpad_up/click"}, + {&m_dpad_down, "/user/hand/left/input/dpad_down/click"}, + {&m_dpad_left, "/user/hand/left/input/dpad_left/click"}, + {&m_dpad_right, "/user/hand/left/input/dpad_right/click"}, + {&m_aim_pose, "/user/hand/left/input/aim/pose"}, + {&m_aim_pose, "/user/hand/right/input/aim/pose"}, + {&m_grip_pose, "/user/hand/left/input/grip/pose"}, + {&m_grip_pose, "/user/hand/right/input/grip/pose"}, + {&m_haptic, "/user/hand/left/output/haptic"}, + {&m_haptic, "/user/hand/right/output/haptic"}, + }; + suggest("/interaction_profiles/valve/frame_controller_valve", frame, false); + } return true; } @@ -868,6 +908,7 @@ void OpenXRInput::Destroy() { } m_thumbstick = m_thumbstick_click = m_trigger = m_squeeze = XR_NULL_HANDLE; m_button_primary = m_button_secondary = m_menu = m_haptic = XR_NULL_HANDLE; + m_dpad_up = m_dpad_down = m_dpad_left = m_dpad_right = XR_NULL_HANDLE; m_aim_pose = m_grip_pose = XR_NULL_HANDLE; m_hand_paths[0] = m_hand_paths[1] = XR_NULL_PATH; m_convert_now_to_xr_time = nullptr; @@ -983,6 +1024,10 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen& inputs.primary = boolean(m_button_primary, hand, &primary_active); inputs.secondary = boolean(m_button_secondary, hand, &secondary_active); inputs.menu = boolean(m_menu, hand); + inputs.dpad_up = boolean(m_dpad_up, hand); + inputs.dpad_down = boolean(m_dpad_down, hand); + inputs.dpad_left = boolean(m_dpad_left, hand); + inputs.dpad_right = boolean(m_dpad_right, hand); inputs.thumbstick_click = boolean(m_thumbstick_click, hand); inputs.trigger = scalar(m_trigger, hand); inputs.squeeze = scalar(m_squeeze, hand, &squeeze_active); @@ -1165,6 +1210,10 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen& pad.buttons[SDL_GAMEPAD_BUTTON_RIGHT_STICK] = right.thumbstick_click; pad.buttons[SDL_GAMEPAD_BUTTON_LEFT_SHOULDER] = left.squeeze > 0.5f; pad.buttons[SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER] = right.squeeze > 0.5f; + pad.buttons[SDL_GAMEPAD_BUTTON_DPAD_UP] = left.dpad_up || right.dpad_up; + pad.buttons[SDL_GAMEPAD_BUTTON_DPAD_DOWN] = left.dpad_down || right.dpad_down; + pad.buttons[SDL_GAMEPAD_BUTTON_DPAD_LEFT] = left.dpad_left || right.dpad_left; + pad.buttons[SDL_GAMEPAD_BUTTON_DPAD_RIGHT] = left.dpad_right || right.dpad_right; Relay().Publish(pad); } diff --git a/runtime/src/vr/openxr_integration.cpp b/runtime/src/vr/openxr_integration.cpp index 86b30e3..e91a65e 100644 --- a/runtime/src/vr/openxr_integration.cpp +++ b/runtime/src/vr/openxr_integration.cpp @@ -407,7 +407,8 @@ public: #if defined(_WIN32) config.required_extensions = {kRequiredAuroraBackend == BACKEND_VULKAN ? "XR_KHR_vulkan_enable2" : "XR_KHR_D3D12_enable"}; config.optional_extensions = {"XR_KHR_win32_convert_performance_counter_time", - "XR_FB_display_refresh_rate", "XR_EXT_performance_settings"}; + "XR_FB_display_refresh_rate", "XR_EXT_performance_settings", + "XR_VALVE_frame_controller_interaction"}; AddHandMeshExtensions(config); #else // Either Vulkan binding extension is acceptable; the backend picks @@ -415,11 +416,13 @@ public: config.required_extensions = {"XR_KHR_android_create_instance"}; // XR_FB_passthrough: the room around the virtual screen (OpenXRPassthrough), asked for // whatever [vr] passthrough says, since the setting is live. + // XR_VALVE_frame_controller_interaction: the Steam Frame's controllers, D-pad included + // (OpenXRInput::SuggestBindings), under SteamVR here or streamed from a PC. config.optional_extensions = {"XR_KHR_vulkan_enable2", "XR_KHR_vulkan_enable", "XR_KHR_convert_timespec_time", "XR_KHR_android_thread_settings", "XR_FB_display_refresh_rate", "XR_EXT_performance_settings", - "XR_FB_passthrough"}; + "XR_FB_passthrough", "XR_VALVE_frame_controller_interaction"}; AddHandMeshExtensions(config); config.instance_create_next = OpenXRAndroidInstanceCreateNext(); #endif diff --git a/runtime/tests/vr_wii_remote_tests.cpp b/runtime/tests/vr_wii_remote_tests.cpp index 7ffe66d..1feed26 100644 --- a/runtime/tests/vr_wii_remote_tests.cpp +++ b/runtime/tests/vr_wii_remote_tests.cpp @@ -282,6 +282,19 @@ void TestButtons() { left.menu = true; Check((RemoteButtons(left, right) & kButtonHome) == 0, "left menu is no longer HOME"); + // The Steam Frame's left D-pad is the remote's, one bit per direction. + left = {}; + left.dpad_up = true; + Check(RemoteButtons(left, right) == kButtonUp, "D-pad up is the remote's up"); + left = {}; + left.dpad_down = true; + left.dpad_left = true; + Check(RemoteButtons(left, right) == (kButtonDown | kButtonLeft), "D-pad down and left together"); + left = {}; + right.dpad_right = true; + Check(RemoteButtons(left, right) == kButtonRight, "a D-pad on either hand counts"); + right = {}; + left.stick_x = 1.0f; left.stick_y = 1.0f; const std::array stick = NunchukStick(left);